// Hero stats, calendar, feeds, profile
const { useState: useStateH, useEffect: useEffectH } = React;

// ─── i18n ───────────────────────────────────────────────────────────
const STR_below = {
  en: {
    perf_calendar: 'Performance Calendar',
    today: 'Today',
    less: 'Less', more: 'More',
    economic_calendar: 'Economic Calendar',
    market_news: 'Market News',
    platform_announcements: 'Platform Announcements',
    hours_settings: 'Hours & Settings',
    hours_subtitle: 'NQ · CME Globex · America/New_York',
    open_at: '18:00 open',
    close_at: 'Close at 16:20',
    closed: 'Closed',
    today_chip: ' · Today',
    notifications: 'Notifications',
    coming_soon: 'Coming soon',
    notif_subtitle: 'Preview delivery options — wiring lands next release.',
    sound_label: 'Desktop sound on signal',
    sound_desc: 'Soft tone when a new signal publishes',
    desktop_label: 'Desktop push',
    desktop_desc: 'Browser notification per signal',
    email_label: 'Daily recap email',
    email_desc: 'Sent at 16:45 ET on trading days',
    profile_risk: 'Profile & Risk',
    profile_subtitle: 'How the Live tab behaves for your account.',
    risk_label: 'Risk preset · Conservative',
    risk_desc: 'Half-size sizing, tighter stops',
    autopause_label: 'Auto-pause after 3 losses',
    autopause_desc: 'Stops signals until you resume',
    hide_rej_label: 'Hide rejected signals',
    hide_rej_desc: 'Only show approved setups in the rail',
    disclaimer_strong: 'Disclaimer.',
    disclaimer_body: 'Informational and educational use only. Live session data is simulated in this preview.',
    live_disclaimer: 'Live signals only publish during weekday CME sessions. Outside of these hours the tab shows the last completed session plus a digest view.',
    breakdown: 'Breakdown',
    trades: 'Trades',
    wins_losses: 'Wins / Losses',
    win_rate: 'Win rate',
    profit_factor: 'Profit factor',
    avg_win: 'Avg win',
    avg_loss: 'Avg loss',
    cal_loading: 'Loading calendar...',
    cal_empty: 'No major US events in the next 48 hours',
    cal_error: 'Calendar temporarily unavailable',
    cal_source_prefix: 'Source:',
    cal_updated: 'Updated',
    news_coming_desc: 'Real market headlines land in PR-NEWS-1.',
    announce_coming_desc: 'Platform updates and bot release notes will appear here.',
    news_loading: 'Loading headlines...',
    news_empty: 'No major NQ-relevant headlines right now',
    news_error: 'News temporarily unavailable',
    news_source_prefix: 'Sources:',
  },
  es: {
    perf_calendar: 'Calendario de Desempeño',
    today: 'Hoy',
    less: 'Menos', more: 'Más',
    economic_calendar: 'Calendario Económico',
    market_news: 'Noticias del Mercado',
    platform_announcements: 'Anuncios de la Plataforma',
    hours_settings: 'Horario y Ajustes',
    hours_subtitle: 'NQ · CME Globex · America/New_York',
    open_at: 'Abre 18:00',
    close_at: 'Cierre 16:20',
    closed: 'Cerrado',
    today_chip: ' · Hoy',
    notifications: 'Notificaciones',
    coming_soon: 'Próximamente',
    notif_subtitle: 'Vista previa de las opciones de entrega — la conexión llega en la próxima versión.',
    sound_label: 'Sonido de escritorio en cada señal',
    sound_desc: 'Tono suave cuando se publica una nueva señal',
    desktop_label: 'Notificación de escritorio',
    desktop_desc: 'Notificación del navegador por cada señal',
    email_label: 'Correo de resumen diario',
    email_desc: 'Enviado a las 16:45 ET en días de operación',
    profile_risk: 'Perfil y Riesgo',
    profile_subtitle: 'Cómo se comporta la pestaña En Vivo para tu cuenta.',
    risk_label: 'Preajuste de riesgo · Conservador',
    risk_desc: 'Tamaño a la mitad, stops más ajustados',
    autopause_label: 'Auto-pausa tras 3 pérdidas',
    autopause_desc: 'Detiene señales hasta que reanudes',
    hide_rej_label: 'Ocultar señales rechazadas',
    hide_rej_desc: 'Solo mostrar setups aprobados en el carril',
    disclaimer_strong: 'Aviso.',
    disclaimer_body: 'Uso informativo y educativo únicamente. Los datos de la sesión en vivo están simulados en esta vista previa.',
    live_disclaimer: 'Las señales en vivo solo se publican durante sesiones CME entre semana. Fuera de este horario la pestaña muestra la última sesión completada más una vista resumen.',
    breakdown: 'Desglose',
    trades: 'Operaciones',
    wins_losses: 'Ganadas / Perdidas',
    win_rate: '% Victorias',
    profit_factor: 'Factor de beneficio',
    avg_win: 'Ganancia media',
    avg_loss: 'Pérdida media',
    cal_loading: 'Cargando calendario...',
    cal_empty: 'Sin eventos importantes de EE.UU. en las próximas 48 h',
    cal_error: 'Calendario temporalmente no disponible',
    cal_source_prefix: 'Fuente:',
    cal_updated: 'Actualizado',
    news_coming_desc: 'Titulares reales del mercado llegan en PR-NEWS-1.',
    announce_coming_desc: 'Actualizaciones de la plataforma y notas del bot aparecerán aquí.',
    news_loading: 'Cargando titulares...',
    news_empty: 'Sin titulares importantes para NQ ahora mismo',
    news_error: 'Noticias temporalmente no disponibles',
    news_source_prefix: 'Fuentes:',
  },
};
function t_below(key) {
  const lang = (document.documentElement.lang || 'en').toLowerCase().split('-')[0];
  return (STR_below[lang] && STR_below[lang][key]) || STR_below.en[key] || key;
}
function lang_below() {
  return (document.documentElement.lang || 'en').toLowerCase().split('-')[0];
}

