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

// ── Pull-to-refresh ───────────────────────────────────────────────────────
function usePullToRefresh(scrollRef, onRefresh) {
  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const THRESHOLD = 64;
    let startY = 0, pullDist = 0, active = false, busy = false;

    let ind = el.querySelector(':scope > .ptr-ind');
    if (!ind) {
      ind = document.createElement('div');
      ind.className = 'ptr-ind';
      ind.innerHTML = '<div class="ptr-ico">↻</div>';
      el.insertBefore(ind, el.firstChild);
    }
    const ico = ind.querySelector('.ptr-ico');

    const setH = (h, anim) => {
      ind.style.transition = anim ? 'height .28s ease' : 'none';
      ind.style.height = h + 'px';
      ico.style.opacity = Math.min(h / THRESHOLD, 1);
      if (!busy) ico.style.transform = `rotate(${Math.min(h / THRESHOLD, 1) * 180}deg)`;
    };

    const onStart = e => {
      if (el.scrollTop > 0 || busy) return;
      startY = e.touches[0].clientY; active = false; pullDist = 0;
    };
    const onMove = e => {
      if (el.scrollTop > 0 || busy) return;
      const dy = e.touches[0].clientY - startY;
      if (dy <= 0) return;
      active = true;
      e.preventDefault();
      pullDist = Math.min(dy * 0.4, THRESHOLD + 20);
      setH(pullDist, false);
    };
    const onEnd = () => {
      if (!active) return;
      active = false;
      if (pullDist >= THRESHOLD && !busy) {
        busy = true;
        setH(THRESHOLD, true);
        ico.style.transform = '';
        ico.style.animation = 'ptr-spin .65s linear infinite';
        onRefresh();
        setTimeout(() => {
          setH(0, true);
          setTimeout(() => { ico.style.animation = ''; pullDist = 0; busy = false; }, 300);
        }, 1300);
      } else {
        setH(0, true);
        setTimeout(() => { pullDist = 0; }, 280);
      }
    };

    el.addEventListener('touchstart', onStart,  { passive: true  });
    el.addEventListener('touchmove',  onMove,   { passive: false });
    el.addEventListener('touchend',   onEnd,    { passive: true  });
    return () => {
      el.removeEventListener('touchstart', onStart);
      el.removeEventListener('touchmove',  onMove);
      el.removeEventListener('touchend',   onEnd);
    };
  }, []);
}

const IS_DEV_ENV = /localhost|pipelinequest-dev|pipelinequest-staging/.test(window.location.hostname);
const IS_DEV_SETUP_ENV = /localhost|pipelinequest-dev/.test(window.location.hostname);
const PQ_TARGET = String(window.__PIPELINEQUEST_TARGET__ || '').toLowerCase();
function buildMetaValue(value, fallback) {
  const raw = String(value || '').trim();
  return raw && !raw.startsWith('__PQ_') ? raw : fallback;
}
const PQ_VERSION = buildMetaValue(window.__PIPELINEQUEST_VERSION__, '0.0.0');
const PQ_BUILD = buildMetaValue(window.__PIPELINEQUEST_BUILD__, (PQ_TARGET || 'local') + '-local');
const PQ_DATA_VERSION = Number.isFinite(Number(window.__PIPELINEQUEST_DATA_VERSION__)) ? Number(window.__PIPELINEQUEST_DATA_VERSION__) : 1;
const IS_STAGING_ENV = PQ_TARGET === 'staging' || /pipelinequest-staging/.test(window.location.hostname);
const IS_PREPROD_FEATURE_ENV = PQ_TARGET === 'dev' || PQ_TARGET === 'staging' || /localhost|pipelinequest-dev|pipelinequest-staging/.test(window.location.hostname);
const IS_DEV_MEDDIC_ENABLED = IS_PREPROD_FEATURE_ENV;
function featureFlag(key, fallback) {
  const override = localStorage.getItem('pq_ff_' + key);
  if (override === '1') return true;
  if (override === '0') return false;
  return fallback;
}
const IS_TODAY_ROW_SIMPLIFIED_ENABLED = featureFlag('today_row_simplified', IS_PREPROD_FEATURE_ENV);
const IS_TODAY_INLINE_UNDO_ENABLED = featureFlag('today_inline_undo', IS_PREPROD_FEATURE_ENV);
const IS_ACTIVITY_FOUR_OPTIONS_ENABLED = featureFlag('activity_four_options', IS_PREPROD_FEATURE_ENV);
const IS_MEDDIC_CLOSE_SCORE_ENABLED = featureFlag('meddic_close_score', IS_PREPROD_FEATURE_ENV);
const IS_SCORE_BREAKDOWN_GRAPH_ENABLED = featureFlag('score_breakdown_graph', IS_PREPROD_FEATURE_ENV);
const IS_PIPELINE_CLEARED_V2_ENABLED = featureFlag('pipeline_cleared_v2', IS_PREPROD_FEATURE_ENV);
const IS_LOCAL_MIDNIGHT_TODAY_ROLLOVER_ENABLED = featureFlag('local_midnight_today_rollover', IS_PREPROD_FEATURE_ENV);
const IS_PROFILE_SETTINGS_V2_ENABLED = featureFlag('profile_settings_v2', IS_PREPROD_FEATURE_ENV);
const IS_PROFESSIONAL_IA_V2_ENABLED = featureFlag('professional_ia_v2', PQ_TARGET === 'dev' || /localhost|pipelinequest-dev/.test(window.location.hostname));
const IS_PROFESSIONAL_PROGRESS_PERIODS_ENABLED = featureFlag('professional_progress_periods_v1', IS_PROFESSIONAL_IA_V2_ENABLED);
const IS_ACCOUNT_AUTH_ENABLED = featureFlag('account_auth', false);
const IS_AI_NEXT_STEPS_ENABLED = featureFlag('ai_next_steps', PQ_TARGET === 'dev' || /localhost|pipelinequest-dev/.test(window.location.hostname));
const DEMO_STARTUP_FALLBACK =
  window.__PIPELINEQUEST_DEMO_STARTUP__ === true ? true :
  window.__PIPELINEQUEST_DEMO_STARTUP__ === false ? false :
  IS_PREPROD_FEATURE_ENV;
const IS_DEMO_STARTUP_ENABLED = IS_STAGING_ENV ? false : featureFlag('demo_startup', DEMO_STARTUP_FALLBACK);
const IS_FORECAST_COMMAND_CENTER_ENABLED = featureFlag('forecast_command_center', IS_PREPROD_FEATURE_ENV);
const IS_FORECAST_CONTROL_ROOM_ENABLED = featureFlag('forecast_control_room_white', PQ_TARGET === 'dev' || /localhost|pipelinequest-dev/.test(window.location.hostname));
const DEV_SETUP_COMPLETE_KEY = 'pq_setup_complete';
const DEV_SETUP_INSTANCE_KEY = 'pq_setup_instance_url';
const DEV_SETUP_ORG_KEY = 'pq_setup_org_name';
const DEV_SETUP_VALIDATED_KEY = 'pq_setup_validated_at';
const DEV_DEMO_MODE_KEY = 'pq_demo_mode';
const FORECAST_CALL_STORAGE_KEY = 'pq_forecast_call_v1';
const ACCOUNT_SESSION_KEY = 'pq_account_session_v1';
const ACCOUNT_STORE_KEY = 'pq_beta_accounts_v1';
const ACCOUNT_INVITED_EMAILS_KEY = 'pq_beta_invited_emails_v1';
const DEFAULT_BETA_INVITE_CODE = 'BETA2026';
const DEFAULT_QUARTERLY_QUOTA = 500000;
const DEFAULT_YEARLY_QUOTA = 2000000;
const QUARTERLY_QUOTA_KEY = 'pq_quarterly_quota';
const YEARLY_QUOTA_KEY = 'pq_yearly_quota';
const TARGET_TERM_KEY = 'pq_target_term';
const TARGET_TERM_OPTIONS = ['quota', 'goal', 'plan'];
const FISCAL_START_MONTH_KEY = 'pq_fiscal_start_month';
const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const MONTH_SHORT_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const MEDDIC_STORAGE_KEY = 'pq_meddic_v1';
const MEDDIC_ENABLED_KEY = 'pq_meddic_enabled';
const SALES_METHODOLOGY_KEY = 'pq_sales_methodology';
const TODAY_NAV_TOUR_SEEN_KEY = 'pq_today_nav_tour_seen_v1';
const SALES_METHODOLOGIES = {
  bant: {
    key: 'bant',
    label: 'BANT',
    pillLabel: 'BANT',
    scoreLabel: 'BANT',
    summaryPrefix: 'BANT',
    profileSub: 'Budget, Authority, Need, Timeline',
    fields: [
      { key: 'budget', label: 'Budget', short: 'B', prompt: 'Budget is identified and viable', weight: 25 },
      { key: 'authority', label: 'Authority', short: 'A', prompt: 'Decision authority is known', weight: 25 },
      { key: 'need', label: 'Need', short: 'N', prompt: 'Business need is confirmed', weight: 30 },
      { key: 'timeline', label: 'Timeline', short: 'T', prompt: 'Buying timeline is defined', weight: 20 },
    ],
  },
  meddic: {
    key: 'meddic',
    label: 'MEDDIC',
    pillLabel: 'MEDDIC',
    scoreLabel: 'MEDDIC',
    summaryPrefix: 'MED',
    profileSub: 'Metrics, Economic buyer, Decision criteria, Decision process, Pain, Champion',
    fields: [
      { key: 'metrics', label: 'Metrics', short: 'M', prompt: 'Business impact is quantified', weight: 10 },
      { key: 'economicBuyer', label: 'Economic Buyer', short: 'E', prompt: 'Budget owner is known', weight: 20 },
      { key: 'decisionCriteria', label: 'Decision Criteria', short: 'D', prompt: 'Buying criteria are clear', weight: 10 },
      { key: 'decisionProcess', label: 'Decision Process', short: 'D', prompt: 'Approval path is mapped', weight: 10 },
      { key: 'identifyPain', label: 'Pain', short: 'I', prompt: 'Pain is documented', weight: 25 },
      { key: 'champion', label: 'Champion', short: 'C', prompt: 'Internal advocate is active', weight: 25 },
    ],
  },
  meddpicc: {
    key: 'meddpicc',
    label: 'MEDDPICC',
    pillLabel: 'MEDDPICC',
    scoreLabel: 'MEDDPICC',
    summaryPrefix: 'MEDP',
    profileSub: 'Metrics, Economic buyer, Decision criteria, Decision process, Paper process, Pain, Champion, Competition',
    fields: [
      { key: 'metrics', label: 'Metrics', short: 'M', prompt: 'Business impact is quantified', weight: 10 },
      { key: 'economicBuyer', label: 'Economic Buyer', short: 'E', prompt: 'Economic buyer is identified', weight: 15 },
      { key: 'decisionCriteria', label: 'Decision Criteria', short: 'D', prompt: 'Decision criteria are documented', weight: 10 },
      { key: 'decisionProcess', label: 'Decision Process', short: 'D', prompt: 'Decision process is mapped', weight: 10 },
      { key: 'paperProcess', label: 'Paper Process', short: 'P', prompt: 'Legal and procurement steps are known', weight: 10 },
      { key: 'identifyPain', label: 'Identify Pain', short: 'I', prompt: 'Critical business pain is confirmed', weight: 20 },
      { key: 'champion', label: 'Champion', short: 'C', prompt: 'Champion is active and tested', weight: 15 },
      { key: 'competition', label: 'Competition', short: 'C', prompt: 'Competitive position is understood', weight: 10 },
    ],
  },
  threeWhys: {
    key: 'threeWhys',
    label: 'The Three Whys',
    pillLabel: '3 Whys',
    scoreLabel: 'Three Whys',
    summaryPrefix: 'WHY',
    profileSub: 'Why Buy Anything, Why Buy Us, Why Buy Now',
    fields: [
      { key: 'whyBuyAnything', label: 'Why Buy Anything?', short: 'A', prompt: 'The cost of doing nothing is clear', weight: 34 },
      { key: 'whyBuyUs', label: 'Why Buy Us?', short: 'U', prompt: 'Our differentiated value is clear', weight: 33 },
      { key: 'whyBuyNow', label: 'Why Buy Now?', short: 'N', prompt: 'Urgency and timing are compelling', weight: 33 },
    ],
  },
};
const SALES_METHODOLOGY_OPTIONS = ['bant', 'meddic', 'meddpicc', 'threeWhys'];
const MEDDIC_FIELDS = SALES_METHODOLOGIES.meddic.fields;
const MEDDIC_CLOSE_SCORE_WEIGHTS = Object.fromEntries(MEDDIC_FIELDS.map(field => [field.key, field.weight]));
const TODAY_PILL_MAX_VISIBLE = 1;
const TODAY_PILL_DEFS = [
  { key: 'forecast', label: 'Forecast' },
  { key: 'stage', label: 'Stage' },
  { key: 'closeDate', label: 'Close Date' },
  { key: 'meddic', label: 'MEDDIC' },
  { key: 'nextStep', label: 'Next Step' },
];
const TODAY_PILL_KEYS = TODAY_PILL_DEFS.map(item => item.key);
const DEFAULT_TODAY_PILL_LAYOUT = {
  collapsed: ['forecast'],
  details: ['stage', 'closeDate', 'meddic', 'nextStep'],
};
const POINTS_STORAGE_KEY = 'pq_points';
const LEGACY_POINTS_STORAGE_KEY = 'pq_' + 'x' + 'p';
const TODAY_POINTS_STORAGE_PREFIX = 'pq_today_points_';
const LEGACY_TODAY_POINTS_STORAGE_PREFIX = 'pq_today_' + 'x' + 'p_';

function readStoredNumber(key, fallbackKey = null) {
  const raw = localStorage.getItem(key);
  const fallbackRaw = raw === null && fallbackKey ? localStorage.getItem(fallbackKey) : null;
  const value = parseInt(raw ?? fallbackRaw ?? '', 10);
  return Number.isFinite(value) ? value : 0;
}

function localIsoDate(date = new Date()) {
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

function appTodayDate(date = new Date()) {
  return IS_LOCAL_MIDNIGHT_TODAY_ROLLOVER_ENABLED
    ? localIsoDate(date)
    : date.toISOString().split('T')[0];
}

function appIsoDateDaysAgo(days, date = new Date()) {
  const d = new Date(date.getTime() - days * 86400000);
  return appTodayDate(d);
}

function msUntilNextLocalMidnight(now = new Date()) {
  const next = new Date(now);
  next.setHours(24, 0, 1, 0);
  return Math.max(1000, next.getTime() - now.getTime());
}

function readJsonStorage(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw === null ? fallback : JSON.parse(raw);
  } catch {
    return fallback;
  }
}

function normalizeAccountEmail(value) {
  return String(value || '').trim().toLowerCase();
}

function readAccountSession() {
  const session = readJsonStorage(ACCOUNT_SESSION_KEY, null);
  return session?.email ? session : null;
}

function readAccountStore() {
  const store = readJsonStorage(ACCOUNT_STORE_KEY, {});
  return store && typeof store === 'object' && !Array.isArray(store) ? store : {};
}

function writeAccountStore(store) {
  localStorage.setItem(ACCOUNT_STORE_KEY, JSON.stringify(store || {}));
}

function accountPasswordDigest(password) {
  return btoa(unescape(encodeURIComponent(String(password || ''))));
}

function isBetaInviteAccepted(email, inviteCode) {
  const invitedEmails = readJsonStorage(ACCOUNT_INVITED_EMAILS_KEY, []);
  const configuredCode = localStorage.getItem('pq_beta_invite_code') || DEFAULT_BETA_INVITE_CODE;
  return invitedEmails.map(normalizeAccountEmail).includes(normalizeAccountEmail(email)) ||
    String(inviteCode || '').trim() === configuredCode;
}

function createAccountSession(account) {
  const session = {
    id: account.id,
    email: account.email,
    fullName: account.fullName,
    workspaceName: account.workspaceName,
    role: account.role || 'rep',
    provider: account.provider || 'local_beta',
    authMode: account.authMode || 'Local beta account',
    createdAt: account.createdAt,
    lastLoginAt: new Date().toISOString(),
  };
  localStorage.setItem(ACCOUNT_SESSION_KEY, JSON.stringify(session));
  return session;
}

function readQuotaValue(key, fallback) {
  const raw = localStorage.getItem(key);
  if (raw === null && key === QUARTERLY_QUOTA_KEY) {
    const legacy = parseInt(localStorage.getItem('pq_quota_target') || '', 10);
    if (Number.isFinite(legacy) && legacy > 0) return legacy;
  }
  const value = parseInt(raw || '', 10);
  return Number.isFinite(value) && value > 0 ? value : fallback;
}

function normalizeTargetTerm(value) {
  const term = String(value || '').trim().toLowerCase();
  return TARGET_TERM_OPTIONS.includes(term) ? term : 'quota';
}

function readTargetTerm() {
  return normalizeTargetTerm(localStorage.getItem(TARGET_TERM_KEY));
}

function targetTermLabel(term, { title = false, plural = false } = {}) {
  const normalized = normalizeTargetTerm(term);
  const word = plural ? normalized + 's' : normalized;
  return title ? word.replace(/^./, ch => ch.toUpperCase()) : word;
}

function readMeddicState() {
  try {
    const raw = JSON.parse(localStorage.getItem(MEDDIC_STORAGE_KEY) || '{}');
    if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
    return Object.fromEntries(Object.entries(raw).map(([dealId, value]) => {
      const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
      return [dealId, Object.fromEntries(Object.entries(source).filter(([, item]) => item === true || item === false))];
    }));
  } catch {
    return {};
  }
}

function normalizeSalesMethodology(value) {
  const raw = String(value || '').trim();
  return SALES_METHODOLOGY_OPTIONS.includes(raw) ? raw : 'meddic';
}

function readSalesMethodology() {
  return normalizeSalesMethodology(localStorage.getItem(SALES_METHODOLOGY_KEY));
}

function salesMethodologyConfig(value) {
  return SALES_METHODOLOGIES[normalizeSalesMethodology(value)] || SALES_METHODOLOGIES.meddic;
}

function normalizeMeddicValue(value, methodology = 'meddic') {
  const method = salesMethodologyConfig(methodology);
  const source = value && typeof value === 'object' ? value : {};
  return Object.fromEntries(method.fields.map(field => [field.key, source[field.key] === true]));
}

function meddicSummary(value, methodology = 'meddic') {
  const method = salesMethodologyConfig(methodology);
  const normalized = normalizeMeddicValue(value, method.key);
  const completed = method.fields.filter(field => normalized[field.key]).length;
  const missing = method.fields.filter(field => !normalized[field.key]);
  const weightedScore = Math.round(method.fields.reduce((sum, field) => (
    sum + (normalized[field.key] ? (field.weight || 0) : 0)
  ), 0));
  const completionRatio = method.fields.length ? completed / method.fields.length : 0;
  const tone = completionRatio >= 0.8 ? 'strong' : completionRatio >= 0.5 ? 'watch' : 'risk';
  return {
    method,
    value: normalized,
    completed,
    total: method.fields.length,
    weightedScore,
    label: method.summaryPrefix + ' ' + completed + '/' + method.fields.length,
    tone,
    nextGap: missing[0] || null,
  };
}

function normalizeTodayPillLayout(value, availableKeys = TODAY_PILL_KEYS) {
  const allowedKeys = Array.isArray(availableKeys) && availableKeys.length ? availableKeys : TODAY_PILL_KEYS;
  const source = value && typeof value === 'object' ? value : DEFAULT_TODAY_PILL_LAYOUT;
  const seen = new Set();
  const cleanRow = row => (Array.isArray(row) ? row : [])
    .filter(key => allowedKeys.includes(key))
    .filter(key => {
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  const collapsed = cleanRow(source.collapsed);
  let details = cleanRow(source.details);
  for (const key of allowedKeys) {
    if (!seen.has(key)) details.push(key);
  }
  while (collapsed.length > TODAY_PILL_MAX_VISIBLE) {
    details.unshift(collapsed.pop());
  }
  return { collapsed, details };
}

function normalizeFiscalStartMonth(value) {
  const month = Number(value);
  return Number.isInteger(month) && month >= 0 && month <= 11 ? month : 0;
}

function normalizeForecastScopeValue(value) {
  if (value === 'full') return 'year';
  return value === 'next' || value === 'year' || value === 'quarter' ? value : 'quarter';
}

function readFiscalStartMonth() {
  return normalizeFiscalStartMonth(localStorage.getItem(FISCAL_START_MONTH_KEY));
}

function fiscalYearEndMonth(startMonth) {
  return (normalizeFiscalStartMonth(startMonth) + 11) % 12;
}

function fiscalYearLabelForDate(date, startMonth) {
  const fiscalStart = normalizeFiscalStartMonth(startMonth);
  const year = date.getFullYear();
  return fiscalStart === 0 || date.getMonth() < fiscalStart ? year : year + 1;
}

function dateForMonthOffset(startYear, startMonth, monthOffset, day = 1) {
  return new Date(startYear, startMonth + monthOffset, day);
}

function fiscalQuarterRangeForIndex(startYear, startMonth, quarterIndex) {
  const start = dateForMonthOffset(startYear, startMonth, quarterIndex * 3, 1);
  const end = dateForMonthOffset(startYear, startMonth, quarterIndex * 3 + 3, 0);
  return { start, end };
}

function fiscalQuarterMonthSummary(startMonth) {
  const fiscalStart = normalizeFiscalStartMonth(startMonth);
  return [0, 1, 2, 3].map(idx => {
    const start = (fiscalStart + idx * 3) % 12;
    const end = (start + 2) % 12;
    return 'Q' + (idx + 1) + ' ' + MONTH_SHORT_NAMES[start] + '-' + MONTH_SHORT_NAMES[end];
  }).join(' · ');
}

function fiscalCalendarSummary(startMonth) {
  const fiscalStart = normalizeFiscalStartMonth(startMonth);
  return MONTH_SHORT_NAMES[fiscalStart] + ' start, ' + MONTH_SHORT_NAMES[fiscalYearEndMonth(fiscalStart)] + ' year-end';
}

function getFiscalPeriods(now = new Date(), startMonth = 0) {
  const fiscalStart = normalizeFiscalStartMonth(startMonth);
  const fyStartYear = now.getMonth() >= fiscalStart ? now.getFullYear() : now.getFullYear() - 1;
  const fiscalMonthOffset = (now.getMonth() - fiscalStart + 12) % 12;
  const quarterIndex = Math.floor(fiscalMonthOffset / 3);
  const fiscalYearLabel = fiscalYearLabelForDate(now, fiscalStart);
  const currentQuarter = fiscalQuarterRangeForIndex(fyStartYear, fiscalStart, quarterIndex);
  const nextQuarterIndex = (quarterIndex + 1) % 4;
  const nextQuarterYearOffset = quarterIndex === 3 ? 1 : 0;
  const nextQuarter = fiscalQuarterRangeForIndex(fyStartYear + nextQuarterYearOffset, fiscalStart, nextQuarterIndex);
  const nextQuarterYearLabel = quarterIndex === 3 ? fiscalYearLabel + 1 : fiscalYearLabel;
  return {
    quarterIndex,
    fiscalYearLabel,
    yearStart: dateForMonthOffset(fyStartYear, fiscalStart, 0, 1),
    yearEnd: dateForMonthOffset(fyStartYear, fiscalStart, 12, 0),
    currentQuarter: {
      ...currentQuarter,
      label: 'Q' + (quarterIndex + 1) + ' FY ' + fiscalYearLabel,
    },
    nextQuarter: {
      ...nextQuarter,
      label: 'Q' + (nextQuarterIndex + 1) + ' FY ' + nextQuarterYearLabel,
    },
  };
}

function cleanQuotaInput(value) {
  return String(value || '').replace(/[^0-9]/g, '');
}

function parseDealAmountInput(value) {
  const cleaned = cleanQuotaInput(value);
  if (!cleaned) return null;
  const amount = Number(cleaned);
  return Number.isFinite(amount) && amount >= 0 ? amount : null;
}

function readDevSetupState() {
  if (!IS_DEV_SETUP_ENV) return { complete: true, instanceUrl: '', orgName: '' };
  return {
    complete: localStorage.getItem(DEV_SETUP_COMPLETE_KEY) === '1',
    instanceUrl: localStorage.getItem(DEV_SETUP_INSTANCE_KEY) || '',
    orgName: localStorage.getItem(DEV_SETUP_ORG_KEY) || '',
  };
}

function clearPipelineQuestLocalState(options = {}) {
  const preserved = options.preserveAccount ? {
    session: localStorage.getItem(ACCOUNT_SESSION_KEY),
    store: localStorage.getItem(ACCOUNT_STORE_KEY),
    invited: localStorage.getItem(ACCOUNT_INVITED_EMAILS_KEY),
  } : null;
  Object.keys(localStorage)
    .filter(k => k.startsWith('pq_'))
    .forEach(k => localStorage.removeItem(k));
  if (preserved?.session) localStorage.setItem(ACCOUNT_SESSION_KEY, preserved.session);
  if (preserved?.store) localStorage.setItem(ACCOUNT_STORE_KEY, preserved.store);
  if (preserved?.invited) localStorage.setItem(ACCOUNT_INVITED_EMAILS_KEY, preserved.invited);
}

const STAGES = ['Prospect', 'Qualified', 'Proposal', 'Closed Won'];

const SF_STAGES = [
  { sf: 'Prospecting',       pq: 'Prospect'    },
  { sf: 'Discovery',         pq: 'Prospect'    },
  { sf: 'Value Proposition', pq: 'Qualified'   },
  { sf: 'Proposal',          pq: 'Proposal'    },
  { sf: 'Negotiation',       pq: 'Proposal'    },
  { sf: 'Agreed to Purchase',pq: 'Proposal'    },
  { sf: 'Closed Won',        pq: 'Closed Won'  },
  { sf: 'Closed Lost',       pq: 'Closed Lost' },
];

const PQ_STAGE_COLORS = {
  'Prospect':    '#6366f1',
  'Qualified':   '#0ea5e9',
  'Proposal':    '#9a5b00',
  'Closed Won':  '#22c55e',
  'Closed Lost': '#ef4444',
};
const SCORE_LABEL = 'Points';
const RISK_REMOVED_POINTS = 10;

const REVERT_STAGE_MAP = {
  'Prospect':    'Prospecting',
  'Qualified':   'Value Proposition',
  'Proposal':    'Proposal',
  'Closed Won':  'Closed Won',
  'Closed Lost': 'Closed Lost',
};

const SF_STAGE_TO_FORECAST = {
  'Prospecting':        'Pipeline',
  'Discovery':          'Pipeline',
  'Value Proposition':  'Pipeline',
  'Proposal':           'Best Case',
  'Negotiation':        'Best Case',
  'Agreed to Purchase': 'Commit',
  'Closed Won':         'Closed',
  'Closed Lost':        'Omitted',
};

function fmtMoney(n) {
  if (n >= 1e6) return '$' + (n / 1e6).toFixed(1) + 'M';
  if (n >= 1e3) return '$' + (n / 1e3).toFixed(0) + 'K';
  return '$' + n.toLocaleString();
}

function escapeRegExp(value) {
  return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function getDealAccountName(deal) {
  return String(deal?.accountName || deal?.account || deal?.contact || '').trim();
}

function formatDealTitle(deal) {
  const name = String(deal?.name || '').trim();
  const account = getDealAccountName(deal);
  if (!name || !account || name.toLowerCase() === account.toLowerCase()) return name;

  const escapedAccount = escapeRegExp(account);
  const withoutPrefix = name.replace(new RegExp(`^${escapedAccount}\\s*[-–—:|/]\\s*`, 'i'), '').trim();
  if (withoutPrefix && withoutPrefix.length < name.length) return withoutPrefix;

  const withoutSuffix = name.replace(new RegExp(`\\s*[-–—:|/]\\s*${escapedAccount}$`, 'i'), '').trim();
  return withoutSuffix || name;
}

function fmtCloseDate(dateStr) {
  if (!dateStr) return 'Close date TBD';
  const date = new Date(dateStr + 'T00:00:00');
  if (Number.isNaN(date.getTime())) return 'Close date TBD';
  return 'Close ' + date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}

function fmtCloseDatePill(dateStr) {
  if (!dateStr) return 'Close TBD';
  const date = new Date(dateStr + 'T00:00:00');
  if (Number.isNaN(date.getTime())) return 'Close TBD';
  const md = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
  return md + ' FY' + String(date.getFullYear()).slice(-2);
}

function closeDateTime(deal) {
  if (!deal?.closeDate) return Number.MAX_SAFE_INTEGER;
  const t = new Date(deal.closeDate + 'T00:00:00').getTime();
  return Number.isNaN(t) ? Number.MAX_SAFE_INTEGER : t;
}

function isPastCloseDate(deal) {
  const closeAt = closeDateTime(deal);
  if (closeAt === Number.MAX_SAFE_INTEGER) return false;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return closeAt < today.getTime();
}

function stageRank(stage) {
  const idx = STAGES.indexOf(stage);
  return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
}

function salesStageRank(deal) {
  const stage = deal?.sfStage || deal?.stage || '';
  const idx = SF_STAGES.findIndex(s => s.sf === stage || s.pq === stage);
  return idx === -1 ? -1 : idx;
}

const FC_RANK = { 'Commit': 0, 'Best Case': 1, 'Pipeline': 2, 'Omitted': 3, '': 4 };
function forecastRank(deal) {
  return FC_RANK[deal.forecastCategory] ?? 4;
}

function forecastPillStyle(deal) {
  const fc = deal.forecastCategory || '';
  if (fc === 'Commit') return { background: '#dcfce7', color: '#15803d', borderColor: '#86efac' };
  if (fc === 'Best Case') return { background: '#e0f2fe', color: '#0369a1', borderColor: '#bae6fd' };
  return { background: '#f3f4f6', color: '#6b7280', borderColor: '#e5e7eb' };
}

function sortTodayCards(cards, mode) {
  return [...cards].sort((a, b) => {
    if (mode === 'stage') {
      return stageRank(a.deal.stage) - stageRank(b.deal.stage) ||
        closeDateTime(a.deal) - closeDateTime(b.deal) ||
        b.score - a.score;
    }
    if (mode === 'forecast') {
      return forecastRank(a.deal) - forecastRank(b.deal) ||
        salesStageRank(b.deal) - salesStageRank(a.deal) ||
        closeDateTime(a.deal) - closeDateTime(b.deal) ||
        b.score - a.score;
    }
    return closeDateTime(a.deal) - closeDateTime(b.deal) ||
      b.score - a.score;
  });
}

// ── Daily Card Algorithm ──────────────────────────────────────────────────

const STAGE_WEIGHT = { 'Proposal': 40, 'Qualified': 25, 'Prospect': 10 };
const NEXT_ACTIVITY_POINTS = { Email: 4, Call: 10, Meeting: 20, Demo: 20, Proposal: 25 };

function daysSinceDate(dateStr) {
  if (!dateStr) return null;
  const t = new Date(dateStr).getTime();
  if (Number.isNaN(t)) return null;
  return Math.max(0, Math.floor((Date.now() - t) / 86400000));
}

function daysSinceLastActivity(dealName, acts) {
  const relevant = acts.filter(a => a.acct === dealName);
  if (!relevant.length) return null;
  let best = null;
  for (const a of relevant) {
    if (a.time) {
      const days = daysSinceDate(a.time);
      if (days !== null) best = best === null ? days : Math.min(best, days);
    }
  }
  return best;
}

function daysSinceLastTouch(deal, acts) {
  const candidates = [
    daysSinceLastActivity(deal.name, acts),
    daysSinceDate(deal.lastActivityDate),
    daysSinceDate(deal.lastModifiedDate),
  ].filter(v => v !== null);
  if (!candidates.length) return 30;
  return Math.min(...candidates);
}

function daysUntilClose(deal) {
  if (!deal?.closeDate) return null;
  const closeMs = new Date(deal.closeDate + 'T00:00:00').getTime();
  if (Number.isNaN(closeMs)) return null;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return Math.ceil((closeMs - today.getTime()) / 86400000);
}

function activitiesForDeal(deal, acts) {
  return (acts || [])
    .filter(a => a.dealId === deal?.id || a.acct === deal?.name)
    .map(a => ({ ...a, days: daysSinceDate(a.time) }))
    .sort((a, b) => (a.days ?? 9999) - (b.days ?? 9999));
}

function dealRiskReason(deal, acts, touchedDealIds) {
  if (!deal || deal.stage === 'Closed Won' || deal.stage === 'Closed Lost') return null;
  if (touchedDealIds?.has(deal.id)) return null;
  if (isPastCloseDate(deal)) return 'Past close date';
  if (daysSinceLastTouch(deal, acts) >= 7) return '7+ days since touch';
  return null;
}

function dealCloseScore(deal, acts, touchedDealIds = new Set(), options = {}) {
  if (!deal) return { score: 0, tone: 'weak', label: 'Weak' };
  if (deal.stage === 'Closed Won' || deal.sfStage === 'Closed Won') return { score: 100, tone: 'strong', label: 'Strong' };
  if (deal.stage === 'Closed Lost' || deal.sfStage === 'Closed Lost') return { score: 0, tone: 'weak', label: 'Weak' };

  const sfStage = deal.sfStage || deal.stage || 'Prospecting';
  const stageScores = {
    'Prospecting': 12,
    'Discovery': 25,
    'Value Proposition': 42,
    'Proposal': 58,
    'Negotiation': 74,
    'Agreed to Purchase': 84,
    'Prospect': 18,
    'Qualified': 40,
  };
  const forecastScores = { 'Commit': 88, 'Best Case': 70, 'Pipeline': 45, 'Omitted': 20 };
  const days = daysSinceLastTouch(deal, acts);
  const activityScore = days === 0 ? 100 : days <= 3 ? 85 : days <= 7 ? 65 : days <= 14 ? 35 : 15;
  const hasNext = !!(deal.nextStep || '').trim();
  const nextText = (deal.nextStep || '').toLowerCase();
  const specificNext = hasNext && (
    /\b(today|tomorrow|monday|tuesday|wednesday|thursday|friday|next week|[0-9]{1,2}\/[0-9]{1,2})\b/.test(nextText) ||
    /\b(call|email|meet|meeting|demo|proposal|review|send|follow up)\b/.test(nextText)
  );
  const nextStepScore = !hasNext ? 20 : specificNext ? 88 : 68;
  const closeMs = deal.closeDate ? new Date(deal.closeDate + 'T00:00:00').getTime() - Date.now() : null;
  const daysToClose = closeMs === null || Number.isNaN(closeMs) ? null : Math.ceil(closeMs / 86400000);
  const meddic = options.meddic || null;
  const includeMeddic = options.includeMeddic === true && meddic && meddic.total > 0;
  const meddicScore = includeMeddic ? Math.max(0, Math.min(100, meddic.weightedScore ?? Math.round((meddic.completed / meddic.total) * 100))) : null;
  let closeDateScore = 50;
  if (daysToClose !== null) {
    if (daysToClose < 0) closeDateScore = 15;
    else if (daysToClose <= 14) closeDateScore = 78;
    else if (daysToClose <= 30) closeDateScore = 75;
    else if (daysToClose <= 60) closeDateScore = 65;
    else if (daysToClose <= 90) closeDateScore = 55;
    else closeDateScore = 45;
  }

  const stageScore = stageScores[sfStage] ?? stageScores[deal.stage] ?? 18;
  const forecastScore = forecastScores[deal.forecastCategory] ?? 45;
  const weights = includeMeddic
    ? { stage: 0.30, forecast: 0.18, activity: 0.18, nextStep: 0.14, closeDate: 0.10, meddic: 0.10 }
    : { stage: 0.35, forecast: 0.20, activity: 0.20, nextStep: 0.15, closeDate: 0.10 };
  const contribution = (inputScore, weight) => Math.round(inputScore * weight);
  const inputs = [
    { key: 'stage', label: 'Stage', detail: sfStage, score: stageScore, weight: weights.stage },
    { key: 'forecast', label: 'Forecast', detail: deal.forecastCategory || 'Pipeline', score: forecastScore, weight: weights.forecast },
    { key: 'activity', label: 'Activity cadence', detail: days === 0 ? 'Touched today' : days + 'd since touch', score: activityScore, weight: weights.activity },
    { key: 'nextStep', label: 'Next step', detail: !hasNext ? 'Missing' : specificNext ? 'Specific' : 'Present', score: nextStepScore, weight: weights.nextStep },
    { key: 'closeDate', label: 'Close timing', detail: daysToClose === null ? 'No close date' : daysToClose < 0 ? 'Past due' : daysToClose + 'd to close', score: closeDateScore, weight: weights.closeDate },
    includeMeddic ? { key: 'meddic', label: meddic.method?.scoreLabel || 'MEDDIC', detail: meddic.completed + ' of ' + meddic.total + ' complete', score: meddicScore, weight: weights.meddic } : null,
  ].filter(Boolean).map(item => ({
    ...item,
    contribution: contribution(item.score, item.weight),
    maxContribution: Math.round(100 * item.weight),
    tone: item.score >= 70 ? 'raise' : item.score >= 50 ? 'steady' : 'lower',
  }));
  const adjustments = [];
  if (daysToClose !== null && daysToClose < 0) adjustments.push({ label: 'Past close date', points: -20, tone: 'lower' });
  if (days >= 14) adjustments.push({ label: '14+ days since touch', points: -22, tone: 'lower' });
  else if (days >= 7) adjustments.push({ label: '7+ days since touch', points: -12, tone: 'lower' });
  if (!hasNext) adjustments.push({ label: 'No next step', points: -12, tone: 'lower' });
  if (daysToClose !== null && daysToClose <= 14 && ['Prospecting', 'Discovery', 'Prospect'].includes(sfStage)) adjustments.push({ label: 'Close date soon for early stage', points: -12, tone: 'lower' });
  if (deal.forecastCategory === 'Commit' && days > 7) adjustments.push({ label: 'Commit deal is stale', points: -15, tone: 'lower' });
  if ((deal.value || 0) >= 100000 && days <= 7) adjustments.push({ label: 'Large deal with recent touch', points: 4, tone: 'raise' });
  if (touchedDealIds?.has(deal.id)) adjustments.push({ label: 'Worked today', points: 6, tone: 'raise' });

  let score = inputs.reduce((sum, item) => sum + item.score * item.weight, 0)
    + adjustments.reduce((sum, item) => sum + item.points, 0);

  const positives = [
    'Stage: ' + sfStage,
    'Forecast: ' + (deal.forecastCategory || 'Pipeline'),
    includeMeddic && meddicScore >= 60 ? (meddic.method?.scoreLabel || 'MEDDIC') + ' ' + meddic.completed + ' of ' + meddic.total + ' complete' : null,
    days === 0 ? 'Touched today' : days <= 7 ? 'Touched ' + days + ' days ago' : null,
    hasNext ? (specificNext ? 'Specific next step' : 'Next step exists') : null,
  ].filter(Boolean);
  const negatives = [
    daysToClose !== null && daysToClose < 0 ? 'Past close date' : null,
    days >= 14 ? '14+ days since touch' : days >= 7 ? '7+ days since touch' : null,
    !hasNext ? 'No next step' : null,
    includeMeddic && meddicScore < 60 ? (meddic.method?.scoreLabel || 'MEDDIC') + ' ' + meddic.completed + ' of ' + meddic.total + ' complete' : null,
    daysToClose !== null && daysToClose <= 14 && ['Prospecting', 'Discovery', 'Prospect'].includes(sfStage) ? 'Close date soon for early stage' : null,
    deal.forecastCategory === 'Commit' && days > 7 ? 'Commit deal is stale' : null,
  ].filter(Boolean);
  const rounded = Math.max(0, Math.min(100, Math.round(score)));
  const tone = rounded >= 80 ? 'strong' : rounded >= 60 ? 'likely' : rounded >= 40 ? 'uncertain' : 'weak';
  const label = rounded >= 80 ? 'Strong' : rounded >= 60 ? 'Likely' : rounded >= 40 ? 'Uncertain' : 'At Risk';
  const breakdown = {
    inputs,
    raises: [
      ...inputs.filter(item => item.score >= 60).map(item => ({
        label: item.label,
        detail: item.detail,
        points: item.contribution,
      })),
      ...adjustments.filter(item => item.points > 0),
    ].slice(0, 4),
    lowers: [
      ...inputs.filter(item => item.score < 60).map(item => ({
        label: item.label,
        detail: item.detail,
        points: -Math.max(1, Math.round((60 - item.score) * item.weight)),
      })),
      ...adjustments.filter(item => item.points < 0),
    ].slice(0, 4),
  };
  return { score: rounded, tone, label, positives, negatives, breakdown };
}

function recommendationActionKind(primaryFix) {
  if (primaryFix === 'nextStep') return 'nextStep';
  if (primaryFix === 'closeDate') return 'closeDate';
  if (primaryFix === 'meddic') return 'meddic';
  if (primaryFix === 'forecastBestCase' || primaryFix === 'commitReview') return 'stage';
  return 'log';
}

function inferActionType(text) {
  const t = (text || '').toLowerCase();
  if (/\b(demo|walkthrough|show)\b/.test(t))                                    return 'Demo';
  if (/\b(meeting|meet|review|workshop|schedule|calendar|sync)\b/.test(t))      return 'Meeting';
  if (/\b(proposal|quote|pricing|price|contract|order form)\b/.test(t))         return 'Proposal';
  if (/\b(email|send|share|forward|recap)\b/.test(t))                           return 'Email';
  if (/\b(call|phone|dial|follow up|follow-up|procurement|legal|signature)\b/.test(t)) return 'Call';
  return 'Call';
}

function nextStepReason(nextStep, deal, days) {
  const text = (nextStep || '').toLowerCase();
  if (days >= 14) return `${days} days since touch — use the committed next step to re-engage`;
  if (days >= 7) return `${days} days since touch — follow through on the committed next step`;
  if (/\b(procurement|legal|security|signature|contract|order form)\b/.test(text)) return 'Committed next step involves a closing blocker';
  if (/\b(demo|review|meeting|meet|schedule)\b/.test(text)) return 'Committed next step needs live discussion';
  if (deal.forecastCategory === 'Commit') return 'Commit deal has a committed next step to confirm';
  return 'Your committed next step for this deal';
}

function activityRecommendationLabel(action, deal, context) {
  const stage = deal.sfStage || deal.stage || '';
  const fc = deal.forecastCategory || '';
  if (context.pastClose) {
    if (action === 'Call') return 'Call to reset close path';
    if (action === 'Meeting') return 'Schedule close-date review';
  }
  if (context.stale) {
    if (action === 'Call') return 'Re-engagement call';
    if (action === 'Email') return 'Re-engagement email';
  }
  if (fc === 'Commit') {
    if (action === 'Call') return 'Confirm commit plan';
    if (action === 'Meeting') return 'Commit review meeting';
  }
  if (fc === 'Best Case') {
    if (action === 'Call') return 'Best Case follow-up';
    if (action === 'Meeting') return 'Review path to Commit';
  }
  if (stage === 'Negotiation' || stage === 'Agreed to Purchase') {
    if (action === 'Call') return 'Negotiation follow-up';
    if (action === 'Meeting') return 'Procurement review';
  }
  if (stage === 'Proposal') {
    if (action === 'Meeting') return 'Proposal review';
    if (action === 'Call') return 'Proposal follow-up';
    if (action === 'Email') return 'Proposal recap email';
  }
  if (stage === 'Value Proposition' || deal.stage === 'Qualified') {
    if (action === 'Demo') return 'Product demo';
    if (action === 'Meeting') return 'Deep-dive meeting';
    if (action === 'Call') return 'Qualification call';
  }
  if (action === 'Email') return 'Intro email';
  return 'Discovery call';
}

function buildNextActivity(deal, acts) {
  const days = daysSinceLastTouch(deal, acts);
  const daysToClose = daysUntilClose(deal);
  const nextStep = (deal.nextStep || '').trim();
  const recent = activitiesForDeal(deal, acts).slice(0, 3);
  const recentTypes = recent.map(a => a.type).filter(Boolean);
  const lastType = recentTypes[0] || '';
  const emailOnlyRecent = recentTypes.length >= 2 && recentTypes.slice(0, 2).every(t => t === 'Email');
  const pastClose = daysToClose !== null && daysToClose < 0;
  const stale = days >= 7;
  const veryStale = days >= 14;
  const highValue = (deal.value || 0) >= 100000;
  const sfStage = deal.sfStage || deal.stage || 'Prospecting';
  const forecast = deal.forecastCategory || 'Pipeline';

  if (nextStep) {
    const action = inferActionType(nextStep);
    return {
      action,
      label: nextStep,
      reason: nextStepReason(nextStep, deal, days),
      points: 15,
      isNextStep: true,
      source: 'next_step',
      activityFitScore: 100,
    };
  }

  const scores = {
    Email: 12,
    Call: 18,
    Meeting: 12,
    Demo: 6,
    Proposal: 4,
  };
  const reasons = [];

  const add = (type, points, reason) => {
    scores[type] += points;
    if (reason) reasons.push({ type, points, reason });
  };

  if (sfStage === 'Prospecting' || deal.stage === 'Prospect') {
    add('Email', 18, 'Early-stage deal needs a light first touch');
    add('Call', 12, 'Early-stage deal may need discovery outreach');
  } else if (sfStage === 'Discovery' || sfStage === 'Value Proposition' || deal.stage === 'Qualified') {
    add('Meeting', 20, 'Qualified deal needs live discovery');
    add('Demo', 18, 'Qualified deal may need product validation');
    add('Call', 14);
  } else if (sfStage === 'Proposal' || deal.stage === 'Proposal') {
    add('Meeting', 24, 'Proposal-stage deal needs review');
    add('Call', 22, 'Proposal-stage deal needs follow-up');
    add('Proposal', 14);
  } else if (sfStage === 'Negotiation' || sfStage === 'Agreed to Purchase') {
    add('Call', 30, 'Late-stage deal needs direct close-path follow-up');
    add('Meeting', 22);
    add('Email', 8);
  }

  if (forecast === 'Commit') {
    add('Call', 26, 'Commit deal should be actively confirmed');
    add('Meeting', 16);
  } else if (forecast === 'Best Case') {
    add('Call', 16, 'Best Case deal needs a path-to-commit check');
    add('Meeting', 12);
  } else if (forecast === 'Pipeline') {
    add('Email', 8);
    add('Call', 4);
  }

  if (pastClose) {
    add('Call', 45, 'Past close date needs immediate reset');
    add('Meeting', 20);
    add('Email', -10);
  } else if (daysToClose !== null && daysToClose <= 14) {
    add('Call', 26, 'Close date is within 14 days');
    add('Meeting', 18);
    if (sfStage === 'Proposal' || sfStage === 'Negotiation') add('Proposal', 10);
  } else if (daysToClose !== null && daysToClose <= 30) {
    add('Call', 12, 'Close date is within 30 days');
    add('Meeting', 10);
  }

  if (veryStale) {
    add('Call', 34, `${days} days since touch`);
    add('Meeting', 14);
    add('Email', -6);
  } else if (stale) {
    add('Call', 24, `${days} days since touch`);
    add('Email', 4);
  } else if (days === 0) {
    add('Email', 8, 'Touched today — use a lighter follow-up if needed');
    add('Call', -18);
    add('Meeting', -10);
  }

  if (highValue) {
    add('Call', 8, 'High-value deal deserves a direct touch');
    add('Meeting', 8);
  }
  if (emailOnlyRecent) add('Call', 18, 'Recent touches were email-only');
  if (lastType === 'Call' && days <= 3) add('Email', 14, 'Recent call can be followed with a written recap');
  if (lastType === 'Meeting' && days <= 3) add('Email', 12, 'Recent meeting needs a recap or next-step email');

  const action = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0];
  const bestReason = reasons
    .filter(r => r.type === action)
    .sort((a, b) => b.points - a.points)[0]?.reason;
  return {
    action,
    label: activityRecommendationLabel(action, deal, { pastClose, stale, veryStale, daysToClose }),
    reason: bestReason || 'Best next activity based on stage, forecast, timing, and recent touch history',
    points: NEXT_ACTIVITY_POINTS[action] || 10,
    isNextStep: false,
    source: 'next_activity',
    activityFitScore: scores[action],
  };
}

function buildDealRecommendation(deal, acts, options = {}) {
  if (!deal) {
    const fallbackActivity = options.nextActivity || {
      label: 'Log activity',
      reason: 'Confirm the sales activity before logging it',
      action: 'Call',
    };
    return {
      dealId: null,
      recommendationType: 'suggested_activity',
      recommendedAction: fallbackActivity.label,
      reason: fallbackActivity.reason,
      priority: 40,
      expectedImpact: 'Creates a confirmed sales activity.',
      primaryFix: 'logActivity',
    };
  }

  const nextActivity = options.nextActivity || buildNextActivity(deal, acts);
  const score = options.closeScore || dealCloseScore(deal, acts);
  const days = daysSinceLastTouch(deal, acts);
  const daysToClose = daysUntilClose(deal);
  const forecast = deal.forecastCategory || 'Pipeline';
  const sfStage = deal.sfStage || deal.stage || 'No stage';
  const hasNextStep = !!(deal.nextStep || '').trim();
  const meddic = options.meddic || null;
  const qualificationGap = meddic?.nextGap || null;

  const make = (recommendationType, recommendedAction, reason, priority, expectedImpact, primaryFix) => ({
    dealId: deal.id,
    recommendationType,
    recommendedAction,
    reason,
    priority,
    expectedImpact,
    primaryFix,
  });

  if (daysToClose !== null && daysToClose < 0 && forecast === 'Commit') {
    return make(
      'close_date_review',
      'Update close date',
      'Commit deal is past its close date',
      100,
      'Removes a forecast blocker before the forecast call.',
      'closeDate'
    );
  }

  if (forecast === 'Commit' && (score.score < 60 || days >= 7)) {
    return make(
      'commit_validation',
      'Review Commit',
      score.score < 60 ? 'Commit deal has a weak close score' : 'Commit deal is stale',
      92,
      'Validates whether this belongs in Commit or needs a reset.',
      'commitReview'
    );
  }

  if (!hasNextStep) {
    return make(
      'next_step_hygiene',
      'Add next step',
      'Deal needs a committed next action',
      86,
      'Gives the rep and forecast owner a clear next move.',
      'nextStep'
    );
  }

  if (days >= 7) {
    return make(
      'stale_activity',
      'Log call',
      `${days} days since last touch`,
      78,
      'Creates a confirmed touch and reduces stale pipeline risk.',
      'logActivity'
    );
  }

  if (score.score >= 80 && forecast === 'Pipeline') {
    return make(
      'forecast_mismatch',
      'Move to Best Case',
      'High-score deal is still marked Pipeline',
      72,
      'Aligns the forecast category with the deal signal.',
      'forecastBestCase'
    );
  }

  if (qualificationGap) {
    return make(
      'meddic_gap',
      'Review ' + (meddic.method?.pillLabel || 'qualification'),
      (meddic.method?.scoreLabel || 'Qualification') + ' is missing ' + qualificationGap.label,
      68,
      'Improves qualification quality on an active opportunity.',
      'meddic'
    );
  }

  return make(
    'suggested_activity',
    nextActivity.label,
    nextActivity.reason,
    48,
    `Confirms the best next ${nextActivity.action.toLowerCase()} for this ${sfStage} deal.`,
    'logActivity'
  );
}

function buildDailyCards(deals, acts, options = {}) {
  const cards = [];
  const touchedDealIds = options.touchedDealIds || new Set();
  const includeMeddic = options.includeMeddic === true;
  const meddicByDeal = options.meddicByDeal || {};
  const salesMethodology = normalizeSalesMethodology(options.salesMethodology);
  for (const deal of deals) {
    if (deal.stage === 'Closed Won') continue;
    const days   = daysSinceLastTouch(deal, acts);
    const valPts = Math.log10(Math.max(deal.value, 1)) * 5;
    const stageW = STAGE_WEIGHT[deal.stage] || 10;
    const nextActivity = buildNextActivity(deal, acts);
    const meddic = meddicSummary(meddicByDeal[String(deal.id)], salesMethodology);
    const closeScore = dealCloseScore(deal, acts, touchedDealIds, { includeMeddic, meddic });
    const recommendation = buildDealRecommendation(deal, acts, { nextActivity, closeScore, meddic: includeMeddic ? meddic : null });

    cards.push({
      id:         String(deal.id),
      deal,
      action:     nextActivity.action,
      label:      recommendation.recommendedAction,
      reason:     recommendation.reason,
      points:         nextActivity.points,
      isNextStep: nextActivity.isNextStep,
      source:     nextActivity.source,
      recommendation,
      score:      stageW + valPts + days * 0.5 + nextActivity.activityFitScore * 0.08,
    });
  }
  return cards.sort((a, b) =>
    closeDateTime(a.deal) - closeDateTime(b.deal) ||
    b.score - a.score
  );
}

function completedActionLabel(card) {
  if (!card) return 'Completed';
  if (card.completionKind === 'skip') return 'Skipped';
  const raw = String(card.actionLabel || card.action || card.label || '').trim();
  const lower = raw.toLowerCase();
  if (card.completionKind === 'nextStep' || card.isNextStep || /\bnext steps?\b/.test(lower)) {
    return /\bnext steps\b/.test(lower) ? 'Added Next Steps' : 'Added Next Step';
  }
  const action = card.action || (raw ? inferActionType(raw) : '');
  const completedLabels = {
    Call: 'Called',
    Email: 'Emailed',
    Meeting: 'Held Meeting',
    Demo: 'Demoed',
    Proposal: 'Sent Proposal',
  };
  if (completedLabels[action]) return completedLabels[action];
  if (/\bcall|phone|dial|follow[- ]?up\b/.test(lower)) return 'Called';
  if (/\bemail|send|sent|share|forward|recap\b/.test(lower)) return 'Emailed';
  if (/\bmeeting|meet|review|workshop|schedule|calendar|sync\b/.test(lower)) return 'Held Meeting';
  if (/\bdemo|walkthrough|show\b/.test(lower)) return 'Demoed';
  if (/\bproposal|quote|pricing|contract|order form\b/.test(lower)) return 'Sent Proposal';
  if (raw) return raw;
  return 'Completed';
}

function todayClearSummary(cards = [], nextStepIds = [], stageIds = []) {
  const countBy = (predicate) => cards.filter(predicate).length;
  const actionCount = (type) => countBy(card => String(card.action || '').toLowerCase() === type.toLowerCase());
  const nextStepCount = Math.max(
    nextStepIds.length,
    countBy(card => card.completionKind === 'nextStep' || /next steps?/i.test(String(card.actionLabel || card.label || '')))
  );
  const stageCount = stageIds.length;
  const dealCount = new Set(cards.map(card => String(card.deal?.id || card.id)).filter(Boolean)).size;
  const loggedCount = countBy(card => card.completionKind !== 'skip');
  const items = [
    { key: 'deals', label: dealCount === 1 ? 'Deal touched' : 'Deals touched', value: dealCount, tone: 'blue' },
    { key: 'next', label: nextStepCount === 1 ? 'Next step updated' : 'Next steps updated', value: nextStepCount, tone: 'green' },
    { key: 'calls', label: actionCount('Call') === 1 ? 'Call logged' : 'Calls logged', value: actionCount('Call'), tone: 'blue' },
    { key: 'meetings', label: actionCount('Meeting') === 1 ? 'Meeting logged' : 'Meetings logged', value: actionCount('Meeting'), tone: 'purple' },
    { key: 'emails', label: actionCount('Email') === 1 ? 'Email logged' : 'Emails logged', value: actionCount('Email'), tone: 'amber' },
    { key: 'stage', label: stageCount === 1 ? 'Stage advanced' : 'Stages advanced', value: stageCount, tone: 'green' },
    { key: 'logged', label: loggedCount === 1 ? 'Activity completed' : 'Activities completed', value: loggedCount, tone: 'blue' },
  ].filter(item => item.value > 0);
  return items.slice(0, 4);
}

// ── Gamification Constants ────────────────────────────────────────────────

const REP_TITLES = ['Rookie','Prospector','Deal Hunter','Closer','Pipeline Pro','Rainmaker','Top Gun','Elite Closer','Quota Crusher','Sales Legend'];

const ACT_TYPES = [
  { name: 'Email',    pts: 4  },
  { name: 'Call',     pts: 10 },
  { name: 'Meeting',  pts: 20 },
  { name: 'Demo',     pts: 20 },
  { name: 'Proposal', pts: 25 },
];
const LOG_ACTIVITY_TYPES = IS_ACTIVITY_FOUR_OPTIONS_ENABLED
  ? ACT_TYPES.filter(t => t.name !== 'Demo')
  : ACT_TYPES;

const LVL_POINTS = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900];

const INIT_DEALS = [
  { id: 1, name: 'TechCorp Inc',  value: 45000, stage: 'Prospect',  closeDate: '2026-06-30', contact: 'Sarah Chen' },
  { id: 2, name: 'StartupXYZ',    value: 12000, stage: 'Prospect',  closeDate: '2026-07-15', contact: 'Mike Ross'  },
  { id: 3, name: 'MegaRetail Co', value: 78000, stage: 'Qualified', closeDate: '2026-06-21', contact: 'Anna Park'  },
  { id: 4, name: 'CloudSystems',  value: 32000, stage: 'Proposal',  closeDate: '2026-05-31', contact: 'Tom Davis'  },
  { id: 5, name: 'FinanceHub',    value: 55000, stage: 'Qualified', closeDate: '2026-07-01', contact: 'Lisa Wong'  },
];

const ACHV = [
  // ── Activities ──────────────────────────────────────────────────────────
  { id: 'first',     name: 'First Blood',      desc: 'Log your first activity',          ico: '⚡', actType: 'lightning', type: 'stat',    stat: 'total',    target: 1,    pointsReward: 20,  rarity: 'common'   },
  { id: 'act10',     name: 'On a Roll',         desc: 'Log 10 activities',               ico: '⚡', actType: 'lightning', type: 'stat',    stat: 'total',    target: 10,   pointsReward: 50,  rarity: 'uncommon' },
  { id: 'act50',     name: 'Activity Machine',  desc: 'Log 50 activities',               ico: '⚡', actType: 'lightning', type: 'stat',    stat: 'total',    target: 50,   pointsReward: 150, rarity: 'rare'     },
  // ── Calls ───────────────────────────────────────────────────────────────
  { id: 'calls5',    name: 'Speed Dialer',      desc: 'Make 5 calls',                    ico: '📞', actType: 'Call',      type: 'stat',    stat: 'calls',    target: 5,    pointsReward: 30,  rarity: 'common'   },
  { id: 'calls20',   name: 'Call Warrior',      desc: 'Make 20 calls',                   ico: '📞', actType: 'Call',      type: 'stat',    stat: 'calls',    target: 20,   pointsReward: 80,  rarity: 'uncommon' },
  { id: 'calls50',   name: 'Phone Legend',      desc: 'Make 50 calls',                   ico: '📞', actType: 'Call',      type: 'stat',    stat: 'calls',    target: 50,   pointsReward: 200, rarity: 'rare'     },
  // ── Emails ──────────────────────────────────────────────────────────────
  { id: 'emails10',  name: 'Email Warrior',     desc: 'Send 10 emails',                  ico: '✉️',  actType: 'Email',     type: 'stat',    stat: 'emails',   target: 10,   pointsReward: 30,  rarity: 'common'   },
  { id: 'emails30',  name: 'Inbox Hero',        desc: 'Send 30 emails',                  ico: '✉️',  actType: 'Email',     type: 'stat',    stat: 'emails',   target: 30,   pointsReward: 80,  rarity: 'uncommon' },
  { id: 'emails100', name: 'Email Legend',      desc: 'Send 100 emails',                 ico: '✉️',  actType: 'Email',     type: 'stat',    stat: 'emails',   target: 100,  pointsReward: 200, rarity: 'rare'     },
  // ── Meetings ────────────────────────────────────────────────────────────
  { id: 'meets3',    name: 'Deal Maker',        desc: 'Hold 3 meetings',                 ico: '🤝', actType: 'Meeting',   type: 'stat',    stat: 'meetings', target: 3,    pointsReward: 40,  rarity: 'common'   },
  { id: 'meets10',   name: 'Meeting Master',    desc: 'Hold 10 meetings',                ico: '🤝', actType: 'Meeting',   type: 'stat',    stat: 'meetings', target: 10,   pointsReward: 100, rarity: 'uncommon' },
  { id: 'meets25',   name: 'Boardroom King',    desc: 'Hold 25 meetings',                ico: '🤝', actType: 'Meeting',   type: 'stat',    stat: 'meetings', target: 25,   pointsReward: 250, rarity: 'rare'     },
  // ── Stage Advances ──────────────────────────────────────────────────────
  { id: 'prop',      name: 'Deal Mover',        desc: 'Advance a deal to Proposal',      ico: '📈', actType: 'trending',  type: 'stage',   target: 'Proposal',    pointsReward: 50,  rarity: 'uncommon' },
  { id: 'closed',    name: 'The Closer',        desc: 'Win your first deal',             ico: '🏆', actType: 'trophy',    type: 'stage',   target: 'Closed Won',  pointsReward: 100, rarity: 'rare'     },
  { id: 'pipeline100', name: 'Pipeline Mover',  desc: 'Advance $100K in pipeline value', ico: '📈', actType: 'trending',  type: 'pipelineValue', target: 100000, pointsReward: 125, rarity: 'rare' },
  { id: 'velocity3', name: 'Velocity Champion', desc: 'Advance 3 deals',                 ico: '⚡', actType: 'lightning', type: 'stageCount', target: 3, pointsReward: 100, rarity: 'uncommon' },
  { id: 'deal100',   name: 'Deal Multiplier',   desc: 'Advance a $100K+ deal',           ico: '🎯', actType: 'target',    type: 'stageValue', target: 100000, pointsReward: 100, rarity: 'rare' },
  { id: 'forecast3', name: 'Forecast Master',   desc: 'Keep 3 advanced deals forecast-ready', ico: '🎯', actType: 'target', type: 'forecastReady', target: 3, pointsReward: 100, rarity: 'uncommon' },
  // ── Streaks ─────────────────────────────────────────────────────────────
  { id: 'streak3',   name: 'Hot Streak',        desc: '3-day activity streak',           ico: '🔥', actType: 'flame',     type: 'streak',  target: 3,    pointsReward: 40,  rarity: 'common'   },
  { id: 'streak7',   name: 'Week Warrior',      desc: '7-day activity streak',           ico: '🔥', actType: 'flame',     type: 'streak',  target: 7,    pointsReward: 100, rarity: 'uncommon' },
  { id: 'streak30',  name: 'Unstoppable',       desc: '30-day activity streak',          ico: '🔥', actType: 'flame',     type: 'streak',  target: 30,   pointsReward: 500, rarity: 'rare'     },
  // ── Points Milestones ───────────────────────────────────────────────────
  { id: 'xp100',     name: 'Centurion',         desc: 'Earn 100 Points',                 ico: '🎯', actType: 'target',    type: 'points',      target: 100,  pointsReward: 25,  rarity: 'common'   },
  { id: 'xp500',     name: 'High Flyer',        desc: 'Earn 500 Points',                 ico: '🎯', actType: 'target',    type: 'points',      target: 500,  pointsReward: 75,  rarity: 'uncommon' },
  { id: 'xp2000',    name: 'Points Legend',     desc: 'Earn 2,000 Points',               ico: '🎯', actType: 'target',    type: 'points',      target: 2000, pointsReward: 200, rarity: 'rare'     },
  // ── On Fire ─────────────────────────────────────────────────────────────
  { id: 'onfire',    name: 'On Fire',           desc: 'Log 3 activities in one session', ico: '🔥', actType: 'flame',     type: 'session', target: 3,    pointsReward: 50,  rarity: 'uncommon' },
];

const LEGACY_NAV = [
  { id: 'today',    lbl: 'Today',    icon: 'today' },
  { id: 'pipeline', lbl: 'Forecast', icon: 'forecast' },
];
const PROFESSIONAL_NAV = [
  { id: 'today',    lbl: 'Today',    icon: 'today' },
  { id: 'pipeline', lbl: 'Forecast', icon: 'forecast' },
  { id: 'account',  lbl: 'Profile',  icon: 'profile' },
];
const NAV = IS_PROFESSIONAL_IA_V2_ENABLED ? PROFESSIONAL_NAV : LEGACY_NAV;

// ── Helper Functions ──────────────────────────────────────────────────────

function getLevel(points) {
  let lv = 1;
  for (let i = LVL_POINTS.length - 1; i >= 0; i--) {
    if (points >= LVL_POINTS[i]) { lv = i + 1; break; }
  }
  return Math.min(lv, LVL_POINTS.length);
}

function pointsNext(points) {
  const lv = getLevel(points);
  return lv >= LVL_POINTS.length ? LVL_POINTS[LVL_POINTS.length - 1] : LVL_POINTS[lv];
}

function calcStats(acts) {
  return {
    total:    acts.length,
    calls:    acts.filter(a => a.type === 'Call').length,
    emails:   acts.filter(a => a.type === 'Email').length,
    meetings: acts.filter(a => a.type === 'Meeting').length,
  };
}

function calcStreak(acts, freezeUsedDates) {
  const today = appTodayDate();
  const days = new Set();
  for (const a of acts) {
    if (!a.time) continue;
    if (/^\d{4}-\d{2}-\d{2}$/.test(a.time)) days.add(a.time);
    else days.add(today);
  }
  if (freezeUsedDates) for (const fd of freezeUsedDates) days.add(fd);
  const d = new Date();
  if (!days.has(today)) d.setDate(d.getDate() - 1);
  let streak = 0;
  for (let i = 0; i < 365; i++) {
    const key = appTodayDate(d);
    if (days.has(key)) { streak++; d.setDate(d.getDate() - 1); }
    else break;
  }
  return streak;
}

function newAchievements(unlocked, points, acts, deals, streak, sessionCount, achievementStageDoneIds = []) {
  const s = calcStats(acts);
  const stageIdSet = new Set(achievementStageDoneIds);
  const advancedDeals = deals.filter(d => stageIdSet.has(d.id));
  return ACHV
    .filter(a => !unlocked.includes(a.id))
    .filter(a => {
      if (a.type === 'stat')    return s[a.stat] >= a.target;
      if (a.type === 'points')     return points >= a.target;
      if (a.type === 'streak')  return streak >= a.target;
      if (a.type === 'stage')   return deals.some(d => stageIdSet.has(d.id) && d.stage === a.target);
      if (a.type === 'stageCount') return stageIdSet.size >= a.target;
      if (a.type === 'pipelineValue') return advancedDeals.reduce((sum, d) => sum + (Number(d.value) || 0), 0) >= a.target;
      if (a.type === 'stageValue') return advancedDeals.some(d => (Number(d.value) || 0) >= a.target);
      if (a.type === 'forecastReady') return advancedDeals.filter(d => d.closeDate && (d.nextStep || '').trim() && !isPastCloseDate(d)).length >= a.target;
      if (a.type === 'session') return (sessionCount || 0) >= a.target;
      return false;
    })
    .map(a => a.id);
}

const SF_CACHE_KEY = 'pq_salesforce_cache_v1';

function readSalesforceCache() {
  try {
    const cached = JSON.parse(localStorage.getItem(SF_CACHE_KEY) || 'null');
    if (!cached || !Array.isArray(cached.deals) || !Array.isArray(cached.activities)) return null;
    if (IS_STAGING_ENV && cached.deals.some(deal => typeof deal?.id === 'number')) {
      localStorage.removeItem(SF_CACHE_KEY);
      return null;
    }
    return cached;
  } catch {
    return null;
  }
}

function writeSalesforceCache(deals, activities) {
  try {
    localStorage.setItem(SF_CACHE_KEY, JSON.stringify({
      deals,
      activities,
      savedAt: new Date().toISOString(),
    }));
  } catch {}
}

// ── Shared Icons / Helpers ────────────────────────────────────────────────

const ACT_ICONS = { Email: '✉️', Call: '📞', Meeting: '🤝', Demo: '💻', Proposal: '📄' };
const ACT_TAG_CLASS = { Email: 'email', Call: 'call', Meeting: 'meeting', Demo: 'demo', Proposal: 'proposal' };

function ActIcon({ type, size = 22 }) {
  const s = size;
  const c = { width: s, height: s, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' };
  if (type === 'Email') return (
    <svg {...c}><rect x="3" y="5" width="18" height="14" rx="2"/><polyline points="3,7 12,13 21,7"/></svg>
  );
  if (type === 'Call') return (
    <svg {...c}><path d="M22 16.92v3a2 2 0 01-2.18 2 19.8 19.8 0 01-8.63-3.07 19.5 19.5 0 01-6-6A19.8 19.8 0 012.12 4.18 2 2 0 014.11 2h3a2 2 0 012 1.72c.13.96.36 1.9.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0122 16.92z"/></svg>
  );
  if (type === 'Meeting') return (
    <svg {...c}><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
  );
  if (type === 'Demo') return (
    <svg {...c}><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><polygon points="10,8 16,11.5 10,15" fill="currentColor" stroke="none"/></svg>
  );
  if (type === 'Proposal') return (
    <svg {...c}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
  );
  if (type === 'lightning') return (
    <svg {...c}><polygon points="13,2 3,14 12,14 11,22 21,10 12,10" fill="currentColor" stroke="none"/></svg>
  );
  if (type === 'trophy') return (
    <svg {...c}><path d="M6 9H4a2 2 0 01-2-2V5h4"/><path d="M18 9h2a2 2 0 002-2V5h-4"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><path d="M4 5h16v6a8 8 0 01-16 0V5z"/></svg>
  );
  if (type === 'flame') return (
    <svg {...c}><path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 01-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"/></svg>
  );
  if (type === 'trending') return (
    <svg {...c}><polyline points="23,6 13.5,15.5 8.5,10.5 1,18"/><polyline points="17,6 23,6 23,12"/></svg>
  );
  if (type === 'target') return (
    <svg {...c}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
  );
  if (type === 'filter') return (
    <svg {...c}><line x1="4" y1="6" x2="20" y2="6"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="10" y1="18" x2="14" y2="18"/></svg>
  );
  if (type === 'mic') return (
    <svg {...c}>
      <path d="M12 14a3 3 0 003-3V5a3 3 0 00-6 0v6a3 3 0 003 3z" />
      <path d="M19 10v1a7 7 0 01-14 0v-1" />
      <line x1="12" y1="18" x2="12" y2="22" />
      <line x1="8" y1="22" x2="16" y2="22" />
    </svg>
  );
  return <span style={{ fontSize: s + 'px' }}>✓</span>;
}

function NavIcon({ type }) {
  const c = { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2.2', strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true' };
  if (type === 'forecast') return (
    <svg {...c}>
      <polyline points="4,16 9,11 13,14 20,7" />
      <polyline points="16,7 20,7 20,11" />
      <line x1="4" y1="20" x2="20" y2="20" />
    </svg>
  );
  if (type === 'trophy') return (
    <svg {...c}>
      <path d="M6 3h12v5a6 6 0 01-12 0V3z" />
      <path d="M18 4h2a2 2 0 010 4h-2" />
      <path d="M6 4H4a2 2 0 000 4h2" />
      <line x1="12" y1="14" x2="12" y2="18" />
      <line x1="8" y1="21" x2="16" y2="21" />
    </svg>
  );
  if (type === 'target') return (
    <svg {...c}>
      <circle cx="12" cy="12" r="9" />
      <circle cx="12" cy="12" r="5" />
      <circle cx="12" cy="12" r="1.8" fill="currentColor" stroke="none" />
    </svg>
  );
  if (type === 'profile') return (
    <svg {...c}>
      <circle cx="12" cy="8" r="4" />
      <path d="M5 21a7 7 0 0114 0" />
    </svg>
  );
  return (
    <svg {...c}>
      <line x1="5" y1="7" x2="19" y2="7" />
      <line x1="5" y1="12" x2="19" y2="12" />
      <line x1="5" y1="17" x2="15" y2="17" />
    </svg>
  );
}

function rowInitials(name) {
  return (name || '').split(/\s+/).slice(0, 2).map(w => w[0] || '').join('').toUpperCase() || '??';
}

function rowAvatarClass(card, daysSince, isTouched) {
  const cls = [];
  cls.push(ACT_TAG_CLASS[card.action] || 'call');
  if (card.isNextStep) cls.push('next-ready');
  if (!isTouched && (isPastCloseDate(card.deal) || daysSince >= 7)) cls.push('at-risk', 'risk-ready');
  return cls.join(' ');
}

function rowActivityIcon(card) {
  return <ActIcon type={card.action} size={24} />;
}

function rowStageClass(stage) {
  const key = (stage || '').toLowerCase().replace(/\s+/g, '-');
  return 'row-stage ' + key;
}

function hapticTick(pattern = 12) {
  if (navigator.vibrate) navigator.vibrate(pattern);
}

// ── SwipeRow ──────────────────────────────────────────────────────────────

function SwipeRow({ card, onComplete, onSkip, onOpenActivity, onOpenStage, onOpenNextStep, onOpenCloseDate, onOpenAmount, onOpenMeddic, onForceDoneComplete, daysSince, closeScore, meddic, meddicEnabled = false, salesMethodology = 'meddic', done, forceDone, onRef, isChanged, isTouched, skipCount = 0, nextStepUpdated = false, stageUpdated = false, updatedPill = null, tourTarget = false }) {
  const [deltaX, setDeltaX] = useState(0);
  const [flyDir, setFlyDir] = useState(null);
  const [collapsing, setCollapsing] = useState(false);
  const [scoreSheetOpen, setScoreSheetOpen] = useState(false);

  const [detailsOpen, setDetailsOpen] = useState(false);

  const rowRef   = useRef(null);
  const startX   = useRef(0);
  const startY   = useRef(0);
  const isDown   = useRef(false);
  const curDeltaX = useRef(0);
  const dragAxis  = useRef(null);
  const movedRef  = useRef(false);
  const finishRef = useRef(false);
  const forceRef  = useRef(false);
  const onCompleteRef = useRef(onComplete);
  const onSkipRef     = useRef(onSkip);
  const onOpenRef     = useRef(onOpenActivity);
  const onOpenStageRef = useRef(onOpenStage);
  const onOpenCloseDateRef = useRef(onOpenCloseDate);
  const onOpenAmountRef = useRef(onOpenAmount);
  const onOpenMeddicRef = useRef(onOpenMeddic);
  const onForceDoneRef = useRef(onForceDoneComplete);
  const cardRef       = useRef(card);
  const longPressedRef = useRef(false);
  useEffect(() => { onCompleteRef.current = onComplete; }, [onComplete]);
  useEffect(() => { onSkipRef.current = onSkip; }, [onSkip]);
  useEffect(() => { onOpenRef.current = onOpenActivity; }, [onOpenActivity]);
  useEffect(() => { onOpenStageRef.current = onOpenStage; }, [onOpenStage]);
  useEffect(() => { onOpenCloseDateRef.current = onOpenCloseDate; }, [onOpenCloseDate]);
  useEffect(() => { onOpenAmountRef.current = onOpenAmount; }, [onOpenAmount]);
  useEffect(() => { onOpenMeddicRef.current = onOpenMeddic; }, [onOpenMeddic]);
  useEffect(() => { onForceDoneRef.current = onForceDoneComplete; }, [onForceDoneComplete]);
  useEffect(() => { cardRef.current = card; }, [card]);
  useEffect(() => {
    document.body.classList.toggle('pq-score-sheet-open', scoreSheetOpen);
    return () => document.body.classList.remove('pq-score-sheet-open');
  }, [scoreSheetOpen]);


  function commit() {
    const d = curDeltaX.current;
    if (d > 80) {
      setDetailsOpen(false);
      setFlyDir('right');
    }
    else if (d < -80) setFlyDir('left');
    else { curDeltaX.current = 0; setDeltaX(0); }
  }

  useEffect(() => {
    const el = rowRef.current;
    if (!el) return;
    function start(x, y) {
      startX.current = x;
      startY.current = y;
      isDown.current = true;
      dragAxis.current = null;
      movedRef.current = false;
      longPressedRef.current = false;
    }
    function move(x, y, e) {
      if (!isDown.current) return;
      const dx = x - startX.current;
      const dy = y - startY.current;
      if (!dragAxis.current && (Math.abs(dx) > 6 || Math.abs(dy) > 6)) {
        dragAxis.current = Math.abs(dx) > Math.abs(dy) * 1.15 ? 'x' : 'y';
        movedRef.current = true;
      }
      if (dragAxis.current === 'y') return;
      if (dragAxis.current !== 'x') return;
      e?.preventDefault?.();
      movedRef.current = true;
      curDeltaX.current = dx;
      setDeltaX(dx);
    }
    function end() {
      if (!isDown.current) return;
      isDown.current = false;
      if (longPressedRef.current) {
        curDeltaX.current = 0;
        setDeltaX(0);
        dragAxis.current = null;
        return;
      }
      if (dragAxis.current === 'x') commit();
      else { curDeltaX.current = 0; setDeltaX(0); }
      dragAxis.current = null;
    }
    function onTS(e) { start(e.touches[0].clientX, e.touches[0].clientY); }
    function onTM(e) {
      if (!isDown.current) return;
      move(e.touches[0].clientX, e.touches[0].clientY, e);
    }
    function onTE() { end(); }
    function onMD(e) { start(e.clientX, e.clientY); }
    function onMM(e) {
      if (!isDown.current) return;
      move(e.clientX, e.clientY, e);
    }
    function onMU() { end(); }
    el.addEventListener('touchstart', onTS, { passive: true });
    el.addEventListener('touchmove',  onTM, { passive: false });
    el.addEventListener('touchend',   onTE, { passive: true });
    el.addEventListener('mousedown',  onMD);
    window.addEventListener('mousemove', onMM);
    window.addEventListener('mouseup',   onMU);
    return () => {
      el.removeEventListener('touchstart', onTS);
      el.removeEventListener('touchmove',  onTM);
      el.removeEventListener('touchend',   onTE);
      el.removeEventListener('mousedown',  onMD);
      window.removeEventListener('mousemove', onMM);
      window.removeEventListener('mouseup',   onMU);
    };
  }, [flyDir]);

  useEffect(() => {
    if (!flyDir || finishRef.current) return;
    finishRef.current = true;
    const collapseTimer = setTimeout(() => setCollapsing(true), 300);
    const finishTimer = setTimeout(() => {
      if (forceRef.current) {
        onForceDoneRef.current?.(cardRef.current);
        return;
      }
      if (flyDir === 'right') {
        onCompleteRef.current(cardRef.current);
      }
      if (flyDir === 'left')  onSkipRef.current(cardRef.current);
    }, 620);
    return () => {
      clearTimeout(collapseTimer);
      clearTimeout(finishTimer);
    };
  }, [flyDir]);

  useEffect(() => {
    if (!forceDone || flyDir || finishRef.current) return;
    forceRef.current = true;
    curDeltaX.current = 92;
    setDeltaX(92);
    const t = setTimeout(() => setFlyDir('right'), 40);
    return () => clearTimeout(t);
  }, [forceDone, flyDir]);

  function openActivityMenu(e) {
    e.stopPropagation();
    if (movedRef.current || dragAxis.current === 'x' || Math.abs(curDeltaX.current) > 6 || flyDir || finishRef.current) {
      e.preventDefault();
      return;
    }
    hapticTick(8);
    onOpenRef.current?.(cardRef.current);
  }

  function openStageMenu(e, sourcePill = 'stage') {
    e.stopPropagation();
    longPressedRef.current = true;
    movedRef.current = true;
    hapticTick(12);
    onOpenStageRef.current?.({ ...cardRef.current, sourcePill });
    setTimeout(() => {
      longPressedRef.current = false;
      movedRef.current = false;
    }, 0);
  }

  function openNextStep(e) {
    e.stopPropagation();
    longPressedRef.current = true;
    movedRef.current = true;
    hapticTick(8);
    if (ReactDOM.flushSync) {
      ReactDOM.flushSync(() => onOpenNextStep?.(card.deal));
    } else {
      onOpenNextStep?.(card.deal);
    }
    const input = document.querySelector('[data-next-step-input="true"]');
    if (input) {
      try { input.focus({ preventScroll: true }); } catch (err) { input.focus(); }
      input.scrollLeft = 0;
      input.scrollTop = 0;
      try { input.setSelectionRange(0, 0); } catch (err) {}
    }
    setTimeout(() => {
      longPressedRef.current = false;
      movedRef.current = false;
    }, 0);
  }

  function openCloseDate(e) {
    e.stopPropagation();
    hapticTick(8);
    onOpenCloseDateRef.current?.(card.deal);
  }

  function openAmount(e) {
    e.stopPropagation();
    hapticTick(8);
    onOpenAmountRef.current?.(card.deal);
  }

  function openMeddic(e) {
    e.stopPropagation();
    hapticTick(8);
    onOpenMeddicRef.current?.(card.deal);
  }

  function toggleDetails(e) {
    e.stopPropagation();
    hapticTick(6);
    setScoreSheetOpen(false);
    setDetailsOpen(prev => !prev);
  }

  function openScoreExplain(e) {
    e.stopPropagation();
    e.preventDefault();
    hapticTick(8);
    setDetailsOpen(false);
    setScoreSheetOpen(true);
  }

  function runScoreSheetAction(kind, e) {
    e?.stopPropagation?.();
    e?.preventDefault?.();
    setScoreSheetOpen(false);
    if (kind === 'nextStep') {
      onOpenNextStep?.(card.deal);
    } else if (kind === 'closeDate') {
      onOpenCloseDateRef.current?.(card.deal);
    } else if (kind === 'amount') {
      onOpenAmountRef.current?.(card.deal);
    } else if (kind === 'stage') {
      onOpenStageRef.current?.({ ...cardRef.current, sourcePill: 'stage' });
    } else if (kind === 'meddic') {
      onOpenMeddicRef.current?.(card.deal);
    } else {
      onOpenRef.current?.(cardRef.current);
    }
  }

  const exitX = flyDir === 'right' ? '118%' : flyDir === 'left' ? '-118%' : '0';
  const transform = flyDir
    ? `translateX(${exitX}) scale(0.985)`
    : `translateX(${deltaX}px)`;
  const transition = flyDir
    ? 'transform 0.38s cubic-bezier(0.16,1,0.3,1), opacity 0.26s ease'
    : deltaX === 0 ? 'transform 0.32s cubic-bezier(0.22,1,0.36,1)' : 'none';

  const avClass = rowAvatarClass(card, daysSince, isTouched);
  const activityIcon = rowActivityIcon(card);
  const hasNextStep = !!(card.deal.nextStep || '').trim();
  const nextStepLabel = hasNextStep ? '✓ Next Step Set' : '+ Add Next Step';
  const scorePillText = closeScore ? closeScore.score + '%' : '';
  const scorePillAria = closeScore ? closeScore.label + ' close score, ' + closeScore.score + ' percent. Tap to explain.' : '';
  const methodConfig = salesMethodologyConfig(salesMethodology);
  const meddicStatus = meddic || meddicSummary(null, methodConfig.key);
  const dealTitle = formatDealTitle(card.deal);
  const accountName = getDealAccountName(card.deal);
  const availablePillKeys = meddicEnabled ? TODAY_PILL_KEYS : TODAY_PILL_KEYS.filter(key => key !== 'meddic');
  const pillLayout = normalizeTodayPillLayout(DEFAULT_TODAY_PILL_LAYOUT, availablePillKeys);
  const rowRecommendation = card.recommendation || buildDealRecommendation(card.deal, [], { closeScore, meddic: meddicEnabled ? meddicStatus : null });
  const rowActionLabel = rowRecommendation?.recommendedAction || card.label;
  const rowReason = rowRecommendation?.reason || card.reason;
  const primaryFixKind = recommendationActionKind(rowRecommendation?.primaryFix);
  const scoreBreakdown = closeScore?.breakdown || { inputs: [], raises: [], lowers: [] };
  const updatedPillClass = kind => (
    updatedPill &&
    String(updatedPill.dealId) === String(card.deal.id) &&
    updatedPill.kind === kind
      ? ' pill-update-pop pill-update-pop-' + updatedPill.key
      : ''
  );

  function openRecommendationAction(e) {
    e?.stopPropagation?.();
    e?.preventDefault?.();
    if (primaryFixKind === 'log') {
      hapticTick(8);
      setScoreSheetOpen(false);
      onOpenRef.current?.(cardRef.current);
    } else {
      runScoreSheetAction(primaryFixKind, e);
    }
  }

  function renderTodayPill(kind) {
    if (kind === 'forecast') {
      return card.deal.forecastCategory ? (
        <button
          key={kind}
          className={'row-forecast-pill' + updatedPillClass('forecast')}
          style={forecastPillStyle(card.deal)}
          onClick={e => openStageMenu(e, 'forecast')}
          onPointerDown={e => e.stopPropagation()}
          onTouchStart={e => e.stopPropagation()}
          onMouseDown={e => e.stopPropagation()}
          title="Change sales stage"
          aria-label={'Change sales stage for ' + card.deal.name}
        >{card.deal.forecastCategory}</button>
      ) : null;
    }
    if (kind === 'amount') {
      return (
        <button
          type="button"
          key={kind}
          className={'row-forecast-amount' + updatedPillClass('amount')}
          onClick={openAmount}
          onPointerDown={e => e.stopPropagation()}
          onTouchStart={e => e.stopPropagation()}
          onMouseDown={e => e.stopPropagation()}
          title={fmtMoney(card.deal.value) + ' deal value'}
          aria-label={'Change deal amount for ' + card.deal.name + ', current value ' + fmtMoney(card.deal.value)}
          data-testid="amount-pill"
        >
          {fmtMoney(card.deal.value)}
        </button>
      );
    }
    if (kind === 'score') {
      return closeScore ? (
        <span key={kind} className="deal-score-wrap">
          <button
            className={'deal-score-pill ' + closeScore.tone}
            onClick={openScoreExplain}
            onPointerDown={e => e.stopPropagation()}
            onTouchStart={e => e.stopPropagation()}
            onMouseDown={e => e.stopPropagation()}
            onContextMenu={e => e.preventDefault()}
            title="Tap to explain score"
            aria-label={scorePillAria}
            aria-expanded={scoreSheetOpen}
            data-testid="score-pill"
          >
            <span>{scorePillText}</span>
          </button>
        </span>
      ) : null;
    }
    if (kind === 'stage') {
      return (
        <button
          key={kind}
          className={'row-stage-tap' + (stageUpdated ? ' done' : '') + updatedPillClass('stage')}
          onClick={e => openStageMenu(e, 'stage')}
          onPointerDown={e => e.stopPropagation()}
          onTouchStart={e => e.stopPropagation()}
          onMouseDown={e => e.stopPropagation()}
          title="Change sales stage"
          aria-label={'Change sales stage for ' + card.deal.name}
          data-testid="stage-pill"
        >
          <span>{card.deal.sfStage || card.deal.stage}</span>
        </button>
      );
    }
    if (kind === 'closeDate') {
      return (
        <button
          key={kind}
          className={'close-date-pill' + (isPastCloseDate(card.deal) ? ' past-due' : '') + updatedPillClass('closeDate')}
          onClick={openCloseDate}
          onTouchStart={e => e.stopPropagation()}
          onMouseDown={e => e.stopPropagation()}
          onPointerDown={e => e.stopPropagation()}
          title="Change close date"
          aria-label={'Change close date for ' + card.deal.name}
          data-testid="close-date-pill"
        >
          {fmtCloseDatePill(card.deal.closeDate)}
        </button>
      );
    }
    if (kind === 'meddic') {
      return meddicEnabled ? (
        <button
          key={kind}
          className={'deal-meddic-pill ' + meddicStatus.tone + updatedPillClass('meddic')}
          onClick={openMeddic}
          onPointerDown={e => e.stopPropagation()}
          onTouchStart={e => e.stopPropagation()}
          onMouseDown={e => e.stopPropagation()}
          title={meddicStatus.nextGap ? 'Next qualification gap: ' + meddicStatus.nextGap.label : methodConfig.label + ' complete'}
          aria-label={'Review ' + methodConfig.label + ' for ' + card.deal.name + ', ' + meddicStatus.completed + ' of ' + meddicStatus.total + ' complete'}
          data-testid="meddic-pill"
        >
          {methodConfig.pillLabel}
        </button>
      ) : null;
    }
    if (kind === 'nextStep') {
      return (
        <span key={kind} className="deal-score-wrap">
          <button
            className={'deal-next-pill ' + (hasNextStep ? 'has-next' : 'missing-next') + (nextStepUpdated ? ' done' : '') + updatedPillClass('nextStep')}
            onClick={openNextStep}
            onPointerDown={e => e.stopPropagation()}
            onTouchStart={e => e.stopPropagation()}
            onMouseDown={e => e.stopPropagation()}
            onContextMenu={e => e.preventDefault()}
            title={hasNextStep ? card.deal.nextStep : 'Add next step'}
            aria-label={'Add or update next step for ' + card.deal.name}
            data-testid="next-step-pill"
          >
            {nextStepLabel}
          </button>
        </span>
      );
    }
    return null;
  }
  if (done) {
    return (
      <div className={'swipe-row' + (collapsing ? ' exiting' : '')}>
        <div className="row-card done-dim">
          <div className={'row-avatar ' + avClass}>{activityIcon}</div>
          <div className="row-main">
            <div className="row-top">
              <div className="row-account">{dealTitle}</div>
            </div>
            <div className="row-task">{accountName || card.label + ' logged'}</div>
            <div className="row-meta">
              <span className="row-pill ns">Synced</span>
              <span className="row-pill">+{card.points} {SCORE_LABEL}</span>
            </div>
          </div>
          <div className="row-right">Done</div>
        </div>
      </div>
    );
  }

  return (
    <div ref={onRef} className={'swipe-row' + (IS_TODAY_ROW_SIMPLIFIED_ENABLED ? ' simplified' : '') + (collapsing ? ' exiting' : '') + (isChanged ? ' stage-moved' : '') + (detailsOpen ? ' expanded' : '')} data-tour-id={tourTarget ? 'today-first-row' : undefined}>
      <div className="action-bg">
        {(deltaX > 5 || flyDir === 'right') && <div className="done-bg">✓ {IS_TODAY_ROW_SIMPLIFIED_ENABLED ? 'Log Activity' : 'Done'}</div>}
        {(deltaX < -5 || flyDir === 'left')  && <div className="skip-bg">✕ Skip for today</div>}
      </div>
      <div
        ref={rowRef}
        className="row-card"
        style={{ transform, transition, opacity: flyDir ? 0.96 : 1 }}
      >
        <button
          className={'row-avatar row-avatar-btn ' + avClass}
          onClick={openActivityMenu}
          title="Log activity"
          aria-label={'Log activity for ' + card.deal.name}
          data-testid="activity-icon"
        >
          {activityIcon}
        </button>
          <div className="row-main">
            <div className="row-top">
              <div className="row-account">
                {dealTitle}
              </div>
            </div>
            <div className="row-account-meta">
              {accountName || 'Account unavailable'}
            </div>
            {!IS_TODAY_ROW_SIMPLIFIED_ENABLED && (
              <button
                type="button"
                className="row-recommendation row-recommendation-btn"
                onClick={openRecommendationAction}
                onPointerDown={e => e.stopPropagation()}
                onTouchStart={e => e.stopPropagation()}
                onMouseDown={e => e.stopPropagation()}
                title={rowReason}
                aria-label={rowActionLabel + ': ' + rowReason}
                data-testid="recommendation-row"
              >
                <span>{rowActionLabel}</span>
                <em>{rowReason}</em>
              </button>
            )}
            <div className="row-close" data-tour-id={tourTarget ? 'today-first-row-pills' : undefined}>
            {pillLayout.collapsed.map(renderTodayPill)}
            {renderTodayPill('score')}
            {renderTodayPill('amount')}
            <button
              className={'row-more-pill' + (detailsOpen ? ' open' : '')}
              onClick={toggleDetails}
              onPointerDown={e => e.stopPropagation()}
              onTouchStart={e => e.stopPropagation()}
              onMouseDown={e => e.stopPropagation()}
              title={detailsOpen ? 'Hide deal details' : 'Show deal details'}
              aria-label={(detailsOpen ? 'Hide' : 'Show') + ' deal details for ' + card.deal.name}
              aria-expanded={detailsOpen}
              data-testid="row-details-toggle"
            >
              <span className="row-more-icon" aria-hidden="true">
                <span className="row-more-icon-line horizontal" />
                <span className="row-more-icon-line vertical" />
              </span>
            </button>
          </div>
          <div className={'row-detail-expand' + (detailsOpen ? ' open' : '')} aria-hidden={!detailsOpen}>
            <div className="row-detail-expand-inner">
              <div className="row-detail-line today-pill-detail-line">
                {pillLayout.details.map(renderTodayPill)}
              </div>
            </div>
          </div>
        </div>
      </div>
      {scoreSheetOpen && closeScore && (
        <div className="score-sheet-backdrop" onClick={() => setScoreSheetOpen(false)} data-testid="score-sheet">
          <div className="activity-sheet score-sheet" onClick={e => e.stopPropagation()}>
            <div className="activity-sheet-handle" />
            <div className="score-sheet-head">
              <div className="score-sheet-title-wrap">
                <div className="activity-sheet-title score-sheet-title">{card.deal.name}</div>
                <div className="activity-sheet-sub score-sheet-account">{accountName || 'Account unavailable'}</div>
                <div className="score-sheet-meta-row">
                  <span>{card.deal.forecastCategory || 'No forecast'}</span>
                  <span>{card.deal.sfStage || card.deal.stage || 'No stage'}</span>
                  <span>{fmtMoney(card.deal.value)}</span>
                </div>
              </div>
              <div className={'deal-score-pill score-sheet-percent ' + closeScore.tone}>{scorePillText}</div>
            </div>
            <button type="button" className="score-increase-btn" onClick={openRecommendationAction}>Improve my close score</button>
            {IS_SCORE_BREAKDOWN_GRAPH_ENABLED ? (
              <div className="score-breakdown" data-testid="score-breakdown">
                <div className="score-breakdown-rows">
                  {scoreBreakdown.inputs.map(item => (
                    <div key={item.key} className={'score-breakdown-row ' + item.tone}>
                      <div className="score-breakdown-copy">
                        <span>{item.label}</span>
                        <strong>{item.detail}</strong>
                      </div>
                      <div className="score-breakdown-meter" aria-hidden="true">
                        <span style={{ width: Math.max(4, item.score) + '%' }} />
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            ) : (
              <div className="stage-sheet-list score-sheet-actions">
                <button type="button" className="stage-sheet-btn score-sheet-action" onClick={e => runScoreSheetAction('closeDate', e)}>
                  <span>Update Close Date</span>
                  <strong>{isPastCloseDate(card.deal) ? 'Past due' : 'On track'}</strong>
                </button>
                <button type="button" className="stage-sheet-btn score-sheet-action" onClick={e => runScoreSheetAction('nextStep', e)}>
                  <span>Add or Edit Next Step</span>
                  <strong>{hasNextStep ? 'Set' : 'Missing'}</strong>
                </button>
                <button type="button" className="stage-sheet-btn score-sheet-action" onClick={e => runScoreSheetAction('stage', e)}>
                  <span>Change Stage</span>
                  <strong>{card.deal.sfStage || card.deal.stage || 'No stage'}</strong>
                </button>
                {meddicEnabled && (
                  <button type="button" className="stage-sheet-btn score-sheet-action" onClick={e => runScoreSheetAction('meddic', e)}>
                    <span>Review {methodConfig.pillLabel}</span>
                    <strong>{meddicStatus.label}</strong>
                  </button>
                )}
                <button type="button" className="stage-sheet-btn score-sheet-action" onClick={e => runScoreSheetAction('log', e)}>
                  <span>Log Activity</span>
                  <strong>Call, Email, Meeting</strong>
                </button>
              </div>
            )}
            <button className="activity-sheet-cancel" onClick={() => setScoreSheetOpen(false)}>Cancel</button>
          </div>
        </div>
      )}

    </div>
  );
}

// ── TodayScreen ───────────────────────────────────────────────────────────

function TodayCompletedDrawer({ cards, onRestore }) {
  const [open, setOpen] = useState(false);
  if (!cards.length) return null;
  return (
    <div className={'today-completed-box' + (IS_TODAY_ROW_SIMPLIFIED_ENABLED ? ' simplified' : '') + (IS_TODAY_INLINE_UNDO_ENABLED ? ' inline-undo' : '')} data-testid="today-completed-box">
      <button
        className="today-completed-toggle"
        type="button"
        onClick={() => setOpen(o => !o)}
        aria-expanded={open}
        aria-label={open ? 'Hide completed activities today' : 'Show completed activities today'}
      >
        <span className="today-completed-label">Completed</span>
        <span className={'today-completed-chev' + (open ? ' open' : '')} aria-hidden="true"></span>
      </button>
      {open && (
        <div className="today-completed-list">
          {cards.map(card => (
            <div
              key={card.id}
              className="today-completed-row"
              onClick={() => onRestore(card)}
              title="Move back to today's list"
            >
              <span className="today-completed-main">
                <span className="today-completed-name">{card.deal.name}</span>
                <span className="today-completed-meta">{card.actionLabel || card.label || card.action}</span>
              </span>
              {IS_TODAY_INLINE_UNDO_ENABLED && (
                <button
                  className="today-completed-undo"
                  type="button"
                  onClick={e => {
                    e.stopPropagation();
                    onRestore(card);
                  }}
                  aria-label={'Undo completion for ' + card.deal.name}
                  data-testid="completed-row-undo"
                >
                  Undo
                </button>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function TodayTourSwipePreview({ card, closeScore }) {
  if (!card?.deal) return null;
  const deal = card.deal;
  const avClass = rowAvatarClass(card);
  const activityIcon = rowActivityIcon(card);
  const dealTitle = formatDealTitle(deal);
  const accountName = getDealAccountName(deal);
  const stageText = deal.forecastCategory || deal.sfStage || deal.stage || 'Pipeline';
  const score = closeScore || dealCloseScore(deal, []);

  return (
    <div className="today-tour-real-demo" aria-hidden="true">
      <div className="today-tour-real-lane today-tour-real-lane-left">Skip</div>
      <div className="today-tour-real-lane today-tour-real-lane-right">Log</div>
      <div className="today-tour-real-card">
        <span className={'row-avatar ' + avClass}>{activityIcon}</span>
        <span className="today-tour-real-main">
          <span className="today-tour-real-title">{dealTitle}</span>
          <span className="today-tour-real-account">{accountName || 'Account unavailable'}</span>
          <span className="today-tour-real-pills">
            <span className="row-forecast-pill" style={forecastPillStyle(deal)}>{stageText}</span>
            <span className={'deal-score-pill ' + score.tone}><span>{score.score}%</span></span>
            <span className="row-forecast-amount">{fmtMoney(deal.value)}</span>
          </span>
        </span>
      </div>
    </div>
  );
}

function TodayNavigationTour({ hasTodayRows = false, previewCard = null, previewCloseScore = null }) {
  const [visible, setVisible] = useState(() => localStorage.getItem(TODAY_NAV_TOUR_SEEN_KEY) !== '1');
  const [stepIndex, setStepIndex] = useState(0);
  const [target, setTarget] = useState(null);
  const steps = hasTodayRows ? [
    {
      id: 'row',
      selector: '[data-tour-id="today-first-row"]',
      title: 'Work one deal at a time',
      noteClass: 'row',
    },
    {
      id: 'swipe',
      selector: '[data-tour-id="today-first-row"]',
      title: 'Swipe the deal card',
      noteClass: 'swipe',
      showSwipeDemo: true,
    },
    {
      id: 'pills',
      selector: '[data-tour-id="today-first-row-pills"]',
      title: 'Read the signals',
      noteClass: 'pills',
    },
    {
      id: 'nav',
      selector: '[data-tour-id="bottom-nav"]',
      title: 'Move between views',
      noteClass: 'nav',
    },
  ] : [
    {
      id: 'nav',
      selector: '[data-tour-id="bottom-nav"]',
      title: 'Move between views',
      noteClass: 'nav',
    },
  ];
  const step = steps[Math.min(stepIndex, steps.length - 1)];

  useEffect(() => {
    if (stepIndex >= steps.length) setStepIndex(Math.max(0, steps.length - 1));
  }, [steps.length, stepIndex]);

  useLayoutEffect(() => {
    if (!visible || !step) return;
    let raf = 0;
    function measure() {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const phone = document.querySelector('.phone');
        const el = document.querySelector(step.selector);
        if (!phone || !el) {
          setTarget(null);
          return;
        }
        const phoneRect = phone.getBoundingClientRect();
        const rect = el.getBoundingClientRect();
        const pad = step.id === 'nav' ? 6 : 8;
        setTarget({
          left: Math.max(8, rect.left - phoneRect.left - pad),
          top: Math.max(8, rect.top - phoneRect.top - pad),
          width: Math.min(phoneRect.width - 16, rect.width + pad * 2),
          height: Math.min(phoneRect.height - 16, rect.height + pad * 2),
        });
      });
    }
    measure();
    const scroller = document.querySelector('.today-content');
    window.addEventListener('resize', measure);
    scroller?.addEventListener('scroll', measure, { passive: true });
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', measure);
      scroller?.removeEventListener('scroll', measure);
    };
  }, [visible, step?.id, step?.selector]);

  useEffect(() => {
    const phone = document.querySelector('.phone');
    const active = visible && step?.id === 'swipe';
    phone?.classList.toggle('today-tour-swipe-active', active);
    return () => phone?.classList.remove('today-tour-swipe-active');
  }, [visible, step?.id]);

  if (!visible || !step) return null;

  function finish() {
    localStorage.setItem(TODAY_NAV_TOUR_SEEN_KEY, '1');
    setVisible(false);
  }

  function next() {
    if (stepIndex >= steps.length - 1) finish();
    else setStepIndex(i => i + 1);
  }

  function arrowStyle() {
    if (!target) return {};
    const arrowHeadX = 156;
    if (step.id === 'pills') {
      return {
        left: Math.max(14, target.left - 126),
        top: Math.max(76, target.top - 48),
      };
    }
    if (step.id === 'nav') {
      return {
        left: Math.max(18, Math.min(target.left + target.width / 2 - 78, 196)),
        top: Math.max(96, target.top - 92),
      };
    }
    return {
      left: Math.max(14, target.left + target.width / 2 - arrowHeadX),
      top: Math.max(74, target.top - 58),
    };
  }

  function noteStyle() {
    if (!target) return {};
    if (step.id === 'pills') {
      return {
        left: Math.max(18, Math.min(target.left - 136, 142)),
        top: Math.max(98, target.top - 92),
      };
    }
    if (step.id === 'nav') {
      return {
        left: 24,
        top: Math.max(120, target.top - 136),
      };
    }
    return {
      left: Math.max(18, target.left + 8),
      top: Math.max(92, target.top - 106),
    };
  }

  return (
    <div className={'today-tour' + (step.id === 'swipe' ? ' showing-swipe' : '')} data-testid="today-navigation-tour" role="dialog" aria-label="Today navigation tour">
      <div className="today-tour-shade" />
      {target && <div className="today-tour-spotlight" style={{ left: target.left, top: target.top, width: target.width, height: target.height }} />}
      <svg className={'today-tour-arrow ' + step.noteClass} style={arrowStyle()} viewBox="0 0 180 80" aria-hidden="true">
        <path d="M14 50C54 18 102 17 148 42" />
        <polyline points="132,27 156,47 126,55" />
      </svg>
      <div className={'today-tour-note ' + step.noteClass} style={noteStyle()}>
        <div className="today-tour-note-title">{step.title}</div>
      </div>
      <div className="today-tour-panel">
        <div className="today-tour-panel-title">{step.title}</div>
        {step.showSwipeDemo && (
          <>
            <div className="today-tour-panel-subtitle">Right logs the activity. Left skips it for today.</div>
            <TodayTourSwipePreview card={previewCard} closeScore={previewCloseScore} />
          </>
        )}
        <div className="today-tour-panel-actions">
          <div className="today-tour-dots" aria-hidden="true">
            {steps.map((item, idx) => <span key={item.id + idx} className={idx === stepIndex ? 'on' : ''} />)}
          </div>
          <div className="today-tour-buttons">
            {stepIndex > 0 && <button type="button" className="today-tour-back" onClick={() => setStepIndex(i => Math.max(0, i - 1))}>Back</button>}
            <button type="button" className="today-tour-skip" onClick={finish}>Skip tour</button>
            <button type="button" className="today-tour-next" onClick={next}>{stepIndex >= steps.length - 1 ? 'Done' : 'Next'}</button>
          </div>
        </div>
      </div>
    </div>
  );
}

function TodayScreen({ activeCards, completedTodayCards = [], onRestoreCompleted, doneCards, onComplete, onSkip, onOpenActivity, onOpenStage, onOpenNextStep, onOpenCloseDate, onOpenAmount, onOpenMeddic, meddicByDeal = {}, meddicEnabled = false, includeMeddicCloseScore = false, salesMethodology = 'meddic', onReload, onForceDoneComplete, pendingDoneCardId, acts, onCardRef, changedCardId, touchedDealIds, updatedNextStepIds = [], updatedStageIds = [], updatedPill = null, skipCounts = {}, onRefresh }) {
  const scrollRef = useRef(null);
  usePullToRefresh(scrollRef, onRefresh || (() => {}));
  const clearSummary = todayClearSummary(completedTodayCards, updatedNextStepIds, updatedStageIds);
  const victoryConfetti = [
    ['12%', '9%', '#facc15', '18deg', '0s'],
    ['24%', '23%', '#fb923c', '-28deg', '0.12s'],
    ['36%', '12%', '#a78bfa', '9deg', '0.22s'],
    ['54%', '18%', '#38bdf8', '-18deg', '0.06s'],
    ['70%', '8%', '#22c55e', '24deg', '0.18s'],
    ['84%', '25%', '#60a5fa', '-34deg', '0.28s'],
    ['18%', '39%', '#22c55e', '31deg', '0.34s'],
    ['78%', '43%', '#f97316', '12deg', '0.4s'],
  ];
  return (
    <div ref={scrollRef} className="today-content">
      {activeCards.length === 0 && (
        <div className="today-all-done" data-testid="today-all-done">
          <div className="today-victory-confetti" aria-hidden="true">
            {victoryConfetti.map(([x, y, color, rot, delay], idx) => (
              <span key={idx} style={{ '--x': x, '--y': y, '--c': color, '--r': rot, '--d': delay }} />
            ))}
          </div>
          <div className="today-victory-burst" aria-hidden="true">
            <div className="today-victory-orbit one" />
            <div className="today-victory-orbit two" />
            <div className="today-victory-core">
              <strong>100%</strong>
              <span>cleared</span>
            </div>
          </div>
          <div className="today-all-done-kicker">Pipeline cleared</div>
          <div className="today-all-done-txt">Congratulations!</div>
          {clearSummary.length > 0 && (
            <div className="today-cleared-summary" data-testid="today-cleared-summary">
              {clearSummary.map(item => (
                <div key={item.key} className={'today-cleared-summary-item ' + (item.tone || 'blue')}>
                  <strong>{item.value}</strong>
                  <span>{item.label}</span>
                </div>
              ))}
            </div>
          )}
          <button type="button" className="today-all-done-reload" onClick={onReload}>
            Reload Current Deals
          </button>
        </div>
      )}

      {activeCards.length > 0 && (
        <>
          {activeCards.map((card, index) => (
            <SwipeRow
              key={card.id}
              card={card}
              tourTarget={index === 0}
              onComplete={onComplete}
              onSkip={onSkip}
              onOpenActivity={onOpenActivity}
              onOpenStage={onOpenStage}
              onOpenNextStep={onOpenNextStep}
              onOpenCloseDate={onOpenCloseDate}
              onOpenAmount={onOpenAmount}
              onOpenMeddic={onOpenMeddic}
              onForceDoneComplete={onForceDoneComplete}
              forceDone={pendingDoneCardId === card.id}
              daysSince={daysSinceLastTouch(card.deal, acts)}
              closeScore={dealCloseScore(card.deal, acts, touchedDealIds, { includeMeddic: includeMeddicCloseScore, meddic: meddicSummary(meddicByDeal[String(card.deal.id)], salesMethodology) })}
              meddic={meddicSummary(meddicByDeal[String(card.deal.id)], salesMethodology)}
              meddicEnabled={meddicEnabled}
              salesMethodology={salesMethodology}
              onRef={el => onCardRef && onCardRef(card.id, el)}
              isChanged={card.id === changedCardId}
              isTouched={touchedDealIds && touchedDealIds.has(card.deal.id)}
              skipCount={skipCounts[String(card.deal.id)] || 0}
              nextStepUpdated={updatedNextStepIds.includes(card.deal.id)}
              stageUpdated={updatedStageIds.includes(card.deal.id)}
              updatedPill={updatedPill}
            />
          ))}
        </>
      )}

      <TodayCompletedDrawer cards={completedTodayCards} onRestore={onRestoreCompleted || (() => {})} />

    </div>
  );
}

// ── Pipeline ──────────────────────────────────────────────────────────────

const FC_OPTIONS = ['Commit', 'Best Case', 'Pipeline', 'Omitted'];
const FC_COLORS  = { 'Commit': '#15803d', 'Best Case': '#176bb5', 'Pipeline': '#555870', 'Closed': '#b45309', 'Omitted': '#9ca3af', '': '#9ca3af' };

function SmoothExpand({ open, children }) {
  const innerRef = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    const el = innerRef.current;
    if (!el) return;
    const measure = () => setHeight(open ? el.scrollHeight : 0);
    measure();
    if (!window.ResizeObserver) return;
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [open, children]);

  return (
    <div className={'smooth-expand' + (open ? ' open' : '')} style={{ height: open ? height : 0 }}>
      <div ref={innerRef} className="smooth-expand-inner">
        {children}
      </div>
    </div>
  );
}

function Pipeline({ deals, acts, filter = 'quarter', onFilterChange, touchedDealIds = new Set(), quotaTargets = { quarterly: DEFAULT_QUARTERLY_QUOTA, yearly: DEFAULT_YEARLY_QUOTA }, targetTerm = 'quota', fiscalStartMonth = 0, meddicByDeal = {}, includeMeddicCloseScore = false, salesMethodology = 'meddic', onRefresh }) {
  const scrollRef = useRef(null);
  const [expandedAnalytics, setExpandedAnalytics] = useState(null);
  const forecastScope = normalizeForecastScopeValue(filter);
  const [stagesOpen, setStagesOpen] = useState(false);
  const [forecastCallEditKind, setForecastCallEditKind] = useState(null);
  const [forecastCallInput, setForecastCallInput] = useState('');
  const [forecastCallError, setForecastCallError] = useState('');
  const [forecastCallInputFocused, setForecastCallInputFocused] = useState(false);
  const [forecastCallKeyboardInset, setForecastCallKeyboardInset] = useState(0);
  const [forecastRiskWizardOpen, setForecastRiskWizardOpen] = useState(false);
  const [forecastRiskWizardStep, setForecastRiskWizardStep] = useState(0);
  const [riskCategoryOpen, setRiskCategoryOpen] = useState({});
  const [forecastCalls, setForecastCalls] = useState(() => {
    try {
      const saved = JSON.parse(localStorage.getItem(FORECAST_CALL_STORAGE_KEY) || '{}');
      return saved && typeof saved === 'object' ? saved : {};
    } catch {
      return {};
    }
  });
  const commandCenterEnabled = IS_FORECAST_COMMAND_CENTER_ENABLED;
  usePullToRefresh(scrollRef, onRefresh || (() => {}));
  useEffect(() => {
    if (!expandedAnalytics) return;
    function closeOnOutsideTap(e) {
      if (e.target.closest?.('.analytics-card, .forecast-command-card')) return;
      setExpandedAnalytics(null);
    }
    document.addEventListener('pointerdown', closeOnOutsideTap);
    return () => document.removeEventListener('pointerdown', closeOnOutsideTap);
  }, [expandedAnalytics]);
  useEffect(() => {
    if (!forecastCallEditKind) {
      setForecastCallInputFocused(false);
      setForecastCallKeyboardInset(0);
      return;
    }
    const vv = window.visualViewport;
    function updateKeyboardInset() {
      if (!vv) {
        setForecastCallKeyboardInset(0);
        return;
      }
      const overlap = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
      setForecastCallKeyboardInset(overlap > 80 ? overlap : 0);
    }
    updateKeyboardInset();
    vv?.addEventListener('resize', updateKeyboardInset);
    vv?.addEventListener('scroll', updateKeyboardInset);
    window.addEventListener('resize', updateKeyboardInset);
    return () => {
      vv?.removeEventListener('resize', updateKeyboardInset);
      vv?.removeEventListener('scroll', updateKeyboardInset);
      window.removeEventListener('resize', updateKeyboardInset);
    };
  }, [forecastCallEditKind]);
  function toggleAnalytics(key) {
    setExpandedAnalytics(prev => prev === key ? null : key);
  }
  function chooseForecastScope(value) {
    const nextScope = normalizeForecastScopeValue(value);
    onFilterChange?.(nextScope);
    localStorage.setItem('pq_forecast_scope', nextScope);
    setExpandedAnalytics(null);
  }

  const isClosedWonDeal = deal => deal.stage === 'Closed Won' || deal.sfStage === 'Closed Won';
  const open    = deals.filter(d => d.stage !== 'Closed Won' && d.stage !== 'Closed Lost');
  const openVal = open.reduce((s, d) => s + d.value, 0);

  const now = new Date();
  const fiscalPeriods = getFiscalPeriods(now, fiscalStartMonth);
  const qStart = fiscalPeriods.currentQuarter.start;
  const qEnd = fiscalPeriods.currentQuarter.end;
  const qLabel = fiscalPeriods.currentQuarter.label;
  const nextQStart = fiscalPeriods.nextQuarter.start;
  const nextQEnd = fiscalPeriods.nextQuarter.end;
  const nextQLabel = fiscalPeriods.nextQuarter.label;
  const closingQ = open.filter(d => {
    if (!d.closeDate) return false;
    const dt = new Date(d.closeDate + 'T00:00:00');
    return dt >= qStart && dt <= qEnd;
  }).sort((a, b) => new Date(a.closeDate + 'T00:00:00') - new Date(b.closeDate + 'T00:00:00'));
  const closingQVal = closingQ.reduce((s, d) => s + d.value, 0);
  const closingNextQ = open.filter(d => {
    if (!d.closeDate) return false;
    const dt = new Date(d.closeDate + 'T00:00:00');
    return dt >= nextQStart && dt <= nextQEnd;
  }).sort((a, b) => new Date(a.closeDate + 'T00:00:00') - new Date(b.closeDate + 'T00:00:00'));
  const yearStart = fiscalPeriods.yearStart;
  const yearEnd = fiscalPeriods.yearEnd;
  const closingYear = open.filter(d => {
    if (!d.closeDate) return false;
    const dt = new Date(d.closeDate + 'T00:00:00');
    return dt >= yearStart && dt <= yearEnd;
  }).sort((a, b) => new Date(a.closeDate + 'T00:00:00') - new Date(b.closeDate + 'T00:00:00'));
  const closedWon = deals.filter(isClosedWonDeal);
  const closedWonInRange = (start, end) => closedWon.filter(d => {
    if (!d.closeDate) return false;
    const dt = new Date(d.closeDate + 'T00:00:00');
    return dt >= start && dt <= end;
  }).sort((a, b) => new Date(a.closeDate + 'T00:00:00') - new Date(b.closeDate + 'T00:00:00'));
  const dateInRange = (dateStr, start, end) => {
    if (!dateStr) return false;
    const dt = new Date(String(dateStr).slice(0, 10) + 'T00:00:00');
    return Number.isFinite(dt.getTime()) && dt >= start && dt <= end;
  };
  const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
  const currentQuarterActualsEnd = todayEnd < qEnd ? todayEnd : qEnd;
  const closedWonCurrentQuarterDeals = closedWonInRange(qStart, currentQuarterActualsEnd);
  const scopedForecastDeals = forecastScope === 'next'
    ? closingNextQ
    : forecastScope === 'year'
      ? closingYear
      : closingQ;
  const forecastScopeOptions = [
    { value: 'quarter', label: qLabel, periodLabel: qLabel },
    { value: 'next', label: nextQLabel, periodLabel: nextQLabel },
    { value: 'year', label: 'FY ' + fiscalPeriods.fiscalYearLabel, periodLabel: 'FY ' + fiscalPeriods.fiscalYearLabel }
  ];
  const selectedForecastScope = forecastScopeOptions.find(option => option.value === forecastScope) || forecastScopeOptions[0];
  const scopeLabel = selectedForecastScope.label || qLabel;
  const scopePeriodLabel = selectedForecastScope.periodLabel || scopeLabel;

  function riskInfo(deal) {
    const reason = dealRiskReason(deal, acts, touchedDealIds);
    return reason ? { label: 'At Risk', reason } : null;
  }

  const commitDeals = open.filter(d => d.forecastCategory === 'Commit');
  const bestCaseDeals = open.filter(d => d.forecastCategory === 'Best Case');
  const pipelineDeals = open.filter(d => d.forecastCategory === 'Pipeline');
  const riskDeals = open.filter(d => riskInfo(d));
  const noNextStepDeals = open.filter(d => !(d.nextStep || '').trim());
  const pastCloseDeals = open.filter(d => isPastCloseDate(d));
  const sumValue = list => list.reduce((s, d) => s + d.value, 0);
  const pct = n => openVal > 0 ? Math.min(100, Math.round((n / openVal) * 100)) : 0;
  const scopedCommitDeals = scopedForecastDeals.filter(d => d.forecastCategory === 'Commit');
  const scopedBestCaseDeals = scopedForecastDeals.filter(d => d.forecastCategory === 'Best Case');
  const scopedPipelineDeals = scopedForecastDeals.filter(d => d.forecastCategory === 'Pipeline');
  const scopedOmittedDeals = scopedForecastDeals.filter(d => d.forecastCategory === 'Omitted');
  const createdThisQuarterDeals = open.filter(d => dateInRange(d.createdDate, qStart, qEnd));
  const createdThisQuarterValue = sumValue(createdThisQuarterDeals);
  const quotaTarget = forecastScope === 'year'
    ? quotaTargets.yearly
    : quotaTargets.quarterly;
  const targetTermLower = targetTermLabel(targetTerm);
  const targetTermTitle = targetTermLabel(targetTerm, { title: true });
  const healthScore = Math.max(0, Math.min(100,
    100 - (riskDeals.length * 14) - (noNextStepDeals.length * 9) - (pastCloseDeals.length * 12)
  ));
  const healthTone = healthScore >= 80 ? 'strong' : healthScore >= 55 ? 'watch' : 'risk';
  const healthLabel = healthScore >= 80 ? 'Strong pipeline' : healthScore >= 55 ? 'Needs attention' : 'High risk';
  const healthSub = riskDeals.length || noNextStepDeals.length
    ? riskDeals.length + ' at risk · ' + noNextStepDeals.length + ' missing next step'
    : 'No visible risk or missing next steps';
  const dealScores = open.map(d => ({ deal: d, ...dealCloseScore(d, acts, touchedDealIds, { includeMeddic: includeMeddicCloseScore, meddic: meddicSummary(meddicByDeal[String(d.id)], salesMethodology) }) }));
  const scoreForDeal = deal => dealScores.find(item => item.deal.id === deal.id)?.score || 0;
  const scopedDealScores = dealScores.filter(item => scopedForecastDeals.some(deal => deal.id === item.deal.id));
  const avgDealScore = scopedDealScores.length
    ? Math.round(scopedDealScores.reduce((sum, item) => sum + item.score, 0) / scopedDealScores.length)
    : 0;
  const allOpenAvgDealScore = dealScores.length
    ? Math.round(dealScores.reduce((sum, item) => sum + item.score, 0) / dealScores.length)
    : 0;
  const strongScoreDeals = scopedDealScores.filter(item => item.score >= 80);
  const likelyScoreDeals = scopedDealScores.filter(item => item.score >= 60 && item.score < 80);
  const uncertainScoreDeals = scopedDealScores.filter(item => item.score >= 40 && item.score < 60);
  const weakScoreDeals = scopedDealScores.filter(item => item.score < 40);
  const stageRows = SF_STAGES
    .filter(s => !['Closed Won', 'Closed Lost'].includes(s.sf))
    .map(s => {
      const stageDeals = scopedForecastDeals.filter(d => (d.sfStage || d.stage) === s.sf);
      return { key: s.sf, title: s.sf, deals: stageDeals, value: sumValue(stageDeals) };
    })
    .filter(row => row.deals.length);
  function sortAnalyticsDeals(list) {
    return [...list].sort((a, b) => closeDateTime(a) - closeDateTime(b) || String(a.name || '').localeCompare(String(b.name || '')));
  }
  function forecastPeriodStats(list) {
    return {
      deals: sortAnalyticsDeals(list),
      value: sumValue(list),
      commit: sumValue(list.filter(d => d.forecastCategory === 'Commit')),
      best: sumValue(list.filter(d => d.forecastCategory === 'Best Case')),
      pipeline: sumValue(list.filter(d => d.forecastCategory === 'Pipeline')),
      riskCount: list.filter(d => riskInfo(d)).length,
      missingNext: list.filter(d => !(d.nextStep || '').trim()).length
    };
  }
  const staleDeals = riskDeals;
  const forecastMismatchDeals = open.filter(d => {
    const score = scoreForDeal(d);
    return (d.forecastCategory === 'Commit' && score < 60) ||
      (d.forecastCategory === 'Best Case' && score < 45) ||
      (d.forecastCategory === 'Pipeline' && score >= 80);
  });
  const scopedCommitBelowScoreDeals = scopedCommitDeals.filter(d => scoreForDeal(d) < 60);
  const scopedStaleDeals = scopedForecastDeals.filter(d => riskInfo(d));
  const scopedForecastMismatchDeals = scopedForecastDeals.filter(d => {
    const score = scoreForDeal(d);
    return (d.forecastCategory === 'Commit' && score < 60) ||
      (d.forecastCategory === 'Best Case' && score < 45) ||
      (d.forecastCategory === 'Pipeline' && score >= 80);
  });
  const scopedNoNextStepDeals = scopedForecastDeals.filter(d => !(d.nextStep || '').trim());
  function forecastDealRiskReasons(deal) {
    const reasons = [];
    const visibleRisk = riskInfo(deal);
    const score = scoreForDeal(deal);
    const method = meddicSummary(meddicByDeal[String(deal.id)], salesMethodology);
    const stageName = deal.sfStage || deal.stage || '';
    const needsQualificationEvidence = includeMeddicCloseScore && method.total > 0 && method.completed < method.total && (
      ['Commit', 'Best Case'].includes(deal.forecastCategory) ||
      ['Proposal', 'Negotiation', 'Agreed to Purchase'].includes(stageName)
    );
    if (visibleRisk?.reason) reasons.push(visibleRisk.reason);
    if (!(deal.nextStep || '').trim()) reasons.push('Missing next step');
    if (score < 40) reasons.push('Weak close score: ' + score + '%');
    else if (deal.forecastCategory === 'Commit' && score < 60) reasons.push('Commit evidence below 60%');
    if (deal.forecastCategory === 'Best Case' && score < 45) reasons.push('Best Case readiness is weak');
    if (deal.forecastCategory === 'Pipeline' && score >= 80) reasons.push('Strong deal still in Pipeline');
    if (needsQualificationEvidence) reasons.push((method.method?.pillLabel || 'Qualification') + ' gaps');
    return [...new Set(reasons)];
  }
  const scopedRiskRows = scopedForecastDeals
    .map(deal => ({ deal, score: scoreForDeal(deal), reasons: forecastDealRiskReasons(deal) }))
    .filter(row => row.reasons.length)
    .sort((a, b) => closeDateTime(a.deal) - closeDateTime(b.deal) || a.score - b.score || String(a.deal.name || '').localeCompare(String(b.deal.name || '')));
  const riskCategoryDefs = [
    {
      key: 'close-date',
      title: 'Close Date Risk',
      sub: 'Past close dates or timing that needs inspection',
      tone: 'red',
      match: row => row.reasons.some(reason => /past close|close date/i.test(reason)),
    },
    {
      key: 'next-step',
      title: 'Missing Next Step',
      sub: 'No clear customer action, owner, or date',
      tone: 'blue',
      match: row => row.reasons.some(reason => /missing next step/i.test(reason)),
    },
    {
      key: 'forecast-quality',
      title: 'Forecast Quality',
      sub: 'Forecast category and deal evidence disagree',
      tone: 'amber',
      match: row => row.reasons.some(reason => /commit evidence|best case|strong deal/i.test(reason)),
    },
    {
      key: 'stale-activity',
      title: 'Stale Activity',
      sub: 'Deals that need a current buyer touch',
      tone: 'purple',
      match: row => row.reasons.some(reason => /days since touch|stale|touch/i.test(reason)),
    },
    {
      key: 'qualification',
      title: 'Qualification Gaps',
      sub: 'Methodology fields are incomplete for important deals',
      tone: 'teal',
      match: row => row.reasons.some(reason => /gaps|qualification|meddic|bant|meddpicc|whys/i.test(reason)),
    },
    {
      key: 'score',
      title: 'Weak Close Score',
      sub: 'Low confidence signals across stage, activity, and timing',
      tone: 'red',
      match: row => row.reasons.some(reason => /weak close score/i.test(reason)),
    },
  ];
  const riskCategoryRows = riskCategoryDefs
    .map(category => {
      const rows = scopedRiskRows.filter(row => category.match(row));
      return {
        ...category,
        rows,
        value: rows.reduce((sum, row) => sum + row.deal.value, 0),
      };
    })
    .filter(category => category.rows.length)
    .map(category => ({ ...category, defaultOpen: false }));
  const uncategorizedRiskRows = scopedRiskRows.filter(row => !riskCategoryDefs.some(category => category.match(row)));
  if (uncategorizedRiskRows.length) {
    riskCategoryRows.push({
      key: 'other',
      title: 'Other Risk',
      sub: 'Additional risk signals to inspect',
      tone: 'gray',
      rows: uncategorizedRiskRows,
      value: uncategorizedRiskRows.reduce((sum, row) => sum + row.deal.value, 0),
      defaultOpen: false,
    });
  }
  const scopedRiskValue = sumValue(scopedStaleDeals);
  const scopeStats = forecastPeriodStats(scopedForecastDeals);
  const closedWonCurrentQuarterValue = sumValue(closedWonCurrentQuarterDeals);
  const scopePct = n => scopeStats.value > 0 ? Math.min(100, Math.round((n / scopeStats.value) * 100)) : 0;
  const forecastCallScopeKey = forecastScope + ':' + scopePeriodLabel;
  const savedForecastCall = forecastCalls[forecastCallScopeKey] || {};
  const forecastCall = {
    low: Number.isFinite(savedForecastCall.low) ? savedForecastCall.low : scopeStats.commit,
    call: Number.isFinite(savedForecastCall.call) ? savedForecastCall.call : scopeStats.commit + scopeStats.best,
    high: Number.isFinite(savedForecastCall.high) ? savedForecastCall.high : scopeStats.value,
  };
  const quotaPct = value => quotaTarget > 0 ? Math.round((value / quotaTarget) * 1000) / 10 : 0;
  const callQuotaRawPct = quotaPct(forecastCall.call);
  const callQuotaPct = Math.min(100, Math.round((forecastCall.call / quotaTarget) * 100));
  const callQuotaGap = Math.max(0, quotaTarget - forecastCall.call);
  const scopedClosedWonStart = forecastScope === 'next' ? nextQStart : forecastScope === 'year' ? yearStart : qStart;
  const scopedClosedWonEnd = forecastScope === 'next' ? nextQEnd : forecastScope === 'year' ? (todayEnd < yearEnd ? todayEnd : yearEnd) : currentQuarterActualsEnd;
  const scopedClosedWonDeals = closedWonInRange(scopedClosedWonStart, scopedClosedWonEnd);
  const scopedClosedWonValue = sumValue(scopedClosedWonDeals);
  const attainmentRawPct = quotaPct(scopedClosedWonValue);
  const attainmentPct = quotaTarget > 0 ? Math.min(100, Math.round((scopedClosedWonValue / quotaTarget) * 100)) : 0;
  const attainmentGap = Math.max(0, quotaTarget - scopedClosedWonValue);
  const closedWonQuotaPct = Math.min(100, Math.round((closedWonCurrentQuarterValue / quotaTargets.quarterly) * 100));
  const closedWonCountLabel = closedWonCurrentQuarterDeals.length + ' deal' + (closedWonCurrentQuarterDeals.length !== 1 ? 's' : '');
  const scopeEnd = forecastScope === 'next' ? nextQEnd : forecastScope === 'year' ? yearEnd : qEnd;
  const daysLeft = Math.max(0, Math.ceil((scopeEnd.getTime() - now.getTime()) / 86400000));
  const totalPipelineMultiple = quotaTarget > 0 ? (scopeStats.value / quotaTarget).toFixed(1) : '0.0';
  function parseForecastCallInput(value) {
    const raw = String(value || '').trim().toLowerCase().replace(/[$,\s]/g, '');
    if (!raw) return null;
    const multiplier = raw.endsWith('m') ? 1000000 : raw.endsWith('k') ? 1000 : 1;
    const number = Number(raw.replace(/[km]$/, ''));
    if (!Number.isFinite(number) || number < 0) return null;
    return Math.round(number * multiplier);
  }
  function forecastCallLabel(kind) {
    if (kind === 'call') return 'Forecast call';
    if (kind === 'low') return 'Low forecast';
    return 'High forecast';
  }
  function openForecastCallEditor(kind) {
    setForecastCallEditKind(kind);
    setForecastCallInput(String(Math.round(forecastCall[kind] || 0)));
    setForecastCallError('');
  }
  function closeForecastCallEditor() {
    setForecastCallEditKind(null);
    setForecastCallInput('');
    setForecastCallError('');
    setForecastCallInputFocused(false);
    setForecastCallKeyboardInset(0);
  }
  function saveForecastCallEditor() {
    if (!forecastCallEditKind) return;
    const kind = forecastCallEditKind;
    const nextValue = parseForecastCallInput(forecastCallInput);
    if (nextValue === null) {
      setForecastCallError('Enter a valid forecast amount.');
      return;
    }
    setForecastCalls(prev => {
      const nextCall = { ...forecastCall, [kind]: nextValue };
      if (kind === 'low' && nextCall.low > nextCall.call) nextCall.call = nextCall.low;
      if (kind === 'call' && nextCall.call < nextCall.low) nextCall.low = nextCall.call;
      if (kind === 'call' && nextCall.call > nextCall.high) nextCall.high = nextCall.call;
      if (kind === 'high' && nextCall.high < nextCall.call) nextCall.call = nextCall.high;
      if (nextCall.call < nextCall.low) nextCall.low = nextCall.call;
      if (nextCall.high < nextCall.call) nextCall.high = nextCall.call;
      const next = {
        ...prev,
        [forecastCallScopeKey]: {
          ...nextCall,
          updatedAt: new Date().toISOString(),
        },
      };
      localStorage.setItem(FORECAST_CALL_STORAGE_KEY, JSON.stringify(next));
      return next;
    });
    closeForecastCallEditor();
  }
  const scoredBands = [
    { key: 'score-strong', title: 'Strong deals', sub: 'Close scores 80%+', value: strongScoreDeals.length + ' deal' + (strongScoreDeals.length !== 1 ? 's' : ''), deals: strongScoreDeals.map(item => item.deal) },
    { key: 'score-watch', title: 'Watch deals', sub: 'Close scores from 40-79%', value: likelyScoreDeals.length + uncertainScoreDeals.length + ' deal' + (likelyScoreDeals.length + uncertainScoreDeals.length !== 1 ? 's' : ''), deals: [...likelyScoreDeals, ...uncertainScoreDeals].map(item => item.deal) },
    { key: 'score-risk', title: 'At-risk deals', sub: 'Close scores below 40%', value: weakScoreDeals.length + ' deal' + (weakScoreDeals.length !== 1 ? 's' : ''), deals: weakScoreDeals.map(item => item.deal) },
  ];
  const fixRows = [
    {
      key: 'cmd-commit',
      title: scopedCommitBelowScoreDeals.length ? 'Review Commit quality' : 'Commit quality clear',
      sub: scopedCommitBelowScoreDeals.length
        ? scopedCommitBelowScoreDeals.length + ' Commit deal' + (scopedCommitBelowScoreDeals.length !== 1 ? 's' : '') + ' below 60% close score'
        : scopedCommitDeals.length ? 'All Commit deals are at or above 60%' : 'No Commit deals in ' + scopeLabel,
      value: scopedCommitBelowScoreDeals.length ? scopedCommitBelowScoreDeals.length + ' review' : 'Clear',
      fill: scopedForecastDeals.length ? Math.round((scopedCommitBelowScoreDeals.length / scopedForecastDeals.length) * 100) : 0,
      tone: scopedCommitBelowScoreDeals.length ? 'risk' : 'commit',
      deals: scopedCommitBelowScoreDeals,
    },
    {
      key: 'cmd-risk',
      title: scopedStaleDeals.length ? 'Re-engage stale deals' : 'No stale deals',
      sub: scopedStaleDeals.length ? 'Untouched, past close, or weak close signal' : 'No visible risk in ' + scopeLabel,
      value: fmtMoney(scopedRiskValue),
      fill: scopePct(scopedRiskValue),
      tone: 'risk',
      deals: scopedStaleDeals,
    },
    {
      key: 'cmd-next',
      title: scopedNoNextStepDeals.length ? 'Add missing next steps' : 'Next steps current',
      sub: scopedNoNextStepDeals.length ? 'Open deals without a committed action' : 'All scoped deals have a next step',
      value: scopedNoNextStepDeals.length ? scopedNoNextStepDeals.length + ' missing' : 'Clear',
      fill: scopedForecastDeals.length ? Math.round((scopedNoNextStepDeals.length / scopedForecastDeals.length) * 100) : 0,
      tone: 'risk',
      deals: scopedNoNextStepDeals,
    },
    {
      key: 'cmd-mismatch',
      title: scopedForecastMismatchDeals.length ? 'Fix category mismatches' : 'Categories aligned',
      sub: scopedForecastMismatchDeals.length ? 'Forecast category and close readiness disagree' : 'Forecast categories align with close scores',
      value: scopedForecastMismatchDeals.length ? scopedForecastMismatchDeals.length + ' mismatch' : 'Clear',
      fill: scopedForecastDeals.length ? Math.round((scopedForecastMismatchDeals.length / scopedForecastDeals.length) * 100) : 0,
      tone: 'risk',
      deals: scopedForecastMismatchDeals,
    },
  ];
  function AnalyticsDealList({ deals }) {
    const visibleDeals = sortAnalyticsDeals(deals);
    return (
      <div className="analytics-deals" onClick={() => setExpandedAnalytics(null)}>
        {visibleDeals.length === 0 && <div className="analytics-empty">No active deals</div>}
        {visibleDeals.map(deal => (
          <div key={deal.id} className="analytics-deal-row">
            <div className="analytics-deal-main">
              <div className="analytics-deal-name">{deal.name}</div>
              <div className="analytics-deal-meta">{fmtCloseDate(deal.closeDate)} · {(deal.sfStage || deal.stage || 'No stage')}</div>
            </div>
            <div className="analytics-deal-value">{fmtMoney(deal.value)}</div>
          </div>
        ))}
      </div>
    );
  }

  function AnalyticsRow({ rowKey, title, sub, value, fill, tone, deals }) {
    const canExpand = !!deals;
    const isOpen = canExpand && expandedAnalytics === rowKey;
    const rowContent = (
      <>
        <div className="analytics-main">
          <div className="analytics-title">{title}</div>
          <div className="analytics-sub">{sub}</div>
          {fill !== undefined && (
            <div className="analytics-meter">
              <div className={'analytics-fill ' + (tone || '')} style={{ width: fill + '%' }} />
            </div>
          )}
        </div>
        <div className="analytics-value">{value}</div>
        {canExpand && <span className={'analytics-chevron' + (isOpen ? ' open' : '')} aria-hidden="true"></span>}
      </>
    );
    return (
      <>
        {canExpand ? (
          <button
            className="analytics-row"
            type="button"
            onClick={() => toggleAnalytics(rowKey)}
            aria-expanded={isOpen}
          >
            {rowContent}
          </button>
        ) : (
          <div className="analytics-row">{rowContent}</div>
        )}
        {canExpand && (
          <SmoothExpand open={isOpen}>
            <AnalyticsDealList deals={deals} />
          </SmoothExpand>
        )}
      </>
    );
  }

  function ForecastBridgeBar({ rowKey, label, value, fill, tone, deals }) {
    const canExpand = deals && deals.length > 0;
    const isOpen = canExpand && expandedAnalytics === rowKey;
    return (
      <div className="forecast-bridge-item">
        <button
          type="button"
          className="forecast-bridge-bar"
          onClick={() => canExpand && toggleAnalytics(rowKey)}
          aria-expanded={isOpen}
        >
          <span>{label}</span>
          <div className="forecast-bridge-track"><span className={tone || ''} style={{ width: fill + '%' }} /></div>
          <strong>{fmtMoney(value)}</strong>
          {canExpand && <span className={'analytics-chevron' + (isOpen ? ' open' : '')} aria-hidden="true"></span>}
        </button>
        {canExpand && (
          <SmoothExpand open={isOpen}>
            <AnalyticsDealList deals={deals} />
          </SmoothExpand>
        )}
      </div>
    );
  }

  const avgTrust = list => {
    if (!list.length) return 0;
    return Math.round(list.reduce((sum, deal) => sum + scoreForDeal(deal), 0) / list.length);
  };
  const scopedCommitRiskDeals = scopedCommitDeals.filter(d => scoreForDeal(d) < 60 || riskInfo(d) || isPastCloseDate(d));
  const forecastConfidence = Math.max(0, Math.min(100, Math.round(
    (callQuotaPct * 0.35) + (avgDealScore * 0.45) + (healthScore * 0.20)
  )));
  const forecastRiskDeal = scopedCommitRiskDeals[0] || scopedCommitDeals[0] || null;
  function forecastRiskFixSteps(deal) {
    if (!deal) return [{
      title: 'No Commit deal to review',
      detail: 'There is no Commit risk in this forecast scope. Keep the call steady and review the bucket table for context.',
    }];
    const steps = [];
    const score = scoreForDeal(deal);
    const riskReason = riskInfo(deal);
    if (isPastCloseDate(deal)) {
      steps.push({
        title: 'Fix the close date',
        detail: 'Move the close date to the real expected close window, or move the deal out of Commit if timing is no longer defensible.',
      });
    }
    if (riskReason && !isPastCloseDate(deal)) {
      steps.push({
        title: 'Log a current customer touch',
        detail: riskReason + ' is weakening the call. Add a call, email, meeting, or demo before the forecast review.',
      });
    }
    if (!(deal.nextStep || '').trim()) {
      steps.push({
        title: 'Add a specific next step',
        detail: 'Capture the next customer action with an owner and date so the forecast has a clear path forward.',
      });
    }
    if (score < 60) {
      steps.push({
        title: 'Raise the close evidence',
        detail: 'Confirm stage, decision process, close timing, and proof of buyer engagement before keeping this deal in Commit.',
      });
    }
    steps.push({
      title: 'Re-check the forecast category',
      detail: 'Keep the deal in Commit only if the evidence supports it; otherwise move it to Best Case or Pipeline before the call.',
    });
    return steps.slice(0, 4);
  }
  const forecastRiskSteps = forecastRiskFixSteps(forecastRiskDeal);
  const forecastControlActions = [
    {
      key: 'control-commit-risk',
      badge: '!',
      tone: 'red',
      title: scopedCommitRiskDeals.length ? 'Commit risk before forecast call' : 'Commit quality clear',
      sub: scopedCommitRiskDeals.length
        ? scopedCommitRiskDeals[0].name + ' is holding down confidence.'
        : scopedCommitDeals.length ? 'Commit deals have current evidence.' : 'No Commit deals in ' + scopeLabel + '.',
      action: scopedCommitRiskDeals.length ? 'Fix' : 'View',
      deals: scopedCommitRiskDeals.length ? scopedCommitRiskDeals : scopedCommitDeals,
    },
  ];
  const forecastControlBuckets = [
    { key: 'commit', label: 'Commit', value: scopeStats.commit, trust: avgTrust(scopedCommitDeals), deals: scopedCommitDeals },
    { key: 'best', label: 'Best Case', value: scopeStats.best, trust: avgTrust(scopedBestCaseDeals), deals: scopedBestCaseDeals },
    { key: 'pipeline', label: 'Pipeline', value: scopeStats.pipeline, trust: avgTrust(scopedPipelineDeals), deals: scopedPipelineDeals },
  ];
  const forecastArenaRows = [
    { key: 'arena-commit', label: 'Commit', value: scopeStats.commit, color: '#54c69d', deals: scopedCommitDeals },
    { key: 'arena-best', label: 'Best case', value: scopeStats.best, color: '#f59e0b', deals: scopedBestCaseDeals },
    { key: 'arena-pipeline', label: 'Pipeline', value: scopeStats.pipeline, color: '#2f80d7', deals: scopedPipelineDeals },
    { key: 'arena-omitted', label: 'Omitted', value: sumValue(scopedOmittedDeals), color: '#8d8e88', deals: scopedOmittedDeals },
  ].filter(row => row.value > 0 || row.key !== 'arena-omitted');
  const stageArenaColors = ['#2f80d7', '#766ee6', '#f59e0b', '#df512b', '#54c69d', '#8d8e88'];
  const stageArenaRows = stageRows.map((row, index) => ({
    key: 'arena-stage-' + row.key,
    label: row.title,
    value: row.value,
    valueLabel: row.deals.length + ' · ' + fmtMoney(row.value),
    color: stageArenaColors[index % stageArenaColors.length],
    deals: row.deals,
  }));
  function ForecastCallTile({ kind, label }) {
    const pctLabel = quotaPct(forecastCall[kind]).toLocaleString(undefined, { maximumFractionDigits: 1 }) + '% ' + targetTermLower;
    return (
      <button className={'forecast-control-tile forecast-call-tile ' + kind} type="button" onClick={() => openForecastCallEditor(kind)}>
        <span>{label}</span>
        <strong>{fmtMoney(forecastCall[kind])}</strong>
        <em>{pctLabel}</em>
      </button>
    );
  }
  function ForecastArenaStack({ rows }) {
    const total = rows.reduce((sum, row) => sum + row.value, 0);
    const visibleRows = rows.filter(row => row.value > 0);
    const expandedRow = rows.find(row => expandedAnalytics === row.key && row.deals?.length > 0);
    return (
      <div className="forecast-arena-stack-wrap">
        <div className="forecast-arena-stack" role="group" aria-label="Forecast graphic segments">
          {visibleRows.map(row => {
            const isOpen = expandedAnalytics === row.key;
            return (
              <button
                key={row.key}
                type="button"
                className={isOpen ? 'open' : ''}
                style={{
                  width: Math.max(2, (row.value / Math.max(total, 1)) * 100) + '%',
                  background: row.color,
                }}
                onClick={() => row.deals?.length && toggleAnalytics(row.key)}
                aria-expanded={!!isOpen}
                aria-label={(isOpen ? 'Hide ' : 'Show ') + row.label + ' deals, ' + (row.valueLabel || fmtMoney(row.value))}
              >
                <span>{row.label}</span>
              </button>
            );
          })}
        </div>
        <div className="forecast-arena-expand">
          <SmoothExpand open={!!expandedRow}>
            {expandedRow && (
              <div className="forecast-arena-expanded-panel">
                <div className="forecast-arena-expanded-head">
                  <span>{expandedRow.label}</span>
                  <strong>{expandedRow.valueLabel || fmtMoney(expandedRow.value)}</strong>
                </div>
                <AnalyticsDealList deals={expandedRow.deals} />
              </div>
            )}
          </SmoothExpand>
        </div>
      </div>
    );
  }
  function ForecastArenaLegend({ rows, total, splitAfter }) {
    const splitIndex = Number.isInteger(splitAfter) && splitAfter > 0 ? splitAfter : Math.ceil(rows.length / 2);
    const columns = [rows.slice(0, splitIndex), rows.slice(splitIndex)].filter(column => column.length);
    const renderLegendRow = row => (
      <button
        key={row.key}
        type="button"
        className="forecast-arena-legend-row"
        onClick={() => row.deals?.length && toggleAnalytics(row.key)}
        aria-expanded={expandedAnalytics === row.key}
      >
        <span className="forecast-arena-dot" style={{ background: row.color }} />
        <span className="forecast-arena-name">{row.label}</span>
        <strong>{row.valueLabel || fmtMoney(row.value)}</strong>
      </button>
    );
    return (
      <div className="forecast-arena-legend">
        <div className="forecast-arena-legend-columns">
          {columns.map((column, index) => (
            <div className="forecast-arena-legend-column" key={'legend-col-' + index}>
              {column.map(renderLegendRow)}
            </div>
          ))}
        </div>
        {total !== undefined && <div className="forecast-arena-total">{fmtMoney(total)} total</div>}
      </div>
    );
  }
  function toggleRiskCategory(key) {
    const defaultOpen = riskCategoryRows.find(category => category.key === key)?.defaultOpen || false;
    setRiskCategoryOpen(prev => ({ ...prev, [key]: !(prev[key] ?? defaultOpen) }));
  }
  function ForecastRiskDealRow({ row }) {
    return (
      <div className="forecast-risk-list-row">
        <span className="forecast-risk-list-top">
          <strong>{row.deal.name}</strong>
          <em>{fmtMoney(row.deal.value)}</em>
        </span>
        <span className="forecast-risk-list-meta">
          {row.deal.forecastCategory || 'No category'} · {(row.deal.sfStage || row.deal.stage || 'No stage')} · Score {row.score}%
        </span>
        <span className="forecast-risk-list-reasons">
          {row.reasons.map(reason => <span key={reason}>{reason}</span>)}
        </span>
      </div>
    );
  }
  function ForecastRiskCategory({ category }) {
    const isOpen = riskCategoryOpen[category.key] ?? category.defaultOpen;
    return (
      <div className={'forecast-risk-category ' + (category.tone || '')}>
        <button
          type="button"
          className="forecast-risk-category-head"
          onClick={() => toggleRiskCategory(category.key)}
          aria-expanded={isOpen}
        >
          <span className="forecast-risk-category-main">
            <span className="forecast-risk-category-title">{category.title}</span>
            <span className="forecast-risk-category-sub">{category.sub}</span>
          </span>
          <span className="forecast-risk-category-meta">
            <strong>{category.rows.length}</strong>
            <em>{fmtMoney(category.value)}</em>
          </span>
          <span className={'analytics-chevron' + (isOpen ? ' open' : '')} aria-hidden="true"></span>
        </button>
        <SmoothExpand open={isOpen}>
          <div className="forecast-risk-category-list">
            {category.rows.map(row => <ForecastRiskDealRow key={category.key + '-' + row.deal.id} row={row} />)}
          </div>
        </SmoothExpand>
      </div>
    );
  }
  function ForecastClosedWonCard() {
    const isOpen = closedWonCurrentQuarterDeals.length && expandedAnalytics === 'closed-won';
    return (
      <div className="forecast-closed-wrap">
        <button
          className="forecast-closed-card"
          type="button"
          onClick={() => closedWonCurrentQuarterDeals.length && toggleAnalytics('closed-won')}
          aria-expanded={!!isOpen}
        >
          <span className="forecast-closed-main">
            <span className="forecast-closed-label">Closed Won This Quarter</span>
            <strong>{fmtMoney(closedWonCurrentQuarterValue)}</strong>
          </span>
          <span className="forecast-closed-meta">
            <span>{closedWonQuotaPct}% of {targetTermLower}</span>
            <em>{qLabel} · {closedWonCountLabel}</em>
          </span>
        </button>
        <div className="forecast-closed-meter" aria-hidden="true">
          <span style={{ width: closedWonQuotaPct + '%' }} />
        </div>
        {closedWonCurrentQuarterDeals.length > 0 && (
          <SmoothExpand open={!!isOpen}>
            <AnalyticsDealList deals={closedWonCurrentQuarterDeals} />
          </SmoothExpand>
        )}
      </div>
    );
  }
  function ForecastCallEditorSheet() {
    if (!forecastCallEditKind) return null;
    const label = forecastCallLabel(forecastCallEditKind);
    const sheet = (
      <div
        className={'next-step-sheet-backdrop forecast-call-sheet-backdrop' + (forecastCallKeyboardInset > 0 ? ' keyboard-open' : '')}
        style={{ '--keyboard-inset': forecastCallKeyboardInset + 'px' }}
        onClick={closeForecastCallEditor}
        data-testid="forecast-call-sheet"
      >
        <div className="activity-sheet next-step-sheet forecast-call-sheet" onClick={e => e.stopPropagation()}>
          <div className="activity-sheet-handle" />
          <div className="score-sheet-head">
            <div className="score-sheet-title-wrap">
              <div className="activity-sheet-title score-sheet-title">{label}</div>
              <div className="activity-sheet-sub score-sheet-meta">{scopeLabel}</div>
            </div>
            <div className="stage-sheet-badge amount-sheet-current">{fmtMoney(forecastCall[forecastCallEditKind])}</div>
          </div>
          <label className="forecast-call-field">
            <span>Amount</span>
            <input
              className="forecast-call-input"
              inputMode="decimal"
              value={forecastCallInput}
              onChange={e => { setForecastCallInput(e.target.value); setForecastCallError(''); }}
              onFocus={() => setForecastCallInputFocused(true)}
              onBlur={() => setForecastCallInputFocused(false)}
              onKeyDown={e => {
                if (e.key === 'Enter') saveForecastCallEditor();
                if (e.key === 'Escape') closeForecastCallEditor();
              }}
              aria-label={label + ' amount'}
            />
          </label>
          {forecastCallError && <div className="forecast-call-error">{forecastCallError}</div>}
          <div className="forecast-call-actions">
            <button type="button" className="next-step-sheet-cancel forecast-call-cancel" onClick={closeForecastCallEditor}>Cancel</button>
            <button type="button" className="next-step-sheet-save forecast-call-save" onClick={saveForecastCallEditor}>Save</button>
          </div>
        </div>
      </div>
    );
    return ReactDOM.createPortal ? ReactDOM.createPortal(sheet, document.body) : sheet;
  }
  function ForecastRiskWizardSheet() {
    if (!forecastRiskWizardOpen) return null;
    const deal = forecastRiskDeal;
    const stepCount = forecastRiskSteps.length || 1;
    const stepIndex = Math.min(forecastRiskWizardStep, stepCount - 1);
    const activeStep = forecastRiskSteps[stepIndex] || forecastRiskSteps[0];
    const closeWizard = () => {
      setForecastRiskWizardOpen(false);
      setForecastRiskWizardStep(0);
    };
    const sheet = (
      <div className="next-step-sheet-backdrop forecast-risk-wizard-backdrop" onClick={closeWizard} data-testid="forecast-risk-wizard">
        <div className="activity-sheet next-step-sheet forecast-risk-wizard" onClick={e => e.stopPropagation()}>
          <div className="activity-sheet-handle" />
          <div className="score-sheet-head">
            <div className="score-sheet-title-wrap">
              <div className="activity-sheet-title score-sheet-title">Forecast Risk Analysis</div>
              <div className="activity-sheet-sub score-sheet-meta">Step {stepIndex + 1} of {stepCount} · Fix Commit risk before the forecast call</div>
            </div>
            <div className="stage-sheet-badge amount-sheet-current">{deal ? scoreForDeal(deal) + '%' : 'Clear'}</div>
          </div>
          {deal && (
            <div className="forecast-risk-deal-card">
              <span>Commit deal</span>
              <strong>{deal.name}</strong>
              <em>{fmtCloseDate(deal.closeDate)} · {(deal.sfStage || deal.stage || 'No stage')} · {fmtMoney(deal.value)}</em>
            </div>
          )}
          <div className="forecast-risk-stepper" role="group" aria-label="Forecast risk fix steps">
            {forecastRiskSteps.map((step, idx) => (
              <span key={step.title} className={idx === stepIndex ? 'active' : idx < stepIndex ? 'done' : ''}>{idx + 1}</span>
            ))}
          </div>
          <div className="forecast-risk-step-panel">
            <strong>{activeStep.title}</strong>
            <em>{activeStep.detail}</em>
          </div>
          <div className="forecast-call-actions">
            <button
              type="button"
              className="next-step-sheet-cancel forecast-call-cancel"
              onClick={() => stepIndex > 0 ? setForecastRiskWizardStep(stepIndex - 1) : closeWizard()}
            >
              {stepIndex > 0 ? 'Back' : 'Close'}
            </button>
            <button
              type="button"
              className="next-step-sheet-save forecast-call-save"
              onClick={() => stepIndex < stepCount - 1 ? setForecastRiskWizardStep(stepIndex + 1) : closeWizard()}
            >
              {stepIndex < stepCount - 1 ? 'Next' : 'Done'}
            </button>
          </div>
        </div>
      </div>
    );
    return ReactDOM.createPortal ? ReactDOM.createPortal(sheet, document.body) : sheet;
  }
  function ForecastControlAction({ row }) {
    const isOpen = row.deals?.length && expandedAnalytics === row.key;
    const opensRiskWizard = row.key === 'control-commit-risk' && row.action === 'Fix';
    return (
      <div className="forecast-control-risk-wrap">
        <div className="forecast-control-risk-row">
          <button
            className="forecast-control-risk-main-button"
            type="button"
            onClick={() => row.deals?.length && toggleAnalytics(row.key)}
            aria-expanded={!!isOpen}
          >
            <span className={'forecast-control-badge ' + row.tone}>{row.badge}</span>
            <span className="forecast-control-risk-main">
              <span className="forecast-control-risk-title">{row.title}</span>
              <span className="forecast-control-risk-sub">{row.sub}</span>
            </span>
          </button>
          <button
            className="forecast-control-action"
            type="button"
            onClick={() => {
              if (opensRiskWizard) {
                setExpandedAnalytics(null);
                setForecastRiskWizardStep(0);
                setForecastRiskWizardOpen(true);
                return;
              }
              if (row.deals?.length) toggleAnalytics(row.key);
            }}
          >
            {row.action}
          </button>
        </div>
        {row.deals?.length > 0 && (
          <SmoothExpand open={!!isOpen}>
            <AnalyticsDealList deals={row.deals} />
          </SmoothExpand>
        )}
      </div>
    );
  }
  function ForecastControlBucketRow({ row }) {
    const isOpen = row.deals?.length && expandedAnalytics === 'bucket-' + row.key;
    return (
      <>
        <button
          className="forecast-control-bucket-row"
          type="button"
          onClick={() => row.deals?.length && toggleAnalytics('bucket-' + row.key)}
          aria-expanded={!!isOpen}
        >
          <span>{row.label}</span>
          <strong>{fmtMoney(row.value)}</strong>
          <strong>{row.trust}%</strong>
        </button>
        {row.deals?.length > 0 && (
          <SmoothExpand open={!!isOpen}>
            <AnalyticsDealList deals={row.deals} />
          </SmoothExpand>
        )}
      </>
    );
  }

  if (!IS_FORECAST_CONTROL_ROOM_ENABLED) return (
    <div ref={scrollRef} className="pipeline-scroll">
      <div className="forecast-scope-tabs" role="tablist" aria-label="Forecast scope">
        {forecastScopeOptions.map(option => (
          <button
            key={option.value}
            type="button"
            role="tab"
            aria-selected={forecastScope === option.value}
            className={forecastScope === option.value ? 'on' : ''}
            onClick={() => chooseForecastScope(option.value)}
          >
            {option.label}
          </button>
        ))}
      </div>

      <div className="forecast-command-card command-dashboard-card">
        <div className="forecast-command-head">
          <div>
            <div className="forecast-command-kicker">{scopeLabel} Forecast Call</div>
            <div className="forecast-command-title">{fmtMoney(forecastCall.call)}</div>
          </div>
          <div className={'forecast-command-score ' + (callQuotaPct >= 85 ? 'ready' : callQuotaPct >= 55 ? 'watch' : 'risk')}>{callQuotaPct}%</div>
        </div>
        <div className="forecast-command-meter">
          <div className={'forecast-command-fill ' + (callQuotaPct >= 85 ? 'ready' : callQuotaPct >= 55 ? 'watch' : 'risk')} style={{ width: callQuotaPct + '%' }} />
        </div>
        <div className="forecast-command-foot">
          <span>{fmtMoney(forecastCall.call)} of {fmtMoney(quotaTarget)} {targetTermLower}</span>
          <strong>{fmtMoney(callQuotaGap)} gap</strong>
        </div>
        <div className="forecast-bridge-bars">
          <ForecastBridgeBar rowKey="bridge-commit" label="Commit" value={scopeStats.commit} fill={scopePct(scopeStats.commit)} tone="ready" deals={scopedCommitDeals} />
          <ForecastBridgeBar rowKey="bridge-best" label="Best Case" value={scopeStats.best} fill={scopePct(scopeStats.best)} tone="best" deals={scopedBestCaseDeals} />
          <ForecastBridgeBar rowKey="bridge-pipeline" label="Pipeline" value={scopeStats.pipeline} fill={scopePct(scopeStats.pipeline)} tone="pipeline" deals={scopedPipelineDeals} />
        </div>
      </div>

      <div className="forecast-section-title">{commandCenterEnabled ? 'Fix These First' : 'Exceptions'}</div>
      <div className="analytics-card command-exception-card">
        {fixRows.map(row => (
          <AnalyticsRow key={row.key} rowKey={row.key} title={row.title} sub={row.sub} value={row.value} fill={row.fill} tone={row.tone} deals={row.deals.length ? row.deals : null} />
        ))}
      </div>

      <div className="forecast-section-title">{commandCenterEnabled ? 'Deal Quality' : 'Details'}</div>
      <div className="analytics-card forecast-detail-card">
        <AnalyticsRow rowKey="quality-score" title={scopeLabel + ' average close score'} sub={scopedForecastDeals.length + ' deal' + (scopedForecastDeals.length !== 1 ? 's' : '') + ' in this scope'} value={avgDealScore + '%'} fill={avgDealScore} tone={avgDealScore >= 70 ? 'commit' : avgDealScore >= 45 ? 'best' : 'risk'} deals={scopedForecastDeals.length ? scopedForecastDeals : null} />
        {scoredBands.map(row => (
          <AnalyticsRow key={row.key} rowKey={row.key} title={row.title} sub={row.sub} value={row.value} deals={row.deals.length ? row.deals : null} />
        ))}
      </div>

      {commandCenterEnabled && (
        <>
          <button
            type="button"
            className="forecast-section-toggle"
            onClick={() => setStagesOpen(prev => !prev)}
            aria-expanded={stagesOpen}
          >
            <span>Sales Stage Distribution</span>
            <strong>{stageRows.length ? stageRows.length + ' active' : 'No active stages'}</strong>
            <span className={'analytics-chevron' + (stagesOpen ? ' open' : '')} aria-hidden="true"></span>
          </button>
          <SmoothExpand open={stagesOpen}>
            <div className="analytics-card forecast-stage-card">
              {stageRows.length ? stageRows.map(row => (
                <AnalyticsRow key={row.key} rowKey={'stage-' + row.key} title={row.title} sub={row.deals.length + ' deal' + (row.deals.length !== 1 ? 's' : '')} value={fmtMoney(row.value)} fill={scopePct(row.value)} deals={row.deals} />
              )) : (
                <div className="analytics-empty stage-empty">No deals in the selected forecast scope</div>
              )}
            </div>
          </SmoothExpand>
        </>
      )}
      <ForecastCallEditorSheet />
      <ForecastRiskWizardSheet />
    </div>
  );

  return (
    <div ref={scrollRef} className="pipeline-scroll forecast-control-room forecast-reference-layout">
      <div className="forecast-scope-tabs" role="tablist" aria-label="Forecast scope">
        {forecastScopeOptions.map(option => (
          <button
            key={option.value}
            type="button"
            role="tab"
            aria-selected={forecastScope === option.value}
            className={forecastScope === option.value ? 'on' : ''}
            onClick={() => chooseForecastScope(option.value)}
          >
            {option.label}
          </button>
        ))}
      </div>

      <div className="forecast-reference-head">
        <div>
          <h1>Forecast</h1>
          <p>{scopePeriodLabel} · {daysLeft} days left</p>
        </div>
      </div>

      <section className="forecast-reference-section forecast-reference-call">
        <div className="forecast-reference-label">Forecast Call</div>
        <div className="forecast-reference-call-row">
          <strong>{fmtMoney(forecastCall.call)}</strong>
          <span>{callQuotaRawPct.toLocaleString(undefined, { maximumFractionDigits: 1 })}% of {targetTermLower}</span>
        </div>
        <div className="forecast-control-tiles">
          <ForecastCallTile kind="low" label="Low" />
          <ForecastCallTile kind="call" label="Call" />
          <ForecastCallTile kind="high" label="High" />
        </div>
      </section>

      <section className="forecast-reference-section">
        <div className="forecast-reference-metric-head">
          <span>{targetTermTitle} Attainment</span>
          <strong>{attainmentRawPct.toLocaleString(undefined, { maximumFractionDigits: 1 })}%</strong>
        </div>
        <div className="forecast-reference-quota-track">
          <span style={{ width: attainmentPct + '%' }} />
        </div>
        <div className="forecast-reference-quota-foot">
          <span>{fmtMoney(scopedClosedWonValue)} closed won</span>
          <strong>{fmtMoney(attainmentGap)} gap</strong>
          <span>{fmtMoney(quotaTarget)} {targetTermLower}</span>
        </div>
      </section>

      <section className="forecast-reference-section forecast-reference-pipeline">
        <div className="forecast-reference-total-row">
          <div>
            <div className="forecast-reference-label">Total Pipeline</div>
            <strong>{fmtMoney(scopeStats.value)}</strong>
            <span>· {scopedForecastDeals.length} deals</span>
          </div>
          <div className="forecast-reference-multiple">
            <strong>{totalPipelineMultiple}x</strong>
            <span>{targetTermTitle}</span>
          </div>
        </div>
      </section>

      <section className="forecast-reference-section forecast-reference-created">
        <div className="forecast-reference-label">Created This Quarter</div>
        <strong>{fmtMoney(createdThisQuarterValue)}</strong>
        <span>· {createdThisQuarterDeals.length} deals</span>
      </section>

      <section className="forecast-reference-section forecast-reference-arena">
        <div className="forecast-reference-metric-head">
          <span>Arena · By Forecast Category</span>
          <strong>{fmtMoney(scopeStats.value)} total</strong>
        </div>
        <ForecastArenaStack rows={forecastArenaRows} />
        <ForecastArenaLegend rows={forecastArenaRows} splitAfter={2} />
      </section>

      <section className="forecast-reference-section forecast-reference-arena">
        <div className="forecast-reference-metric-head">
          <span>Arena · By Sales Stage</span>
          <strong>{scopedForecastDeals.length} deals</strong>
        </div>
        <ForecastArenaStack rows={stageArenaRows} />
        {stageArenaRows.length ? <ForecastArenaLegend rows={stageArenaRows} splitAfter={3} /> : (
          <div className="analytics-empty stage-empty">No deals in the selected forecast scope</div>
        )}
      </section>

      <section className="forecast-reference-section forecast-risk-list-section">
        <div className="forecast-reference-metric-head">
          <span>Deals at Risk</span>
          <strong>{scopedRiskRows.length ? scopedRiskRows.length + ' deal' + (scopedRiskRows.length !== 1 ? 's' : '') : 'Clear'}</strong>
        </div>
        <div className="forecast-risk-list">
          {riskCategoryRows.length ? riskCategoryRows.map(category => (
            <ForecastRiskCategory key={category.key} category={category} />
          )) : (
            <div className="forecast-risk-list-empty">No visible risk in this forecast period.</div>
          )}
        </div>
      </section>

      <ForecastCallEditorSheet />
      <ForecastRiskWizardSheet />
    </div>
  );
}

// ── Awards ────────────────────────────────────────────────────────────────

function Awards({ unlocked, stats, points, streak, nextStepCount, todayActDealCount, openDealsCount, sessionActCount, freezeTokens, dailyQuests, weeklyQuests, filter = 'all', onRefresh }) {
  const level = getLevel(points);
  const title = REP_TITLES[Math.min(level - 1, REP_TITLES.length - 1)];
  const total = openDealsCount || 0;
  const scrollRef = useRef(null);
  usePullToRefresh(scrollRef, onRefresh || (() => {}));
  const [questsOpen, setQuestsOpen] = useState(true);
  const [weeklyQuestsOpen, setWeeklyQuestsOpen] = useState(false);
  const [unlockedOpen, setUnlockedOpen] = useState(true);
  const [lockedOpen, setLockedOpen] = useState(true);

  // Find next achievable achievement for "Next Up" widget
  const nextAchv = ACHV.find(a => {
    if (unlocked.includes(a.id)) return false;
    return a.type === 'stat' || a.type === 'points' || a.type === 'streak' || a.type === 'session';
  });
  function achvProgress(a) {
    if (!a) return { current: 0, target: 1 };
    if (a.type === 'stat')    return { current: stats[a.stat] || 0, target: a.target };
    if (a.type === 'points')     return { current: points, target: a.target };
    if (a.type === 'streak')  return { current: streak, target: a.target };
    if (a.type === 'session') return { current: sessionActCount || 0, target: a.target };
    return { current: 0, target: a.target };
  }
  function achvCategory(a) {
    if (a.type === 'points') return SCORE_LABEL;
    if (a.type === 'streak') return 'Streaks';
    if (a.type === 'session') return 'Session';
    if (['stage', 'stageCount', 'pipelineValue', 'stageValue', 'forecastReady'].includes(a.type)) return 'Deal Progress';
    if (a.stat === 'calls') return 'Calls';
    if (a.stat === 'emails') return 'Emails';
    if (a.stat === 'meetings') return 'Meetings';
    return 'Activity';
  }
  const categoryOrder = ['Activity', 'Calls', 'Emails', 'Meetings', 'Deal Progress', 'Streaks', SCORE_LABEL, 'Session'];
  const unlockedAchv = ACHV.filter(a => unlocked.includes(a.id));
  const lockedAchv = ACHV.filter(a => !unlocked.includes(a.id));
  const unlockedGroups = categoryOrder
    .map(name => ({ name, items: unlockedAchv.filter(a => achvCategory(a) === name) }))
    .filter(group => group.items.length);
  const weeklyDoneCount = (weeklyQuests || []).filter(q => q.count >= q.target).length;
  const showStats = filter === 'all' || filter === 'stats';
  const showQuests = filter === 'all' || filter === 'quests';
  const showAchievements = filter === 'all' || filter === 'achievements';
  function QuestTile({ quest }) {
    const pct = quest.target > 0 ? Math.min(100, Math.round((quest.count / quest.target) * 100)) : 0;
    const done = quest.count >= quest.target;
    return (
      <div className={'daily-quest-tile ' + quest.tone + (done ? ' done' : '')}>
        <div className="daily-quest-count">{Math.min(quest.count, quest.target)}<span className="profile-today-denom">/{quest.target}</span></div>
        <div className="daily-quest-label">{done ? 'Complete' : quest.label}</div>
        <div className="daily-quest-progress"><div className="daily-quest-fill" style={{ width: pct + '%' }} /></div>
      </div>
    );
  }
  function AchievementIcon({ achv, size = 24 }) {
    return achv.actType ? <ActIcon type={achv.actType} size={size} /> : achv.ico;
  }
  function AchievementRow({ a, done }) {
    const rarityBadge = a.rarity && a.rarity !== 'common' ? a.rarity : null;
    return (
      <div key={a.id} className={'achv-item' + (done ? '' : ' locked') + (a.rarity ? ' rarity-' + a.rarity : '')}>
        <div className="achv-item-ico"><AchievementIcon achv={a} size={28} /></div>
        <div>
          <div className="achv-item-name">
            {a.name}
            {rarityBadge && <span className={'achv-rarity-badge ' + a.rarity}>{a.rarity}</span>}
          </div>
          <div className="achv-item-desc">{a.desc}</div>
          {done && <div className="achv-item-done">✓ Unlocked</div>}
          {!done && a.pointsReward && <div className="achv-item-reward">+{a.pointsReward} {SCORE_LABEL} on unlock</div>}
        </div>
      </div>
    );
  }

  return (
    <div ref={scrollRef} className="awards-scroll">
      {showStats && <div className="prof-stats-grid">
        <div className="prof-stat-tile blue">
          <div className="prof-stat-val">{todayActDealCount}<span className="prof-stat-denom">/{total}</span></div>
          <div className="prof-stat-lbl">Today's Touches</div>
        </div>
        <div className="prof-stat-tile green">
          <div className="prof-stat-val">{nextStepCount}<span className="prof-stat-denom">/{total}</span></div>
          <div className="prof-stat-lbl">Next Steps</div>
        </div>
        <div className="prof-stat-tile purple">
          <div className="prof-stat-val">{level}</div>
          <div className="prof-stat-lbl">{title}</div>
        </div>
        <div className="prof-stat-tile orange">
          <div className="prof-stat-val">{streak > 0 ? streak : '—'}</div>
          <div className="prof-stat-lbl">{streak > 0 ? 'Day Streak' : 'No Streak'}</div>
        </div>
      </div>}
      {showQuests && !!(dailyQuests && dailyQuests.length) && (() => {
        return (
          <div className="next-up-card" style={{ marginBottom: 14 }} data-testid="daily-quest-board">
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: questsOpen ? 9 : 0 }}>
              <div className="next-up-label" style={{ marginBottom: 0 }}>Daily Quest Board</div>
              <button
                className="pipeline-health-toggle"
                type="button"
                onClick={() => setQuestsOpen(o => !o)}
                aria-label={questsOpen ? 'Minimize daily quest board' : 'Expand daily quest board'}
                style={{ width: 26, height: 26, borderRadius: 8 }}
              >
                <span className={'collapse-chev' + (questsOpen ? ' up' : ' down')} aria-hidden="true" />
              </button>
            </div>
            <div className={'analytics-expand-wrap' + (questsOpen ? ' open' : '')}>
              <div className="analytics-expand-inner">
                {dailyQuests.map(q => {
                  const done = q.count >= q.target;
                  const pct = Math.min(100, Math.round((q.count / q.target) * 100));
                  return (
                    <div key={q.id} className={'prof-quest-row' + (done ? ' done' : '')}>
                      <div className={'prof-quest-check' + (done ? ' done' : '')}>{done ? '✓' : ''}</div>
                      <div className="prof-quest-body">
                        <div className="prof-quest-label">{q.label}</div>
                        {!done && (
                          <div className="prof-quest-bar-wrap">
                            <div className="prof-quest-bar-track">
                              <div className="prof-quest-bar-fill" style={{ width: pct + '%' }} />
                            </div>
                            <span className="prof-quest-count">{q.count}/{q.target}</span>
                          </div>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
        );
      })()}
      {showQuests && !!(weeklyQuests && weeklyQuests.length) && (() => {
        return (
          <div className="next-up-card" style={{ marginBottom: 14 }} data-testid="weekly-quest-board">
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: weeklyQuestsOpen ? 9 : 0 }}>
              <div className="next-up-label" style={{ marginBottom: 0 }}>Weekly Progress <span className="prof-card-meta">{weeklyDoneCount}/{weeklyQuests.length} complete</span></div>
              <button
                className="pipeline-health-toggle"
                type="button"
                onClick={() => setWeeklyQuestsOpen(o => !o)}
                aria-label={weeklyQuestsOpen ? 'Minimize weekly quest board' : 'Expand weekly quest board'}
                style={{ width: 26, height: 26, borderRadius: 8 }}
              >
                <span className={'collapse-chev' + (weeklyQuestsOpen ? ' up' : ' down')} aria-hidden="true" />
              </button>
            </div>
            <div className={'analytics-expand-wrap' + (weeklyQuestsOpen ? ' open' : '')}>
              <div className="analytics-expand-inner">
                {weeklyQuests.map(q => {
                  const done = q.count >= q.target;
                  const pct = Math.min(100, Math.round((q.count / q.target) * 100));
                  return (
                    <div key={q.id} className={'prof-quest-row' + (done ? ' done' : '')}>
                      <div className={'prof-quest-check' + (done ? ' done' : '')}>{done ? '✓' : ''}</div>
                      <div className="prof-quest-body">
                        <div className="prof-quest-label">{q.label}</div>
                        {!done && (
                          <div className="prof-quest-bar-wrap">
                            <div className="prof-quest-bar-track">
                              <div className="prof-quest-bar-fill" style={{ width: pct + '%' }} />
                            </div>
                            <span className="prof-quest-count">{q.count}/{q.target}</span>
                          </div>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
        );
      })()}
      {showStats && nextAchv && (() => {
        const prog = achvProgress(nextAchv);
        const pct  = Math.min(100, Math.round((prog.current / prog.target) * 100));
        return (
          <div className="next-up-card">
            <div className="next-up-label">Next Up</div>
            <div className="next-up-body">
              <div className="next-up-ico">
                {nextAchv.actType ? <ActIcon type={nextAchv.actType} size={26} /> : nextAchv.ico}
              </div>
              <div className="next-up-info">
                <div className="next-up-name">{nextAchv.name}</div>
                <div className="next-up-desc">{nextAchv.desc}</div>
                <div className="next-up-progress-wrap">
                  <div className="next-up-progress-track">
                    <div className="next-up-progress-fill" style={{ width: pct + '%' }} />
                  </div>
                  <span className="next-up-progress-num">{prog.current}/{prog.target}</span>
                </div>
              </div>
              {nextAchv.pointsReward && <div className="next-up-reward">+{nextAchv.pointsReward} {SCORE_LABEL}</div>}
            </div>
          </div>
        );
      })()}
      {showAchievements && <div className="achv-list">
        <div className="achv-org-card">
          <div className="achv-org-head" style={{ marginBottom: unlockedOpen ? 12 : 0 }}>
            <div className="achv-org-title">Unlocked Achievements</div>
            <button
              className="pipeline-health-toggle"
              type="button"
              onClick={() => setUnlockedOpen(o => !o)}
              aria-label={unlockedOpen ? 'Minimize achievements' : 'Expand achievements'}
              style={{ width: 26, height: 26, borderRadius: 8 }}
            >
              <span className={'collapse-chev' + (unlockedOpen ? ' up' : ' down')} aria-hidden="true" />
            </button>
          </div>
          <div className={'analytics-expand-wrap' + (unlockedOpen ? ' open' : '')}>
            <div className="analytics-expand-inner">
              {!unlockedGroups.length && <div className="achv-empty">No achievements unlocked yet</div>}
              {unlockedGroups.map(group => (
                <div key={group.name} className="achv-group">
                  <div className="achv-group-title"><span>{group.name}</span></div>
                  {group.items.map(a => <AchievementRow key={a.id} a={a} done={true} />)}
                </div>
              ))}
            </div>
          </div>
        </div>
        <div className="achv-org-card">
          <div className="achv-org-head" style={{ marginBottom: lockedOpen ? 12 : 0 }}>
            <div className="achv-org-title">Locked Achievements</div>
            <button
              className="pipeline-health-toggle"
              type="button"
              onClick={() => setLockedOpen(o => !o)}
              aria-label={lockedOpen ? 'Minimize locked achievements' : 'Expand locked achievements'}
              style={{ width: 26, height: 26, borderRadius: 8 }}
            >
              <span className={'collapse-chev' + (lockedOpen ? ' up' : ' down')} aria-hidden="true" />
            </button>
          </div>
          <div className={'analytics-expand-wrap' + (lockedOpen ? ' open' : '')}>
            <div className="analytics-expand-inner">
              {lockedAchv.map(a => <AchievementRow key={a.id} a={a} done={false} />)}
            </div>
          </div>
        </div>
      </div>}

    </div>
  );
}

// ── Profile / Account Screen ──────────────────────────────────────────────

function readProfilePref(key, fallback) {
  const raw = localStorage.getItem('pq_profile_pref_' + key);
  return raw === null ? fallback : raw;
}

function ProfileToggle({ on, label }) {
  return (
    <span className={'prof-toggle' + (on ? ' on' : '')} aria-label={label || (on ? 'On' : 'Off')}>
      <span />
    </span>
  );
}

function ProfileGlyph({ type, size = 20 }) {
  const c = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true' };
  if (type === 'target') return <ActIcon type="target" size={size} />;
  if (type === 'calendar') return <svg {...c}><rect x="4" y="5" width="16" height="15" rx="2" /><line x1="8" y1="3" x2="8" y2="7" /><line x1="16" y1="3" x2="16" y2="7" /><line x1="4" y1="10" x2="20" y2="10" /></svg>;
  if (type === 'method') return <svg {...c}><path d="M9 11l2 2 4-5" /><rect x="4" y="4" width="16" height="16" rx="3" /></svg>;
  if (type === 'refresh') return <svg {...c}><path d="M20 11a8 8 0 10-2.34 5.66" /><path d="M20 4v7h-7" /></svg>;
  if (type === 'retry') return <svg {...c}><path d="M3 12a9 9 0 0115.3-6.36" /><path d="M18 3v6h-6" /><path d="M21 12a9 9 0 01-15.3 6.36" /><path d="M6 21v-6h6" /></svg>;
  if (type === 'write') return <svg {...c}><path d="M12 20h9" /><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4 12.5-12.5z" /></svg>;
  if (type === 'demo') return <svg {...c}><rect x="3" y="4" width="18" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 18v3" /><path d="M8 9h8" /><path d="M8 13h5" /></svg>;
  if (type === 'disconnect') return <svg {...c}><path d="M18 8a5 5 0 010 8" /><path d="M6 8a5 5 0 000 8" /><path d="M8 12h8" /><path d="M3 3l18 18" /></svg>;
  if (type === 'sun') return <svg {...c}><circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="M4.93 4.93l1.41 1.41" /><path d="M17.66 17.66l1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="M4.93 19.07l1.41-1.41" /><path d="M17.66 6.34l1.41-1.41" /></svg>;
  if (type === 'goal') return <svg {...c}><path d="M12 21V9" /><path d="M7 14l5-5 5 5" /><path d="M5 21h14" /></svg>;
  if (type === 'help') return <svg {...c}><circle cx="12" cy="12" r="9" /><path d="M9.5 9a2.7 2.7 0 015 1.6c0 1.8-2.5 2.1-2.5 4" /><path d="M12 18h.01" /></svg>;
  if (type === 'feedback') return <svg {...c}><path d="M21 15a4 4 0 01-4 4H8l-5 3V7a4 4 0 014-4h10a4 4 0 014 4z" /><path d="M8 9h8" /><path d="M8 13h5" /></svg>;
  if (type === 'support') return <svg {...c}><path d="M4 12a8 8 0 0116 0" /><path d="M4 12v3a2 2 0 002 2h1v-6H6a2 2 0 00-2 2" /><path d="M20 12v3a2 2 0 01-2 2h-1v-6h1a2 2 0 012 2" /><path d="M14 19h2a4 4 0 004-4" /></svg>;
  if (type === 'version') return <svg {...c}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /><path d="M14 2v6h6" /><path d="M8 13h8" /><path d="M8 17h5" /></svg>;
  if (type === 'bell') return <svg {...c}><path d="M18 8a6 6 0 00-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9" /><path d="M13.73 21a2 2 0 01-3.46 0" /></svg>;
  if (type === 'quiet') return <svg {...c}><path d="M21 12.8A8.5 8.5 0 1111.2 3 6.5 6.5 0 0021 12.8z" /></svg>;
  if (type === 'export') return <svg {...c}><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" /><path d="M7 10l5 5 5-5" /><path d="M12 15V3" /></svg>;
  if (type === 'signout') return <svg {...c}><path d="M10 17l5-5-5-5" /><path d="M15 12H3" /><path d="M21 3v18" /></svg>;
  if (type === 'menu') return <svg {...c}><line x1="5" y1="7" x2="19" y2="7" /><line x1="5" y1="12" x2="19" y2="12" /><line x1="5" y1="17" x2="19" y2="17" /></svg>;
  return <ActIcon type="target" size={size} />;
}

function ProfileSection({ title, children, compact = false }) {
  return (
    <section className="profile-redesign-section">
      <div className="profile-redesign-section-title">{title}</div>
      <div className={'profile-redesign-card' + (compact ? ' compact' : '')}>{children}</div>
    </section>
  );
}

function ProfileMenuRow({ icon, tone = 'blue', title, sub, value, onClick, toggle, danger = false, actionLabel }) {
  const Tag = onClick ? 'button' : 'div';
  return (
    <Tag
      className={'profile-redesign-row' + (danger ? ' danger' : '')}
      onClick={onClick}
      type={onClick ? 'button' : undefined}
    >
      <span className={'profile-redesign-icon ' + tone}><ProfileGlyph type={icon} size={19} /></span>
      <span className="profile-redesign-row-main">
        <span>{title}</span>
        {sub && <em>{sub}</em>}
      </span>
      {toggle !== undefined ? (
        <ProfileToggle on={toggle} />
      ) : (
        <span className="profile-redesign-row-side">
          {actionLabel && <span className="profile-redesign-mini-btn">{actionLabel}</span>}
          {value && <strong>{value}</strong>}
          {onClick && !actionLabel && <i aria-hidden="true">›</i>}
        </span>
      )}
    </Tag>
  );
}

function ProfileInfoRow({ title, value, sub, action, onClick, danger = false }) {
  const Tag = onClick ? 'button' : 'div';
  return (
    <Tag className={'prof-settings-row prof-settings-row-rich' + (danger ? ' danger' : '')} onClick={onClick}>
      <span className="prof-settings-main">
        <span>{title}</span>
        {sub && <em>{sub}</em>}
      </span>
      <span className="prof-settings-val">{action || value}</span>
    </Tag>
  );
}

function ProfilePerformanceSection({ performance }) {
  if (!performance) return null;
  const {
    level = 1,
    points = 0,
    progressPct = 0,
    pointsToNext = 0,
    todayActDealCount = 0,
    openDealsCount = 0,
    nextStepCount = 0,
    forecastReadyCount = 0,
    streak = 0,
    weeklyDoneCount = 0,
    weeklyTotalCount = 0,
    periodModelEnabled = false,
    dailyActivityCount = 0,
    dailyActivityTarget = 5,
    weeklyExecutionPct = 0,
    quarterLabel = 'This quarter',
    quarterDealCount = 0,
    quarterForecastReadyCount = 0,
    quarterForecastReadyPct = 0,
    quarterPipelineHygieneCount = 0,
    quarterPipelineHygienePct = 0,
  } = performance;
  const totalDeals = Math.max(0, openDealsCount);
  const todayCoveragePct = totalDeals > 0 ? Math.round((todayActDealCount / totalDeals) * 100) : 0;
  const forecastReadyPct = totalDeals > 0 ? Math.round((forecastReadyCount / totalDeals) * 100) : 100;
  const nextStepPct = totalDeals > 0 ? Math.round((nextStepCount / totalDeals) * 100) : 100;
  const progressLabel = pointsToNext > 0 ? pointsToNext + ' points to next level' : 'Current level complete';
  const weeklyLabel = weeklyTotalCount > 0
    ? (weeklyDoneCount === 1 ? '1 weekly goal complete' : weeklyDoneCount + ' weekly goals complete')
    : 'No weekly targets yet';
  if (periodModelEnabled) {
    const dailyPct = dailyActivityTarget > 0 ? Math.min(100, Math.round((dailyActivityCount / dailyActivityTarget) * 100)) : 0;
    const lifetimeLabel = pointsToNext > 0 ? pointsToNext + ' points to next level' : 'Current level complete';
    const periodRows = [
      {
        key: 'today',
        label: 'Today',
        name: 'Activity momentum',
        pct: dailyPct,
        detail: dailyActivityCount === 1 ? '1 action completed today' : dailyActivityCount + ' actions completed today',
      },
      {
        key: 'week',
        label: 'This week',
        name: 'Execution momentum',
        pct: weeklyExecutionPct,
        detail: weeklyDoneCount === 1 ? '1 weekly goal complete' : weeklyDoneCount + ' weekly goals complete',
      },
      {
        key: 'forecast',
        label: quarterLabel,
        name: 'Forecast readiness',
        pct: quarterForecastReadyPct,
        detail: quarterForecastReadyCount > 0
          ? quarterForecastReadyCount + ' forecast-ready quarter ' + (quarterForecastReadyCount === 1 ? 'deal' : 'deals')
          : quarterDealCount > 0 ? 'No forecast-ready quarter deals yet' : 'No quarter deals yet',
      },
      {
        key: 'hygiene',
        label: quarterLabel,
        name: 'Pipeline hygiene',
        pct: quarterPipelineHygienePct,
        detail: quarterPipelineHygieneCount > 0
          ? quarterPipelineHygieneCount + ' healthy quarter ' + (quarterPipelineHygieneCount === 1 ? 'deal' : 'deals')
          : quarterDealCount > 0 ? 'No healthy quarter deals yet' : 'No quarter deals yet',
      },
    ];
    return (
      <ProfileSection title="Momentum">
        <div className="profile-performance-card period-model">
          <div className="profile-lifetime-card">
            <div className="profile-lifetime-main">
              <span>Lifetime</span>
              <strong>Level {level}</strong>
              <em>{lifetimeLabel}</em>
            </div>
            <div className="profile-lifetime-side">
              <strong>{points}</strong>
              <span>points</span>
            </div>
            <div className="profile-period-meter lifetime-meter" role="progressbar" aria-label="Lifetime level progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow={Math.round(progressPct)}>
              <span style={{ width: Math.max(4, Math.min(100, progressPct)) + '%' }} />
            </div>
          </div>
          <div className="profile-period-list">
            {periodRows.map(row => (
              <div className="profile-period-item" key={row.key}>
                <div className="profile-period-copy">
                  <span>{row.label}</span>
                  <strong>{row.name}</strong>
                  <em>{row.detail}</em>
                </div>
                <div className="profile-period-score">{row.pct}%</div>
                <div className="profile-period-meter" role="progressbar" aria-label={row.name} aria-valuemin="0" aria-valuemax="100" aria-valuenow={row.pct}>
                  <span style={{ width: Math.max(4, Math.min(100, row.pct)) + '%' }} />
                </div>
              </div>
            ))}
          </div>
          <div className="profile-performance-foot">
            <span>Level is lifetime and does not reset automatically.</span>
            <span>Daily, weekly, and quarterly measures refresh with their period.</span>
          </div>
        </div>
      </ProfileSection>
    );
  }
  return (
    <ProfileSection title="Momentum">
      <div className="profile-performance-card">
        <div className="profile-performance-head">
          <div>
            <span>Level {level}</span>
            <strong>Professional momentum</strong>
          </div>
          <em>{progressLabel}</em>
        </div>
        <div className="profile-performance-meter" role="progressbar" aria-label="Momentum" aria-valuemin="0" aria-valuemax="100" aria-valuenow={Math.round(progressPct)}>
          <span style={{ width: Math.max(4, Math.min(100, progressPct)) + '%' }} />
        </div>
        <div className="profile-performance-grid">
          <div className="profile-performance-metric">
            <strong>{todayCoveragePct}%</strong>
          <em>Activity momentum</em>
          </div>
          <div className="profile-performance-metric">
            <strong>{forecastReadyPct}%</strong>
            <em>Forecast readiness</em>
          </div>
          <div className="profile-performance-metric">
            <strong>{streak > 0 ? streak : '-'}</strong>
            <em>Execution streak</em>
          </div>
          <div className="profile-performance-metric">
            <strong>{forecastReadyPct}%</strong>
            <em>Pipeline hygiene</em>
          </div>
        </div>
        <div className="profile-performance-foot">
          <span>{todayActDealCount === 1 ? '1 open deal touched today' : todayActDealCount + ' open deals touched today'}</span>
          <span>Next steps set on {nextStepPct}% of open deals</span>
          <span>Momentum this week: {weeklyLabel}</span>
        </div>
      </div>
    </ProfileSection>
  );
}

function ProfileScreen({ userName, orgName, lastSync, theme, accountSession = null, quotaTargets = { quarterly: DEFAULT_QUARTERLY_QUOTA, yearly: DEFAULT_YEARLY_QUOTA }, targetTerm = 'quota', fiscalStartMonth = 0, meddicAvailable = true, salesMethodology = 'meddic', showMeddicSetting = false, syncPending = 0, syncFailures = [], environment = PQ_TARGET || 'local', salesforceInstanceUrl = '', salesforceReadOnly = false, isDemoMode = false, performance = null, onToggleMeddic, onSaveQuotas, onSaveTargetTerm, onSaveFiscalStartMonth, onSaveSalesMethodology, onToggleTheme, onToggleSalesforceReadOnly, onDisconnectSalesforce, onSaveName, onAccountSignOut, onDeleteAccount, onSignOut, onExportData, onRefresh, onRetryFailedSync, showDemoReset = false, onEnterDemoMode, onResetDemo, showLevelReset = false, onResetLevel }) {
  const scrollRef = useRef(null);
  usePullToRefresh(scrollRef, onRefresh || (() => {}));
  const [editingName, setEditingName] = useState(false);
  const [nameInput, setNameInput] = useState(userName);
  const [editingQuotas, setEditingQuotas] = useState(false);
  const [editingTargetTerm, setEditingTargetTerm] = useState(false);
  const [editingSalesMethodology, setEditingSalesMethodology] = useState(false);
  const [editingFiscalCalendar, setEditingFiscalCalendar] = useState(false);
  const [quarterlyQuotaInput, setQuarterlyQuotaInput] = useState(String(quotaTargets.quarterly || ''));
  const [yearlyQuotaInput, setYearlyQuotaInput] = useState(String(quotaTargets.yearly || ''));
  const [fiscalStartInput, setFiscalStartInput] = useState(String(normalizeFiscalStartMonth(fiscalStartMonth)));
  const [confirmSignOut, setConfirmSignOut] = useState(false);
  const [confirmDeleteAccount, setConfirmDeleteAccount] = useState(false);
  const [confirmDemoReset, setConfirmDemoReset] = useState(false);
  const [confirmLevelReset, setConfirmLevelReset] = useState(false);
  const initials = (userName || 'SR').split(/\s+/).map(w => w[0] || '').join('').slice(0, 2).toUpperCase() || 'SR';
  const lastSyncStr = lastSync ? (() => {
    const mins = Math.round((Date.now() - new Date(lastSync).getTime()) / 60000);
    if (mins < 1) return 'Just now';
    if (mins < 60) return mins + 'm ago';
    return Math.round(mins / 60) + 'h ago';
  })() : null;
  const syncStatus = syncFailures.length > 0 ? 'Needs attention' : syncPending > 0 ? 'Syncing' : lastSyncStr ? 'Connected' : 'Saved locally';
  const displayOrgName = (orgName || '').trim().toLowerCase() === 'stealth mode' ? '' : orgName;
  const connectionLabel = displayOrgName || 'Salesforce org';
  const envLabel = (environment || 'local').replace(/^./, ch => ch.toUpperCase());
  const normalizedFiscalStart = normalizeFiscalStartMonth(fiscalStartMonth);
  const fiscalInputMonth = normalizeFiscalStartMonth(fiscalStartInput);
  const normalizedTargetTerm = normalizeTargetTerm(targetTerm);
  const targetTermLower = targetTermLabel(normalizedTargetTerm);
  const targetTermTitle = targetTermLabel(normalizedTargetTerm, { title: true });
  const targetTermPluralTitle = targetTermLabel(normalizedTargetTerm, { title: true, plural: true });
  const selectedMethodology = salesMethodologyConfig(salesMethodology);
  function saveQuotas() {
    const quarterly = parseInt(quarterlyQuotaInput || '0', 10);
    const yearly = parseInt(yearlyQuotaInput || '0', 10);
    onSaveQuotas?.({
      quarterly: Number.isFinite(quarterly) && quarterly > 0 ? quarterly : DEFAULT_QUARTERLY_QUOTA,
      yearly: Number.isFinite(yearly) && yearly > 0 ? yearly : DEFAULT_YEARLY_QUOTA,
    });
    setEditingQuotas(false);
  }
  function saveTargetTerm(nextTerm) {
    onSaveTargetTerm?.(normalizeTargetTerm(nextTerm));
    setEditingTargetTerm(false);
  }
  function saveSalesMethodology(nextMethodology) {
    onSaveSalesMethodology?.(normalizeSalesMethodology(nextMethodology));
    setEditingSalesMethodology(false);
  }
  function saveFiscalCalendar() {
    const nextMonth = normalizeFiscalStartMonth(fiscalStartInput);
    onSaveFiscalStartMonth?.(nextMonth);
    setEditingFiscalCalendar(false);
  }
  const profileOrg = displayOrgName || 'Acme Sales';
  const connectionSub = lastSyncStr ? 'Last sync ' + lastSyncStr : 'Ready to sync';
  const identitySyncText = syncFailures.length > 0
    ? 'Salesforce sync needs attention'
    : syncPending > 0
      ? 'Salesforce syncing'
      : lastSyncStr
        ? 'Last successful sync ' + lastSyncStr
        : 'Salesforce ready';
  const signOutLabel = confirmSignOut ? 'Confirm sign out' : 'Sign out';
  function handleSignOut() {
    if (accountSession) {
      onAccountSignOut?.();
      return;
    }
    if (!confirmSignOut) {
      setConfirmSignOut(true);
      return;
    }
    onSignOut?.();
  }
  const failedSyncLabel = syncFailures.length === 1 ? '1 failed write-back' : syncFailures.length + ' failed write-backs';
  const demoModeSub = isDemoMode
    ? 'Seeded sample data · Salesforce writes off'
    : 'Seeded sample data for App Store review';
  function openSupportMail(subject) {
    const mailSubject = encodeURIComponent(subject || 'PipelineQuest support');
    const body = encodeURIComponent('\n\nApp: PipelineQuest ' + PQ_VERSION + '\nBuild: ' + PQ_BUILD + '\nEnvironment: ' + (environment || 'local'));
    window.location.href = 'mailto:support@pipelinequest.app?subject=' + mailSubject + '&body=' + body;
  }
  return (
    <div ref={scrollRef} className={'awards-scroll profile-scroll profile-redesign' + (performance ? ' profile-professional' : '')}>
      <section className="profile-redesign-identity">
        <div className="prof-avatar profile-redesign-avatar">{initials}</div>
        <div className="profile-redesign-identity-main">
          {editingName ? (
            <div className="prof-name-edit-row">
              <input
                className="prof-name-input"
                value={nameInput}
                onChange={e => setNameInput(e.target.value)}
                autoFocus
                onKeyDown={e => {
                  if (e.key === 'Enter') { onSaveName && onSaveName(nameInput.trim() || 'Sales Rep'); setEditingName(false); }
                  if (e.key === 'Escape') setEditingName(false);
                }}
              />
              <button className="prof-name-save" onClick={() => { onSaveName && onSaveName(nameInput.trim() || 'Sales Rep'); setEditingName(false); }}>Save</button>
              <button className="prof-name-cancel" onClick={() => setEditingName(false)}>✕</button>
            </div>
          ) : (
            <div className="prof-name" onClick={() => { setNameInput(userName); setEditingName(true); }}>
              {userName} <span className="prof-name-edit-ico">✎</span>
            </div>
          )}
          <div className="profile-redesign-role">{profileOrg}</div>
          <div className={'profile-redesign-sync-text' + (syncFailures.length ? ' warn' : '')}>{identitySyncText}</div>
        </div>
      </section>

      <ProfilePerformanceSection performance={performance} />

      <ProfileSection title="Targets">
        <ProfileMenuRow
          icon="target"
          tone="gold"
          title={targetTermTitle}
          sub={'Quarterly & yearly ' + targetTermLower + 's'}
          value={fmtMoney(quotaTargets.quarterly) + ' / ' + fmtMoney(quotaTargets.yearly)}
          onClick={() => {
            setQuarterlyQuotaInput(String(quotaTargets.quarterly || ''));
            setYearlyQuotaInput(String(quotaTargets.yearly || ''));
            setEditingQuotas(o => !o);
          }}
        />
        <ProfileMenuRow
          icon="calendar"
          tone="blue"
          title="Fiscal calendar"
          sub={'Year starts ' + MONTH_NAMES[normalizedFiscalStart]}
          onClick={() => {
            setFiscalStartInput(String(normalizedFiscalStart));
            setEditingFiscalCalendar(o => !o);
          }}
        />
        <ProfileMenuRow
          icon="method"
          tone="blue"
          title="Sales methodology"
          sub="Qualification framework"
          value={selectedMethodology.label}
          onClick={() => setEditingSalesMethodology(o => !o)}
        />
      </ProfileSection>

      {editingQuotas && (
        <div className="prof-settings-card profile-redesign-editor">
          <div className="prof-settings-hdr">Sales {targetTermPluralTitle}</div>
          <div className="prof-quota-editor">
            <label className="prof-quota-field">
              <span>Quarterly {targetTermLower}</span>
              <input
                className="prof-quota-input"
                inputMode="numeric"
                value={quarterlyQuotaInput}
                onChange={e => setQuarterlyQuotaInput(cleanQuotaInput(e.target.value))}
                placeholder="500000"
              />
            </label>
            <label className="prof-quota-field">
              <span>Yearly {targetTermLower}</span>
              <input
                className="prof-quota-input"
                inputMode="numeric"
                value={yearlyQuotaInput}
                onChange={e => setYearlyQuotaInput(cleanQuotaInput(e.target.value))}
                placeholder="2000000"
              />
            </label>
            <div className="prof-target-term-options" role="radiogroup" aria-label="Sales target term">
              {TARGET_TERM_OPTIONS.map(option => {
                const selected = normalizedTargetTerm === option;
                return (
                  <button
                    key={option}
                    type="button"
                    className={selected ? 'selected' : ''}
                    onClick={() => saveTargetTerm(option)}
                    aria-checked={selected}
                    role="radio"
                  >
                    {targetTermLabel(option, { title: true })}
                  </button>
                );
              })}
            </div>
            <div className="prof-quota-summary">
              Forecast uses {fmtMoney(parseInt(quarterlyQuotaInput || '0', 10) || DEFAULT_QUARTERLY_QUOTA)} for the current and next quarter {targetTermLower}, and {fmtMoney(parseInt(yearlyQuotaInput || '0', 10) || DEFAULT_YEARLY_QUOTA)} for the yearly {targetTermLower}.
            </div>
            <div className="prof-quota-actions">
              <button type="button" className="prof-quota-cancel" onClick={() => setEditingQuotas(false)}>Cancel</button>
              <button type="button" className="prof-quota-save" onClick={saveQuotas}>Save {targetTermPluralTitle}</button>
            </div>
          </div>
        </div>
      )}

      {editingFiscalCalendar && (
        <div className="prof-settings-card profile-redesign-editor">
          <div className="prof-settings-hdr">Fiscal Calendar</div>
          <div className="prof-fiscal-editor">
            <label className="prof-quota-field">
              <span>Fiscal year starts</span>
              <select
                className="prof-fiscal-select"
                value={String(fiscalInputMonth)}
                onChange={e => setFiscalStartInput(e.target.value)}
              >
                {MONTH_NAMES.map((month, index) => (
                  <option key={month} value={String(index)}>{month}</option>
                ))}
              </select>
            </label>
            <div className="prof-fiscal-preview">
              <span>{fiscalQuarterMonthSummary(fiscalInputMonth)}</span>
              <strong>Year-end: {MONTH_NAMES[fiscalYearEndMonth(fiscalInputMonth)]}</strong>
            </div>
            <div className="prof-quota-actions">
              <button type="button" className="prof-quota-cancel" onClick={() => setEditingFiscalCalendar(false)}>Cancel</button>
              <button type="button" className="prof-quota-save" onClick={saveFiscalCalendar}>Save Calendar</button>
            </div>
          </div>
        </div>
      )}

      {editingSalesMethodology && (
        <div className="prof-settings-card profile-redesign-editor">
          <div className="prof-settings-hdr">Sales Methodology</div>
          <div className="prof-methodology-editor">
            <div className="prof-methodology-options" role="radiogroup" aria-label="Sales methodology">
              {SALES_METHODOLOGY_OPTIONS.map(optionKey => {
                const option = SALES_METHODOLOGIES[optionKey];
                const selected = selectedMethodology.key === option.key;
                return (
                  <button
                    key={option.key}
                    type="button"
                    className={selected ? 'selected' : ''}
                    onClick={() => saveSalesMethodology(option.key)}
                    aria-checked={selected}
                    role="radio"
                  >
                    <span>{option.label}</span>
                    <em>{option.profileSub}</em>
                  </button>
                );
              })}
            </div>
            <div className="prof-quota-summary">
              Today qualification pills and review sheets will use {selectedMethodology.label}.
            </div>
            {showMeddicSetting && (
              <button
                className="profile-redesign-inline-toggle"
                type="button"
                onClick={onToggleMeddic}
                aria-pressed={meddicAvailable}
                data-testid="profile-meddic-toggle"
              >
                <span>Use qualification scoring</span>
                <ProfileToggle on={meddicAvailable} />
              </button>
            )}
          </div>
        </div>
      )}

      <ProfileSection title="Connection">
        <ProfileMenuRow
          icon="refresh"
          tone="green"
          title="Refresh Salesforce Data"
          sub={connectionSub + ' · pulls latest deals and activities'}
          actionLabel="Sync"
          onClick={onRefresh}
        />
        {syncFailures.length > 0 && (
          <ProfileMenuRow
            icon="retry"
            tone="blue"
            title="Retry Failed Write-backs"
            sub={failedSyncLabel}
            actionLabel="Retry"
            onClick={onRetryFailedSync}
          />
        )}
        <ProfileMenuRow
          icon="write"
          tone="slate"
          title="Write-back"
          sub={isDemoMode ? 'Locked off while using review demo data' : 'Push activity to Salesforce'}
          toggle={!salesforceReadOnly}
          onClick={isDemoMode ? undefined : onToggleSalesforceReadOnly}
        />
        <ProfileMenuRow
          icon="disconnect"
          tone="red"
          title="Disconnect Salesforce"
          sub="Clear connection setup and cached Salesforce data"
          actionLabel="Disconnect"
          danger
          onClick={onDisconnectSalesforce}
        />
      </ProfileSection>

      {showDemoReset && (
        <ProfileSection title="Review Demo Mode" compact>
          <ProfileMenuRow
            icon="demo"
            tone="green"
            title="App Store Review Demo"
            sub={demoModeSub}
            actionLabel={isDemoMode ? 'Active' : 'Start'}
            onClick={onEnterDemoMode}
          />
          <ProfileMenuRow
            icon="refresh"
            tone="blue"
            title={confirmDemoReset ? 'Confirm Reset Demo Data' : 'Reset Demo Data'}
            sub="Rebuild seeded opportunities and clear local progress"
            actionLabel={confirmDemoReset ? 'Confirm' : 'Reset'}
            onClick={() => {
              if (!confirmDemoReset) {
                setConfirmDemoReset(true);
                return;
              }
              onResetDemo?.();
            }}
          />
          {showLevelReset && (
            <ProfileMenuRow
              icon="goal"
              tone="slate"
              title={confirmLevelReset ? 'Confirm Reset Progress' : 'Reset Progress'}
              sub="Keep current Salesforce connection and return to level one"
              actionLabel={confirmLevelReset ? 'Confirm' : 'Reset'}
              onClick={() => {
                if (!confirmLevelReset) {
                  setConfirmLevelReset(true);
                  return;
                }
                onResetLevel?.();
              }}
            />
          )}
        </ProfileSection>
      )}

      <ProfileSection title="Support" compact>
        <ProfileMenuRow
          icon="help"
          tone="blue"
          title="Help"
          sub="Get setup or workflow help"
          onClick={() => openSupportMail('PipelineQuest help')}
        />
        <ProfileMenuRow
          icon="feedback"
          tone="green"
          title="Send feedback"
          sub="Share UX issues or requests"
          onClick={() => openSupportMail('PipelineQuest feedback')}
        />
        <ProfileMenuRow
          icon="support"
          tone="slate"
          title="Contact support"
          sub="Include app and build details"
          onClick={() => openSupportMail('PipelineQuest support')}
        />
      </ProfileSection>

      <ProfileSection title="App" compact>
        <ProfileMenuRow
          icon="version"
          tone="slate"
          title="Version"
          sub={'Build ' + PQ_BUILD}
          value={'v' + PQ_VERSION}
        />
      </ProfileSection>

      <ProfileSection title="Preferences" compact>
        <ProfileMenuRow
          icon="sun"
          tone="blue"
          title="Appearance"
          sub="Change app color mode"
          value={theme === 'dark' ? 'Dark' : 'Light'}
          onClick={onToggleTheme}
        />
        <ProfileMenuRow
          icon="bell"
          tone="blue"
          title="Notification nudges"
          sub="Daily reminders and streak prompts"
          value="On"
        />
        <ProfileMenuRow
          icon="quiet"
          tone="blue"
          title="Frequency / quiet mode"
          sub="Reminder cadence controls"
          value="Daily"
        />
      </ProfileSection>

      <ProfileSection title="Data" compact>
        <ProfileMenuRow
          icon="export"
          tone="slate"
          title="Export data"
          sub="Activity log + local progress JSON"
          actionLabel="JSON"
          onClick={onExportData}
        />
      </ProfileSection>

      <div className="profile-redesign-card profile-redesign-signout">
        <ProfileMenuRow
          icon="signout"
          tone="red"
          title={signOutLabel}
          danger
          onClick={handleSignOut}
        />
      </div>
    </div>
  );
}

// ── Activity Sheet ────────────────────────────────────────────────────────

function ActivitySheet({ card, onSelect, onClose, onFire }) {
  return (
    <div className="activity-sheet-backdrop" onClick={onClose}>
      <div className={'activity-sheet log-activity-sheet' + (IS_ACTIVITY_FOUR_OPTIONS_ENABLED ? ' four-option-sheet' : '')} onClick={e => e.stopPropagation()} data-testid="activity-sheet">
        <div className="activity-sheet-handle" />
        <div className="activity-sheet-title">Log activity</div>
        <div className="activity-sheet-sub">
          {card.deal.name} · {card.deal.stage} · {fmtMoney(card.deal.value)}
        </div>
        <div className={'activity-sheet-grid' + (IS_ACTIVITY_FOUR_OPTIONS_ENABLED ? ' four-options' : '')}>
          {LOG_ACTIVITY_TYPES.map(t => {
            return (
              <button key={t.name} className="activity-sheet-btn" onClick={() => onSelect(t.name)}>
                <span className="type-btn-ico"><ActIcon type={t.name} size={26} /></span>
                <div className="type-btn-name">{t.name}</div>
                <div className="type-btn-pts">+{t.pts} {SCORE_LABEL}</div>
              </button>
            );
          })}
        </div>
        <button className="activity-sheet-cancel" onClick={onClose}>Cancel</button>
      </div>
    </div>
  );
}

function StageSheet({ card, onSelect, onClose }) {
  if (!card) return null;
  const currentSf = card.deal.sfStage || '';
  return (
    <div className="activity-sheet-backdrop" onClick={onClose}>
      <div className="activity-sheet" onClick={e => e.stopPropagation()} data-testid="stage-sheet">
        <div className="activity-sheet-handle" />
        <div className="activity-sheet-title">Change sales stage</div>
        <div className="activity-sheet-sub">{card.deal.name}</div>
        <div className="stage-sheet-list">
          {SF_STAGES.map(({ sf, pq }) => {
            const isCurrent  = sf === currentSf;
            const forecast   = SF_STAGE_TO_FORECAST[sf] || '';
            const color      = FC_COLORS[forecast] || '#9ca3af';
            const badgeStyle = sf === 'Closed Won'
              ? { color: '#9a5b00', background: '#fff4d7', border: '1px solid #f9c76b' }
              : { color, background: color + '18', border: '1px solid ' + color + '44' };
            return (
              <button
                key={sf}
                className={'stage-sheet-btn' + (isCurrent ? ' current-group' : '')}
                onClick={() => onSelect(card.deal, sf)}
              >
                <span className="stage-sheet-name">{isCurrent && <span className="stage-current-check">✓ </span>}{sf}</span>
                <span className="stage-sheet-badge" style={badgeStyle}>
                  {forecast}
                </span>
              </button>
            );
          })}
        </div>
        <button className="activity-sheet-cancel" onClick={onClose}>Cancel</button>
      </div>
    </div>
  );
}

function NextStepSheet({ deal, activities = [], notes = [], apiSecret = '', salesforceInstanceUrl = '', aiEnabled = false, onSave, onClose }) {
  const [value, setValue] = useState(deal?.nextStep || '');
  const [listening, setListening] = useState(false);
  const [inputFocused, setInputFocused] = useState(false);
  const [keyboardInset, setKeyboardInset] = useState(0);
  const [aiSuggestions, setAiSuggestions] = useState([]);
  const [aiLoading, setAiLoading] = useState(false);
  const [aiError, setAiError] = useState('');
  const inputRef = useRef(null);
  const savingRef = useRef(false);
  const recognitionRef = useRef(null);
  const dictationBaseRef = useRef('');
  const aiRequestRef = useRef(0);
  const hasExistingNextStep = !!(deal?.nextStep || '').trim();
  const isValueNonEmpty = value.trim().length > 0;

  useLayoutEffect(() => {
    const el = inputRef.current;
    if (!el) return;
    function resetCaretToStart() {
      el.scrollLeft = 0;
      el.scrollTop = 0;
      try { el.setSelectionRange(0, 0); } catch (err) {}
    }
    requestAnimationFrame(resetCaretToStart);
    const t1 = setTimeout(resetCaretToStart, 60);
    const t2 = setTimeout(resetCaretToStart, 180);
    return () => {
      clearTimeout(t1);
      clearTimeout(t2);
    };
  }, [deal?.id]);

  useEffect(() => {
    if (!inputFocused) {
      setKeyboardInset(0);
      return;
    }
    const vv = window.visualViewport;
    function updateKeyboardInset() {
      if (!vv) {
        setKeyboardInset(0);
        return;
      }
      const inset = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
      setKeyboardInset(inset > 80 ? inset : 0);
    }
    updateKeyboardInset();
    vv?.addEventListener('resize', updateKeyboardInset);
    vv?.addEventListener('scroll', updateKeyboardInset);
    window.addEventListener('resize', updateKeyboardInset);
    return () => {
      vv?.removeEventListener('resize', updateKeyboardInset);
      vv?.removeEventListener('scroll', updateKeyboardInset);
      window.removeEventListener('resize', updateKeyboardInset);
    };
  }, [inputFocused]);

  useEffect(() => () => {
    try { recognitionRef.current?.stop?.(); } catch (err) {}
  }, []);

  useEffect(() => {
    setAiSuggestions([]);
    setAiError('');
    setAiLoading(false);
    aiRequestRef.current += 1;
  }, [deal?.id]);

  function focusNextStepInput({ end = true } = {}) {
    const el = inputRef.current;
    if (!el) return;
    try { el.focus({ preventScroll: true }); } catch (err) { el.focus(); }
    if (end) {
      const len = el.value.length;
      try { el.setSelectionRange(len, len); } catch (err) {}
    }
  }

  function toggleMic(e) {
    e?.stopPropagation?.();
    e?.preventDefault?.();
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    focusNextStepInput();
    if (listening) {
      try { recognitionRef.current?.stop?.(); } catch (err) {}
      setListening(false);
      return;
    }
    if (!SpeechRecognition) return;
    let recognition;
    try {
      recognition = new SpeechRecognition();
    } catch (err) {
      setListening(false);
      return;
    }
    dictationBaseRef.current = value.trim();
    recognition.continuous = false;
    recognition.interimResults = true;
    recognition.lang = navigator.language || 'en-US';
    recognition.onresult = event => {
      const transcript = Array.from(event.results || [], result => result[0]?.transcript || '')
        .join(' ')
        .trim();
      if (!transcript) return;
      const base = dictationBaseRef.current;
      setValue(base ? base + ' ' + transcript : transcript);
    };
    recognition.onend = () => setListening(false);
    recognition.onerror = () => {
      setListening(false);
      focusNextStepInput();
    };
    recognitionRef.current = recognition;
    setListening(true);
    try {
      recognition.start();
    } catch (err) {
      setListening(false);
      focusNextStepInput();
    }
  }

  function saveFromSheet(e) {
    e?.preventDefault?.();
    e?.stopPropagation?.();
    if (savingRef.current || !deal) return;
    const nextValue = inputRef.current?.value ?? value;
    if (!nextValue.trim()) return;
    savingRef.current = true;
    try { recognitionRef.current?.stop?.(); } catch (err) {}
    onSave(deal.id, nextValue);
  }

  async function readApiSecret() {
    if (apiSecret) return apiSecret;
    const response = await fetch('/api/config');
    const data = await response.json().catch(() => ({}));
    return data.apiSecret || '';
  }

  async function suggestNextSteps(e) {
    e?.preventDefault?.();
    e?.stopPropagation?.();
    if (!deal || aiLoading) return;
    const requestId = aiRequestRef.current + 1;
    aiRequestRef.current = requestId;
    setAiLoading(true);
    setAiError('');
    setAiSuggestions([]);
    try { inputRef.current?.blur?.(); } catch (err) {}
    try {
      const secret = await readApiSecret();
      if (!secret) throw new Error('AI suggestions are not configured yet.');
      const headers = {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + secret,
      };
      if (salesforceInstanceUrl) headers['X-PQ-SF-Instance-URL'] = salesforceInstanceUrl;
      const response = await fetch('/api/next-step-suggestions', {
        method: 'POST',
        headers,
        body: JSON.stringify({
          deal: {
            id: deal.id,
            name: deal.name,
            account: deal.contact,
            stage: deal.stage,
            sfStage: deal.sfStage,
            forecastCategory: deal.forecastCategory,
            value: deal.value,
            closeDate: deal.closeDate,
            nextStep: value,
            nextStepHistory: deal.nextStepHistory || [],
            notes,
            lastActivityDate: deal.lastActivityDate,
          },
          activities,
        }),
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(data?.error?.message || 'Could not generate suggestions.');
      if (requestId !== aiRequestRef.current) return;
      const suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
      setAiSuggestions(suggestions);
      if (!suggestions.length) setAiError('No suggestions returned.');
    } catch (err) {
      if (requestId !== aiRequestRef.current) return;
      setAiError(err.message || 'Could not generate suggestions.');
    } finally {
      if (requestId === aiRequestRef.current) setAiLoading(false);
    }
  }

  function chooseAiSuggestion(suggestion) {
    const nextValue = suggestion?.nextStep || '';
    if (!nextValue) return;
    setValue(nextValue);
    setAiError('');
    requestAnimationFrame(() => focusNextStepInput({ end: true }));
  }

  if (!deal) return null;
  return (
    <div
      className={'next-step-sheet-backdrop' + (keyboardInset > 0 ? ' keyboard-open' : '')}
      style={{ '--keyboard-inset': keyboardInset + 'px' }}
      onClick={onClose}
    >
      <div className="activity-sheet next-step-sheet" onClick={e => e.stopPropagation()} data-testid="next-step-sheet">
        <div className="activity-sheet-handle" />
        <div className="next-step-sheet-head">
          <div className="next-step-sheet-title-wrap">
            <div className="activity-sheet-title">Next Step</div>
            <div className="activity-sheet-sub">{deal.name}</div>
          </div>
          <button
            type="button"
            className={'next-step-mic-btn' + (listening ? ' listening' : '')}
            onClick={toggleMic}
            title={listening ? 'Stop dictation' : 'Dictate next step'}
            aria-label={listening ? 'Stop dictation' : 'Dictate next step'}
          >
            <ActIcon type="mic" size={20} />
          </button>
        </div>
        <div className="next-step-input-wrap">
          <textarea
            ref={inputRef}
            data-next-step-input="true"
            className="next-step-sheet-input"
            value={value}
            onChange={e => setValue(e.target.value)}
            onFocus={() => setInputFocused(true)}
            onBlur={() => setInputFocused(false)}
            placeholder="Add next step..."
            enterKeyHint="done"
          />
        </div>
        {aiEnabled && (
          <div className="next-step-ai-panel">
            <button
              type="button"
              className={'next-step-ai-btn' + (aiLoading ? ' loading' : '')}
              onClick={suggestNextSteps}
              disabled={aiLoading}
            >
              {aiLoading ? 'Thinking...' : 'Suggest next steps'}
            </button>
            {aiError && <div className="next-step-ai-error">{aiError}</div>}
            {aiSuggestions.length > 0 && (
              <div className="next-step-ai-list">
                {aiSuggestions.map((suggestion, index) => (
                  <button
                    key={index}
                    type="button"
                    className="next-step-ai-option"
                    onClick={() => chooseAiSuggestion(suggestion)}
                  >
                    <span>{suggestion.title || 'Suggested next step'}</span>
                    <strong>{suggestion.nextStep}</strong>
                    {suggestion.reason && <em>{suggestion.reason}</em>}
                  </button>
                ))}
              </div>
            )}
          </div>
        )}
        <div className="next-step-sheet-actions">
          <button className="next-step-sheet-cancel" onClick={onClose}>Cancel</button>
          <button
            className="next-step-sheet-save"
            onPointerDown={saveFromSheet}
            onClick={saveFromSheet}
            aria-disabled={!isValueNonEmpty}
          >Save</button>
        </div>
      </div>
    </div>
  );
}

function MeddicSheet({ deal, value, salesMethodology = 'meddic', onToggle, onClose }) {
  if (!deal) return null;
  const method = salesMethodologyConfig(salesMethodology);
  const summary = meddicSummary(value, method.key);
  return (
    <div className="next-step-sheet-backdrop" onClick={onClose}>
      <div className="activity-sheet meddic-sheet" onClick={e => e.stopPropagation()} data-testid="meddic-sheet">
        <div className="activity-sheet-handle" />
        <div className="meddic-sheet-head">
          <div className="next-step-sheet-title-wrap">
            <div className="activity-sheet-title">{method.label}</div>
            <div className="activity-sheet-sub">{deal.name}</div>
          </div>
          <div className={'deal-meddic-pill meddic-sheet-score ' + summary.tone}>{method.pillLabel}</div>
        </div>
        <div className="meddic-progress" aria-hidden="true">
          <span style={{ width: Math.round((summary.completed / summary.total) * 100) + '%' }} />
        </div>
        <div className="meddic-grid">
          {method.fields.map(field => {
            const checked = summary.value[field.key] === true;
            return (
              <button
                key={field.key}
                type="button"
                className={'meddic-toggle' + (checked ? ' checked' : '')}
                onClick={() => onToggle(deal.id, field.key)}
                aria-pressed={checked}
              >
                <span className="meddic-toggle-mark">{field.short}</span>
                <span className="meddic-toggle-copy">
                  <strong>{field.label}</strong>
                  <em>{field.prompt}</em>
                </span>
                <span className="meddic-toggle-state">{checked ? 'Done' : 'Open'}</span>
              </button>
            );
          })}
        </div>
        <button className="activity-sheet-cancel" onClick={onClose}>Done</button>
      </div>
    </div>
  );
}

function CloseDateSheet({ deal, onSave, onClose }) {
  function isoFromDate(date) {
    const y = date.getFullYear();
    const m = String(date.getMonth() + 1).padStart(2, '0');
    const d = String(date.getDate()).padStart(2, '0');
    return `${y}-${m}-${d}`;
  }
  function parseDate(value) {
    const d = new Date((value || isoFromDate(new Date())) + 'T00:00:00');
    return Number.isNaN(d.getTime()) ? new Date() : d;
  }
  const initialDate = parseDate(deal?.closeDate);
  const [value, setValue] = useState(isoFromDate(initialDate));
  const [viewDate, setViewDate] = useState(new Date(initialDate.getFullYear(), initialDate.getMonth(), 1));
  if (!deal) return null;
  const year = viewDate.getFullYear();
  const month = viewDate.getMonth();
  const firstDay = new Date(year, month, 1).getDay();
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7;
  const selected = parseDate(value);
  const monthLabel = viewDate.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
  const weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
  const todayIso = isoFromDate(new Date());
  function moveMonth(delta) {
    setViewDate(prev => new Date(prev.getFullYear(), prev.getMonth() + delta, 1));
  }
  function setToday() {
    const today = new Date();
    setValue(isoFromDate(today));
    setViewDate(new Date(today.getFullYear(), today.getMonth(), 1));
  }
  return (
    <div className="next-step-sheet-backdrop" onClick={onClose}>
      <div className="activity-sheet" onClick={e => e.stopPropagation()} data-testid="close-date-sheet">
        <div className="activity-sheet-handle" />
        <div className="activity-sheet-title">Close Date</div>
        <div className="activity-sheet-sub">{deal.name}</div>
        <div className="close-date-calendar">
          <div className="close-calendar-head">
            <button className="close-calendar-nav" onClick={() => moveMonth(-1)} aria-label="Previous month">‹</button>
            <div className="close-calendar-month">{monthLabel}</div>
            <button className="close-calendar-nav" onClick={() => moveMonth(1)} aria-label="Next month">›</button>
          </div>
          <div className="close-calendar-grid">
            {weekdays.map((d, i) => <div key={d + i} className="close-calendar-dow">{d}</div>)}
            {Array.from({ length: totalCells }).map((_, i) => {
              const day = i - firstDay + 1;
              if (day < 1 || day > daysInMonth) return <div key={i} className="close-calendar-day blank" />;
              const dayDate = new Date(year, month, day);
              const iso = isoFromDate(dayDate);
              const isSelected =
                selected.getFullYear() === year &&
                selected.getMonth() === month &&
                selected.getDate() === day;
              const isToday = iso === todayIso;
              return (
                <button
                  key={iso}
                  className={'close-calendar-day' + (isSelected ? ' selected' : '') + (isToday ? ' today' : '')}
                  onClick={() => setValue(iso)}
                >
                  {day}
                </button>
              );
            })}
          </div>
        </div>
        <div className="close-date-quick-row">
          <button className="close-date-today-btn" type="button" onClick={setToday}>Today</button>
          <input
            className="close-date-input"
            type="date"
            value={value}
            onChange={e => {
              setValue(e.target.value);
              const typed = parseDate(e.target.value);
              setViewDate(new Date(typed.getFullYear(), typed.getMonth(), 1));
            }}
            aria-label="Close date"
          />
        </div>
        <div className="next-step-sheet-actions">
          <button className="next-step-sheet-cancel" onClick={onClose}>Cancel</button>
          <button className="next-step-sheet-save" onClick={() => onSave(deal.id, value)}>Save</button>
        </div>
      </div>
    </div>
  );
}

function AmountSheet({ deal, onSave, onClose }) {
  const [value, setValue] = useState(String(Math.max(0, Number(deal?.value) || 0)));
  const [error, setError] = useState('');
  const [keyboardInset, setKeyboardInset] = useState(0);
  useEffect(() => {
    const vv = window.visualViewport;
    function updateKeyboardInset() {
      if (!vv) {
        setKeyboardInset(0);
        return;
      }
      const overlap = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
      setKeyboardInset(overlap > 80 ? overlap : 0);
    }
    updateKeyboardInset();
    vv?.addEventListener('resize', updateKeyboardInset);
    vv?.addEventListener('scroll', updateKeyboardInset);
    window.addEventListener('resize', updateKeyboardInset);
    return () => {
      vv?.removeEventListener('resize', updateKeyboardInset);
      vv?.removeEventListener('scroll', updateKeyboardInset);
      window.removeEventListener('resize', updateKeyboardInset);
    };
  }, []);
  if (!deal) return null;
  const parsedAmount = parseDealAmountInput(value);
  const canSave = parsedAmount !== null;
  function save() {
    if (!canSave) {
      setError('Enter a valid deal amount.');
      return;
    }
    onSave(deal.id, parsedAmount);
  }
  return (
    <div className={'next-step-sheet-backdrop amount-sheet-backdrop' + (keyboardInset > 0 ? ' keyboard-open' : '')} onClick={onClose}>
      <div className="activity-sheet next-step-sheet amount-sheet" onClick={e => e.stopPropagation()} data-testid="amount-sheet">
        <div className="activity-sheet-handle" />
        <div className="score-sheet-head">
          <div className="score-sheet-title-wrap">
            <div className="activity-sheet-title score-sheet-title">Deal Amount</div>
            <div className="activity-sheet-sub score-sheet-meta">{deal.name}</div>
          </div>
          <div className="stage-sheet-badge amount-sheet-current">{fmtMoney(deal.value)}</div>
        </div>
        <label className="forecast-call-field">
          <span>Amount</span>
          <input
            className="forecast-call-input"
            type="text"
            inputMode="numeric"
            pattern="[0-9]*"
            value={value}
            onChange={e => {
              setValue(cleanQuotaInput(e.target.value));
              setError('');
            }}
            onKeyDown={e => {
              if (e.key === 'Enter') save();
              if (e.key === 'Escape') onClose();
            }}
            aria-label={'Deal amount for ' + deal.name}
            autoFocus
          />
        </label>
        {error && <div className="forecast-call-error">{error}</div>}
        <div className="forecast-call-actions">
          <button type="button" className="next-step-sheet-cancel forecast-call-cancel" onClick={onClose}>Cancel</button>
          <button type="button" className="next-step-sheet-save forecast-call-save" onClick={save} disabled={!canSave}>Save</button>
        </div>
      </div>
    </div>
  );
}

// ── Activity Celebration ──────────────────────────────────────────────────

function ActivityCelebration({ burst }) {
  const colors = ['#2f80ed', '#f4c542', '#ffffff', '#9ee7d8', '#5aa2ff', '#f8d36a'];
  const ACT_TYPE_SET = new Set(['Email','Call','Meeting','Demo','Proposal']);
  const toneClass = (burst.tone || burst.type || '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
  const iconEl = burst.tone === 'left'
    ? <span style={{fontSize:'38px',lineHeight:1}}>✕</span>
    : ACT_TYPE_SET.has(burst.type)
      ? <ActIcon type={burst.type} size={42} />
      : <span style={{fontSize:'38px',lineHeight:1}}>{burst.icon || '✓'}</span>;
  const sparks = Array.from({ length: 78 }, (_, i) => {
    const wide = i % 4 !== 0;
    const col = i % 13;
    const row = Math.floor(i / 13);
    const x = 2 + col * 8;
    const drift = ((col % 5) - 2) * 14 + (row % 2 ? 10 : -10);
    return {
      id: i,
      x,
      drift,
      color: colors[i % colors.length],
      delay: row * 0.1 + col * 0.018,
      duration: 2.45 + (row % 3) * 0.18,
      size: wide ? 4 + (i % 3) * 2 : 6 + (i % 3),
      height: wide ? 10 + (i % 3) * 3 : 6 + (i % 3),
      radius: wide ? '2px' : '999px',
      rot: i * 31,
      spin: 360 + (i % 5) * 70,
      dot: !wide,
    };
  });
  return (
    <div className="activity-burst">
      {sparks.map(s => (
        <div key={s.id} className={'activity-spark' + (s.dot ? ' dot' : '')} style={{
          '--spark-x': s.x + '%', '--spark-drift': s.drift + 'px', '--spark-color': s.color,
          '--spark-delay': s.delay + 's', '--spark-duration': s.duration + 's',
          '--spark-size': s.size + 'px', '--spark-height': s.height + 'px',
          '--spark-radius': s.radius, '--spark-rot': s.rot + 'deg', '--spark-spin': s.spin + 'deg',
        }} />
      ))}
      <div className={'activity-burst-card' + (toneClass ? ' ' + toneClass : '')}>
        <div className="activity-burst-icon">{iconEl}</div>
        <div className="activity-burst-title">{burst.label || burst.type}</div>
        {burst.sub && <div className="activity-burst-sub">{burst.sub}</div>}
        {burst.pts > 0 && <div className="activity-burst-sub">+{burst.pts} {SCORE_LABEL}</div>}
      </div>
    </div>
  );
}

function BurstStack({ stack }) {
  if (!stack.length) return null;
  const ACT_TYPE_SET = new Set(['Email','Call','Meeting','Demo','Proposal']);
  return (
    <div className="burst-stack">
      {stack.map((burst, i) => {
        const isFirst = i === 0;
        const toneClass = (burst.tone || burst.type || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
        const iconEl = burst.tone === 'left'
          ? <span style={{ fontSize: isFirst ? '17px' : '14px', lineHeight: 1 }}>✕</span>
          : ACT_TYPE_SET.has(burst.type)
            ? <ActIcon type={burst.type} size={isFirst ? 20 : 17} />
            : <span style={{ fontSize: isFirst ? '17px' : '14px', lineHeight: 1 }}>{burst.icon || '✓'}</span>;
        const metaParts = [];
        if (burst.pts > 0) metaParts.push('+' + burst.pts + ' ' + SCORE_LABEL);
        if (burst.sub) metaParts.push(burst.sub);
        return (
          <div key={burst.id} className={'burst-pill ' + toneClass + (isFirst ? ' first' : '') + (burst.exiting ? ' exiting' : '')}>
            <div className="burst-pill-icon">{iconEl}</div>
            <div className="burst-pill-body">
              <div className="burst-pill-label">{burst.label || burst.type}</div>
              {metaParts.length > 0 && <div className="burst-pill-meta">{metaParts.join(' · ')}</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function UndoBar({ action, onUndo }) {
  if (!action) return null;
  const label = action.kind === 'skip'
    ? 'Skipped ' + action.card.deal.name
    : action.kind === 'log'
      ? action.type + ' ready to log'
      : action.card.action + ' marked done';
  return (
    <div className="undo-bar">
      <div className="undo-copy">
        <div>{label}</div>
        <div className="undo-sub">Committing in a moment</div>
      </div>
      <button className="undo-btn" onClick={onUndo}>Undo</button>
    </div>
  );
}

// ── Closed Won Celebration ────────────────────────────────────────────────

function ClosedWonCelebration({ deal }) {
  return <ActivityCelebration burst={{ type: 'Closed Won', pts: 100, label: 'Deal Closed!', icon: '🏆', sub: deal.name + ' — ' + fmtMoney(deal.value), kicker: 'Achievement Unlocked!' }} />;
}

// ── Pipeline Cleared Celebration ──────────────────────────────────────────

function PipelineClearedCelebration({ data, onDismiss, onReload }) {
  const [animatedPct, setAnimatedPct] = useState(0);
  const [settled, setSettled] = useState(false);
  useEffect(() => {
    const t = setTimeout(() => {
      if (IS_PIPELINE_CLEARED_V2_ENABLED) setSettled(true);
      else onDismiss();
    }, 5000);
    return () => clearTimeout(t);
  }, [onDismiss]);

  const clearPct = Math.max(0, Math.min(100, Number(data.clearPct ?? 100)));
  useEffect(() => {
    if (!IS_PIPELINE_CLEARED_V2_ENABLED) return;
    let frame = null;
    const startedAt = performance.now();
    const duration = 980;
    function tick(now) {
      const progress = Math.min(1, (now - startedAt) / duration);
      const eased = 1 - Math.pow(1 - progress, 3);
      setAnimatedPct(Math.round(clearPct * eased));
      if (progress < 1) frame = requestAnimationFrame(tick);
    }
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [clearPct]);

  const colors = ['#fbbf24','#38bdf8','#ffffff','#4ade80','#f472b6','#fde68a','#7dd3fc'];
  const pieces = Array.from({ length: 76 }, (_, i) => {
    const wide = i % 3 !== 0;
    return {
      id: i,
      x: (2 + (i % 19) * 5.2) + '%',
      color: colors[i % colors.length],
      w: wide ? (4 + i % 4) + 'px' : (5 + i % 3) + 'px',
      h: wide ? (11 + i % 5) + 'px' : (5 + i % 3) + 'px',
      r: wide ? '2px' : '999px',
      dur: (2.1 + (i % 6) * 0.2) + 's',
      delay: ((i % 14) * 0.055) + 's',
      spin: (200 + i * 23) + 'deg',
    };
  });

  const title = (data.title || 'Congratulations').replace(/[.!?]*$/, '') + '!';
  const dealsLabel = data.subtitle || (data.doneCount === 1 ? '1 deal touched' : data.doneCount + ' deals touched');
  const summary = data.summary?.length ? data.summary : [
    { key: 'done', label: data.doneCount === 1 ? 'Activity completed' : 'Activities completed', value: data.doneCount || 0, tone: 'blue' },
  ].filter(item => item.value > 0);

  return (
    <div className={'pipeline-cleared-overlay' + (settled ? ' settled' : '')} onClick={onDismiss} data-testid="pipeline-cleared-celebration">
      {!settled && pieces.map(p => (
        <div key={p.id} className="pc-confetti-piece" style={{
          '--pc-x': p.x, '--pc-color': p.color,
          '--pc-w': p.w, '--pc-h': p.h, '--pc-r': p.r,
          '--pc-dur': p.dur, '--pc-delay': p.delay, '--pc-spin': p.spin,
        }} />
      ))}
      <div className="pipeline-cleared-card" onClick={e => e.stopPropagation()}>
        {IS_PIPELINE_CLEARED_V2_ENABLED ? (
          <>
            <div className="pipeline-cleared-ring" style={{ '--pc-ring': (animatedPct * 3.6) + 'deg' }} aria-label={animatedPct + ' percent cleared'}>
              <div className="pipeline-cleared-ring-inner">
                <strong>{animatedPct}%</strong>
                <span>cleared</span>
              </div>
            </div>
            <div className="pipeline-cleared-title animated">{title}</div>
            <div className="pipeline-cleared-sub">{dealsLabel} today</div>
            <div className="pipeline-cleared-summary" data-testid="pipeline-cleared-summary">
              {summary.map(item => (
                <div key={item.key} className={'pipeline-cleared-summary-item ' + (item.tone || 'blue')}>
                  <div className="pipeline-cleared-summary-value">{item.value}</div>
                  <div className="pipeline-cleared-summary-label">{item.label}</div>
                </div>
              ))}
            </div>
            <div className="pipeline-cleared-stats compact">
              <div className="pipeline-cleared-stat">
                <div className="pipeline-cleared-stat-val">+{data.pointsToday}</div>
                <div className="pipeline-cleared-stat-lbl">Points Today</div>
              </div>
              <div className="pipeline-cleared-stat">
                <div className="pipeline-cleared-stat-val">+{data.bonusPts}</div>
                <div className="pipeline-cleared-stat-lbl">Clear Bonus</div>
              </div>
            </div>
            <button
              type="button"
              className="pipeline-cleared-reload"
              onClick={e => {
                e.stopPropagation();
                onReload?.();
              }}
            >
              Reload Current Deals
            </button>
          </>
        ) : (
          <>
            <div className="pipeline-cleared-stars">⭐</div>
            <div className="pipeline-cleared-title">{title}</div>
            <div className="pipeline-cleared-sub">{dealsLabel} today</div>
            <div className="pipeline-cleared-stats">
              <div className="pipeline-cleared-stat">
                <div className="pipeline-cleared-stat-val">+{data.pointsToday}</div>
                <div className="pipeline-cleared-stat-lbl">Points Today</div>
              </div>
              {data.streak > 0 && (
                <div className="pipeline-cleared-stat">
                  <div className="pipeline-cleared-stat-val">{data.streak} 🔥</div>
                  <div className="pipeline-cleared-stat-lbl">Day Streak</div>
                </div>
              )}
              <div className="pipeline-cleared-stat">
                <div className="pipeline-cleared-stat-val">+{data.bonusPts}</div>
                <div className="pipeline-cleared-stat-lbl">Bonus Points</div>
              </div>
            </div>
            <button
              type="button"
              className="pipeline-cleared-reload"
              onClick={e => {
                e.stopPropagation();
                onReload?.();
              }}
            >
              Reload Current Deals
            </button>
          </>
        )}
        <div className="pipeline-cleared-tap">Tap anywhere to continue</div>
      </div>
    </div>
  );
}

// ── Loading / Error ───────────────────────────────────────────────────────

function LoadingScreen() {
  return (
    <div style={{ display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', height:'100%', gap:16 }}>
      <div style={{ width:40, height:40, border:'4px solid #dbeafe', borderTopColor:'#176bb5', borderRadius:'50%', animation:'spin 0.8s linear infinite' }} />
      <div style={{ fontSize:14, color:'#64748b', fontWeight:600 }}>Loading from Salesforce...</div>
      <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
    </div>
  );
}

function ErrorScreen({ error, onRetry }) {
  const [spinning, setSpinning] = React.useState(false);
  function handleRetry() {
    setSpinning(true);
    setTimeout(() => { setSpinning(false); onRetry(); }, 400);
  }
  return (
    <div className="err-screen">
      <div className="err-dog-wrap">
        {/* Cartoon dog SVG */}
        <svg viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
          {/* Body */}
          <ellipse cx="80" cy="112" rx="38" ry="28" fill="#f5c87a"/>
          {/* Head */}
          <ellipse cx="80" cy="72" rx="32" ry="30" fill="#f5c87a"/>
          {/* Left ear (floppy) */}
          <path d="M51 58 Q38 38 48 26 Q54 44 58 56Z" fill="#e8a84a"/>
          {/* Right ear (floppy) */}
          <path d="M109 58 Q122 38 112 26 Q106 44 102 56Z" fill="#e8a84a"/>
          {/* Snout */}
          <ellipse cx="80" cy="84" rx="16" ry="12" fill="#f9dfa0"/>
          {/* Nose */}
          <ellipse cx="80" cy="78" rx="7" ry="5" fill="#2d2d2d"/>
          {/* Nose highlight */}
          <ellipse cx="77" cy="76.5" rx="2" ry="1.5" fill="white" opacity="0.6"/>
          {/* Left eye */}
          <circle cx="66" cy="66" r="6" fill="white"/>
          <circle cx="66" cy="67" r="3.5" fill="#2d2d2d"/>
          <circle cx="64.5" cy="65.5" r="1.2" fill="white"/>
          {/* Right eye */}
          <circle cx="94" cy="66" r="6" fill="white"/>
          <circle cx="94" cy="67" r="3.5" fill="#2d2d2d"/>
          <circle cx="92.5" cy="65.5" r="1.2" fill="white"/>
          {/* Mouth — sad curve */}
          <path d="M72 90 Q80 86 88 90" stroke="#b07030" strokeWidth="2.5" strokeLinecap="round" fill="none"/>
          {/* Front legs */}
          <rect x="57" y="126" width="14" height="22" rx="7" fill="#f5c87a"/>
          <rect x="89" y="126" width="14" height="22" rx="7" fill="#f5c87a"/>
          {/* Paw left */}
          <ellipse cx="64" cy="149" rx="8" ry="5" fill="#e8a84a"/>
          {/* Paw right */}
          <ellipse cx="96" cy="149" rx="8" ry="5" fill="#e8a84a"/>
          {/* Tail */}
          <path d="M118 110 Q140 92 132 76 Q128 90 116 98Z" fill="#f5c87a"/>
        </svg>
        <div className="err-dog-shadow"/>
      </div>

      <div className="err-title">Woof! Can't Connect</div>
      <div className="err-sub">Your pipeline is playing fetch,<br/>but Salesforce isn't throwing the ball.</div>
      <button className="err-retry-btn" onClick={handleRetry} disabled={spinning}>
        {spinning ? '↻ Connecting…' : '↻ Try Again'}
      </button>
    </div>
  );
}

function AuthScreen({ onComplete }) {
  const [mode, setMode] = useState('signin');
  const [fullName, setFullName] = useState(localStorage.getItem('pq_user_name') || '');
  const [email, setEmail] = useState('');
  const [company, setCompany] = useState('');
  const [password, setPassword] = useState('');
  const [inviteCode, setInviteCode] = useState('');
  const [error, setError] = useState('');
  const isCreate = mode === 'create';

  function resetError() {
    if (error) setError('');
  }

  function completeWithAccount(account) {
    const session = createAccountSession(account);
    if (account.fullName) localStorage.setItem('pq_user_name', account.fullName);
    if (account.workspaceName) localStorage.setItem('pq_account_workspace_name', account.workspaceName);
    onComplete?.(session);
  }

  function submit(e) {
    e.preventDefault();
    const cleanEmail = normalizeAccountEmail(email);
    if (!cleanEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cleanEmail)) {
      setError('Enter a valid work email.');
      return;
    }
    if (!password || password.length < 6) {
      setError('Use a password with at least 6 characters.');
      return;
    }

    const store = readAccountStore();
    if (isCreate) {
      if (!isBetaInviteAccepted(cleanEmail, inviteCode)) {
        setError('Enter a valid beta invite code or use an invited email.');
        return;
      }
      if (store[cleanEmail]) {
        setError('That account already exists. Sign in instead.');
        setMode('signin');
        return;
      }
      const account = {
        id: 'acct-' + Date.now(),
        email: cleanEmail,
        fullName: fullName.trim() || cleanEmail.split('@')[0],
        workspaceName: company.trim() || cleanEmail.split('@')[1],
        role: 'rep',
        provider: 'local_beta',
        authMode: 'Local beta account',
        passwordDigest: accountPasswordDigest(password),
        createdAt: new Date().toISOString(),
        inviteAcceptedAt: new Date().toISOString(),
      };
      store[cleanEmail] = account;
      writeAccountStore(store);
      completeWithAccount(account);
      return;
    }

    const account = store[cleanEmail];
    if (!account || account.passwordDigest !== accountPasswordDigest(password)) {
      setError('Email or password does not match a beta account.');
      return;
    }
    completeWithAccount(account);
  }

  function useDemoAccount() {
    const account = {
      id: 'acct-demo',
      email: 'rep@pipelinequest.beta',
      fullName: 'Sales Rep',
      workspaceName: 'PipelineQuest Beta',
      role: 'rep',
      provider: 'local_beta',
      authMode: 'Local beta demo',
      createdAt: new Date().toISOString(),
    };
    const store = readAccountStore();
    store[account.email] = { ...account, passwordDigest: accountPasswordDigest('pipelinequest') };
    writeAccountStore(store);
    localStorage.setItem('pq_today_nav_tour_seen_v1', '1');
    completeWithAccount(account);
  }

  return (
    <div className="auth-screen" data-testid="auth-screen">
      <form className="auth-card" onSubmit={submit}>
        <div className="setup-logo">
          <img className="setup-logo-mark" src="/icons/pipelinequest-logo-mark.png" alt="" aria-hidden="true" />
        </div>
        <div className="auth-kicker">PipelineQuest beta</div>
        <div className="auth-title">{isCreate ? 'Create your account' : 'Sign in'}</div>
        <div className="auth-sub">
          {isCreate
            ? 'Use your work email to join an approved beta workspace.'
            : 'Sign in to your PipelineQuest beta account.'}
        </div>

        <div className="auth-tabs" role="tablist" aria-label="Account mode">
          <button type="button" className={mode === 'signin' ? 'on' : ''} onClick={() => { setMode('signin'); resetError(); }}>Sign in</button>
          <button type="button" className={mode === 'create' ? 'on' : ''} onClick={() => { setMode('create'); resetError(); }}>Create</button>
        </div>

        {isCreate && (
          <>
            <label className="setup-label" htmlFor="auth-name">Full name</label>
            <input id="auth-name" className="setup-input" value={fullName} onChange={e => { setFullName(e.target.value); resetError(); }} placeholder="Jane Smith" autoComplete="name" />
            <label className="setup-label" htmlFor="auth-company">Company / workspace</label>
            <input id="auth-company" className="setup-input" value={company} onChange={e => { setCompany(e.target.value); resetError(); }} placeholder="Acme Sales" autoComplete="organization" />
          </>
        )}

        <label className="setup-label" htmlFor="auth-email">Work email</label>
        <input
          id="auth-email"
          className="setup-input"
          value={email}
          onChange={e => { setEmail(e.target.value); resetError(); }}
          placeholder="you@company.com"
          inputMode="email"
          autoCapitalize="none"
          autoCorrect="off"
          autoComplete="email"
          data-testid="auth-email-input"
        />
        <label className="setup-label" htmlFor="auth-password">Password</label>
        <input
          id="auth-password"
          className="setup-input"
          value={password}
          onChange={e => { setPassword(e.target.value); resetError(); }}
          placeholder="At least 6 characters"
          type="password"
          autoComplete={isCreate ? 'new-password' : 'current-password'}
          data-testid="auth-password-input"
        />
        {isCreate && (
          <>
            <label className="setup-label" htmlFor="auth-invite">Beta invite code</label>
            <input
              id="auth-invite"
              className="setup-input"
              value={inviteCode}
              onChange={e => { setInviteCode(e.target.value); resetError(); }}
              placeholder="Invite code"
              autoCapitalize="characters"
              data-testid="auth-invite-input"
            />
          </>
        )}
        {error && <div className="setup-result error" role="alert">{error}</div>}
        <button className="auth-primary-btn" type="submit" data-testid="auth-submit-btn">
          {isCreate ? 'Create account' : 'Sign in'}
        </button>
        {IS_DEV_ENV && (
          <button className="auth-demo-btn" type="button" onClick={useDemoAccount}>
            Use beta demo account
          </button>
        )}
        <div className="auth-footnote">
          Salesforce connects after account sign-in. New beta accounts start with Salesforce read-only mode available in Profile.
        </div>
      </form>
    </div>
  );
}

// ── App ───────────────────────────────────────────────────────────────────

function App() {
  const [todayDate, setTodayDate] = useState(() => appTodayDate());
  const todayDateRef = useRef(todayDate);
  const initialSfCache = useRef(readSalesforceCache());

  const [screen,          setScreen]          = useState('today');
  const [devSetup,        setDevSetup]        = useState(readDevSetupState);
  const [accountSession,  setAccountSession]  = useState(readAccountSession);
  const [screenTransition, setScreenTransition] = useState('');
  const [points,              setPoints]              = useState(() => readStoredNumber(POINTS_STORAGE_KEY, LEGACY_POINTS_STORAGE_KEY));
  const [todayPoints,         setTodayPoints]         = useState(() => readStoredNumber(TODAY_POINTS_STORAGE_PREFIX + todayDate, LEGACY_TODAY_POINTS_STORAGE_PREFIX + todayDate));
  const [deals,           setDeals]           = useState(() => initialSfCache.current?.deals || INIT_DEALS);
  const [acts,            setActs]            = useState(() => initialSfCache.current?.activities || []);
  const [unlocked,        setUnlocked]        = useState(() => JSON.parse(localStorage.getItem('pq_unlocked') || '[]'));
  const [loading,         setLoading]         = useState(() => IS_DEMO_STARTUP_ENABLED || !initialSfCache.current);
  const [loadErr,         setLoadErr]         = useState(null);
  const [retryCount,      setRetryCount]      = useState(0);
  const [sessionActCount, setSessionActCount] = useState(0);
  const [confettiDeal,    setConfettiDeal]    = useState(null);
  const [pipelineCleared, setPipelineCleared] = useState(null);
  const [activityCard,    setActivityCard]    = useState(null);
  const [burstStack,      setBurstStack]      = useState([]);
  const [pendingLog,      setPendingLog]      = useState(null);
  const [stageCard,       setStageCard]       = useState(null);
  const [nextStepDeal,    setNextStepDeal]    = useState(null);
  const [closeDateDeal,   setCloseDateDeal]   = useState(null);
  const [amountDeal,      setAmountDeal]      = useState(null);
  const [meddicDeal,      setMeddicDeal]      = useState(null);
  const [pendingUndo,     setPendingUndo]     = useState(null);
  const [hiddenCardIds,   setHiddenCardIds]   = useState([]);
  const [todaySortMode,   setTodaySortMode]   = useState('forecast');
  const [forecastFilter,  setForecastFilter]  = useState(() => normalizeForecastScopeValue(localStorage.getItem('pq_forecast_scope') || 'quarter'));
  const [progressFilter,  setProgressFilter]  = useState('all');
  const [navFilterOpen,   setNavFilterOpen]   = useState(false);
  const [navFilterClosing, setNavFilterClosing] = useState(false);
  const [theme,           setTheme]           = useState(() => localStorage.getItem('pq_theme') || 'light');
  const [meddicAvailable, setMeddicAvailable] = useState(() => localStorage.getItem(MEDDIC_ENABLED_KEY) !== '0');
  const [salesMethodology, setSalesMethodology] = useState(readSalesMethodology);
  const [doneCards,       setDoneCards]       = useState([]);
  const todayKey = 'pq_swiped_' + todayDate;
  const nextStepKey = 'pq_next_steps_' + todayDate;
  const stageStepKey = 'pq_stage_steps_' + todayDate;
  const [swipedIds, setSwipedIds] = useState(() => readJsonStorage(todayKey, []));
  const [nextStepDoneIds, setNextStepDoneIds] = useState(() => readJsonStorage(nextStepKey, []));
  const [stageDoneIds, setStageDoneIds] = useState(() => readJsonStorage(stageStepKey, []));
  const [meddicByDeal, setMeddicByDeal] = useState(() => IS_DEV_MEDDIC_ENABLED ? readMeddicState() : {});
  const [updatedPill, setUpdatedPill] = useState(null);
  const [stageBar,      setStageBar]      = useState(null);
  const burstIdRef = useRef(0);
  const undoTimer  = useRef(null);
  const stageBarTimer = useRef(null);
  const updatedPillTimer = useRef(null);
  const meddicUpdatedDealRef = useRef(null);
  const pendingUndoRef = useRef(null);
  const pipelineClearedPendingRef = useRef(null);
  const [syncPending,  setSyncPending]  = useState(0);
  const [syncFailures, setSyncFailures] = useState([]);
  const [syncDiagnosticOpen, setSyncDiagnosticOpen] = useState(false);
  const [salesforceReadOnly, setSalesforceReadOnly] = useState(() => readProfilePref('salesforce_read_only', (IS_DEMO_STARTUP_ENABLED || IS_ACCOUNT_AUTH_ENABLED) ? '1' : '0') === '1');
  const [isDemoMode, setIsDemoMode] = useState(() => localStorage.getItem(DEV_DEMO_MODE_KEY) === '1' || IS_DEMO_STARTUP_ENABLED);
  const writingDeals = useRef(new Set());
  const navFilterRef = useRef(null);
  const screenTouchStart = useRef(null);
  const screenTransitionTimer = useRef(null);
  const [frozenOrderIds, setFrozenOrderIds] = useState(null);
  const [changedCardId,  setChangedCardId]  = useState(null);
  const cardDomRefs         = useRef({});
  const preReorderPositions = useRef(null);
  const resetActIdsRef      = useRef(new Set());
  const forceAtRiskRef      = useRef(new Set());
  const forceUntouchedDaysRef = useRef(new Map());
  const [touchedDealIds,  setTouchedDealIds]  = useState(new Set());
  const [streakOverride, setStreakOverride] = useState(null);
  const [userName, setUserName] = useState(() => accountSession?.fullName || localStorage.getItem('pq_user_name') || 'Sales Rep');
  const [orgName,  setOrgName]  = useState(() => localStorage.getItem('pq_org_name')  || '');
  const [lastSync, setLastSync] = useState(() => localStorage.getItem('pq_last_sync')  || null);
  const [quotaTargets, setQuotaTargets] = useState(() => ({
    quarterly: readQuotaValue(QUARTERLY_QUOTA_KEY, DEFAULT_QUARTERLY_QUOTA),
    yearly: readQuotaValue(YEARLY_QUOTA_KEY, DEFAULT_YEARLY_QUOTA),
  }));
  const [targetTerm, setTargetTerm] = useState(readTargetTerm);
  const [fiscalStartMonth, setFiscalStartMonth] = useState(readFiscalStartMonth);
  const streakOverrideRef   = useRef(null);
  const afterResetRef       = useRef(false);
  const apiSecretRef        = useRef('');
  const progressLoadedRef   = useRef(false);
  const progressTimerRef    = useRef(null);
  const applyingProgressRef = useRef(false);
  const todayRolloverTimerRef = useRef(null);

  const [sessionDealActIds, setSessionDealActIds] = useState(new Set());
  const [skipCounts, setSkipCounts] = useState(() => {
    try { return JSON.parse(localStorage.getItem('pq_skip_counts') || '{}'); } catch { return {}; }
  });
  const [questRewardedIds, setQuestRewardedIds] = useState(() => {
    return readJsonStorage('pq_quest_rewarded_' + todayDate, []);
  });
  const questRewardedIdsRef = useRef(questRewardedIds);
  const [freezeTokens, setFreezeTokens] = useState(() => parseInt(localStorage.getItem('pq_freeze_tokens') || '0'));
  const [freezeUsedDates, setFreezeUsedDates] = useState(() => {
    try { return JSON.parse(localStorage.getItem('pq_freeze_used') || '[]'); } catch { return []; }
  });
  const [resetActIds, setResetActIds] = useState(() => {
    try {
      const ids = JSON.parse(localStorage.getItem('pq_reset_act_ids') || '[]');
      const set = new Set(Array.isArray(ids) ? ids : []);
      resetActIdsRef.current = set;
      return set;
    } catch {
      return new Set();
    }
  });

  function pulseUpdatedPill(dealId, kind) {
    if (!dealId || !kind) return;
    if (updatedPillTimer.current) clearTimeout(updatedPillTimer.current);
    setUpdatedPill({ dealId, kind, key: Date.now() });
    updatedPillTimer.current = setTimeout(() => {
      setUpdatedPill(null);
      updatedPillTimer.current = null;
    }, 1250);
  }
  const accountAuthRequired = IS_ACCOUNT_AUTH_ENABLED && !accountSession;

  function salesforceHeaders(extra = {}) {
    const headers = { ...extra };
    if (apiSecretRef.current) headers.Authorization = 'Bearer ' + apiSecretRef.current;
    if (IS_DEV_SETUP_ENV && devSetup.instanceUrl) headers['X-PQ-SF-Instance-URL'] = devSetup.instanceUrl;
    return headers;
  }

  function startReviewDemoMode() {
    clearPipelineQuestLocalState({ preserveAccount: true });
    writeSalesforceCache(INIT_DEALS, []);
    localStorage.setItem('pq_ff_demo_startup', '1');
    localStorage.setItem(DEV_SETUP_COMPLETE_KEY, '1');
    localStorage.setItem(DEV_DEMO_MODE_KEY, '1');
    localStorage.removeItem(DEV_SETUP_INSTANCE_KEY);
    localStorage.setItem(DEV_SETUP_ORG_KEY, 'Salesforce Demo');
    localStorage.setItem(DEV_SETUP_VALIDATED_KEY, new Date().toISOString());
    localStorage.setItem('pq_profile_pref_salesforce_read_only', '1');
    localStorage.removeItem(TODAY_NAV_TOUR_SEEN_KEY);
    window.location.reload();
  }

  function resetDemoMode() {
    startReviewDemoMode();
  }

  function completeAccountAuth(session) {
    setAccountSession(session);
    if (session?.fullName) setUserName(session.fullName);
  }

  function signOutAccount() {
    localStorage.removeItem(ACCOUNT_SESSION_KEY);
    setAccountSession(null);
    setScreen('today');
  }

  function deleteLocalAccount() {
    if (accountSession?.email) {
      const store = readAccountStore();
      delete store[normalizeAccountEmail(accountSession.email)];
      writeAccountStore(store);
    }
    clearPipelineQuestLocalState();
    window.location.reload();
  }

  function disconnectSalesforce() {
    localStorage.removeItem(DEV_SETUP_COMPLETE_KEY);
    localStorage.removeItem(DEV_SETUP_INSTANCE_KEY);
    localStorage.removeItem(DEV_SETUP_ORG_KEY);
    localStorage.removeItem(DEV_SETUP_VALIDATED_KEY);
    localStorage.removeItem(DEV_DEMO_MODE_KEY);
    localStorage.setItem('pq_ff_demo_startup', '0');
    localStorage.removeItem(SF_CACHE_KEY);
    localStorage.removeItem('pq_last_sync');
    localStorage.removeItem('pq_org_name');
    setOrgName('');
    setLastSync(null);
    setIsDemoMode(false);
    setDevSetup(readDevSetupState());
    setRetryCount(c => c + 1);
  }

  const QUEST_POINTS = 25;
  const SWEEP_POINTS = 50;
  const MAX_FREEZE_TOKENS = 3;

  const onFire   = sessionActCount >= 3;
  const gameActs = acts.filter(a => !resetActIdsRef.current.has(a.id));
  const streak   = streakOverride !== null ? streakOverride : calcStreak(gameActs, freezeUsedDates);
  const lv       = getLevel(points);
  const repTitle = REP_TITLES[Math.min(lv - 1, REP_TITLES.length - 1)];

  const DAILY_TARGET = 5;
  const todayActCount = gameActs.filter(a => a.time === todayDate).length + doneCards.length;
  const openDeals = deals.filter(d => d.stage !== 'Closed Won' && d.stage !== 'Closed Lost');
  const todayActDealIds = new Set([
    ...doneCards.map(c => c.deal.id),
    ...gameActs.filter(a => a.time === todayDate)
      .map(a => deals.find(d => d.name === a.acct)?.id)
      .filter(Boolean),
  ]);
  const todayActDealCount = todayActDealIds.size;
  const riskyOpenDeals = openDeals.filter(d => isPastCloseDate(d) || daysSinceLastTouch(d, acts) >= 7);
  const touchedTodayIds = new Set([
    ...Array.from(todayActDealIds),
    ...nextStepDoneIds,
    ...Array.from(touchedDealIds),
  ]);
	  const rescuedRiskCount = riskyOpenDeals.filter(d => touchedTodayIds.has(d.id)).length;
	  const commitTouchedCount = openDeals.filter(d => d.forecastCategory === 'Commit' && touchedTodayIds.has(d.id)).length;
	  const openDealsWithNextStepCount = openDeals.filter(d => (d.nextStep || '').trim()).length;
	  const cleanPipelineCount = openDeals.filter(d => (d.nextStep || '').trim() && d.closeDate && !isPastCloseDate(d)).length;
  const dailyQuests = [
    { id: 'activities',  tone: 'act',   label: 'Log activities', count: Math.min(todayActDealCount,      3), target: 3 },
    { id: 'next-steps',  tone: 'next',  label: 'Set next steps', count: Math.min(nextStepDoneIds.length, 3), target: 3 },
    { id: 'stage-moves', tone: 'stage', label: 'Advance stage',  count: Math.min(stageDoneIds.length,    3), target: 3 },
    riskyOpenDeals.length
      ? { id: 'risk-rescue',    tone: 'risk', label: 'Rescue risk',    count: Math.min(rescuedRiskCount,      3), target: 3 }
      : { id: 'touch-pipeline', tone: 'risk', label: 'Touch pipeline', count: Math.min(touchedTodayIds.size,  3), target: 3 },
  ];

	  const weeklyQuests = (() => {
    const now = new Date();
    const dayOfWeek = now.getDay();
    const daysBack = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
    const mon = new Date(now);
    mon.setDate(now.getDate() - daysBack);
	    const weekStart = appTodayDate(mon);
	    const wActs = gameActs.filter(a => typeof a.time === 'string' && a.time >= weekStart);
	    const wTouchedAccounts = new Set(wActs.map(a => a.acct).filter(Boolean));
	    const wTouchedDealIds = new Set([
	      ...openDeals.filter(d => wTouchedAccounts.has(d.name)).map(d => d.id),
	      ...nextStepDoneIds,
	      ...stageDoneIds,
	      ...Array.from(touchedDealIds),
	    ]);
	    const weeklyRiskRescued = riskyOpenDeals.filter(d => wTouchedDealIds.has(d.id)).length;
	    const weeklyCommitTouched = openDeals.filter(d => d.forecastCategory === 'Commit' && wTouchedDealIds.has(d.id)).length;
	    const dailyQuestCompletions = dailyQuests.filter(q => q.count >= q.target).length;
	    return [
	      { id: 'w-activities', tone: 'act',   label: 'Log 5 activities',     count: Math.min(wActs.length, 5), target: 5 },
	      { id: 'w-pipeline',   tone: 'next',  label: 'Touch pipeline',       count: Math.min(wTouchedDealIds.size, 7), target: 7 },
	      { id: 'w-calls',      tone: 'stage', label: 'Make calls',           count: Math.min(wActs.filter(a => a.type === 'Call').length, 7), target: 7 },
	      { id: 'w-meetings',   tone: 'risk',  label: 'Hold meeting/demo',    count: Math.min(wActs.filter(a => a.type === 'Meeting' || a.type === 'Demo').length, 1), target: 1 },
	      { id: 'w-next',       tone: 'next',  label: 'Add next steps',       count: Math.min(nextStepDoneIds.length, 3), target: 3 },
	      { id: 'w-stage',      tone: 'stage', label: 'Move 1 stage',         count: Math.min(stageDoneIds.length, 1), target: 1 },
	      { id: 'w-risk',       tone: 'risk',  label: 'Rescue risk',          count: Math.min(weeklyRiskRescued, 1), target: 1 },
	      { id: 'w-commit',     tone: 'act',   label: 'Touch commit deals',   count: Math.min(weeklyCommitTouched || commitTouchedCount, 3), target: 3 },
	      { id: 'w-clean',      tone: 'next',  label: 'Clean pipeline',       count: Math.min(cleanPipelineCount, Math.max(1, Math.min(openDeals.length, 5))), target: Math.max(1, Math.min(openDeals.length, 5)) },
		    ];
	  })();
	  const weeklyDoneCount = weeklyQuests.filter(q => q.count >= q.target).length;
	  const weeklyExecutionPct = weeklyQuests.length ? Math.round((weeklyDoneCount / weeklyQuests.length) * 100) : 0;
	  const progressFiscalPeriods = getFiscalPeriods(new Date(todayDate + 'T00:00:00'), fiscalStartMonth);
	  const progressQuarterStart = progressFiscalPeriods.currentQuarter.start;
	  const progressQuarterEnd = progressFiscalPeriods.currentQuarter.end;
	  const currentQuarterOpenDeals = openDeals.filter(deal => {
	    if (!deal.closeDate) return false;
	    const closeDate = new Date(deal.closeDate + 'T00:00:00');
	    return closeDate >= progressQuarterStart && closeDate <= progressQuarterEnd;
	  });
	  const quarterForecastReadyCount = currentQuarterOpenDeals.filter(deal => (
	    (deal.nextStep || '').trim() &&
	    deal.closeDate &&
	    deal.forecastCategory &&
	    !isPastCloseDate(deal)
	  )).length;
	  const quarterPipelineHygieneCount = currentQuarterOpenDeals.filter(deal => (
	    (deal.nextStep || '').trim() &&
	    !isPastCloseDate(deal) &&
	    daysSinceLastTouch(deal, acts) < 7
	  )).length;
	  const quarterDealCount = currentQuarterOpenDeals.length;
	  const quarterForecastReadyPct = quarterDealCount ? Math.round((quarterForecastReadyCount / quarterDealCount) * 100) : 100;
	  const quarterPipelineHygienePct = quarterDealCount ? Math.round((quarterPipelineHygieneCount / quarterDealCount) * 100) : 100;
	  const prevLevelPointsForProgress = LVL_POINTS[lv - 1] || 0;
	  const nextLevelPointsForProgress = pointsNext(points);
	  const pointsToNextLevel = Math.max(0, nextLevelPointsForProgress - points);
	  const levelProgressPct = lv >= LVL_POINTS.length
	    ? 100
	    : Math.min(100, Math.round(((points - prevLevelPointsForProgress) / (nextLevelPointsForProgress - prevLevelPointsForProgress)) * 100));
	  const professionalPerformance = IS_PROFESSIONAL_IA_V2_ENABLED ? {
	    level: lv,
	    points,
	    progressPct: levelProgressPct,
	    pointsToNext: pointsToNextLevel,
	    periodModelEnabled: IS_PROFESSIONAL_PROGRESS_PERIODS_ENABLED,
	    dailyActivityCount: Math.min(todayActCount, DAILY_TARGET),
	    dailyActivityTarget: DAILY_TARGET,
	    todayActDealCount,
	    openDealsCount: openDeals.length,
	    nextStepCount: openDealsWithNextStepCount,
	    forecastReadyCount: cleanPipelineCount,
	    streak,
	    weeklyDoneCount,
	    weeklyTotalCount: weeklyQuests.length,
	    weeklyExecutionPct,
	    quarterLabel: progressFiscalPeriods.currentQuarter.label,
	    quarterDealCount,
	    quarterForecastReadyCount,
	    quarterForecastReadyPct,
	    quarterPipelineHygieneCount,
	    quarterPipelineHygienePct,
	  } : null;

	  const includeMeddicCloseScore = IS_DEV_MEDDIC_ENABLED && meddicAvailable && IS_MEDDIC_CLOSE_SCORE_ENABLED;
  const allCards    = buildDailyCards(deals, acts, { touchedDealIds: touchedTodayIds, meddicByDeal, includeMeddic: includeMeddicCloseScore, salesMethodology });
  const doneCardById = new Map(doneCards.map(card => [card.id, card]));
  const completedTodayCards = swipedIds
    .map(id => doneCardById.get(id) || allCards.find(card => card.id === id))
    .filter(Boolean)
    .map(card => {
      const doneCard = doneCardById.get(card.id);
      return {
        ...card,
        actionLabel: doneCard ? completedActionLabel(doneCard) : 'Skipped',
      };
    });
  const baseActive  = allCards.filter(c => !swipedIds.includes(c.id) && !hiddenCardIds.includes(c.id));
  const sortedActive = sortTodayCards(baseActive, todaySortMode);
  const activeCards = frozenOrderIds
    ? frozenOrderIds.map(id => sortedActive.find(c => c.id === id)).filter(Boolean)
        .concat(sortedActive.filter(c => !frozenOrderIds.includes(c.id)))
    : sortedActive;

  function refreshTodayStateForDate(nextDate) {
    if (!nextDate || nextDate === todayDateRef.current) return;
    if (undoTimer.current) clearTimeout(undoTimer.current);
    undoTimer.current = null;
    pendingUndoRef.current = null;
    pipelineClearedPendingRef.current = null;
    todayDateRef.current = nextDate;
    const nextQuestRewardedIds = readJsonStorage('pq_quest_rewarded_' + nextDate, []);
    questRewardedIdsRef.current = nextQuestRewardedIds;
    setTodayDate(nextDate);
    setTodayPoints(readStoredNumber(TODAY_POINTS_STORAGE_PREFIX + nextDate, LEGACY_TODAY_POINTS_STORAGE_PREFIX + nextDate));
    setSwipedIds(readJsonStorage('pq_swiped_' + nextDate, []));
    setNextStepDoneIds(readJsonStorage('pq_next_steps_' + nextDate, []));
    setStageDoneIds(readJsonStorage('pq_stage_steps_' + nextDate, []));
    setQuestRewardedIds(nextQuestRewardedIds);
    setDoneCards([]);
    setHiddenCardIds([]);
    setTouchedDealIds(new Set());
    setSessionActCount(0);
    setSessionDealActIds(new Set());
    setPendingUndo(null);
    setPendingLog(null);
    setActivityCard(null);
    setStageCard(null);
    setNextStepDeal(null);
    setCloseDateDeal(null);
    setAmountDeal(null);
    setPipelineCleared(null);
    setFrozenOrderIds(null);
    setChangedCardId(null);
    clearBurstQueue();
  }

  function refreshTodayIfDateChanged() {
    refreshTodayStateForDate(appTodayDate());
  }

  useEffect(() => {
    todayDateRef.current = todayDate;
  }, [todayDate]);

  useEffect(() => {
    if (!IS_LOCAL_MIDNIGHT_TODAY_ROLLOVER_ENABLED) return;
    function scheduleNextRollover() {
      if (todayRolloverTimerRef.current) clearTimeout(todayRolloverTimerRef.current);
      todayRolloverTimerRef.current = setTimeout(() => {
        refreshTodayIfDateChanged();
        scheduleNextRollover();
      }, msUntilNextLocalMidnight());
    }
    function handleWake() {
      refreshTodayIfDateChanged();
      scheduleNextRollover();
    }
    scheduleNextRollover();
    window.addEventListener('focus', handleWake);
    document.addEventListener('visibilitychange', handleWake);
    return () => {
      if (todayRolloverTimerRef.current) clearTimeout(todayRolloverTimerRef.current);
      window.removeEventListener('focus', handleWake);
      document.removeEventListener('visibilitychange', handleWake);
    };
  }, []);

  useEffect(() => {
    localStorage.setItem(POINTS_STORAGE_KEY, points);
    localStorage.setItem(TODAY_POINTS_STORAGE_PREFIX + todayDate, todayPoints);
  }, [points, todayPoints, todayDate]);

  useEffect(() => {
    localStorage.setItem('pq_unlocked', JSON.stringify(unlocked));
  }, [unlocked]);

  useEffect(() => {
    localStorage.setItem('pq_today_sort_mode', todaySortMode);
  }, [todaySortMode]);

  useEffect(() => {
    localStorage.setItem(stageStepKey, JSON.stringify(stageDoneIds));
  }, [stageDoneIds, stageStepKey]);

  useEffect(() => {
    if (!IS_DEV_MEDDIC_ENABLED) return;
    localStorage.setItem(MEDDIC_STORAGE_KEY, JSON.stringify(meddicByDeal));
  }, [meddicByDeal]);

  useEffect(() => {
    resetActIdsRef.current = resetActIds;
    localStorage.setItem('pq_reset_act_ids', JSON.stringify(Array.from(resetActIds)));
  }, [resetActIds]);

  useEffect(() => {
    localStorage.setItem('pq_theme', theme);
    document.documentElement.style.setProperty('color-scheme', theme);
  }, [theme]);

  useEffect(() => {
    localStorage.setItem(MEDDIC_ENABLED_KEY, meddicAvailable ? '1' : '0');
  }, [meddicAvailable]);

  useEffect(() => {
    localStorage.setItem(SALES_METHODOLOGY_KEY, normalizeSalesMethodology(salesMethodology));
  }, [salesMethodology]);

  useEffect(() => {
    scheduleProgressSync();
  }, [points, todayPoints, todayDate, unlocked, userName, theme, meddicAvailable, salesMethodology, quotaTargets, targetTerm, fiscalStartMonth, skipCounts, freezeTokens, freezeUsedDates, questRewardedIds, nextStepDoneIds, stageDoneIds, swipedIds, resetActIds]);

  useEffect(() => {
    if (loading || loadErr || !Array.isArray(deals) || !Array.isArray(acts) || !deals.length) return;
    writeSalesforceCache(deals, acts);
  }, [deals, acts, loading, loadErr]);

  useEffect(() => {
    if (!loading && window.pipelineQuestResizeApp) window.pipelineQuestResizeApp();
  }, [loading, loadErr, screen]);

  // FLIP animation: fires after frozenOrderIds is released (re-sort happened)
  useLayoutEffect(() => {
    if (frozenOrderIds !== null || !preReorderPositions.current) return;
    const oldPos = preReorderPositions.current;
    preReorderPositions.current = null;
    requestAnimationFrame(() => {
      Object.entries(cardDomRefs.current).forEach(([id, el]) => {
        if (!el || oldPos[id] == null) return;
        const diff = oldPos[id] - el.getBoundingClientRect().top;
        if (Math.abs(diff) < 2) return;
        el.style.transition = 'none';
        el.style.willChange = 'transform';
        el.style.transform  = `translateY(${diff}px)`;
        requestAnimationFrame(() => {
          el.style.transition = 'transform 0.68s cubic-bezier(0.22,1,0.36,1)';
          el.style.transform  = '';
          setTimeout(() => {
            if (!el) return;
            el.style.transition = '';
            el.style.willChange = '';
          }, 700);
        });
      });
    });
  }, [frozenOrderIds]);

  useEffect(() => {
    if (accountAuthRequired) {
      setLoading(false);
      return;
    }
    if (IS_DEMO_STARTUP_ENABLED) {
      setLoading(true);
      setLoadErr(null);
      const timer = setTimeout(() => {
        clearPipelineQuestLocalState({ preserveAccount: true });
        writeSalesforceCache(INIT_DEALS, []);
        localStorage.setItem(DEV_SETUP_COMPLETE_KEY, '1');
        localStorage.setItem(DEV_DEMO_MODE_KEY, '1');
        localStorage.removeItem(DEV_SETUP_INSTANCE_KEY);
        localStorage.setItem(DEV_SETUP_ORG_KEY, 'Salesforce Demo');
        localStorage.setItem(DEV_SETUP_VALIDATED_KEY, new Date().toISOString());
        localStorage.setItem('pq_profile_pref_salesforce_read_only', '1');
        localStorage.removeItem(TODAY_NAV_TOUR_SEEN_KEY);
        setDevSetup({ complete: true, instanceUrl: '', orgName: 'Salesforce Demo' });
        setSalesforceReadOnly(true);
        setIsDemoMode(true);
        setDeals(INIT_DEALS);
        setActs([]);
        setOrgName('Salesforce Demo');
        setLastSync(null);
        progressLoadedRef.current = true;
        setLoading(false);
      }, 650);
      return () => clearTimeout(timer);
    }
    const cached = readSalesforceCache();
    if (cached) {
      setDeals(cached.deals);
      setActs(cached.activities);
    }
    setLoading(!cached);
    setLoadErr(null);
    const doSfFetch = (secret) => {
      if (secret) apiSecretRef.current = secret;
      return fetch('/api/salesforce', { headers: salesforceHeaders() })
        .then(async r => {
          const data = await r.json().catch(() => ({}));
          if (!r.ok) throw new Error(data?.error?.message || data?.error || 'HTTP ' + r.status);
          return data;
        });
    };
    const sfFetchPromise = apiSecretRef.current
      ? doSfFetch(apiSecretRef.current)
      : fetch('/api/config').then(r => r.json()).then(cfg => {
          apiSecretRef.current = cfg.apiSecret || '';
          return doSfFetch(apiSecretRef.current);
        }).catch(() => doSfFetch(''));
    sfFetchPromise
      .then(data => {
        if (data.error) throw new Error(extractApiError(data, 'Salesforce sync failed'));
        let loadedDeals = data.deals || [];
        const rawActs     = data.activities || [];
        // Strip acts and locally age touch dates for reset demo deals.
        const forceAtRisk = forceAtRiskRef.current;
        const forceUntouchedDays = forceUntouchedDaysRef.current;
        const excludeIds  = resetActIdsRef.current;
        const loadedActs  = rawActs.filter(a => !forceAtRisk.has(a.acct));
        if (forceUntouchedDays.size) {
          loadedDeals = loadedDeals.map(deal => {
            const days = forceUntouchedDays.get(deal.name);
            if (!days) return deal;
            const touchDate = new Date(Date.now() - days * 86400000).toISOString();
            return { ...deal, lastActivityDate: touchDate, lastModifiedDate: touchDate };
          });
        }
        forceAtRiskRef.current = new Set();
        forceUntouchedDaysRef.current = new Map();
        resetActIdsRef.current = new Set(excludeIds); // keep for counter filtering
        setDeals(loadedDeals);
        setActs(loadedActs);
        writeSalesforceCache(loadedDeals, loadedActs);
        if (data.orgName) { setOrgName(data.orgName); localStorage.setItem('pq_org_name', data.orgName); }
        if (data.sfUserName && !localStorage.getItem('pq_user_name')) {
          setUserName(data.sfUserName);
        }
        const syncTs = new Date().toISOString();
        setLastSync(syncTs);
        localStorage.setItem('pq_last_sync', syncTs);
        return loadProgressState().then(() => {

        const storedPoints = readStoredNumber(POINTS_STORAGE_KEY, LEGACY_POINTS_STORAGE_KEY);
        const basePoints = storedPoints;
        streakOverrideRef.current = null;
        // Auto-consume streak freeze token if yesterday is missing.
        // Uses React state (freezeTokens/freezeUsedDates) rather than re-reading localStorage
        // to ensure consistency if state and storage ever diverge during a rapid re-render.
        {
          const today  = appTodayDate();
          const yest   = appIsoDateDaysAgo(1);
          const actDays = new Set(loadedActs.filter(a => /^\d{4}-\d{2}-\d{2}$/.test(a.time)).map(a => a.time));
          const currentTokens    = freezeTokens;
          const currentFreezeUsed = freezeUsedDates;
          if (currentTokens > 0 && !actDays.has(yest) && !currentFreezeUsed.includes(yest) && !actDays.has(today)) {
            const newTokens = currentTokens - 1;
            const newFreezeUsed = [...currentFreezeUsed, yest];
            localStorage.setItem('pq_freeze_tokens', String(newTokens));
            localStorage.setItem('pq_freeze_used', JSON.stringify(newFreezeUsed));
            setFreezeTokens(newTokens);
            setFreezeUsedDates(newFreezeUsed);
            setTimeout(() => fire('Streak freeze used! Your streak is safe 🧊'), 1200);
          }
        }

        if (!afterResetRef.current) {
          const gameLoadedActs = loadedActs.filter(a => !resetActIdsRef.current.has(a.id));
          const effectiveStreak = calcStreak(gameLoadedActs, JSON.parse(localStorage.getItem('pq_freeze_used') || '[]'));
          const gained = newAchievements(
            JSON.parse(localStorage.getItem('pq_unlocked') || '[]'),
            basePoints, gameLoadedActs, loadedDeals, effectiveStreak
          );
          if (gained.length) setUnlocked(prev => [...new Set([...prev, ...gained])]);
        }
        // Delay clearing so post-reset effects (quest useEffect) can check it first
        setTimeout(() => { afterResetRef.current = false; }, 300);

        setLoading(false);

        const loginKey = 'pq_login_' + todayDate;
        if (!localStorage.getItem(loginKey)) {
          localStorage.setItem(loginKey, '1');
          setTimeout(() => {
            setPoints(prev => prev + 10);
            setTodayPoints(prev => prev + 10);
            celebrateDailyBonus(10);
          }, 700);
        }
        });
      })
      .catch(err => {
        const fallback = readSalesforceCache();
        if (fallback) {
          setDeals(fallback.deals);
          setActs(fallback.activities);
          setLoadErr(null);
          setLoading(false);
          fire('Salesforce sync failed — showing saved data');
        } else {
          setLoadErr(err.message);
          setLoading(false);
        }
      });
  }, [retryCount, accountAuthRequired, devSetup.instanceUrl]);

  // Quest completion point rewards — handles non-swipe quest progress (next step saves, stage moves).
  // checkQuestPoints fires synchronously on swipe; this useEffect covers the remaining cases.
  // applyingProgressRef guard prevents double-fire during cross-device progress load.
  useEffect(() => {
    if (afterResetRef.current || applyingProgressRef.current) return;
    checkQuestPoints(dailyQuests, points);
  }, [dailyQuests]);

  // Today menu completion acknowledgement — fires once per day when the last Today card is cleared.
  const PIPELINE_CLEARED_POINTS = 30;
  useEffect(() => {
    if (screen !== 'today' || loading || loadErr) return;
    if (pendingUndo || pendingUndoRef.current) return;
    if (activeCards.length !== 0 || allCards.length === 0) return;
    const clearedCount = completedTodayCards.length || swipedIds.length || doneCards.length;
    if (!clearedCount) return;
    const clearedKey = 'pq_today_menu_complete_' + todayDate;
    if (localStorage.getItem(clearedKey)) return;
    if (pipelineClearedPendingRef.current === clearedKey) return;
    pipelineClearedPendingRef.current = clearedKey;
    let fired = false;
    const t = setTimeout(() => {
      fired = true;
      pipelineClearedPendingRef.current = null;
      localStorage.setItem(clearedKey, '1');
      const bonusPts = PIPELINE_CLEARED_POINTS;
      setPoints(prev => prev + bonusPts);
      setTodayPoints(prev => prev + bonusPts);
      setPipelineCleared({
        doneCount: clearedCount,
        pointsToday: todayPoints + bonusPts,
        streak,
        bonusPts,
        title: 'Congratulations!',
        subtitle: clearedCount === 1 ? '1 activity cleared' : clearedCount + ' activities cleared',
        clearPct: 100,
        summary: todayClearSummary(completedTodayCards, nextStepDoneIds, stageDoneIds),
      });
    }, 680);
    return () => {
      if (!fired) {
        clearTimeout(t);
        if (pipelineClearedPendingRef.current === clearedKey) pipelineClearedPendingRef.current = null;
      }
    };
  }, [activeCards.length, allCards.length, completedTodayCards.length, swipedIds.length, doneCards.length, pendingUndo, screen, loading, loadErr, todayPoints, streak, todayDate, nextStepDoneIds.length, stageDoneIds.length]);

  function fire(msg) {
    queueBurst({ type: 'Alert', pts: 0, label: msg, icon: '!' });
  }

  function extractApiError(data, fallback) {
    if (!data) return fallback;
    if (typeof data.error === 'string') return data.error;
    if (data.error?.message) return data.error.message;
    return fallback;
  }

  function syncLabel(method, body) {
    if (body?.progressState) return 'Progress sync';
    if (method === 'POST') return (body?.type || 'Activity') + ' activity';
    if (body?.nextStep !== undefined) return 'Next step update';
    if (body?.closeDate !== undefined) return 'Close date update';
    if (body?.amount !== undefined) return 'Amount update';
    if (body?.forecastCategory !== undefined && body?.stage === undefined) return 'Forecast update';
    if (body?.stage !== undefined) return 'Stage update';
    return 'Salesforce update';
  }

  function syncWrite(method, body, dedupeKey, onError, opts = {}) {
    if (dedupeKey && writingDeals.current.has(dedupeKey)) return Promise.resolve({ skipped: true });
    if (dedupeKey) writingDeals.current.add(dedupeKey);
    if (salesforceReadOnly) {
      if (dedupeKey) writingDeals.current.delete(dedupeKey);
      return Promise.resolve({ skipped: true, readOnly: true });
    }
    setSyncPending(p => p + 1);
    const requestBody = method === 'POST' && !body.clientRequestId
      ? { ...body, clientRequestId: dedupeKey || ('pq-' + Date.now()) }
      : body;
    return fetch('/api/salesforce', {
      method,
      headers: salesforceHeaders({ 'Content-Type': 'application/json' }),
      body: JSON.stringify(requestBody),
    })
      .then(async r => {
        const data = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(extractApiError(data, 'HTTP ' + r.status));
        return data;
      })
      .catch(err => {
        if (!opts.silentFailure) {
          const failure = {
            id: (dedupeKey || method + '-' + Date.now()) + '-' + Date.now(),
            method,
            body: requestBody,
            dedupeKey,
            label: opts.label || syncLabel(method, requestBody),
            message: err.message || 'Sync failed',
          };
          setSyncFailures(prev => [failure, ...prev].slice(0, 4));
          fire('Sync failed — retry from the banner');
        }
        if (onError) onError();
        if (opts.throwOnFailure) throw err;
        return { failed: true };
      })
      .finally(() => {
        setSyncPending(p => p - 1);
        if (dedupeKey) writingDeals.current.delete(dedupeKey);
      });
  }

  function retrySyncFailure(failure) {
    setSyncFailures(prev => prev.filter(f => f.id !== failure.id));
    syncWrite(failure.method, failure.body, failure.dedupeKey, null, { label: failure.label })
      .then(result => { if (!result?.failed) fire(failure.label + ' synced'); })
      .catch(() => {});
  }

  function retryFailedWritebacks() {
    const failures = [...syncFailures];
    if (!failures.length) return;
    setSyncDiagnosticOpen(false);
    failures.forEach(retrySyncFailure);
  }

  function toggleSalesforceReadOnly() {
    setSalesforceReadOnly(prev => {
      const next = !prev;
      localStorage.setItem('pq_profile_pref_salesforce_read_only', next ? '1' : '0');
      return next;
    });
  }

  function dismissSyncFailure(id) {
    setSyncFailures(prev => prev.filter(f => f.id !== id));
    setSyncDiagnosticOpen(false);
  }

  function captureProgressState() {
    return {
      points,
      todayPoints,
      todayDate,
      unlocked,
      userName,
      theme,
      meddicAvailable,
      salesMethodology,
      quotaTargets,
      targetTerm,
      fiscalStartMonth,
      skipCounts,
      freezeTokens,
      freezeUsedDates,
      questRewardedIds,
      nextStepDoneIds,
      stageDoneIds,
      swipedIds,
      resetActIds: Array.from(resetActIdsRef.current),
      loginDates: Object.keys(localStorage)
        .filter(k => k.startsWith('pq_login_') && localStorage.getItem(k) === '1')
        .map(k => k.replace('pq_login_', '')),
      sweepDates: Object.keys(localStorage)
        .filter(k => k.startsWith('pq_sweep_') && localStorage.getItem(k) === '1')
        .map(k => k.replace('pq_sweep_', '')),
      freezeEarnDates: Object.keys(localStorage)
        .filter(k => k.startsWith('pq_freeze_earn_') && localStorage.getItem(k) === '1')
        .map(k => k.replace('pq_freeze_earn_', '')),
    };
  }

  function applyProgressState(progress) {
    const state = progress?.state;
    if (!state) return;
    const serverTime = progress.updatedAt || '';
    const localTime = localStorage.getItem('pq_progress_updated_at') || '';
    if (localTime && serverTime && localTime > serverTime) return;
    applyingProgressRef.current = true;
    if (Number.isFinite(state.points)) setPoints(state.points);
    if (state.todayDate === todayDate && Number.isFinite(state.todayPoints)) setTodayPoints(state.todayPoints);
    if (Array.isArray(state.unlocked)) setUnlocked(state.unlocked);
    if (typeof state.userName === 'string' && state.userName.trim()) {
      setUserName(state.userName);
      localStorage.setItem('pq_user_name', state.userName);
    }
    if (state.theme === 'dark' || state.theme === 'light') setTheme(state.theme);
    if (typeof state.meddicAvailable === 'boolean') {
      setMeddicAvailable(state.meddicAvailable);
      localStorage.setItem(MEDDIC_ENABLED_KEY, state.meddicAvailable ? '1' : '0');
    }
    if (typeof state.salesMethodology === 'string') {
      const nextMethodology = normalizeSalesMethodology(state.salesMethodology);
      setSalesMethodology(nextMethodology);
      localStorage.setItem(SALES_METHODOLOGY_KEY, nextMethodology);
    }
    if (state.quotaTargets && typeof state.quotaTargets === 'object') {
      const quarterly = Number(state.quotaTargets.quarterly);
      const yearly = Number(state.quotaTargets.yearly);
      if (Number.isFinite(quarterly) && quarterly > 0 && Number.isFinite(yearly) && yearly > 0) {
        const nextQuotaTargets = { quarterly: Math.round(quarterly), yearly: Math.round(yearly) };
        setQuotaTargets(nextQuotaTargets);
        localStorage.setItem(QUARTERLY_QUOTA_KEY, String(nextQuotaTargets.quarterly));
        localStorage.setItem(YEARLY_QUOTA_KEY, String(nextQuotaTargets.yearly));
        localStorage.setItem('pq_quota_target', String(nextQuotaTargets.quarterly));
      }
    }
    if (state.targetTerm !== undefined) {
      const nextTargetTerm = normalizeTargetTerm(state.targetTerm);
      setTargetTerm(nextTargetTerm);
      localStorage.setItem(TARGET_TERM_KEY, nextTargetTerm);
    }
    if (state.fiscalStartMonth !== undefined) {
      const nextFiscalStartMonth = normalizeFiscalStartMonth(state.fiscalStartMonth);
      setFiscalStartMonth(nextFiscalStartMonth);
      localStorage.setItem(FISCAL_START_MONTH_KEY, String(nextFiscalStartMonth));
    }
    if (state.skipCounts && typeof state.skipCounts === 'object') {
      setSkipCounts(state.skipCounts);
      localStorage.setItem('pq_skip_counts', JSON.stringify(state.skipCounts));
    }
    if (Number.isFinite(state.freezeTokens)) {
      setFreezeTokens(state.freezeTokens);
      localStorage.setItem('pq_freeze_tokens', String(state.freezeTokens));
    }
    if (Array.isArray(state.freezeUsedDates)) {
      setFreezeUsedDates(state.freezeUsedDates);
      localStorage.setItem('pq_freeze_used', JSON.stringify(state.freezeUsedDates));
    }
    if (state.todayDate === todayDate) {
      if (Array.isArray(state.questRewardedIds)) {
        questRewardedIdsRef.current = state.questRewardedIds;
        setQuestRewardedIds(state.questRewardedIds);
        localStorage.setItem('pq_quest_rewarded_' + todayDate, JSON.stringify(state.questRewardedIds));
      }
      if (Array.isArray(state.nextStepDoneIds)) {
        setNextStepDoneIds(state.nextStepDoneIds);
        localStorage.setItem(nextStepKey, JSON.stringify(state.nextStepDoneIds));
      }
      if (Array.isArray(state.stageDoneIds)) {
        setStageDoneIds(state.stageDoneIds);
        localStorage.setItem(stageStepKey, JSON.stringify(state.stageDoneIds));
      }
      if (Array.isArray(state.swipedIds)) {
        setSwipedIds(state.swipedIds);
        localStorage.setItem(todayKey, JSON.stringify(state.swipedIds));
      }
    }
    if (Array.isArray(state.resetActIds)) {
      const nextResetActIds = new Set(state.resetActIds);
      resetActIdsRef.current = nextResetActIds;
      setResetActIds(nextResetActIds);
      localStorage.setItem('pq_reset_act_ids', JSON.stringify(state.resetActIds));
    }
    (state.loginDates || []).forEach(d => localStorage.setItem('pq_login_' + d, '1'));
    (state.sweepDates || []).forEach(d => localStorage.setItem('pq_sweep_' + d, '1'));
    (state.freezeEarnDates || []).forEach(d => localStorage.setItem('pq_freeze_earn_' + d, '1'));
    if (serverTime) localStorage.setItem('pq_progress_updated_at', serverTime);
    setTimeout(() => { applyingProgressRef.current = false; }, 0);
  }

  // v1 limitation: loadProgressState only runs on initial Salesforce fetch.
  // A second device opened mid-session will not pick up progress from the first until it reloads.
  function loadProgressState() {
    if (!apiSecretRef.current) return Promise.resolve();
    return fetch('/api/salesforce?progress=1', {
      headers: salesforceHeaders(),
    })
      .then(r => r.json())
      .then(data => {
        if (data.progress) applyProgressState(data.progress);
        progressLoadedRef.current = true;
      })
      .catch(() => {
        progressLoadedRef.current = true;
      });
  }

  function scheduleProgressSync() {
    if (accountAuthRequired) return;
    if (!progressLoadedRef.current || applyingProgressRef.current) return;
    if (progressTimerRef.current) clearTimeout(progressTimerRef.current);
    progressTimerRef.current = setTimeout(() => {
      const progressState = captureProgressState();
      localStorage.setItem('pq_progress_updated_at', new Date().toISOString());
      syncWrite('PATCH', { progressState }, 'PROGRESS', null, { silentFailure: true }).catch(() => {});
    }, 1200);
  }

  function queueBurst(burst) {
    const id = ++burstIdRef.current;
    setBurstStack(prev => [...prev, { ...burst, id, exiting: false }]);
    setTimeout(() => {
      setBurstStack(prev => prev.map(b => b.id === id ? { ...b, exiting: true } : b));
      setTimeout(() => setBurstStack(prev => prev.filter(b => b.id !== id)), 240);
    }, 2100);
  }

  function clearBurstQueue() {
    setBurstStack([]);
  }

  function celebrateActivity(type, pts) {
    queueBurst({ type, pts, label: type + ' logged' });
  }

  function celebrateDailyBonus(pts) {
    queueBurst({ type: 'Daily Bonus', pts, label: 'Welcome back', icon: '⚡' });
  }

  function celebrateSwipe(label, tone) {
    queueBurst({ type: tone === 'left' ? 'Skip' : 'Done', pts: 0, label, tone });
  }

  function celebrateNextStep(pts) {
    queueBurst({ type: 'Next Step', pts, label: 'Next step updated', icon: '✓' });
  }

  function celebrateRiskRemoved(pts) {
    queueBurst({ type: 'Risk Removed', pts, label: 'Risk removed!', icon: '✓' });
  }

  function celebrateStageAdvance(pts, stage) {
    queueBurst({ type: 'Stage Advance', pts, label: 'Stage advanced!', icon: '↑', sub: stage });
  }

	  function celebrateLevelUp(level, title) {
	    setTimeout(() => {
	      if (IS_PROFESSIONAL_IA_V2_ENABLED) {
	        queueBurst({ type: 'Momentum', pts: 0, label: 'Momentum updated', icon: '✓', sub: 'Level ' + level });
	      } else {
	        queueBurst({ type: 'Level Up', pts: 0, label: 'Level ' + level + ' unlocked!', icon: '★', sub: title });
	      }
	    }, 650);
	  }

  function dealHasVisibleRisk(deal) {
    return !!dealRiskReason(deal, acts, touchedDealIds);
  }

  function stageAdvancePoints(deal, sfStage) {
    if (!deal || !sfStage || sfStage === 'Closed Lost') return 0;
    const currentRank = salesStageRank(deal);
    const nextRank    = salesStageRank({ sfStage });
    if (nextRank <= currentRank) return 0;
    if (sfStage === 'Closed Won') return 100;
    if (['Proposal', 'Negotiation', 'Agreed to Purchase'].includes(sfStage)) return 50;
    return 25;
  }

  function earn(pts, newPoints, newActs, newDeals, msg, prevPoints, silent, sessionCount, achievementStageDoneIds = stageDoneIds) {
    setStreakOverride(null);
    streakOverrideRef.current = null;
    const prevLv    = getLevel(prevPoints);
    const newLv     = getLevel(newPoints);
    const achievementActs = newActs.filter(a => !resetActIdsRef.current.has(a.id));
    const curStreak = calcStreak(achievementActs, freezeUsedDates);
    if (!silent) {
      if (newLv <= prevLv) {
        fire(msg);
      }
    }
    if (newLv > prevLv) {
      const title = REP_TITLES[Math.min(newLv - 1, REP_TITLES.length - 1)];
      celebrateLevelUp(newLv, title);
    }
    const scnt = sessionCount !== undefined ? sessionCount : sessionActCount;
    const gained = newAchievements(unlocked, newPoints, achievementActs, newDeals, curStreak, scnt, achievementStageDoneIds);
    if (gained.length) {
      setUnlocked(prev => [...new Set([...prev, ...gained])]);
      // Award points for each newly unlocked achievement.
      const bonusPts = gained.reduce((s, id) => s + (ACHV.find(a => a.id === id)?.pointsReward || 0), 0);
      if (bonusPts) {
        setPoints(prev => prev + bonusPts);
        setTodayPoints(prev => prev + bonusPts);
      }
      if (!silent) {
        const name = ACHV.find(a => a.id === gained[0]).name;
        setTimeout(() => fire('Achievement unlocked: ' + name + '!'), 900);
      }
    }
  }

  function advanceDeal(dealId) {
    const deal      = deals.find(d => d.id === dealId);
    const idx       = STAGES.indexOf(deal.stage);
    if (idx >= STAGES.length - 1) return;
    const next      = STAGES[idx + 1];
    const sfNext    = REVERT_STAGE_MAP[next] || next;
    const newForecast = SF_STAGE_TO_FORECAST[sfNext] || '';
    const wonCloseDate = sfNext === 'Closed Won' ? todayDate : null;
    const pts       = next === 'Closed Won' ? 100 : next === 'Proposal' ? 50 : 25;
    const newDeals  = deals.map(d => d.id === dealId ? { ...d, stage: next, sfStage: sfNext, forecastCategory: newForecast, ...(wonCloseDate ? { closeDate: wonCloseDate } : {}) } : d);
    const newPoints     = points + pts;
    setDeals(newDeals);
    setPoints(newPoints);
    setTodayPoints(p => p + pts);
    setStageDoneIds(prev => prev.includes(dealId) ? prev : [...prev, dealId]);
    earn(pts, newPoints, acts, newDeals, deal.name + ' moved to ' + next + ' (+' + pts + ' ' + SCORE_LABEL + ')', points, false, undefined, [...new Set([...stageDoneIds, dealId])]);

    if (next === 'Closed Won') {
      setConfettiDeal(deal);
      setTimeout(() => setConfettiDeal(null), 4200);
    }
    syncWrite('PATCH', { dealId, stage: sfNext, forecastCategory: newForecast, ...(wonCloseDate ? { closeDate: wonCloseDate } : {}) }, 'PATCH-' + dealId);
  }

  function dismissCard(cardId) {
    const updated = [...swipedIds, cardId];
    localStorage.setItem(todayKey, JSON.stringify(updated));
    setSwipedIds(updated);
  }

  function queueTodayAction(kind, card, type) {
    if (!card) return;
    if (kind !== 'log') hapticTick(kind === 'skip' ? [10, 24, 10] : 14);
    if (pendingUndoRef.current) finalizeTodayAction(pendingUndoRef.current);
    if (undoTimer.current) clearTimeout(undoTimer.current);
    const action = { key: Date.now(), kind, card, type };
    if (IS_TODAY_INLINE_UNDO_ENABLED) {
      undoTimer.current = null;
      pendingUndoRef.current = null;
      setPendingUndo(null);
      setHiddenCardIds(prev => prev.filter(id => id !== card.id));
      finalizeTodayAction(action);
      return;
    }
    pendingUndoRef.current = action;
    setPendingUndo(action);
    setHiddenCardIds(prev => prev.includes(card.id) ? prev : [...prev, card.id]);
    undoTimer.current = setTimeout(() => finalizeTodayAction(action), 2500);
  }

  function undoTodayAction() {
    const action = pendingUndoRef.current;
    if (!action) return;
    if (undoTimer.current) clearTimeout(undoTimer.current);
    undoTimer.current = null;
    pendingUndoRef.current = null;
    setPendingUndo(null);
    setPendingLog(null);
    setHiddenCardIds(prev => prev.filter(id => id !== action.card.id));
  }

  function finalizeTodayAction(action) {
    if (!action) return;
    if (undoTimer.current) clearTimeout(undoTimer.current);
    undoTimer.current = null;
    pendingUndoRef.current = null;
    setPendingUndo(null);
    setHiddenCardIds(prev => prev.filter(id => id !== action.card.id));
    if (action.kind === 'skip') {
      commitSkipCard(action.card);
    } else if (action.kind === 'log') {
      commitLoggedActivity(action.card, action.type);
    } else {
      commitCompleteCard(action.card);
    }
  }

  function computeDailyQuestsFrom(newActs, newDoneCards) {
    const openDealsLocal = deals.filter(d => d.stage !== 'Closed Won' && d.stage !== 'Closed Lost');
    const actDealIds = new Set([
      ...newDoneCards.map(c => c.deal.id),
      ...newActs.filter(a => a.time === todayDate && !resetActIdsRef.current.has(a.id))
        .map(a => deals.find(d => d.name === a.acct)?.id)
        .filter(Boolean),
    ]);
    const riskyLocal = openDealsLocal.filter(d => isPastCloseDate(d) || daysSinceLastTouch(d, newActs) >= 7);
    const touchedToday = new Set([
      ...Array.from(actDealIds),
      ...nextStepDoneIds,
      ...Array.from(touchedDealIds),
    ]);
    const rescued = riskyLocal.filter(d => touchedToday.has(d.id)).length;
    return [
      { id: 'activities',  tone: 'act',   label: 'Log activities', count: Math.min(actDealIds.size,              3), target: 3 },
      { id: 'next-steps',  tone: 'next',  label: 'Set next steps', count: Math.min(nextStepDoneIds.length,       3), target: 3 },
      { id: 'stage-moves', tone: 'stage', label: 'Advance stage',  count: Math.min(stageDoneIds.length,          3), target: 3 },
      riskyLocal.length
        ? { id: 'risk-rescue',    tone: 'risk', label: 'Rescue risk',    count: Math.min(rescued,            3), target: 3 }
        : { id: 'touch-pipeline', tone: 'risk', label: 'Touch pipeline', count: Math.min(touchedToday.size,  3), target: 3 },
    ];
  }

  function checkQuestPoints(newQuestsArg, afterPoints) {
    const newlyDone = newQuestsArg.filter(q => q.count >= q.target && !questRewardedIdsRef.current.includes(q.id));
    if (!newlyDone.length) return;
    const newIds = [...new Set([...questRewardedIdsRef.current, ...newlyDone.map(q => q.id)])];
    questRewardedIdsRef.current = newIds;
    setQuestRewardedIds(newIds);
    localStorage.setItem('pq_quest_rewarded_' + todayDate, JSON.stringify(newIds));
    const totalPts = newlyDone.length * QUEST_POINTS;
    const newPoints    = afterPoints + totalPts;
    setPoints(newPoints);
    setTodayPoints(prev => prev + totalPts);
    newlyDone.forEach((q, i) => {
      setTimeout(() => queueBurst({ type: 'Quest', pts: QUEST_POINTS, label: q.label + ' complete!', icon: '🏅' }), i * 320);
    });
    const prevLv = getLevel(afterPoints);
    const newLv  = getLevel(newPoints);
    if (newLv > prevLv) {
      const title = REP_TITLES[Math.min(newLv - 1, REP_TITLES.length - 1)];
      celebrateLevelUp(newLv, title);
    }
    const allQuestsDone = newQuestsArg.every(q => q.count >= q.target);
    if (allQuestsDone) {
      const sweepKey = 'pq_sweep_' + todayDate;
      if (!localStorage.getItem(sweepKey)) {
        localStorage.setItem(sweepKey, '1');
        const delay = newlyDone.length * 320 + 700;
        setTimeout(() => {
          setPoints(prev => prev + SWEEP_POINTS);
          setTodayPoints(prev => prev + SWEEP_POINTS);
          queueBurst({ type: 'Daily Sweep', pts: SWEEP_POINTS, label: 'Daily Sweep!', icon: '⭐' });
        }, delay);
      }
      const freezeEarnKey = 'pq_freeze_earn_' + todayDate;
      if (!localStorage.getItem(freezeEarnKey)) {
        localStorage.setItem(freezeEarnKey, '1');
        if (freezeTokens < MAX_FREEZE_TOKENS) {
          const newTokens = freezeTokens + 1;
          localStorage.setItem('pq_freeze_tokens', String(newTokens));
          setFreezeTokens(newTokens);
          setTimeout(() => fire('Streak Freeze earned! 🧊 ' + newTokens + '/' + MAX_FREEZE_TOKENS + ' tokens.'), newlyDone.length * 320 + 1400);
        } else {
          setTimeout(() => fire("Freeze tokens full 🧊 — they'll auto-protect your streak if you miss a day."), newlyDone.length * 320 + 1400);
        }
      }
    }
  }

  function commitCompleteCard(card) {
    const riskPts    = dealHasVisibleRisk(card.deal) ? RISK_REMOVED_POINTS : 0;
    const isDD       = sessionDealActIds.has(card.deal.id);
    const ddPts      = isDD ? 15 : 0;
    setTouchedDealIds(prev => new Set([...prev, card.deal.id]));
    setSessionDealActIds(prev => new Set([...prev, card.deal.id]));
    dismissCard(card.id);
    const completedCard = {
      ...card,
      completionKind: card.isNextStep ? 'nextStep' : 'activity',
      actionLabel: card.isNextStep ? 'Next step' : card.action,
    };
    setDoneCards(prev => [completedCard, ...prev]);
    const multiplier = onFire ? 1.5 : 1;
    const pts        = Math.round(card.points * multiplier) + riskPts + ddPts;
    const newPoints      = points + pts;
    setPoints(newPoints);
    setTodayPoints(p => p + pts);

    const act = {
      id:   Date.now(),
      type: card.action,
      acct: card.deal.name,
      pts,
      time: todayDate,
    };
    const newActs = [act, ...acts];
    setActs(newActs);

    const newCount = sessionActCount + 1;
    setSessionActCount(newCount);

    const suffix = onFire ? ' 🔥' : '';
    earn(pts, newPoints, newActs, deals, '+' + pts + ' ' + SCORE_LABEL + ' — ' + card.label + '!' + suffix, points, true, newCount);
    if (isDD) setTimeout(() => queueBurst({ type: 'Double Down', pts: 15, label: 'Double Down!', icon: '🎯' }), 600);
    if (riskPts) celebrateRiskRemoved(riskPts);
    else celebrateActivity(card.action, pts);

    checkQuestPoints(computeDailyQuestsFrom(newActs, [completedCard, ...doneCards]), newPoints);

    syncWrite('POST', { type: card.action, dealId: card.deal.id, clientRequestId: 'act-' + act.id }, 'POST-' + card.deal.id + '-' + act.id);
    if (card.isNextStep) {
      setDeals(prev => prev.map(d => d.id === card.deal.id ? { ...d, nextStep: '' } : d));
      syncWrite('PATCH', { dealId: card.deal.id, nextStep: '' }, 'NS-' + card.deal.id);
    }
  }

  function commitLoggedActivity(card, type) {
    const t = ACT_TYPES.find(t => t.name === type);
    if (!card || !t) return;
    const riskPts = dealHasVisibleRisk(card.deal) ? RISK_REMOVED_POINTS : 0;
    const isDD    = sessionDealActIds.has(card.deal.id);
    const ddPts   = isDD ? 15 : 0;
    setTouchedDealIds(prev => new Set([...prev, card.deal.id]));
    setSessionDealActIds(prev => new Set([...prev, card.deal.id]));
    dismissCard(card.id);
    const completedCard = {
      ...card,
      action: type,
      label: type,
      points: t.pts,
      completionKind: 'activity',
      actionLabel: type,
    };
    setDoneCards(prev => [completedCard, ...prev]);

    const multiplier = onFire ? 1.5 : 1;
    const pts        = Math.round(t.pts * multiplier) + riskPts + ddPts;
    const act = {
      id:   Date.now(),
      type,
      acct: card.deal.name,
      pts,
      time: todayDate,
    };
    const newActs = [act, ...acts];
    const newPoints   = points + pts;

    setActs(newActs);
    setPoints(newPoints);
    setTodayPoints(p => p + pts);

    const newCount = sessionActCount + 1;
    setSessionActCount(newCount);

    const suffix = onFire ? ' 🔥' : '';
    earn(pts, newPoints, newActs, deals, '+' + pts + ' ' + SCORE_LABEL + ' — ' + type + ' logged!' + suffix, points, true, newCount);
    if (isDD) setTimeout(() => queueBurst({ type: 'Double Down', pts: 15, label: 'Double Down!', icon: '🎯' }), 600);
    if (riskPts) celebrateRiskRemoved(riskPts);
    else celebrateActivity(type, pts);

    checkQuestPoints(computeDailyQuestsFrom(newActs, [completedCard, ...doneCards]), newPoints);

    syncWrite('POST', { type, dealId: card.deal.id, clientRequestId: 'act-' + act.id }, 'POST-' + card.deal.id + '-' + act.id);
    if (card.isNextStep) {
      setDeals(prev => prev.map(d => d.id === card.deal.id ? { ...d, nextStep: '' } : d));
      syncWrite('PATCH', { dealId: card.deal.id, nextStep: '' }, 'NS-' + card.deal.id);
    }
  }

  function logCardActivity(card, type) {
    const t = ACT_TYPES.find(t => t.name === type);
    if (!card || !t) return;
    hapticTick(12);
    const loggedCard = { ...card, action: type, label: type, points: t.pts };
    setActivityCard(null);
    setPendingLog({ card: loggedCard, type });
  }

  function openActivitySheet(card) {
    if (!card) return;
    setActivityCard(card);
  }

  function closeActivitySheet() {
    setActivityCard(null);
  }

  function finishPendingLog() {
    if (!pendingLog) return;
    const { card, type } = pendingLog;
    setPendingLog(null);
    queueTodayAction('log', card, type);
  }

  function commitSkipCard(card) {
    dismissCard(card.id);
    setSkipCounts(prev => {
      const next = { ...prev, [String(card.deal.id)]: (prev[String(card.deal.id)] || 0) + 1 };
      localStorage.setItem('pq_skip_counts', JSON.stringify(next));
      return next;
    });
  }

  function completeCard(card) {
    queueTodayAction('done', card);
  }

  function skipCard(card) {
    queueTodayAction('skip', card);
  }

  function restoreCompletedCard(card) {
    if (!card) return;
    if (pendingUndoRef.current?.card?.id === card.id) undoTodayAction();
    const updated = swipedIds.filter(id => id !== card.id);
    localStorage.setItem(todayKey, JSON.stringify(updated));
    setSwipedIds(updated);
    setDoneCards(prev => prev.filter(done => done.id !== card.id));
    setHiddenCardIds(prev => prev.filter(id => id !== card.id));
  }

  function reloadDailyCards() {
    if (undoTimer.current) clearTimeout(undoTimer.current);
    undoTimer.current = null;
    pendingUndoRef.current = null;
    meddicUpdatedDealRef.current = null;
    if (updatedPillTimer.current) clearTimeout(updatedPillTimer.current);
    updatedPillTimer.current = null;
    setUpdatedPill(null);
    localStorage.removeItem(todayKey);
    localStorage.removeItem('pq_today_menu_complete_' + todayDate);
    localStorage.removeItem('pq_pipeline_cleared_' + todayDate);
    setSwipedIds([]);
    setHiddenCardIds([]);
    setPendingUndo(null);
    setPendingLog(null);
    setStageCard(null);
    setDoneCards([]);
    setPipelineCleared(null);
  }

  function resetToLevelOne() {
    if (undoTimer.current) clearTimeout(undoTimer.current);
    undoTimer.current = null;
    pendingUndoRef.current = null;
    meddicUpdatedDealRef.current = null;
    if (updatedPillTimer.current) clearTimeout(updatedPillTimer.current);
    updatedPillTimer.current = null;
    const savedSetup = {
      complete: localStorage.getItem(DEV_SETUP_COMPLETE_KEY),
      instanceUrl: localStorage.getItem(DEV_SETUP_INSTANCE_KEY),
      orgName: localStorage.getItem(DEV_SETUP_ORG_KEY),
      validatedAt: localStorage.getItem(DEV_SETUP_VALIDATED_KEY),
      accountSession: localStorage.getItem(ACCOUNT_SESSION_KEY),
      accountStore: localStorage.getItem(ACCOUNT_STORE_KEY),
      salesforceReadOnly: localStorage.getItem('pq_profile_pref_salesforce_read_only'),
    };
    // Clear all game state and cached Salesforce data from localStorage
    clearPipelineQuestLocalState();
    if (savedSetup.complete) localStorage.setItem(DEV_SETUP_COMPLETE_KEY, savedSetup.complete);
    if (savedSetup.instanceUrl) localStorage.setItem(DEV_SETUP_INSTANCE_KEY, savedSetup.instanceUrl);
    if (savedSetup.orgName) localStorage.setItem(DEV_SETUP_ORG_KEY, savedSetup.orgName);
    if (savedSetup.validatedAt) localStorage.setItem(DEV_SETUP_VALIDATED_KEY, savedSetup.validatedAt);
    if (savedSetup.accountSession) localStorage.setItem(ACCOUNT_SESSION_KEY, savedSetup.accountSession);
    if (savedSetup.accountStore) localStorage.setItem(ACCOUNT_STORE_KEY, savedSetup.accountStore);
    if (savedSetup.salesforceReadOnly) localStorage.setItem('pq_profile_pref_salesforce_read_only', savedSetup.salesforceReadOnly);
    localStorage.setItem('pq_progress_updated_at', new Date().toISOString());
    // Re-stamp login so the daily bonus doesn't re-fire after reload
    localStorage.setItem('pq_login_' + todayDate, '1');
    // Capture current act IDs to zero the activities counter after reload
    const resetIds = new Set(acts.map(a => a.id));
    resetActIdsRef.current = resetIds;
    setResetActIds(resetIds);
    localStorage.setItem('pq_reset_act_ids', JSON.stringify(Array.from(resetIds)));
    forceAtRiskRef.current = new Set();
    forceUntouchedDaysRef.current = new Map();
    // Zero the streak and suppress achievement re-unlock on the post-reset reload
    streakOverrideRef.current = 0;
    afterResetRef.current = true;
    setStreakOverride(0);
    setPoints(0);
    setTodayPoints(0);
    setUnlocked([]);
    setSessionActCount(0);
    setSessionDealActIds(new Set());
    setSkipCounts({});
    questRewardedIdsRef.current = [];
    setQuestRewardedIds([]);
    setFreezeTokens(0);
    setFreezeUsedDates([]);
    setStageDoneIds([]);
    setSwipedIds([]);
    setNextStepDoneIds([]);
    setHiddenCardIds([]);
    setDoneCards([]);
    setTouchedDealIds(new Set());
    setRetryCount(c => c + 1);
    setPendingUndo(null);
    setPendingLog(null);
    setActivityCard(null);
    setStageCard(null);
    setNextStepDeal(null);
    setCloseDateDeal(null);
    setPipelineCleared(null);
    setUpdatedPill(null);
    clearBurstQueue();
  }

  function saveNextStep(dealId, value) {
    const deal = deals.find(d => d.id === dealId);
    const riskPts = dealHasVisibleRisk(deal) ? RISK_REMOVED_POINTS : 0;
    const currentIds = sortTodayCards(
      buildDailyCards(deals, acts, { touchedDealIds: touchedTodayIds, meddicByDeal, includeMeddic: includeMeddicCloseScore, salesMethodology }).filter(c => !swipedIds.includes(c.id) && !hiddenCardIds.includes(c.id)),
      todaySortMode
    ).map(c => c.id);
    setFrozenOrderIds(currentIds);
    setChangedCardId(String(dealId));
    setTouchedDealIds(prev => new Set([...prev, dealId]));
    const NEXT_STEP_POINTS = 15;
    const totalPts = NEXT_STEP_POINTS + riskPts;
    const newDeals = deals.map(d => {
      if (d.id !== dealId) return d;
      const nextStepHistory = [value, ...(Array.isArray(d.nextStepHistory) ? d.nextStepHistory : [])]
        .map(item => String(item || '').replace(/\s+/g, ' ').trim())
        .filter(Boolean)
        .filter((item, index, list) => list.findIndex(other => other.toLowerCase() === item.toLowerCase()) === index)
        .slice(0, 3);
      return { ...d, nextStep: value, nextStepHistory };
    });
    const newPoints    = points + totalPts;
    setDeals(newDeals);
    setPoints(newPoints);
    setTodayPoints(p => p + totalPts);
    setNextStepDoneIds(prev => {
      const updated = prev.includes(dealId) ? prev : [...prev, dealId];
      localStorage.setItem(nextStepKey, JSON.stringify(updated));
      return updated;
    });
    setNextStepDeal(null);
    pulseUpdatedPill(dealId, 'nextStep');
    earn(totalPts, newPoints, acts, newDeals, 'Next step saved! +' + totalPts + ' ' + SCORE_LABEL, points, true);
    celebrateNextStep(totalPts);
    syncWrite('PATCH', { dealId, nextStep: value }, 'NS-' + dealId);
    setTimeout(() => {
      setFrozenOrderIds(null);
      setTimeout(() => setChangedCardId(null), 600);
    }, 800);
  }

  function recentActivitiesForDeal(deal) {
    if (!deal) return [];
    const names = new Set([deal.name, deal.contact].filter(Boolean).map(value => String(value).toLowerCase()));
    return acts
      .filter(activity => activity.dealId === deal.id || names.has(String(activity.acct || '').toLowerCase()))
      .slice(0, 6);
  }

  function notesForDeal(deal) {
    if (!deal) return [];
    const notes = Array.isArray(deal.notes) ? [...deal.notes] : [];
    const names = new Set([deal.name, deal.contact].filter(Boolean).map(value => String(value).toLowerCase()));
    acts.forEach(activity => {
      const note = String(activity.note || '').trim();
      if (!note) return;
      if (activity.dealId !== deal.id && !names.has(String(activity.acct || '').toLowerCase())) return;
      notes.push({
        subject: activity.subject || activity.type || '',
        date: activity.time || '',
        note,
      });
    });
    const seen = new Set();
    return notes.filter(item => {
      const key = String(item.note || item.text || item.description || item.body || item).trim().toLowerCase();
      if (!key || seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function toggleMeddicField(dealId, fieldKey) {
    const method = salesMethodologyConfig(salesMethodology);
    if (!IS_DEV_MEDDIC_ENABLED || !dealId || !method.fields.some(field => field.key === fieldKey)) return;
    setMeddicByDeal(prev => {
      const key = String(dealId);
      const current = normalizeMeddicValue(prev[key], method.key);
      const next = { ...current, [fieldKey]: !current[fieldKey] };
      return { ...prev, [key]: next };
    });
    meddicUpdatedDealRef.current = dealId;
    const field = method.fields.find(item => item.key === fieldKey);
    fire((field?.label || method.pillLabel) + ' updated');
  }

  function closeMeddicSheet() {
    const updatedDealId = meddicUpdatedDealRef.current;
    meddicUpdatedDealRef.current = null;
    setMeddicDeal(null);
    if (updatedDealId) pulseUpdatedPill(updatedDealId, 'meddic');
  }

  function saveCloseDate(dealId, value) {
    if (!dealId || !value) return;
    const currentIds = sortTodayCards(
      buildDailyCards(deals, acts, { touchedDealIds: touchedTodayIds, meddicByDeal, includeMeddic: includeMeddicCloseScore, salesMethodology }).filter(c => !swipedIds.includes(c.id) && !hiddenCardIds.includes(c.id)),
      todaySortMode
    ).map(c => c.id);
    setFrozenOrderIds(currentIds);
    setChangedCardId(String(dealId));
    const newDeals = deals.map(d => d.id === dealId ? { ...d, closeDate: value } : d);
    setDeals(newDeals);
    setCloseDateDeal(null);
    pulseUpdatedPill(dealId, 'closeDate');
    syncWrite('PATCH', { dealId, closeDate: value }, 'CD-' + dealId);
    setTimeout(() => {
      setFrozenOrderIds(null);
      setTimeout(() => setChangedCardId(null), 600);
    }, 800);
  }

  function saveDealAmount(dealId, amount) {
    if (!dealId) return;
    const nextAmount = Number(amount);
    if (!Number.isFinite(nextAmount) || nextAmount < 0) return;
    const currentIds = sortTodayCards(
      buildDailyCards(deals, acts, { touchedDealIds: touchedTodayIds, meddicByDeal, includeMeddic: includeMeddicCloseScore, salesMethodology }).filter(c => !swipedIds.includes(c.id) && !hiddenCardIds.includes(c.id)),
      todaySortMode
    ).map(c => c.id);
    setFrozenOrderIds(currentIds);
    setChangedCardId(String(dealId));
    const newDeals = deals.map(d => d.id === dealId ? { ...d, value: nextAmount } : d);
    setDeals(newDeals);
    setAmountDeal(null);
    pulseUpdatedPill(dealId, 'amount');
    fire('Amount updated');
    syncWrite('PATCH', { dealId, amount: nextAmount }, 'AMT-' + dealId);
    setTimeout(() => {
      setFrozenOrderIds(null);
      setTimeout(() => setChangedCardId(null), 600);
    }, 800);
  }

  function saveForecastCategory(dealId, value) {
    setDeals(prev => prev.map(d => d.id === dealId ? { ...d, forecastCategory: value } : d));
    pulseUpdatedPill(dealId, 'forecast');
    fire('Forecast updated');
    syncWrite('PATCH', { dealId, forecastCategory: value }, 'FC-' + dealId);
  }

  function handleLogDeal(deal) {
    const syntheticCard = {
      deal,
      id:         deal.id + '-log',
      action:     'Call',
      label:      'Quick log',
      reason:     '',
      points:         10,
      isNextStep: false,
    };
    setActivityCard(syntheticCard);
  }

  function changeDealStage(deal, sfStage, sourcePill = 'stage') {
    if (!deal || !sfStage) return;
    const riskPts = dealHasVisibleRisk(deal) ? RISK_REMOVED_POINTS : 0;
    setTouchedDealIds(prev => new Set([...prev, deal.id]));
    const entry      = SF_STAGES.find(s => s.sf === sfStage);
    const pqStage    = entry ? entry.pq : sfStage;
    const newForecast = SF_STAGE_TO_FORECAST[sfStage] || '';
    const wonCloseDate = sfStage === 'Closed Won' ? todayDate : null;
    const stagePts   = stageAdvancePoints(deal, sfStage);
    hapticTick(12);
    setStageDoneIds(prev => prev.includes(deal.id) ? prev : [...prev, deal.id]);
    setStageCard(null);
    pulseUpdatedPill(deal.id, sourcePill === 'forecast' ? 'forecast' : 'stage');
    const newDeals = deals.map(d => d.id === deal.id ? { ...d, stage: pqStage, sfStage, forecastCategory: newForecast, ...(wonCloseDate ? { closeDate: wonCloseDate } : {}) } : d);

    // Freeze current card order so the card stays put while we show the pulse animation
    const currentIds = sortTodayCards(
      buildDailyCards(deals, acts, { touchedDealIds: touchedTodayIds, meddicByDeal, includeMeddic: includeMeddicCloseScore, salesMethodology }).filter(c => !swipedIds.includes(c.id) && !hiddenCardIds.includes(c.id)),
      todaySortMode
    ).map(c => c.id);
    setChangedCardId(String(deal.id));
    setFrozenOrderIds(currentIds);

    setDeals(newDeals);
    if (stageBarTimer.current) clearTimeout(stageBarTimer.current);
    setStageBar({ dealId: deal.id, dealName: deal.name, stage: sfStage, prevPqStage: deal.stage, prevSfStage: deal.sfStage, prevForecast: deal.forecastCategory, prevCloseDate: deal.closeDate, key: Date.now() });
    stageBarTimer.current = setTimeout(() => setStageBar(null), 3500);
    if (pqStage === 'Closed Won') {
      setConfettiDeal({ ...deal, stage: pqStage });
      setTimeout(() => setConfettiDeal(null), 4200);
    }

    // Award points only after Salesforce confirms the write; revert on failure.
    syncWrite('PATCH', { dealId: deal.id, stage: sfStage, forecastCategory: newForecast, ...(wonCloseDate ? { closeDate: wonCloseDate } : {}) }, 'PATCH-' + deal.id, null, { label: 'Stage update', throwOnFailure: true })
      .then(() => {
        if (stagePts || riskPts) {
          const totalPts = stagePts + riskPts;
          const newPoints = points + totalPts;
          setPoints(newPoints);
          setTodayPoints(p => p + totalPts);
          const msg = stagePts
            ? deal.name + ' advanced to ' + sfStage + ' (+' + totalPts + ' ' + SCORE_LABEL + ')'
            : 'Risk removed! +' + totalPts + ' ' + SCORE_LABEL;
          earn(totalPts, newPoints, acts, newDeals, msg, points, true, undefined, [...new Set([...stageDoneIds, deal.id])]);
          if (stagePts) celebrateStageAdvance(totalPts, sfStage);
          else celebrateRiskRemoved(totalPts);
        }
      })
      .catch(() => {
        setDeals(prev => prev.map(d => d.id === deal.id
          ? { ...d, stage: deal.stage, sfStage: deal.sfStage || deal.stage, forecastCategory: deal.forecastCategory, closeDate: deal.closeDate }
          : d
        ));
        setStageDoneIds(prev => prev.filter(id => id !== deal.id));
        clearTimeout(stageBarTimer.current);
        setStageBar(null);
        fire('Stage sync failed — change reverted');
      });

    // After pulse settles, capture positions then release frozen order to trigger FLIP
    setTimeout(() => {
      preReorderPositions.current = {};
      Object.entries(cardDomRefs.current).forEach(([id, el]) => {
        if (el) preReorderPositions.current[id] = el.getBoundingClientRect().top;
      });
      setFrozenOrderIds(null);
      setTimeout(() => setChangedCardId(null), 600);
    }, 800);
  }

  function toggleTheme() {
    setTheme(prev => {
      const next = prev === 'dark' ? 'light' : 'dark';
      fire(next === 'dark' ? 'Dark mode' : 'Light mode');
      return next;
    });
  }

  function saveQuotaTargets(nextQuotaTargets) {
    setQuotaTargets(nextQuotaTargets);
    localStorage.setItem(QUARTERLY_QUOTA_KEY, String(nextQuotaTargets.quarterly));
    localStorage.setItem(YEARLY_QUOTA_KEY, String(nextQuotaTargets.yearly));
    localStorage.setItem('pq_quota_target', String(nextQuotaTargets.quarterly));
    fire(targetTermLabel(targetTerm, { title: true, plural: true }) + ' saved');
  }

  function saveTargetTerm(nextTerm) {
    const normalizedTerm = normalizeTargetTerm(nextTerm);
    setTargetTerm(normalizedTerm);
    localStorage.setItem(TARGET_TERM_KEY, normalizedTerm);
    fire('Sales target term saved');
  }

  function saveFiscalStartMonth(nextMonth) {
    const normalizedMonth = normalizeFiscalStartMonth(nextMonth);
    setFiscalStartMonth(normalizedMonth);
    localStorage.setItem(FISCAL_START_MONTH_KEY, String(normalizedMonth));
    fire('Fiscal calendar saved');
  }

  function undoStageChange() {
    if (!stageBar) return;
    clearTimeout(stageBarTimer.current);
    const revertSf       = REVERT_STAGE_MAP[stageBar.prevPqStage] || stageBar.prevPqStage;
    const revertPq       = stageBar.prevPqStage;
    const revertForecast = stageBar.prevForecast || SF_STAGE_TO_FORECAST[revertSf] || '';
    const revertSfStage  = stageBar.prevSfStage || revertSf;
    const revertPatch = { dealId: stageBar.dealId, stage: revertSf, forecastCategory: revertForecast };
    if (stageBar.prevCloseDate) revertPatch.closeDate = stageBar.prevCloseDate;
    setDeals(prev => prev.map(d => d.id === stageBar.dealId ? { ...d, stage: revertPq, sfStage: revertSfStage, forecastCategory: revertForecast, closeDate: stageBar.prevCloseDate || d.closeDate } : d));
    setStageDoneIds(prev => prev.filter(id => id !== stageBar.dealId));
    setStageBar(null);
    syncWrite('PATCH', revertPatch, 'PATCH-' + stageBar.dealId);
  }

  const primaryScreenIds = NAV.map(n => n.id);
  function saveForecastFilter(value) {
    const nextScope = normalizeForecastScopeValue(value);
    setForecastFilter(nextScope);
    localStorage.setItem('pq_forecast_scope', nextScope);
  }

  function readLocalProgressForExport() {
    const exactKeys = new Set([
      POINTS_STORAGE_KEY,
      LEGACY_POINTS_STORAGE_KEY,
      'pq_unlocked',
      'pq_theme',
      MEDDIC_ENABLED_KEY,
      SALES_METHODOLOGY_KEY,
      QUARTERLY_QUOTA_KEY,
      YEARLY_QUOTA_KEY,
      TARGET_TERM_KEY,
      FISCAL_START_MONTH_KEY,
      'pq_quota_target',
      'pq_skip_counts',
      'pq_freeze_tokens',
      'pq_freeze_used',
      'pq_reset_act_ids',
      'pq_forecast_scope',
      FORECAST_CALL_STORAGE_KEY,
      'pq_progress_updated_at',
    ]);
    const prefixes = [
      TODAY_POINTS_STORAGE_PREFIX,
      LEGACY_TODAY_POINTS_STORAGE_PREFIX,
      'pq_swiped_',
      'pq_next_steps_',
      'pq_stage_steps_',
      'pq_quest_rewarded_',
      'pq_login_',
      'pq_sweep_',
      'pq_freeze_earn_',
      'pq_meddic',
      'pq_profile_pref_',
    ];
    const snapshot = {};
    for (let i = 0; i < localStorage.length; i += 1) {
      const key = localStorage.key(i);
      if (!key) continue;
      const allowed = exactKeys.has(key) || prefixes.some(prefix => key.startsWith(prefix));
      if (!allowed) continue;
      const raw = localStorage.getItem(key);
      try {
        snapshot[key] = JSON.parse(raw);
      } catch {
        snapshot[key] = raw;
      }
    }
    return snapshot;
  }

  function exportPipelineQuestData() {
    const payload = {
      exportedAt: new Date().toISOString(),
      app: {
        name: 'PipelineQuest',
        version: PQ_VERSION,
        build: PQ_BUILD,
        target: PQ_TARGET || (IS_DEV_ENV ? 'dev' : 'local'),
      },
      profile: {
        userName,
        orgName,
        lastSync,
        salesforceReadOnly,
      },
      progress: {
        points,
        todayPoints,
        todayDate,
        streak,
        unlocked,
        quotaTargets,
        targetTerm,
        fiscalStartMonth,
        freezeTokens,
        freezeUsedDates,
        skipCounts,
        questRewardedIds,
        nextStepDoneIds,
        stageDoneIds,
      },
      activityLog: acts,
      localProgress: readLocalProgressForExport(),
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = 'pipelinequest-export-' + todayDate + '.json';
    document.body.appendChild(link);
    link.click();
    link.remove();
    URL.revokeObjectURL(url);
    fire('PipelineQuest data exported');
  }

  const filterConfig = {
    today: {
      title: 'Sort today deals',
      value: todaySortMode,
      setValue: setTodaySortMode,
      options: [
        { value: 'forecast', label: 'Forecast' },
        { value: 'closeDate', label: 'Close date' },
        { value: 'stage', label: 'Sales stage' },
      ],
    },
    pipeline: {
      title: 'Filter forecast',
      value: forecastFilter,
      setValue: saveForecastFilter,
      options: [
        { value: 'quarter', label: 'Current Quarter' },
        { value: 'next', label: 'Next Quarter' },
        { value: 'year', label: 'Fiscal Year' },
      ],
    },
  };
  const activeFilter = filterConfig[screen] || null;
  function closeNavFilter() {
    if (!navFilterOpen || navFilterClosing) return;
    setNavFilterClosing(true);
    setTimeout(() => { setNavFilterOpen(false); setNavFilterClosing(false); }, 140);
  }
  useEffect(() => {
    if (!navFilterOpen) return;
    function onDown(e) {
      if (navFilterRef.current && !navFilterRef.current.contains(e.target)) closeNavFilter();
    }
    document.addEventListener('pointerdown', onDown);
    return () => document.removeEventListener('pointerdown', onDown);
  }, [navFilterOpen, navFilterClosing]);
  useEffect(() => {
    setNavFilterOpen(false);
    setNavFilterClosing(false);
  }, [screen]);
  function chooseNavFilter(value) {
    if (!activeFilter) return;
    activeFilter.setValue(value);
    closeNavFilter();
  }
  function goToScreen(nextScreen) {
    if (!nextScreen || nextScreen === screen) return;
    const fromIdx = primaryScreenIds.indexOf(screen);
    const toIdx = primaryScreenIds.indexOf(nextScreen);
    if (fromIdx !== -1 && toIdx !== -1) {
      setScreenTransition(toIdx > fromIdx ? 'slide-left' : 'slide-right');
      clearTimeout(screenTransitionTimer.current);
      screenTransitionTimer.current = setTimeout(() => setScreenTransition(''), 280);
    } else {
      setScreenTransition('');
    }
    setScreen(nextScreen);
  }
  function onScreenTouchStart(e) {
    if (!primaryScreenIds.includes(screen)) {
      screenTouchStart.current = null;
      return;
    }
    const target = e.target;
    if (target.closest?.('button, input, textarea, select, a, .swipe-row, .today-completed-box, .activity-sheet, .score-sheet-backdrop, .next-step-sheet-backdrop')) {
      screenTouchStart.current = null;
      return;
    }
    const touch = e.touches?.[0];
    if (!touch) return;
    screenTouchStart.current = { x: touch.clientX, y: touch.clientY };
  }
  function onScreenTouchEnd(e) {
    const start = screenTouchStart.current;
    screenTouchStart.current = null;
    if (!start) return;
    const touch = e.changedTouches?.[0];
    if (!touch) return;
    const dx = touch.clientX - start.x;
    const dy = touch.clientY - start.y;
    if (Math.abs(dx) < 64 || Math.abs(dx) < Math.abs(dy) * 1.25) return;
    const idx = primaryScreenIds.indexOf(screen);
    if (idx === -1) return;
    const nextIdx = dx < 0 ? idx + 1 : idx - 1;
    if (nextIdx < 0 || nextIdx >= primaryScreenIds.length) return;
    goToScreen(primaryScreenIds[nextIdx]);
  }

  if (accountAuthRequired) {
    return <AuthScreen onComplete={completeAccountAuth} />;
  }

  return (
    <div className={'phone ' + theme + (syncFailures.length > 0 ? ' has-sync-failure' : '') + (IS_TODAY_ROW_SIMPLIFIED_ENABLED ? ' ff-today-row-simplified' : '') + (IS_PROFESSIONAL_IA_V2_ENABLED ? ' ff-professional-ia-v2' : '')}>
      {/* Header */}
      <div className="hdr">
        <div className="hdr-row1">
          <button className="logo" type="button" onClick={() => goToScreen('today')} aria-label="Go to Today">
            <img className="logo-mark" src="/icons/pipelinequest-logo-mark.png" alt="" aria-hidden="true" />
          </button>
          <div className="hdr-icons">
            {syncPending > 0 && <span className="sync-dot" title="Syncing..." />}
            {syncFailures.length > 0 && (
              <button className="hdr-sync-err-btn" onClick={() => setSyncDiagnosticOpen(v => !v)} title="Sync failures — tap to view" aria-label={syncFailures.length + ' sync failure' + (syncFailures.length > 1 ? 's' : '')}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
                <span className="hdr-sync-err-badge">{syncFailures.length}</span>
              </button>
            )}
            <button className="hdr-profile" onClick={() => goToScreen('account')} title="Profile">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <circle cx="12" cy="8" r="4" />
                <path d="M4 21a8 8 0 0116 0" />
              </svg>
            </button>
          </div>
        </div>
      </div>

      {/* Points progress bar */}
      {(() => {
        const prevLevelPoints = LVL_POINTS[lv - 1] || 0;
        const nextLevelPoints = pointsNext(points);
        const pct = lv >= LVL_POINTS.length ? 100 : Math.min(100, Math.round(((points - prevLevelPoints) / (nextLevelPoints - prevLevelPoints)) * 100));
        const pointsToNext = Math.max(0, nextLevelPoints - points);
        return (
          <div className="points-bar-wrap">
            <div className="points-bar-main">
              <div className="points-bar-body">
                <div className="points-bar-row">
                  <span className="points-bar-level">{IS_PROFESSIONAL_IA_V2_ENABLED ? 'Level ' + lv : 'LVL ' + lv}</span>
                  <span className="points-bar-title">{IS_PROFESSIONAL_IA_V2_ENABLED ? 'Momentum' : repTitle}</span>
                  <span className="points-bar-nums">{IS_PROFESSIONAL_IA_V2_ENABLED ? (pointsToNext > 0 ? pointsToNext + ' to next' : 'Complete') : points + ' / ' + nextLevelPoints + ' ' + SCORE_LABEL}</span>
                </div>
                <div className="points-bar-track" role="progressbar" aria-label={IS_PROFESSIONAL_IA_V2_ENABLED ? 'Momentum' : 'Level progress'} aria-valuemin="0" aria-valuemax="100" aria-valuenow={pct}>
                  <div className={'points-bar-fill' + (onFire ? ' fire' : '')} style={{ width: pct + '%' }} />
                </div>
              </div>
              {activeFilter && (
                <div className="today-filter points-filter" ref={navFilterRef}>
                  <button
                    className="today-filter-btn"
                    onClick={() => navFilterOpen ? closeNavFilter() : setNavFilterOpen(true)}
                    title={activeFilter.title}
                    aria-label={activeFilter.title}
                    aria-expanded={navFilterOpen}
                  >
                    <ActIcon type="filter" size={21} />
                  </button>
                  {navFilterOpen && (
                    <div className={'today-filter-menu points-filter-menu' + (navFilterClosing ? ' closing' : '')}>
                      {activeFilter.options.map(option => (
                        <button
                          key={option.value}
                          className={'today-filter-option' + (activeFilter.value === option.value ? ' on' : '')}
                          onClick={() => chooseNavFilter(option.value)}
                        >
                          <span>{option.label}</span>
                          {activeFilter.value === option.value && <span className="today-filter-check">✓</span>}
                        </button>
                      ))}
                    </div>
                  )}
                </div>
              )}
            </div>
          </div>
        );
      })()}

      {syncFailures.length > 0 && syncDiagnosticOpen && (
        <div className="sync-err-panel">
          <div className="sync-err-panel-hdr">
            <span>Sync {syncFailures.length === 1 ? 'Failure' : syncFailures.length + ' Failures'}</span>
            <button className="sync-err-panel-close" onClick={() => setSyncDiagnosticOpen(false)} aria-label="Close sync panel">×</button>
          </div>
          {syncFailures.map(failure => (
            <div className="sync-err-item" key={failure.id}>
              <div className="sync-err-item-label">{failure.label}</div>
              <div className="sync-err-item-msg">{failure.message}</div>
              <div className="sync-err-item-actions">
                <button className="sync-retry-btn" onClick={() => retrySyncFailure(failure)}>Retry</button>
                <button className="sync-dismiss-btn" onClick={() => dismissSyncFailure(failure.id)}>Dismiss</button>
              </div>
            </div>
          ))}
        </div>
      )}
      {confettiDeal && <ClosedWonCelebration deal={confettiDeal} />}
      {pipelineCleared && <PipelineClearedCelebration data={pipelineCleared} onDismiss={() => setPipelineCleared(null)} onReload={reloadDailyCards} />}
      {burstStack.length > 0 && <BurstStack stack={burstStack} />}
      {pendingUndo && !IS_TODAY_INLINE_UNDO_ENABLED && <UndoBar action={pendingUndo} onUndo={undoTodayAction} />}
      {stageBar && !pendingUndo && !IS_TODAY_INLINE_UNDO_ENABLED && (
        <div key={stageBar.key} className="undo-bar">
          <div className="undo-copy">
            <div>{stageBar.dealName}</div>
            <div className="undo-sub">Moved to {stageBar.stage}</div>
          </div>
          <button className="undo-btn" onClick={undoStageChange}>Undo</button>
        </div>
      )}
      {activityCard && (
        <ActivitySheet
          card={activityCard}
          onSelect={type => logCardActivity(activityCard, type)}
          onClose={closeActivitySheet}
          onFire={onFire}
        />
      )}
      {stageCard && (
        <StageSheet
          card={stageCard}
          onSelect={(deal, sfStage) => changeDealStage(deal, sfStage, stageCard.sourcePill)}
          onClose={() => setStageCard(null)}
        />
      )}
      {nextStepDeal && (
        <NextStepSheet
          deal={nextStepDeal}
          activities={recentActivitiesForDeal(nextStepDeal)}
          notes={notesForDeal(nextStepDeal)}
          apiSecret={apiSecretRef.current}
          salesforceInstanceUrl={IS_DEV_SETUP_ENV ? devSetup.instanceUrl : ''}
          aiEnabled={IS_AI_NEXT_STEPS_ENABLED}
          onSave={saveNextStep}
          onClose={() => setNextStepDeal(null)}
        />
      )}
      {closeDateDeal && (
        <CloseDateSheet
          deal={closeDateDeal}
          onSave={saveCloseDate}
          onClose={() => setCloseDateDeal(null)}
        />
      )}
      {amountDeal && (
        <AmountSheet
          deal={amountDeal}
          onSave={saveDealAmount}
          onClose={() => setAmountDeal(null)}
        />
      )}
      {IS_DEV_MEDDIC_ENABLED && meddicDeal && (
        <MeddicSheet
          deal={meddicDeal}
          value={meddicByDeal[String(meddicDeal.id)]}
          salesMethodology={salesMethodology}
          onToggle={toggleMeddicField}
          onClose={closeMeddicSheet}
        />
      )}

      <div
        className="scroll"
        onTouchStart={onScreenTouchStart}
        onTouchEnd={onScreenTouchEnd}
      >
        <div key={screen} className={'screen-pane ' + screenTransition}>
        {loading && screen !== 'account' && <div style={{ flex:1, display:'flex', flexDirection:'column' }}><LoadingScreen /></div>}
        {loadErr && screen !== 'account' && <div style={{ flex:1, display:'flex', flexDirection:'column' }}><ErrorScreen error={loadErr} onRetry={() => setRetryCount(c => c + 1)} /></div>}
        {!loading && !loadErr && screen === 'today' && (
          <TodayScreen
            activeCards={activeCards}
            completedTodayCards={completedTodayCards}
            onRestoreCompleted={restoreCompletedCard}
            doneCards={doneCards}
            onComplete={completeCard}
            onSkip={skipCard}
            onOpenActivity={openActivitySheet}
            onOpenStage={setStageCard}
            onOpenNextStep={setNextStepDeal}
            onOpenCloseDate={setCloseDateDeal}
            onOpenAmount={setAmountDeal}
            onOpenMeddic={setMeddicDeal}
            meddicByDeal={meddicByDeal}
            meddicEnabled={IS_DEV_MEDDIC_ENABLED && meddicAvailable}
            includeMeddicCloseScore={includeMeddicCloseScore}
            salesMethodology={salesMethodology}
            onReload={reloadDailyCards}
            onForceDoneComplete={finishPendingLog}
            pendingDoneCardId={pendingLog?.card.id}
            acts={acts}
            onCardRef={(id, el) => { cardDomRefs.current[id] = el; }}
            changedCardId={changedCardId}
            touchedDealIds={touchedDealIds}
            updatedNextStepIds={nextStepDoneIds}
            updatedStageIds={stageDoneIds}
            updatedPill={updatedPill}
            skipCounts={skipCounts}
            onRefresh={() => setRetryCount(c => c + 1)}
          />
        )}
        {!loading && !loadErr && screen === 'pipeline' && (
          <Pipeline deals={deals} acts={acts} filter={forecastFilter} onFilterChange={saveForecastFilter} quotaTargets={quotaTargets} targetTerm={targetTerm} fiscalStartMonth={fiscalStartMonth} onAdvance={advanceDeal} onSaveNextStep={setNextStepDeal} onSaveForecast={saveForecastCategory} updatedNextStepIds={nextStepDoneIds} touchedDealIds={touchedDealIds} meddicByDeal={meddicByDeal} includeMeddicCloseScore={includeMeddicCloseScore} salesMethodology={salesMethodology} onRefresh={() => setRetryCount(c => c + 1)} />
        )}
        {!loading && !loadErr && screen === 'progress' && (
          <Awards unlocked={unlocked} stats={calcStats(gameActs)} points={points} streak={streak} nextStepCount={nextStepDoneIds.length} todayActDealCount={todayActDealCount} openDealsCount={openDeals.length} sessionActCount={sessionActCount} freezeTokens={freezeTokens} dailyQuests={dailyQuests} weeklyQuests={weeklyQuests} filter={progressFilter} onRefresh={() => setRetryCount(c => c + 1)} />
        )}
        {screen === 'account' && (
          <ProfileScreen userName={userName} orgName={orgName} lastSync={lastSync} theme={theme} accountSession={accountSession} quotaTargets={quotaTargets} targetTerm={targetTerm} fiscalStartMonth={fiscalStartMonth} meddicAvailable={meddicAvailable} salesMethodology={salesMethodology} showMeddicSetting={IS_DEV_MEDDIC_ENABLED} syncPending={syncPending} syncFailures={syncFailures} environment={PQ_TARGET || (IS_DEV_ENV ? 'dev' : 'prod')} salesforceInstanceUrl={devSetup.instanceUrl} salesforceReadOnly={salesforceReadOnly} isDemoMode={isDemoMode} performance={professionalPerformance} onToggleMeddic={() => setMeddicAvailable(v => !v)} onSaveQuotas={saveQuotaTargets} onSaveTargetTerm={saveTargetTerm} onSaveSalesMethodology={setSalesMethodology} onSaveFiscalStartMonth={saveFiscalStartMonth} onToggleTheme={toggleTheme} onToggleSalesforceReadOnly={toggleSalesforceReadOnly} onDisconnectSalesforce={disconnectSalesforce} onSaveName={name => { setUserName(name); localStorage.setItem('pq_user_name', name); }} onAccountSignOut={signOutAccount} onDeleteAccount={deleteLocalAccount} onSignOut={() => { clearPipelineQuestLocalState({ preserveAccount: true }); window.location.reload(); }} onExportData={exportPipelineQuestData} onRefresh={() => setRetryCount(c => c + 1)} onRetryFailedSync={retryFailedWritebacks} showDemoReset={IS_DEV_SETUP_ENV} onEnterDemoMode={startReviewDemoMode} onResetDemo={resetDemoMode} showLevelReset={IS_DEV_ENV} onResetLevel={resetToLevelOne} />
        )}
        </div>
      </div>

      <div className={'bnav' + (NAV.length === 2 ? ' two-up' : '')} data-tour-id="bottom-nav">
        {NAV.map(n => (
          <button key={n.id} className={'bnav-btn' + (screen === n.id ? ' on' : '')} onClick={() => goToScreen(n.id)} data-testid={'bottom-nav-' + n.id}>
            <span className="bnav-ico"><NavIcon type={n.icon} /></span>
            <span className="bnav-lbl">{n.lbl}</span>
          </button>
        ))}
      </div>
      <div className="safe-area-filler" />
      {!loading && !loadErr && screen === 'today' && !activityCard && !stageCard && !nextStepDeal && !closeDateDeal && !amountDeal && !meddicDeal && (
        <TodayNavigationTour
          hasTodayRows={activeCards.length > 0}
          previewCard={activeCards[0]}
          previewCloseScore={activeCards[0] ? dealCloseScore(activeCards[0].deal, acts, touchedDealIds, { includeMeddic: includeMeddicCloseScore, meddic: meddicSummary(meddicByDeal[String(activeCards[0].deal.id)], salesMethodology) }) : null}
        />
      )}
    </div>
  );
}

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