// predict-timing.jsx — Trend Prediction for Release Timing
// ─────────────────────────────────────────────────────────────────
// Augments window.PredictEngine with releaseTimingForecast() and
// exports a UI surface (window.PredictTimingTab) that the Predictions
// screen mounts as a 6th tab.
//
// Algorithm (deterministic, scrubable to seed):
//   1. Build a 52-week forward calendar starting next Friday.
//   2. For each week, compute a composite Release Score (0–100) =
//        seasonality   ×  0.32       — genre × month curve
//      + dsp_cycle     ×  0.18       — distance from editorial refresh days
//      + audio_fit     ×  0.16       — energy/valence vs season
//      + competition   ×  0.14       — known major-artist drop windows
//      + momentum      ×  0.12       — rising velocity / TikTok trend
//      + dow_bonus     ×  0.08       — day-of-week (Friday DSP norm)
//   3. Penalize blackout windows (holidays, mega-release weeks).
//   4. Pick top-3 launch windows + render heatmap + reasoning.
//
// EXPORT: window.PredictTimingTab
// Side effect: extends window.PredictEngine.releaseTimingForecast
// ─────────────────────────────────────────────────────────────────
(function () {
  if (typeof window === 'undefined') return;
  if (!window.React || !window.PredictEngine) {
    console.error('predict-timing.jsx: React or PredictEngine missing');
    return;
  }
  const _S = React.useState, _M = React.useMemo, _E = React.useEffect;
  const E = window.PredictEngine;

  // ─── helpers ───────────────────────────────────────────────────
  const fmtK = (n) => n >= 1_000_000 ? (n/1_000_000).toFixed(2)+'M' : n >= 1000 ? (n/1000).toFixed(1)+'k' : Math.round(n).toString();
  const fmtPct = (n, sign) => (sign && n > 0 ? '+' : '') + Math.round(n*100) + '%';

  function pseed(seed) {
    let s = 0; for (let i = 0; i < seed.length; i++) s = (s * 31 + seed.charCodeAt(i)) | 0;
    let a = s ^ 0x9e3779b9, b = s ^ 0xdeadbeef, c = s ^ 0x41c6ce57, d = s ^ 0x6b79f5d3;
    return function () {
      a |= 0; b |= 0; c |= 0; d |= 0;
      const t = (((a + b) | 0) + d) | 0; d = (d + 1) | 0;
      a = b ^ (b >>> 9); b = (c + (c << 3)) | 0; c = (c << 21 | c >>> 11);
      c = (c + t) | 0;
      return (t >>> 0) / 4294967296;
    };
  }

  function Mono({ children, upper, size, color, style, ...rest }) {
    return <span className={'ff-mono' + (upper?' upper':'')} style={{ fontSize: size||11, color: color||'var(--ink)', letterSpacing: upper?'.08em':0, ...style }} {...rest}>{children}</span>;
  }

  // ─── domain knowledge tables ───────────────────────────────────
  // Seasonality: per-genre 12-month strength curve (0..1).
  // Pop/dance peak summer; folk/country peak fall; hip-hop late spring; lo-fi back-to-school.
  const SEASONALITY = {
    'Indie Pop':    [0.55, 0.50, 0.65, 0.78, 0.88, 0.92, 0.90, 0.82, 0.70, 0.60, 0.52, 0.58],
    'Hip-Hop':      [0.62, 0.58, 0.72, 0.85, 0.90, 0.85, 0.78, 0.74, 0.72, 0.68, 0.70, 0.78],
    'R&B':          [0.70, 0.78, 0.72, 0.68, 0.65, 0.68, 0.72, 0.74, 0.72, 0.68, 0.72, 0.82],
    'Electronic':   [0.50, 0.48, 0.55, 0.62, 0.78, 0.92, 0.95, 0.88, 0.72, 0.58, 0.48, 0.62],
    'Folk':         [0.55, 0.52, 0.58, 0.62, 0.60, 0.55, 0.50, 0.55, 0.72, 0.85, 0.78, 0.68],
    'Country':      [0.60, 0.55, 0.62, 0.70, 0.78, 0.82, 0.78, 0.72, 0.78, 0.82, 0.70, 0.65],
    'Rock':         [0.65, 0.62, 0.65, 0.70, 0.75, 0.78, 0.72, 0.70, 0.72, 0.72, 0.68, 0.72],
    'Latin':        [0.62, 0.60, 0.65, 0.70, 0.78, 0.85, 0.88, 0.85, 0.75, 0.68, 0.62, 0.65],
    'K-Pop':        [0.72, 0.68, 0.65, 0.70, 0.75, 0.78, 0.72, 0.78, 0.82, 0.75, 0.68, 0.72],
    'Lo-Fi':        [0.62, 0.58, 0.55, 0.55, 0.55, 0.50, 0.55, 0.78, 0.88, 0.78, 0.70, 0.68],
  };

  // Day-of-week multiplier (DSP editorial cycle – Friday is the global drop day).
  const DOW = { 0: 0.55, 1: 0.62, 2: 0.65, 3: 0.78, 4: 0.85, 5: 1.00, 6: 0.70 };
  const DOW_LABEL = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

  // Editorial refresh anchors per major DSP (day-of-week, freshness window).
  // Score = how aligned a Friday is with these refresh cycles.
  const DSP_CYCLES = [
    { name: 'Spotify',       refreshDow: 5, weight: 0.40, lookback: 1 }, // RapCaviar/NMF refresh Friday morning
    { name: 'Apple Music',   refreshDow: 5, weight: 0.25, lookback: 1 },
    { name: 'YouTube Music', refreshDow: 5, weight: 0.15, lookback: 2 },
    { name: 'Amazon Music',  refreshDow: 5, weight: 0.10, lookback: 2 },
    { name: 'Tidal',         refreshDow: 5, weight: 0.05, lookback: 2 },
    { name: 'Deezer',        refreshDow: 5, weight: 0.05, lookback: 2 },
  ];

  // Known industry blackouts and high-competition windows (relative to 2026 cal).
  // Tagged month/week (1-indexed week-of-month).
  const BLACKOUTS = [
    { tag: 'Grammy Week',         month: 2,  weeks: [1,2],  penalty: 0.30, note: 'Industry attention concentrated on awards coverage' },
    { tag: 'SXSW',                month: 3,  weeks: [2,3],  penalty: 0.18, note: 'Press cycle saturated by festival showcases' },
    { tag: 'Coachella W1',        month: 4,  weeks: [2],    penalty: 0.22, note: 'Festival lineup news dominates discovery feeds' },
    { tag: 'July 4 Week',         month: 7,  weeks: [1],    penalty: 0.25, note: 'US listenership softens; editorial teams off' },
    { tag: 'Labor Day Week',      month: 9,  weeks: [1],    penalty: 0.15, note: 'Long weekend depresses first-day streams' },
    { tag: 'Thanksgiving Week',   month: 11, weeks: [4],    penalty: 0.30, note: 'Historic dead zone for new releases (US)' },
    { tag: 'Christmas Week',      month: 12, weeks: [4,5],  penalty: 0.40, note: 'Catalog/holiday playlists dominate; minimal editorial' },
    { tag: 'Mega-Drop Window',    month: 6,  weeks: [3],    penalty: 0.20, note: 'Two top-10 artists confirmed for this week' },
    { tag: 'Mega-Drop Window',    month: 10, weeks: [4],    penalty: 0.18, note: 'Major label tentpole release window' },
  ];

  // Tailwind windows (positive boosts).
  const TAILWINDS = [
    { tag: 'New Year Reset',      month: 1,  weeks: [2,3],   boost: 0.10, note: 'Resolution-driven discovery surge across moods' },
    { tag: 'Spring Discovery',    month: 4,  weeks: [3,4],   boost: 0.12, note: 'Editorial teams refreshing playlists post-Q1' },
    { tag: 'Summer Up-Tempo',     month: 5,  weeks: [3,4],   boost: 0.15, note: 'Energy/valence-skewed tracks over-index' },
    { tag: 'Back-to-School',      month: 8,  weeks: [3,4],   boost: 0.13, note: 'Lo-fi, study-pop, and indie peak this window' },
    { tag: 'Awards Buzz Window',  month: 10, weeks: [1,2],   boost: 0.08, note: 'Critic lists begin forming for year-end coverage' },
  ];

  // ─── pure: composite release score for a candidate week ────────
  function scoreWeek(date, feat, ctx) {
    const month = date.getMonth();         // 0-11
    const dow   = date.getDay();           // 0-6 (Sun..Sat)
    const wkOfMonth = Math.ceil(date.getDate() / 7);

    // Seasonality
    const curve = SEASONALITY[feat.catalog.genre] || SEASONALITY['Indie Pop'];
    const season = curve[month];

    // Audio fit vs season — energy/valence preferred in summer; acoustic in fall/winter
    const summerness = [0.0,0.1,0.3,0.5,0.7,0.9,1.0,0.9,0.7,0.4,0.2,0.1][month];
    const winterness = 1 - summerness;
    const audioFit = (
      feat.audio.energy * summerness +
      feat.audio.acousticness * winterness +
      feat.audio.valence * (0.4 + summerness * 0.5)
    ) / 2.2;

    // DSP cycle alignment — Friday near editorial refresh
    let dspAlign = 0;
    DSP_CYCLES.forEach(c => {
      const diff = Math.min(Math.abs(dow - c.refreshDow), 7 - Math.abs(dow - c.refreshDow));
      const fit = Math.max(0, 1 - diff / 3.5);
      dspAlign += fit * c.weight;
    });

    // DOW base
    const dowBase = DOW[dow];

    // Momentum — rising track gets bigger boost on near-term windows
    const weeksOut = ctx.weeksOut;
    const decay = Math.exp(-weeksOut / 18);  // earlier weeks favored if hot
    const momentum = 0.3 + 0.7 * Math.max(0, feat.velocity.growth30) * decay;

    // Competition — penalty from blackouts; boost from tailwinds
    let blackout = null, tailwind = null, compMult = 1.0;
    BLACKOUTS.forEach(b => {
      if (b.month === month + 1 && b.weeks.includes(wkOfMonth)) {
        compMult *= (1 - b.penalty);
        blackout = b;
      }
    });
    TAILWINDS.forEach(t => {
      if (t.month === month + 1 && t.weeks.includes(wkOfMonth)) {
        compMult *= (1 + t.boost);
        tailwind = t;
      }
    });

    // Composite
    const raw =
        season   * 0.32
      + dspAlign * 0.18
      + audioFit * 0.16
      + momentum * 0.12
      + dowBase  * 0.08
      + 0.14;  // baseline / competition slot

    const score = Math.round(Math.max(2, Math.min(98, raw * 100 * compMult)));

    return {
      date, month, dow, wkOfMonth, score,
      components: { season, dspAlign, audioFit, momentum, dowBase, compMult },
      blackout, tailwind,
    };
  }

  // ─── pure: build forecast for one recording ────────────────────
  function releaseTimingForecast(feat, opts) {
    if (!feat) return null;
    opts = opts || {};
    const horizonWeeks = opts.horizonWeeks || 52;
    const startDate = opts.startDate ? new Date(opts.startDate) : nextFriday(new Date());

    const weeks = [];
    for (let i = 0; i < horizonWeeks; i++) {
      const d = new Date(startDate); d.setDate(d.getDate() + i * 7);
      weeks.push(scoreWeek(d, feat, { weeksOut: i }));
    }

    // Find top 3 windows (best score, separated by 4+ weeks)
    const sorted = [...weeks].sort((a, b) => b.score - a.score);
    const picks = [];
    for (const w of sorted) {
      if (picks.every(p => Math.abs((p.date - w.date) / 86400000 / 7) >= 4)) {
        picks.push(w);
        if (picks.length === 3) break;
      }
    }

    // Daily heatmap for the top window (7 days × 4 surrounding weeks = 28-day grid)
    const top = picks[0];
    const grid = [];
    if (top) {
      const anchor = new Date(top.date); anchor.setDate(anchor.getDate() - 14);
      for (let r = 0; r < 4; r++) {
        const row = [];
        for (let c = 0; c < 7; c++) {
          const d = new Date(anchor); d.setDate(anchor.getDate() + r * 7 + c);
          const ws = scoreWeek(d, feat, { weeksOut: Math.max(0, (d - new Date()) / 86400000 / 7) });
          row.push({ date: d, score: ws.score, isPeak: sameDay(d, top.date) });
        }
        grid.push(row);
      }
    }

    // Confidence — narrow if best score >> mean
    const mean = weeks.reduce((s, w) => s + w.score, 0) / weeks.length;
    const std = Math.sqrt(weeks.reduce((s, w) => s + (w.score - mean) ** 2, 0) / weeks.length);
    const conf = Math.max(0.45, Math.min(0.95, (top ? (top.score - mean) / Math.max(std, 1) : 1) * 0.30 + 0.55));

    // Projected lift vs releasing on a worst-week
    const worst = weeks.reduce((w, c) => c.score < w.score ? c : w, weeks[0]);
    const projectedLift = top ? (top.score - worst.score) / Math.max(worst.score, 1) : 0;

    // Reasoning bullets for top pick
    const reasoning = top ? buildReasoning(top, feat) : [];

    return {
      weeks, picks, grid, conf, projectedLift, mean, top, worst, reasoning,
      genreCurve: SEASONALITY[feat.catalog.genre] || SEASONALITY['Indie Pop'],
      genre: feat.catalog.genre,
    };
  }

  function buildReasoning(pick, feat) {
    const out = [];
    const monthName = ['January','February','March','April','May','June','July','August','September','October','November','December'][pick.month];
    const c = pick.components;
    if (c.season > 0.78) out.push({ label: 'Seasonal peak', detail: `${feat.catalog.genre} over-indexes in ${monthName} (curve at ${Math.round(c.season*100)}%).` });
    else if (c.season < 0.55) out.push({ label: 'Off-season', detail: `${feat.catalog.genre} historically softer in ${monthName}; consider a stronger window.` });
    if (c.dspAlign > 0.85) out.push({ label: 'DSP cycle aligned', detail: 'Friday release captures all major editorial refresh windows (Spotify, Apple, YT Music).' });
    if (c.audioFit > 0.55) out.push({ label: 'Audio profile fit', detail: `Energy ${feat.audio.energy.toFixed(2)} / valence ${feat.audio.valence.toFixed(2)} matches the seasonal listening mood.` });
    if (c.momentum > 0.6) out.push({ label: 'Rising momentum', detail: `+${Math.round(feat.velocity.growth30*100)}% 30-day growth — release sooner to ride the curve.` });
    if (pick.tailwind) out.push({ label: pick.tailwind.tag, detail: pick.tailwind.note });
    if (pick.blackout) out.push({ label: '⚠ ' + pick.blackout.tag, detail: pick.blackout.note + ' — score is despite this penalty.' });
    return out;
  }

  function nextFriday(from) {
    const d = new Date(from);
    const diff = (5 - d.getDay() + 7) % 7 || 7;
    d.setDate(d.getDate() + diff);
    d.setHours(0,0,0,0);
    return d;
  }
  function sameDay(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }

  // Extend the engine
  E.releaseTimingForecast = releaseTimingForecast;
  E.SEASONALITY = SEASONALITY;
  E.BLACKOUTS = BLACKOUTS;
  E.TAILWINDS = TAILWINDS;

  // ─────────────────────────── UI ───────────────────────────────
  function fmtDateShort(d) {
    return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  }
  function fmtDateLong(d) {
    return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
  }

  const SCORE_COLOR = (s) => {
    if (s >= 80) return '#0a8754';
    if (s >= 65) return '#5fa83a';
    if (s >= 50) return '#d4881f';
    if (s >= 35) return '#a35418';
    return '#a32a18';
  };

  // Mini sparkline of all 52 weeks
  function ForecastSpark({ weeks, picks }) {
    const W = 800, H = 90, P = 4;
    const max = Math.max(...weeks.map(w => w.score));
    const min = Math.min(...weeks.map(w => w.score));
    const range = Math.max(1, max - min);
    const pickIds = new Set(picks.map(p => p.date.getTime()));
    return (
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ width: '100%', height: H, display: 'block' }}>
        {/* gridlines */}
        {[25, 50, 75].map(v => {
          const y = H - P - ((v - min) / range) * (H - 2*P);
          return <line key={v} x1={0} x2={W} y1={y} y2={y} stroke="var(--rule-soft)" strokeWidth="0.5" strokeDasharray="2 3"/>;
        })}
        {/* bars */}
        {weeks.map((w, i) => {
          const x = (i / weeks.length) * W;
          const bw = W / weeks.length - 1;
          const h = ((w.score - min) / range) * (H - 2*P);
          const y = H - P - h;
          const isPick = pickIds.has(w.date.getTime());
          return (
            <rect key={i} x={x} y={y} width={bw} height={h}
              fill={isPick ? 'var(--ink)' : SCORE_COLOR(w.score)}
              opacity={isPick ? 1 : 0.55}/>
          );
        })}
      </svg>
    );
  }

  // 12-month seasonality curve
  function GenreCurve({ curve, genre }) {
    const W = 360, H = 80, P = 6;
    const months = ['J','F','M','A','M','J','J','A','S','O','N','D'];
    const pts = curve.map((v, i) => {
      const x = P + (i / 11) * (W - 2*P);
      const y = H - P - v * (H - 2*P);
      return [x, y];
    });
    const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0] + ' ' + p[1]).join(' ');
    const area = path + ` L ${W-P} ${H-P} L ${P} ${H-P} Z`;
    return (
      <div>
        <Mono upper size={9} color="var(--ink-3)" style={{ display:'block', marginBottom: 8 }}>SEASONALITY · {genre}</Mono>
        <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: H, display: 'block' }}>
          <path d={area} fill="var(--ink)" opacity="0.10"/>
          <path d={path} fill="none" stroke="var(--ink)" strokeWidth="1.5"/>
          {pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r="2" fill="var(--ink)"/>)}
          {months.map((m, i) => (
            <text key={i} x={P + (i / 11) * (W - 2*P)} y={H - 1} fontSize="8" textAnchor="middle" fill="var(--ink-3)" fontFamily="var(--ff-mono, monospace)">{m}</text>
          ))}
        </svg>
      </div>
    );
  }

  // 4-week × 7-day heatmap around top pick
  function Heatmap({ grid }) {
    if (!grid || !grid.length) return null;
    return (
      <div>
        <Mono upper size={9} color="var(--ink-3)" style={{ display:'block', marginBottom: 8 }}>4-WEEK MICRO-GRID · DAILY SCORE</Mono>
        <div style={{ display: 'grid', gridTemplateColumns: '40px repeat(7, 1fr)', gap: 2, fontSize: 9 }}>
          <div></div>
          {DOW_LABEL.map(d => <Mono upper size={8} color="var(--ink-3)" key={d} style={{ textAlign: 'center', padding: '2px 0' }}>{d}</Mono>)}
          {grid.map((row, r) => (
            <React.Fragment key={r}>
              <Mono upper size={8} color="var(--ink-3)" style={{ alignSelf: 'center' }}>W{r+1}</Mono>
              {row.map((cell, c) => (
                <div key={c} style={{
                  position: 'relative',
                  aspectRatio: '1.4 / 1',
                  background: SCORE_COLOR(cell.score),
                  opacity: cell.isPeak ? 1 : 0.18 + (cell.score / 100) * 0.6,
                  outline: cell.isPeak ? '2px solid var(--ink)' : 'none',
                  outlineOffset: cell.isPeak ? 1 : 0,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  color: cell.score > 50 ? '#fff' : 'var(--ink)',
                  fontFamily: 'var(--ff-mono, monospace)', fontSize: 10, fontWeight: 500,
                  cursor: 'default',
                }} title={fmtDateLong(cell.date) + ' · score ' + cell.score}>
                  {cell.score}
                </div>
              ))}
            </React.Fragment>
          ))}
        </div>
      </div>
    );
  }

  // Component breakdown bars
  function ComponentBars({ pick }) {
    const c = pick.components;
    const items = [
      { k: 'Seasonality',   v: c.season,   w: 0.32 },
      { k: 'DSP cycle',     v: c.dspAlign, w: 0.18 },
      { k: 'Audio fit',     v: c.audioFit, w: 0.16 },
      { k: 'Momentum',      v: c.momentum, w: 0.12 },
      { k: 'Day-of-week',   v: c.dowBase,  w: 0.08 },
      { k: 'Comp window',   v: c.compMult, w: 0.14 },
    ];
    return (
      <div>
        <Mono upper size={9} color="var(--ink-3)" style={{ display:'block', marginBottom: 10 }}>SCORE COMPONENTS</Mono>
        {items.map((it, i) => (
          <div key={i} style={{ display:'grid', gridTemplateColumns:'120px 1fr 50px 40px', gap: 10, alignItems:'center', padding:'6px 0', borderBottom:'1px solid var(--rule-soft)' }}>
            <div style={{ fontSize: 11, color:'var(--ink-2)' }}>{it.k}</div>
            <div style={{ height: 5, background:'var(--bg-2)', position:'relative' }}>
              <div style={{ position:'absolute', left:0, top:0, height:'100%', width: Math.min(100, it.v*100)+'%', background:'var(--ink)' }}/>
            </div>
            <Mono size={11} style={{ textAlign:'right' }}>{Math.round(it.v*100)}</Mono>
            <Mono size={9} color="var(--ink-3)" style={{ textAlign:'right' }}>×{it.w.toFixed(2)}</Mono>
          </div>
        ))}
      </div>
    );
  }

  // Recording picker — uses same shape as other tabs
  function RecPicker({ allRecs, value, onChange }) {
    return (
      <select value={value || ''} onChange={(e) => onChange(allRecs.find(r => (r.id || r.title) === e.target.value))} style={{
        background: 'var(--paper)', border: '1px solid var(--rule)', padding: '8px 12px',
        fontSize: 13, color: 'var(--ink)', minWidth: 320, fontFamily: 'inherit',
      }}>
        <option value="">— Select a recording —</option>
        {allRecs.slice(0, 80).map(r => (
          <option key={r.id || r.title} value={r.id || r.title}>{r.title || r.name || r.id}</option>
        ))}
      </select>
    );
  }

  // ─── MAIN TAB ─────────────────────────────────────────────────
  function PredictTimingTab({ allRecs, onPick }) {
    const [rec, setRec] = _S(allRecs[0] || null);
    const feat = _M(() => rec ? E.getFeatures(rec) : null, [rec && rec.id]);
    const fc = _M(() => feat ? releaseTimingForecast(feat) : null, [feat && feat.id]);

    if (!fc) return <div style={{ padding: 20, color: 'var(--ink-3)' }}>No recordings available.</div>;

    return (
      <div>
        {/* Header row: picker + headline */}
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', gap: 24, marginBottom: 22, flexWrap: 'wrap' }}>
          <div>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 6 }}>RECORDING</Mono>
            <RecPicker allRecs={allRecs} value={rec ? (rec.id || rec.title) : ''} onChange={setRec}/>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6 }}>
              {fc.genre} · BPM {feat.audio.bpm} · energy {feat.audio.energy.toFixed(2)} · valence {feat.audio.valence.toFixed(2)}
            </div>
          </div>
          <div style={{ textAlign: 'right' }}>
            <Mono upper size={9} color="var(--ink-3)">RECOMMENDED LAUNCH WINDOW</Mono>
            <div style={{ fontSize: 26, fontWeight: 500, marginTop: 4, letterSpacing: '-0.01em' }}>
              {fmtDateLong(fc.top.date)}
            </div>
            <div style={{ display: 'flex', gap: 14, justifyContent: 'flex-end', marginTop: 6 }}>
              <Mono size={11} color="var(--ink-2)">score <span style={{ color: SCORE_COLOR(fc.top.score), fontWeight: 600 }}>{fc.top.score}</span></Mono>
              <Mono size={11} color="var(--ink-2)">conf {Math.round(fc.conf*100)}%</Mono>
              <Mono size={11} color="var(--ink-2)">vs worst-week <span style={{ color: '#0a8754' }}>{fmtPct(fc.projectedLift, true)}</span></Mono>
            </div>
          </div>
        </div>

        {/* 52-week sparkline */}
        <div style={{ border: '1px solid var(--rule)', padding: '14px 16px 8px', marginBottom: 22 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
            <Mono upper size={9} color="var(--ink-3)">52-WEEK FORECAST · COMPOSITE RELEASE SCORE</Mono>
            <div style={{ display: 'flex', gap: 12, fontSize: 9, color: 'var(--ink-3)', fontFamily: 'var(--ff-mono, monospace)', letterSpacing: '0.08em', textTransform: 'uppercase' }}>
              <span><span style={{ display: 'inline-block', width: 9, height: 9, background: '#0a8754', verticalAlign: 'middle', marginRight: 4 }}/>STRONG</span>
              <span><span style={{ display: 'inline-block', width: 9, height: 9, background: '#d4881f', verticalAlign: 'middle', marginRight: 4 }}/>SOFT</span>
              <span><span style={{ display: 'inline-block', width: 9, height: 9, background: 'var(--ink)', verticalAlign: 'middle', marginRight: 4 }}/>PICKED</span>
            </div>
          </div>
          <ForecastSpark weeks={fc.weeks} picks={fc.picks}/>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
            <Mono size={9} color="var(--ink-3)">{fmtDateShort(fc.weeks[0].date)}</Mono>
            <Mono size={9} color="var(--ink-3)">{fmtDateShort(fc.weeks[Math.floor(fc.weeks.length/2)].date)}</Mono>
            <Mono size={9} color="var(--ink-3)">{fmtDateShort(fc.weeks[fc.weeks.length-1].date)}</Mono>
          </div>
        </div>

        {/* Two-column: Heatmap + Reasoning */}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 28, marginBottom: 22 }}>
          <div style={{ border: '1px solid var(--rule)', padding: '16px 18px' }}>
            <Heatmap grid={fc.grid}/>
            <div style={{ marginTop: 14 }}>
              <GenreCurve curve={fc.genreCurve} genre={fc.genre}/>
            </div>
          </div>
          <div style={{ border: '1px solid var(--rule)', padding: '16px 18px' }}>
            <ComponentBars pick={fc.top}/>
            <div style={{ marginTop: 18 }}>
              <Mono upper size={9} color="var(--ink-3)" style={{ display:'block', marginBottom: 10 }}>WHY THIS WEEK</Mono>
              {fc.reasoning.length === 0 && (
                <div style={{ fontSize: 12, color:'var(--ink-3)' }}>No standout drivers — score reflects baseline conditions.</div>
              )}
              {fc.reasoning.map((r, i) => (
                <div key={i} style={{ borderLeft: '2px solid var(--ink)', padding: '6px 12px', marginBottom: 8, background: 'var(--bg-2)' }}>
                  <div style={{ fontSize: 12, fontWeight: 500 }}>{r.label}</div>
                  <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 3, lineHeight: 1.5 }}>{r.detail}</div>
                </div>
              ))}
            </div>
          </div>
        </div>

        {/* Top-3 picks strip */}
        <div>
          <Mono upper size={9} color="var(--ink-3)" style={{ display:'block', marginBottom: 12 }}>TOP 3 LAUNCH WINDOWS</Mono>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 }}>
            {fc.picks.map((p, i) => (
              <div key={i} style={{ border: '1px solid '+(i===0?'var(--ink)':'var(--rule)'), padding: '14px 16px', background: i===0?'var(--bg-2)':'var(--paper)' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                  <Mono upper size={9} color="var(--ink-3)">RANK {i+1}</Mono>
                  <Mono size={11} style={{ color: SCORE_COLOR(p.score), fontWeight: 600 }}>{p.score}</Mono>
                </div>
                <div style={{ fontSize: 16, fontWeight: 500, marginTop: 6, letterSpacing: '-0.005em' }}>{fmtDateLong(p.date)}</div>
                <div style={{ display: 'flex', gap: 10, marginTop: 6 }}>
                  {p.tailwind && <span style={{ fontSize: 10, padding: '2px 6px', background: '#0a8754', color: '#fff', letterSpacing: '0.05em' }}>{p.tailwind.tag.toUpperCase()}</span>}
                  {p.blackout && <span style={{ fontSize: 10, padding: '2px 6px', background: '#a32a18', color: '#fff', letterSpacing: '0.05em' }}>⚠ {p.blackout.tag.toUpperCase()}</span>}
                </div>
                <div style={{ marginTop: 10, fontSize: 11, color: 'var(--ink-2)', lineHeight: 1.6 }}>
                  {p.tailwind?.note || p.blackout?.note || `${p.dow === 5 ? 'Friday DSP refresh' : DOW_LABEL[p.dow] + ' release'} · ${(p.components.season*100).toFixed(0)}% seasonal strength.`}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    );
  }

  window.PredictTimingTab = PredictTimingTab;
  console.log('[PredictTiming] loaded · 52-week forecast · ' + Object.keys(SEASONALITY).length + ' genre curves · ' + BLACKOUTS.length + ' blackouts · ' + TAILWINDS.length + ' tailwinds');
})();