// Slice B: re-render hook
function useLiveTickH() {
  const [, setT] = useStateH(0);
  useEffectH(() => {
    const onUpdate = () => setT(t => t + 1);
    window.addEventListener('mmm-live-update', onUpdate);
    return () => window.removeEventListener('mmm-live-update', onUpdate);
  }, []);
}

function getStatData() {
  return (typeof window !== 'undefined' && window.STAT_DATA) || {};
}

function HeroStat({ id, data }) {
  if (!data || !data.tooltip) {
    return <div className="hero-stat"><div className="hero-stat__label">—</div><div className="hero-stat__value">—</div><div className="hero-stat__sub"><span>—</span></div></div>;
  }
  const tt = data.tooltip;
  const winPct = (tt.wins / (tt.wins + tt.losses || 1)) * 100;
  return (
    <div className="hero-stat">
      <div className="hero-stat__label">{data.label}</div>
      <div className={`hero-stat__value ${data.down ? 'down' : 'up'}`}>{data.value}</div>
      <div className="hero-stat__sub">
        <span>{data.sub}</span>
        {tt.wins !== undefined && (
          <>
            <span className="chip win">●{tt.wins}W</span>
            <span className="chip loss">●{tt.losses}L</span>
          </>
        )}
      </div>
      <div className="stat-tooltip">
        <div style={{fontSize:'var(--fs-xs)', textTransform:'uppercase', letterSpacing:'0.08em', color:'var(--muted)', marginBottom:8}}>
          {data.label} · {t_below('breakdown')}
        </div>
        <div className="stat-tooltip__row"><span>{t_below('trades')}</span><span>{tt.trades}</span></div>
        <div className="stat-tooltip__row"><span>{t_below('wins_losses')}</span><span><span className="up">{tt.wins}</span> / <span className="down">{tt.losses}</span></span></div>
        <div className="stat-tooltip__bar">
          <span className="wins" style={{width:`${winPct}%`}}></span>
          <span className="losses" style={{width:`${100-winPct}%`}}></span>
        </div>
        <div className="stat-tooltip__row"><span>{t_below('win_rate')}</span><span>{tt.winRate}</span></div>
        <div className="stat-tooltip__row"><span>{t_below('profit_factor')}</span><span>{tt.pf}</span></div>
        <div className="stat-tooltip__row"><span>{t_below('avg_win')}</span><span className="up">{tt.avgWin}</span></div>
        <div className="stat-tooltip__row"><span>{t_below('avg_loss')}</span><span className="down">{tt.avgLoss}</span></div>
      </div>
    </div>
  );
}

function HeroStats({ variation = 'classic' }) {
  useLiveTickH();
  const order = ['session', 'wtd', 'mtd', 'ytd', 'winrate', 'pf'];
  return (
    <div className="hero-stats">
      {order.map(k => <HeroStat key={k} id={k} data={getStatData()[k]} />)}
    </div>
  );
}

// ─── Calendar heatmap ──────────────────────────────────────────────
function genMonthData(year, month) {
  const days = new Date(year, month + 1, 0).getDate();
  const rng = mulberry32(year * 100 + month);
  const out = [];
  const today = new Date();
  for (let d = 1; d <= days; d++) {
    const dt = new Date(year, month, d);
    const dow = dt.getDay();
    const isFuture = dt > today;
    if (dow === 0 || dow === 6 || isFuture) {
      out.push({ day: d, weekend: dow === 0 || dow === 6, future: isFuture });
      continue;
    }
    const r = rng();
    let pl;
    if (r < 0.38) pl = -(50 + rng() * 240);
    else pl = (30 + rng() * 340);
    if (rng() < 0.08) pl = pl * 2.2;
    const trades = 1 + Math.floor(rng() * 5);
    out.push({ day: d, pl: Math.round(pl), trades, dow });
  }
  return out;
}

function plClass(pl) {
  if (pl === undefined || pl === null) return '';
  const abs = Math.abs(pl);
  if (pl > 0) {
    if (abs > 400) return 'pl-4';
    if (abs > 200) return 'pl-3';
    if (abs > 80) return 'pl-2';
    return 'pl-1';
  } else {
    if (abs > 400) return 'pl-n4';
    if (abs > 200) return 'pl-n3';
    if (abs > 80) return 'pl-n2';
    return 'pl-n1';
  }
}

function CalendarHeatmap() {
  const today = new Date();
  const [cur, setCur] = useStateH({ y: today.getFullYear(), m: today.getMonth() });
  const data = genMonthData(cur.y, cur.m);
  const localeStr = lang_below() === 'es' ? 'es-ES' : 'en-US';
  const monthName = new Date(cur.y, cur.m, 1).toLocaleString(localeStr, { month: 'long' });
  const firstDow = new Date(cur.y, cur.m, 1).getDay();
  const blanks = Array.from({length: firstDow}, (_, i) => i);

  // Day-of-week headers — localized
  const dowHeaders = lang_below() === 'es'
    ? ['Dom','Lun','Mar','Mié','Jue','Vie','Sáb']
    : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

  return (
    <div className="cal-card">
      <div className="cal-card__main">
      <div className="cal-header">
        <div>
          <div style={{fontSize:'var(--fs-xs)', textTransform:'uppercase', letterSpacing:'0.08em', color:'var(--muted)'}}>{t_below('perf_calendar')}</div>
          <div style={{fontFamily:'var(--serif)', fontSize:22, letterSpacing:'-0.01em', marginTop:2}}>{monthName} {cur.y}</div>
        </div>
        <div className="cal-nav">
          <button onClick={() => setCur(c => { const m = c.m-1; return m < 0 ? {y:c.y-1, m:11} : {y:c.y, m}; })}>‹</button>
          <span className="label">{monthName.slice(0,3)} {cur.y}</span>
          <button onClick={() => setCur(c => { const m = c.m+1; return m > 11 ? {y:c.y+1, m:0} : {y:c.y, m}; })}>›</button>
          <button style={{marginLeft:6, padding:'0 8px', width:'auto', fontSize:'var(--fs-xs)'}} onClick={() => setCur({y:today.getFullYear(), m:today.getMonth()})}>{t_below('today')}</button>
        </div>
      </div>
      <div className="cal-grid">
        {dowHeaders.map(d => (
          <div className="cal-dow" key={d}>{d}</div>
        ))}
        {blanks.map(i => <div className="cal-day empty" key={`b${i}`} />)}
        {data.map(d => {
          const isToday = cur.y === today.getFullYear() && cur.m === today.getMonth() && d.day === today.getDate();
          const classes = ['cal-day'];
          if (d.weekend) classes.push('weekend');
          if (d.future) classes.push('weekend');
          if (isToday) classes.push('today');
          if (typeof d.pl === 'number') classes.push(plClass(d.pl));
          return (
            <div className={classes.join(' ')} key={d.day} title={typeof d.pl === 'number' ? `${d.day}: ${d.pl>0?'+':''}$${d.pl} · ${d.trades} ${t_below('trades').toLowerCase()}` : ''}>
              <div className="dn">{d.day}</div>
              {typeof d.pl === 'number' && (
                <div className="pl">{d.pl>0?'+':''}{d.pl}</div>
              )}
            </div>
          );
        })}
      </div>
      <div className="cal-legend">
        <span>{t_below('less')}</span>
        <div className="cal-legend__swatches">
          <span style={{background:'rgba(185,52,36,0.95)'}}></span>
          <span style={{background:'rgba(185,52,36,0.38)'}}></span>
          <span style={{background:'var(--rule-2)'}}></span>
          <span style={{background:'rgba(46,125,79,0.38)'}}></span>
          <span style={{background:'rgba(46,125,79,0.95)'}}></span>
        </div>
        <span>{t_below('more')}</span>
      </div>
      </div>
    </div>
  );
}

// ─── Feeds ─────────────────────────────────────────────────────────
// PR-CAL-1: Economic Calendar wired to ForexFactory community JSON
// (free, CORS-enabled, refreshes every 30 min on the client). Market
// News and Platform Announcements remain placeholders -- those land
// in PR-NEWS-1 once we pick a news source.

const FF_CALENDAR_URL = 'https://nfs.faireconomy.media/ff_calendar_thisweek.json';
const FF_REFRESH_MS = 30 * 60 * 1000;          // 30 min
const FF_LOOKAHEAD_MS = 48 * 60 * 60 * 1000;   // 48 hours
const FF_MAX_ITEMS = 6;

// Map ForexFactory impact strings to our internal {high, med, low}.
// CSS classes already exist for these via the .imp.high / .imp.med /
// .imp.low rules. "Holiday" and unknown impacts are filtered out.
function ffImpactToImp(impact) {
  const i = (impact || '').toLowerCase();
  if (i === 'high') return 'high';
  if (i === 'medium') return 'med';
  if (i === 'low') return 'low';
  return null;
}

// Format a Date as ET HH:MM (24h). Stable regardless of viewer TZ.
function ffTimeLabel(dt) {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: 'America/New_York',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  }).format(dt);
}

// Get ET YYYY-MM-DD key for a Date. Used for day-equality comparison
// (Today vs Tomorrow) without local-TZ contamination.
function ffEtDateKey(dt) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: 'America/New_York',
    year: 'numeric', month: '2-digit', day: '2-digit',
  }).formatToParts(dt);
  const y = parts.find(p => p.type === 'year').value;
  const m = parts.find(p => p.type === 'month').value;
  const d = parts.find(p => p.type === 'day').value;
  return y + '-' + m + '-' + d;
}

function ffDayLabel(eventDt, nowDt, lang) {
  const evKey = ffEtDateKey(eventDt);
  const todayKey = ffEtDateKey(nowDt);
  if (evKey === todayKey) return lang === 'es' ? 'Hoy' : 'Today';
  const tomDt = new Date(nowDt.getTime() + 24 * 60 * 60 * 1000);
  if (evKey === ffEtDateKey(tomDt)) return lang === 'es' ? 'Mañana' : 'Tomorrow';
  const wk = new Intl.DateTimeFormat(lang === 'es' ? 'es-ES' : 'en-US', {
    timeZone: 'America/New_York',
    weekday: 'short',
  }).format(eventDt);
  // Spanish short weekdays come back lowercase with trailing period
  // ('lun.'); capitalize and strip period for consistency with EN.
  const cleaned = wk.replace('.', '');
  return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}

function ffEventDesc(ev, lang) {
  const parts = [];
  const labels = lang === 'es'
    ? { est: 'Est: ', prev: 'Anterior: ' }
    : { est: 'Est: ', prev: 'Prior: ' };
  if (ev.forecast) parts.push(labels.est + ev.forecast);
  if (ev.previous) parts.push(labels.prev + ev.previous);
  return parts.join(' · ');
}

function ffMapEvents(ffData, lang) {
  if (!Array.isArray(ffData)) return [];
  const now = new Date();
  const horizon = new Date(now.getTime() + FF_LOOKAHEAD_MS);
  const items = [];
  for (const ev of ffData) {
    if (!ev || !ev.date || !ev.title) continue;
    if (ev.country !== 'USD') continue;
    const dt = new Date(ev.date);
    if (isNaN(dt.getTime())) continue;
    if (dt < now || dt > horizon) continue;
    const imp = ffImpactToImp(ev.impact);
    if (!imp) continue;
    const desc = ffEventDesc(ev, lang)
      || (lang === 'es' ? 'Sin datos previos' : 'No prior data');
    items.push({
      time: ffDayLabel(dt, now, lang) + ' · ' + ffTimeLabel(dt),
      imp,
      title: ev.title,
      desc,
      _ts: dt.getTime(),
    });
  }
  items.sort((a, b) => a._ts - b._ts);
  return items.slice(0, FF_MAX_ITEMS);
}

async function fetchFFCalendar() {
  const resp = await fetch(FF_CALENDAR_URL, { cache: 'no-store' });
  if (!resp.ok) throw new Error('FF fetch HTTP ' + resp.status);
  return await resp.json();
}

// ─── Market News -- PR-NEWS-1 ──────────────────────────────────────
// Multi-source RSS via rss2json.com (free, CORS-enabled). Headlines
// filtered to NQ-relevant macro + tech keywords, deduped by title,
// sorted newest-first, capped to NEWS_MAX_ITEMS.
const RSS2JSON_BASE = 'https://api.rss2json.com/v1/api.json';
const NEWS_SOURCES = [
  { label: 'CNBC',        url: 'https://www.cnbc.com/id/10000664/device/rss/rss.html' },
  { label: 'Reuters',     url: 'https://www.reuters.com/rssfeed/businessNews' },
  { label: 'MarketWatch', url: 'https://feeds.content.dowjones.io/public/rss/mw_topstories' },
];
const NEWS_REFRESH_MS = 15 * 60 * 1000;     // 15 min (news moves faster than calendar)
const NEWS_MAX_ITEMS = 6;
const NEWS_PER_SOURCE = 15;                 // pre-filter cap per source
const NEWS_DESC_MAXLEN = 140;

// NQ-relevant keywords. Broad on purpose -- if the filter eliminates
// everything we fall back to top unfiltered headlines (better than blank).
const NEWS_KEYWORDS = new RegExp(
  '\\b(fed|fomc|federal reserve|powell|trump|cpi|pce|inflation|nfp|jobless|payroll|' +
  'unemployment|employment|pmi|gdp|retail sales|ism|nasdaq|s&p|nyse|treasury|yield|' +
  '10[- ]?year|bond|tech|semiconductor|chip|nvidia|apple|microsoft|google|alphabet|' +
  'meta|amazon|tesla|earnings|guidance|rate|hike|cut|hawkish|dovish|economy|recession|' +
  'ai\\b|gold|oil|crude|opec|china|tariff)\\b',
  'i'
);

function newsStripHtml(s) {
  if (!s) return '';
  // Strip tags and decode a couple of common entities. Headlines/snippets
  // don't have complex markup so a regex is fine here.
  return String(s)
    .replace(/<[^>]+>/g, '')
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .trim();
}

function newsTruncate(s, n) {
  if (!s) return '';
  if (s.length <= n) return s;
  return s.slice(0, n - 1).trimEnd() + '…';
}

function newsFormatRelativeTime(dt, now, lang) {
  const deltaSec = Math.round((dt.getTime() - now.getTime()) / 1000);  // negative for past
  const abs = Math.abs(deltaSec);
  const locale = lang === 'es' ? 'es' : 'en';
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  if (abs < 60) return rtf.format(Math.round(deltaSec), 'second');
  if (abs < 3600) return rtf.format(Math.round(deltaSec / 60), 'minute');
  if (abs < 86400) return rtf.format(Math.round(deltaSec / 3600), 'hour');
  return rtf.format(Math.round(deltaSec / 86400), 'day');
}

function newsMatchesKeywords(item) {
  const haystack = (item.title || '') + ' ' + (item.description || '');
  return NEWS_KEYWORDS.test(haystack);
}

function newsMapSourceItems(items, sourceLabel, lang, now) {
  if (!Array.isArray(items)) return [];
  const out = [];
  for (const raw of items) {
    if (!raw || !raw.title) continue;
    const pubStr = raw.pubDate || raw.published || raw.date;
    if (!pubStr) continue;
    // rss2json normalizes pubDate to "YYYY-MM-DD HH:MM:SS" (UTC). Add Z for Date parsing.
    const isoStr = /[Zz]|[+\-]\d\d:?\d\d$/.test(pubStr)
      ? pubStr
      : pubStr.replace(' ', 'T') + 'Z';
    const dt = new Date(isoStr);
    if (isNaN(dt.getTime())) continue;
    // Skip very stale items (> 48h old)
    if (now.getTime() - dt.getTime() > 48 * 60 * 60 * 1000) continue;
    // Skip future-dated items (clock skew or feed errors)
    if (dt.getTime() > now.getTime() + 60 * 1000) continue;
    out.push({
      time: newsFormatRelativeTime(dt, now, lang),
      title: newsStripHtml(raw.title),
      desc: newsTruncate(newsStripHtml(raw.description || raw.content || ''), NEWS_DESC_MAXLEN),
      link: raw.link || raw.guid || null,
      _ts: dt.getTime(),
      _source: sourceLabel,
    });
  }
  return out;
}

async function fetchNewsSource(source, lang, now) {
  const url = RSS2JSON_BASE + '?rss_url=' + encodeURIComponent(source.url) + '&count=' + NEWS_PER_SOURCE;
  const resp = await fetch(url, { cache: 'no-store' });
  if (!resp.ok) throw new Error('rss2json HTTP ' + resp.status + ' for ' + source.label);
  const data = await resp.json();
  if (data.status !== 'ok') throw new Error('rss2json status ' + data.status + ' for ' + source.label);
  return newsMapSourceItems(data.items, source.label, lang, now);
}

async function fetchAllNews(lang) {
  const now = new Date();
  const settled = await Promise.allSettled(
    NEWS_SOURCES.map(s => fetchNewsSource(s, lang, now))
  );
  const successCount = settled.filter(r => r.status === 'fulfilled').length;
  if (successCount === 0) {
    throw new Error('all news sources failed');
  }
  // Merge all successful results
  let all = [];
  for (const r of settled) {
    if (r.status === 'fulfilled') all = all.concat(r.value);
  }
  // Dedupe by lowercase title (Map preserves insertion order, keep the
  // first occurrence which has the freshest source ordering)
  const seen = new Map();
  for (const it of all) {
    const key = (it.title || '').toLowerCase().trim();
    if (key && !seen.has(key)) seen.set(key, it);
  }
  all = Array.from(seen.values());
  // Sort newest-first
  all.sort((a, b) => b._ts - a._ts);
  // Apply NQ-relevant keyword filter
  let filtered = all.filter(newsMatchesKeywords);
  // If filter wiped everything, fall back to unfiltered top (better than blank)
  if (filtered.length === 0 && all.length > 0) {
    filtered = all;
  }
  return filtered.slice(0, NEWS_MAX_ITEMS);
}

function FeedCard({ title, items, icon, state, source, updatedAt, emptyMessage, comingSoon, comingSoonDesc, sourceLabel }) {
  const lang = lang_below();
  // ── Coming-soon state (PR-CAL-1.5) -- News + Announcements until PR-NEWS-1 ──
  if (comingSoon) {
    return (
      <div className="feed-card">
        <h3>
          <span>{title}</span>
          <span className="mono muted" style={{fontSize:'var(--fs-xxs)'}}>·</span>
        </h3>
        <div style={{
          padding:'28px 0 22px',
          textAlign:'center',
        }}>
          <div style={{
            fontWeight:600,
            fontSize:'var(--fs-sm)',
            color:'var(--text)',
            marginBottom:6,
          }}>
            {t_below('coming_soon')}
          </div>
          {comingSoonDesc && (
            <div style={{
              fontSize:'var(--fs-xs)',
              color:'var(--muted)',
              maxWidth:280,
              margin:'0 auto',
              lineHeight:1.45,
            }}>
              {comingSoonDesc}
            </div>
          )}
        </div>
      </div>
    );
  }
  // ── Honest non-data states (PR-CAL-1 / PR-NEWS-1) ──
  // loadingMessage / errorMessage props override the default cal_* strings
  // so the same FeedCard can serve calendar + news contexts.
  if (state === 'loading') {
    return (
      <div className="feed-card">
        <h3>
          <span>{title}</span>
          <span className="mono muted" style={{fontSize:'var(--fs-xxs)'}}>…</span>
        </h3>
        <div style={{fontSize:'var(--fs-xs)', color:'var(--muted)', padding:'12px 0'}}>
          {t_below(source === 'news' ? 'news_loading' : 'cal_loading')}
        </div>
      </div>
    );
  }
  if (state === 'error') {
    return (
      <div className="feed-card">
        <h3>
          <span>{title}</span>
          <span className="mono muted" style={{fontSize:'var(--fs-xxs)'}}>!</span>
        </h3>
        <div style={{fontSize:'var(--fs-xs)', color:'var(--muted)', padding:'12px 0'}}>
          {t_below(source === 'news' ? 'news_error' : 'cal_error')}
        </div>
      </div>
    );
  }
  if (!items || items.length === 0) {
    return (
      <div className="feed-card">
        <h3>
          <span>{title}</span>
          <span className="mono muted" style={{fontSize:'var(--fs-xxs)'}}>0</span>
        </h3>
        <div style={{fontSize:'var(--fs-xs)', color:'var(--muted)', padding:'12px 0'}}>
          {emptyMessage || (lang === 'es' ? 'Sin elementos' : 'No items')}
        </div>
      </div>
    );
  }
  return (
    <div className="feed-card">
      <h3>
        <span>{title}</span>
        <span className="mono muted" style={{fontSize:'var(--fs-xxs)'}}>{items.length}</span>
      </h3>
      {items.map((it, i) => {
        const inner = (
          <>
            <div className="when">{it.time}</div>
            <div className="body">
              <div className="title">
                {it.imp && (
                  <span className={`imp ${it.imp}`}>
                    <span></span><span></span><span></span>
                  </span>
                )}
                {it.title}
              </div>
              {it.desc && <div className="desc">{it.desc}</div>}
            </div>
          </>
        );
        // PR-NEWS-1: when item has a link, wrap in <a> for click-through.
        if (it.link) {
          return (
            <a
              key={i}
              href={it.link}
              target="_blank"
              rel="noopener noreferrer"
              className="feed-item"
              style={{
                display:'flex',
                color:'inherit',
                textDecoration:'none',
              }}
            >
              {inner}
            </a>
          );
        }
        return (
          <div className="feed-item" key={i}>
            {inner}
          </div>
        );
      })}
      {source === 'forexfactory' && (
        <div style={{
          fontSize:'var(--fs-xxs)',
          color:'var(--muted)',
          marginTop:8,
          paddingTop:8,
          borderTop:'1px solid var(--border)',
        }}>
          {t_below('cal_source_prefix')}{' '}
          <a href="https://www.forexfactory.com/calendar"
             target="_blank" rel="noopener noreferrer"
             style={{color:'var(--muted)'}}>ForexFactory</a>
          {updatedAt && (
            <span> · {t_below('cal_updated')} {ffTimeLabel(updatedAt)} ET</span>
          )}
        </div>
      )}
      {source === 'news' && sourceLabel && (
        <div style={{
          fontSize:'var(--fs-xxs)',
          color:'var(--muted)',
          marginTop:8,
          paddingTop:8,
          borderTop:'1px solid var(--border)',
        }}>
          {t_below('news_source_prefix')} {sourceLabel}
          {updatedAt && (
            <span> · {t_below('cal_updated')} {ffTimeLabel(updatedAt)} ET</span>
          )}
        </div>
      )}
    </div>
  );
}

function Feeds() {
  const lang = lang_below();
  // Economic calendar -- live state machine (PR-CAL-1).
  // Market News -- live state machine (PR-NEWS-1).
  // Platform Announcements -- coming-soon card (PR-CAL-1.5).
  const [econItems, setEconItems] = useStateH([]);
  const [econState, setEconState] = useStateH('loading');  // loading | live | empty | error
  const [econUpdatedAt, setEconUpdatedAt] = useStateH(null);

  const [newsItems, setNewsItems] = useStateH([]);
  const [newsState, setNewsState] = useStateH('loading');
  const [newsUpdatedAt, setNewsUpdatedAt] = useStateH(null);

  useEffectH(() => {
    let canceled = false;
    let intervalId = null;
    async function load() {
      try {
        const data = await fetchFFCalendar();
        if (canceled) return;
        const items = ffMapEvents(data, lang);
        setEconItems(items);
        setEconState(items.length === 0 ? 'empty' : 'live');
        setEconUpdatedAt(new Date());
      } catch (e) {
        if (canceled) return;
        console.warn('Economic calendar fetch failed:', e);
        setEconState(prev => (prev === 'live' || prev === 'empty' ? prev : 'error'));
      }
    }
    load();
    intervalId = setInterval(load, FF_REFRESH_MS);
    return () => {
      canceled = true;
      if (intervalId) clearInterval(intervalId);
    };
  }, [lang]);

  useEffectH(() => {
    let canceled = false;
    let intervalId = null;
    async function loadNews() {
      try {
        const items = await fetchAllNews(lang);
        if (canceled) return;
        setNewsItems(items);
        setNewsState(items.length === 0 ? 'empty' : 'live');
        setNewsUpdatedAt(new Date());
      } catch (e) {
        if (canceled) return;
        console.warn('Market news fetch failed:', e);
        setNewsState(prev => (prev === 'live' || prev === 'empty' ? prev : 'error'));
      }
    }
    loadNews();
    intervalId = setInterval(loadNews, NEWS_REFRESH_MS);
    return () => {
      canceled = true;
      if (intervalId) clearInterval(intervalId);
    };
  }, [lang]);

  const newsSourcesLabel = NEWS_SOURCES.map(s => s.label).join(', ');

  return (
    <div className="feeds-grid">
      <FeedCard
        title={t_below('economic_calendar')}
        items={econItems}
        state={econState}
        source={(econState === 'live' || econState === 'empty') ? 'forexfactory' : null}
        updatedAt={econUpdatedAt}
        emptyMessage={t_below('cal_empty')}
      />
      <FeedCard
        title={t_below('market_news')}
        items={newsItems}
        state={newsState}
        source={(newsState === 'live' || newsState === 'empty') ? 'news' : null}
        sourceLabel={newsSourcesLabel}
        updatedAt={newsUpdatedAt}
        emptyMessage={t_below('news_empty')}
      />
      <FeedCard
        title={t_below('platform_announcements')}
        comingSoon
        comingSoonDesc={t_below('announce_coming_desc')}
      />
    </div>
  );
}

// ─── Profile / Hours ───────────────────────────────────────────────
function MarketHours() {
  const today = new Date();
  const jsDow = today.getDay();
  const dowIdx = jsDow === 0 ? 6 : jsDow - 1;
  const isES = lang_below() === 'es';
  const dayLabels = isES
    ? ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom']
    : ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
  const hours = [
    { day: dayLabels[0], time: isES ? '18:00 → 16:20 (Mar)' : '18:00 → 16:20 (Tue)' },
    { day: dayLabels[1], time: isES ? '18:00 → 16:20 (Mié)' : '18:00 → 16:20 (Wed)' },
    { day: dayLabels[2], time: isES ? '18:00 → 16:20 (Jue)' : '18:00 → 16:20 (Thu)' },
    { day: dayLabels[3], time: isES ? '18:00 → 16:20 (Vie)' : '18:00 → 16:20 (Fri)' },
    { day: dayLabels[4], time: t_below('close_at'), closed: false },
    { day: dayLabels[5], time: t_below('closed'), closed: true },
    { day: dayLabels[6], time: t_below('open_at'), closed: false },
  ];
  return (
    <div className="profile-card">
      <h3>{t_below('hours_settings')}</h3>
      <div style={{fontSize:'var(--fs-xs)', color:'var(--muted)', marginBottom:8}}>{t_below('hours_subtitle')}</div>
      {hours.map((h, i) => (
        <div className={`hours-row ${i === dowIdx ? 'now' : ''}`} key={h.day}>
          <span className="day">{h.day}{i === dowIdx ? t_below('today_chip') : ''}</span>
          <span className={`time ${h.closed ? 'closed' : ''}`}>{h.time}</span>
        </div>
      ))}
      <div className="disclaimer-text">
        {t_below('live_disclaimer')}
      </div>
    </div>
  );
}

function NotificationsCard() {
  const [sound, setSound] = useStateH(true);
  const [emails, setEmails] = useStateH(false);
  const [desktop, setDesktop] = useStateH(true);
  return (
    <div className="profile-card">
      <h3>{t_below('notifications')} <span className="pill warn" style={{marginLeft:8, fontSize:'var(--fs-xxs)', verticalAlign:'middle'}}><span className="dot"></span>{t_below('coming_soon')}</span></h3>
      <div style={{fontSize:'var(--fs-xs)', color:'var(--muted)', marginBottom:8, marginTop:-4}}>{t_below('notif_subtitle')}</div>
      <div className="toggle-row">
        <div>
          <div className="label">{t_below('sound_label')}</div>
          <div className="desc">{t_below('sound_desc')}</div>
        </div>
        <div className={`toggle ${sound ? 'on':''}`} onClick={() => setSound(!sound)}></div>
      </div>
      <div className="toggle-row">
        <div>
          <div className="label">{t_below('desktop_label')}</div>
          <div className="desc">{t_below('desktop_desc')}</div>
        </div>
        <div className={`toggle ${desktop ? 'on':''}`} onClick={() => setDesktop(!desktop)}></div>
      </div>
      <div className="toggle-row">
        <div>
          <div className="label">{t_below('email_label')}</div>
          <div className="desc">{t_below('email_desc')}</div>
        </div>
        <div className={`toggle ${emails ? 'on':''}`} onClick={() => setEmails(!emails)}></div>
      </div>
    </div>
  );
}

function RiskSettings() {
  const [risk, setRisk] = useStateH(true);
  const [autopause, setAutopause] = useStateH(true);
  const [hideRej, setHideRej] = useStateH(false);
  return (
    <div className="profile-card">
      <h3>{t_below('profile_risk')}</h3>
      <div style={{fontSize:'var(--fs-xs)', color:'var(--muted)', marginBottom:8, marginTop:-4}}>{t_below('profile_subtitle')}</div>
      <div className="toggle-row">
        <div>
          <div className="label">{t_below('risk_label')}</div>
          <div className="desc">{t_below('risk_desc')}</div>
        </div>
        <div className={`toggle ${risk ? 'on':''}`} onClick={() => setRisk(!risk)}></div>
      </div>
      <div className="toggle-row">
        <div>
          <div className="label">{t_below('autopause_label')}</div>
          <div className="desc">{t_below('autopause_desc')}</div>
        </div>
        <div className={`toggle ${autopause ? 'on':''}`} onClick={() => setAutopause(!autopause)}></div>
      </div>
      <div className="toggle-row">
        <div>
          <div className="label">{t_below('hide_rej_label')}</div>
          <div className="desc">{t_below('hide_rej_desc')}</div>
        </div>
        <div className={`toggle ${hideRej ? 'on':''}`} onClick={() => setHideRej(!hideRej)}></div>
      </div>
      <div className="disclaimer-text">
        <strong style={{color:'var(--ink-2)'}}>{t_below('disclaimer_strong')}</strong> {t_below('disclaimer_body')}
      </div>
    </div>
  );
}



Object.assign(window, { HeroStats, CalendarHeatmap, Feeds, MarketHours, NotificationsCard, RiskSettings });
