// Florio Analytics — Overview tab (real data via window.FlorioAPI).
(function () {
  const NS = window.HyperChartsDesignSystem_e51a95;
  const { LineChart, ProgressRing, Badge, TrendChip } = NS;
  const I = window.HyperIcons;
  const { useState, useEffect } = React;

  const rangeToDays = (r) => ({ Day: 1, Week: 7, Month: 30, Year: 365 }[r] || 30);
  const HOURS = ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];

  const COUNTRY_NAMES = {
    US: 'United States', GB: 'United Kingdom', NL: 'Netherlands', DE: 'Germany', FR: 'France',
    ES: 'Spain', IT: 'Italy', BR: 'Brazil', CA: 'Canada', AU: 'Australia', IN: 'India', MX: 'Mexico',
    JP: 'Japan', KR: 'South Korea', CN: 'China', RU: 'Russia', ZA: 'South Africa', SE: 'Sweden',
    NO: 'Norway', DK: 'Denmark', FI: 'Finland', PL: 'Poland', PT: 'Portugal', BE: 'Belgium',
    AT: 'Austria', CH: 'Switzerland', IE: 'Ireland', NZ: 'New Zealand', AR: 'Argentina', CL: 'Chile',
    CO: 'Colombia', PE: 'Peru', TR: 'Turkey', SA: 'Saudi Arabia', AE: 'UAE', EG: 'Egypt',
    NG: 'Nigeria', KE: 'Kenya', TH: 'Thailand', PH: 'Philippines', ID: 'Indonesia', MY: 'Malaysia',
    SG: 'Singapore', VN: 'Vietnam', TW: 'Taiwan', HK: 'Hong Kong', IL: 'Israel', GR: 'Greece',
    CZ: 'Czech Republic', RO: 'Romania', HU: 'Hungary', UA: 'Ukraine', SK: 'Slovakia', BG: 'Bulgaria',
  };
  const flagEmoji = (code) => {
    if (!code || code.length !== 2) return '🏳️';
    return code.toUpperCase().replace(/./g, (c) => String.fromCodePoint(127397 + c.charCodeAt(0)));
  };

  // ── range-independent fetch cache (survives range changes; no failure cached) ──
  const _cache = {};
  async function cached(key, fn, fallback) {
    if (key in _cache) return _cache[key];
    try { const v = await fn(); _cache[key] = v; return v; }
    catch (e) { console.error('[Overview] ' + key + ' failed:', e); return fallback; }
  }

  const shortDate = (s) => { const d = new Date(s + 'T00:00:00'); return `${d.getDate()}/${d.getMonth() + 1}`; };

  function buildHeat(dates, data, field) {
    const grid = Array.from({ length: 7 }, () => Array(12).fill(0));
    for (const date of dates) {
      const dow = new Date(date + 'T00:00:00').getDay(); // 0=Sun..6=Sat
      const row = (dow + 6) % 7; // Mon=0 .. Sun=6
      const hourMap = (data[date] && data[date][field]) || {};
      HOURS.forEach((h, ci) => { grid[row][ci] += (hourMap[h] || 0); });
    }
    return grid;
  }

  // Day-of-week + hour for a Date in a given IANA timezone (falls back to viewer tz).
  function dowHourInTz(date, tz) {
    try {
      const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz || undefined, weekday: 'short', hour: '2-digit', hour12: false }).formatToParts(date);
      const wd = (parts.find((p) => p.type === 'weekday') || {}).value;
      const hh = parseInt((parts.find((p) => p.type === 'hour') || {}).value, 10);
      const map = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
      const dow = map[wd];
      if (dow == null || !Number.isFinite(hh)) throw new Error('bad');
      return { dow, hour: hh % 24 };
    } catch (e) {
      return { dow: date.getDay(), hour: date.getHours() };
    }
  }

  // Onboarding-completions heatmap (dow × 2h) built from each user's REAL
  // completion timestamp (analytics_onboarding.completeAt), in the user's tz.
  function buildHeatFromTimes(times) {
    const grid = Array.from({ length: 7 }, () => Array(12).fill(0));
    for (const t of (times || [])) {
      if (!t || !t.at) continue;
      const { dow, hour } = dowHourInTz(t.at, t.tz);
      const row = (dow + 6) % 7; // Mon=0..Sun=6
      const col = Math.floor((hour % 24) / 2);
      if (grid[row] && grid[row][col] != null) grid[row][col] += 1;
    }
    return grid;
  }

  function countNewUsersByDate(users, dates) {
    const counts = {}; for (const d of dates) counts[d] = 0;
    for (const u of users) {
      const ts = u.createdAt; if (!ts) continue;
      const d = ts.toDate ? ts.toDate() : new Date(ts);
      const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
      if (key in counts) counts[key]++;
    }
    return dates.map((d) => counts[d]);
  }

  const lastDelta = (series) => {
    const a = series.filter((v) => typeof v === 'number');
    if (a.length < 2 || !a[a.length - 2]) return null;
    return Number(((a[a.length - 1] - a[a.length - 2]) / a[a.length - 2] * 100).toFixed(1));
  };

  // ── Cohort table (preserves the existing D1/D7/D14/D30 milestone data) ──
  function CohortTable({ cohort }) {
    const [busy, setBusy] = useState(false);
    const [msg, setMsg] = useState('Backfill');
    const milestones = [{ key: 'd1', label: 'D1', days: 1 }, { key: 'd7', label: 'D7', days: 7 }, { key: 'd14', label: 'D14', days: 14 }, { key: 'd30', label: 'D30', days: 30 }];
    const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    const label = (s) => { const d = new Date(s + 'T00:00:00Z'); return `${months[d.getUTCMonth()]} ${d.getUTCDate()}`; };
    const age = (s) => Math.floor((Date.now() - new Date(s + 'T00:00:00Z').getTime()) / 86400000);
    const backfill = async () => {
      if (busy || !confirm('Recompute all cohort retention data with daily grouping? This overwrites existing data.')) return;
      setBusy(true); setMsg('Running…');
      try {
        const token = await window.FlorioAuth.auth.currentUser.getIdToken();
        const res = await fetch('https://us-central1-florio-a6f48.cloudfunctions.net/backfillCohortRetention', { headers: { Authorization: `Bearer ${token}` } });
        const data = await res.json();
        if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
        setMsg(`Done — ${data.cohortDays} cohorts, ${data.dailyDocsUpdated} docs`);
      } catch (e) { setMsg('Failed'); alert('Backfill failed: ' + e.message); } finally { setBusy(false); }
    };
    const action = (
      <button onClick={backfill} disabled={busy}
        style={{ fontFamily: 'var(--font-sans)', fontSize: 12, padding: '6px 12px', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-default)', background: 'var(--surface-card)', color: 'var(--text-secondary)', cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1 }}>{msg}</button>
    );
    const days = cohort ? Object.keys(cohort).sort().slice(-30) : [];
    return (
      <window.PA.ChartCard title="Cohort retention" eyebrow="Daily cohorts · D1 → D30" span={12} action={action}>
        {!cohort || days.length === 0 ? (
          <div style={{ padding: 16, color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No cohort data yet. Data accumulates after the next daily aggregate runs.</div>
        ) : (
          <div style={{ overflowX: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'separate', borderSpacing: '3px', minWidth: 480 }}>
              <thead><tr>
                <th style={{ textAlign: 'left', fontFamily: 'var(--font-mono-display)', fontSize: 9, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--text-muted)', padding: '4px 8px' }}>Cohort</th>
                <th style={{ textAlign: 'right', fontFamily: 'var(--font-mono-display)', fontSize: 9, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--text-muted)', padding: '4px 8px' }}>Users</th>
                {milestones.map((m) => <th key={m.key} style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)', padding: '4px' }}>{m.label}</th>)}
              </tr></thead>
              <tbody>
                {days.map((day) => {
                  const c = cohort[day]; const a = age(day);
                  return (
                    <tr key={day}>
                      <td style={{ fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600, color: 'var(--text-primary)', padding: '6px 8px' }}>{label(day)}</td>
                      <td style={{ textAlign: 'right', fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-secondary)', padding: '6px 8px' }}>{(c.size || 0).toLocaleString()}</td>
                      {milestones.map((m) => {
                        if (a < m.days) return <td key={m.key} style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)', background: 'var(--surface-sunken)', borderRadius: 6, padding: '8px 4px' }}>—</td>;
                        const ratio = c.size > 0 ? (c[m.key] || 0) / c.size : 0;
                        const pct = Math.round(ratio * 100);
                        const alpha = 0.12 + ratio * 0.88;
                        return <td key={m.key} style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: 11.5, fontWeight: 600, color: alpha > 0.55 ? '#fff' : 'var(--text-secondary)', background: `color-mix(in srgb, var(--app-accent) ${Math.round(alpha * 100)}%, #fff)`, borderRadius: 6, padding: '8px 4px' }}>{pct}%</td>;
                      })}
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </window.PA.ChartCard>
    );
  }

  // Clearly-marked placeholder for data that arrives with the App Store Connect API.
  function PendingCard({ title, eyebrow, span, note }) {
    return (
      <window.PA.ChartCard title={title} eyebrow={eyebrow} span={span} action={<Badge variant="neutral">Pending API</Badge>}>
        <div style={{ minHeight: 140, display: 'grid', placeItems: 'center', textAlign: 'center', gap: 8, border: '1px dashed var(--border-default)', borderRadius: 'var(--radius-lg)', padding: 22 }}>
          <span style={{ color: 'var(--text-muted)', display: 'flex' }}>{I.dollar({ size: 26 })}</span>
          <span style={{ fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--text-secondary)', maxWidth: 320 }}>{note || 'Connects once the App Store Connect API is wired up.'}</span>
        </div>
      </window.PA.ChartCard>
    );
  }

  function Overview({ range }) {
    const P = window.PA;
    const [d, setD] = useState(null);

    useEffect(() => {
      let alive = true;
      setD(null);
      (async () => {
        await window.FlorioReady;
        const API = window.FlorioAPI;
        const dates = (range && range.start && range.end)
          ? API.getDateRangeBetween(range.start, range.end)
          : API.getDateRange(rangeToDays(range && range.key ? range.key : range));
        const days = dates.length;

        const [agg, onbTimes, users, recent, health, count] = await Promise.all([
          API.fetchDailyAggregates(dates).catch((e) => { console.error('[Overview] aggregates', e); return {}; }),
          API.fetchOnboardingCompletionTimes(dates).catch((e) => { console.error('[Overview] onboarding times', e); return []; }),
          cached('users', () => API.fetchUsers(10000), []),
          cached('recent', () => API.fetchRecentUsers(30), []),
          cached('health', () => API.fetchPlantHealthSnapshot(), { happy: 0, normal: 0, recovering: 0, struggling: 0 }),
          cached('count', () => API.fetchUserCount(), 0),
        ]);
        const today = API.getTodayStr();
        const todayAgg = await cached('today', async () => (await API.fetchDailyAggregates([today]))[today] || {}, {});
        const cohortAgg = await cached('cohort', async () => { const x = await API.fetchDailyAggregates([API.daysAgo(1), API.daysAgo(2)]); return x[API.daysAgo(1)] || x[API.daysAgo(2)] || null; }, null);
        if (!alive) return;

        // KPIs
        const totalDur = API.sumDailyField(agg, 'totalSessionDuration');
        const totalSess = API.sumDailyField(agg, 'sessions');
        let avg = totalSess > 0 ? Math.round(totalDur / totalSess) : 0;
        if (avg === 0) { try { avg = await API.computeAvgSessionDuration(dates[0], dates[dates.length - 1]); } catch {} }
        if (!alive) return;
        const wateringsTotal = API.sumDailyField(agg, 'waterings');

        // time series
        const dau = dates.map((x) => agg[x]?.uniqueActiveUsers ?? 0);
        const sessions = dates.map((x) => agg[x]?.sessions ?? 0);
        const waterings = dates.map((x) => agg[x]?.waterings ?? 0);
        const newUsers = countNewUsersByDate(users, dates);

        // OS
        let android = 0, ios = 0;
        for (const u of recent) { if (u.os === 'ios') ios++; else if (u.os === 'android') android++; }

        // add-plant method
        const addScan = API.sumDailyField(agg, 'plantsAddedByScan');
        const addSearch = API.sumDailyField(agg, 'plantsAddedBySearch');
        const obScan = API.sumDailyField(agg, 'plantsAddedByOnboardingScan');
        const obSearch = API.sumDailyField(agg, 'plantsAddedByOnboardingSearch');
        const obLegacy = API.sumDailyField(agg, 'plantsAddedByOnboarding');
        const obSearchAdj = obSearch + (obScan === 0 && obSearch === 0 ? obLegacy : 0);

        // page traffic (today)
        const PAGE_MAP = { home: 'Home', index: 'Home', '(tabs)': 'Home', '(tabs)/index': 'Home', friends: 'Friends', '(tabs)/friends': 'Friends', plants: 'Garden', '(tabs)/plants': 'Garden', 'garden-v2': 'Garden', '(tabs)/garden-v2': 'Garden', settings: 'Settings', '(tabs)/settings': 'Settings', feed: 'Feed', '(tabs)/feed': 'Feed', 'plant/[id]': 'Plant Detail', 'add-plant': 'Add Plant', 'water-confirm': 'Water Confirm', marketplace: 'Marketplace', '(auth)/onboarding': 'Onboarding' };
        const sv = todayAgg.screenViews || {};
        const trafficAgg = {};
        for (const [key, c] of Object.entries(sv)) { const name = PAGE_MAP[key] || PAGE_MAP[key.replace(/^\//, '')] || key; trafficAgg[name] = (trafficAgg[name] || 0) + c; }
        for (const n of ['Home', 'Garden', 'Friends', 'Feed', 'Settings']) if (!(n in trafficAgg)) trafficAgg[n] = 0;
        const traffic = Object.entries(trafficAgg).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([label, value]) => ({ label, value }));

        // country
        const cc = {};
        for (const u of users) { const code = u.country; if (code && typeof code === 'string' && code.length === 2) cc[code.toUpperCase()] = (cc[code.toUpperCase()] || 0) + 1; }
        const ccTotal = Object.values(cc).reduce((a, b) => a + b, 0);
        const top = Object.entries(cc).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([code, n]) => ({ name: COUNTRY_NAMES[code] || code, flag: flagEmoji(code), pct: ccTotal ? Number((n / ccTotal * 100).toFixed(1)) : 0 }));

        // retention
        let retained = 0, totalSnap = 0;
        for (let i = dates.length - 1; i >= 0; i--) { const x = agg[dates[i]]; if (x && x.totalUsersSnapshot) { totalSnap = x.totalUsersSnapshot; retained = days <= 7 ? (x.retentionWau || x.retentionDau || 0) : days <= 14 ? (x.retentionWau || 0) : (x.retentionMau || 0); break; } }
        const retRate = totalSnap > 0 ? Number((retained / totalSnap * 100).toFixed(1)) : 0;

        setD({
          dates, labels: dates.map(shortDate),
          totalUsers: count, avgSession: `${Math.floor(avg / 60)}m ${avg % 60}s`, wateringsTotal,
          dau, sessions, waterings, newUsers,
          os: { android, ios }, addScan, addSearch, obScan, obSearch: obSearchAdj,
          traffic, country: { values: cc, top, total: ccTotal },
          health, retRate, retained, totalSnap,
          heatSessions: buildHeat(dates, agg, 'sessionsByHour'),
          heatOnboarding: buildHeatFromTimes(onbTimes),
          cohort: cohortAgg ? cohortAgg.cohortRetention : null,
        });
      })();
      return () => { alive = false; };
    }, [range]);

    if (!d) {
      return <div style={{ display: 'grid', placeItems: 'center', minHeight: '60vh' }}><div className="pa-spin" style={{ width: 38, height: 38, borderRadius: '50%', border: '3px solid var(--border-default)', borderTopColor: 'var(--app-accent)' }} /></div>;
    }

    const fmt = API_FMT;
    const osTotal = d.os.android + d.os.ios;
    const healthTotal = (d.health.happy || 0) + (d.health.normal || 0) + (d.health.recovering || 0) + (d.health.struggling || 0);
    const hp = (v) => healthTotal ? Math.round(v / healthTotal * 100) : 0;
    const op = (v) => osTotal ? Math.round(v / osTotal * 100) : 0;
    const rk = (range && range.key === 'custom') ? (range.start + '_' + range.end) : String((range && range.key) || range); // stable redraw key per range

    const lineAction = (val, series, sub) => (
      <div style={{ textAlign: 'right' }}>
        <div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 24, letterSpacing: '-.02em', color: 'var(--text-primary)' }}>{fmt(val)}</div>
        <div style={{ display: 'flex', gap: 7, justifyContent: 'flex-end', alignItems: 'center', marginTop: 2 }}>
          {lastDelta(series) != null && <TrendChip value={lastDelta(series)} size="sm" />}
          {sub && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-muted)' }}>{sub}</span>}
        </div>
      </div>
    );

    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, minmax(0,1fr))', gap: 20 }}>
        <div style={{ gridColumn: 'span 12', display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 16 }}>
          <P.StatTile icon={I.users({ size: 18 })} label="Total users" value={fmt(d.totalUsers)} sub="All registered accounts" accent="#B771F3" />
          <P.StatTile icon={I.clock({ size: 18 })} label="Avg. session" value={d.avgSession} sub="Across the selected range" accent="#5B8DEF" />
          <P.StatTile icon={I.droplet({ size: 18 })} label="Waterings" value={fmt(d.wateringsTotal)} delta={lastDelta(d.waterings)} sub="In the selected range" accent="#2BB0A6" />
        </div>

        <P.ChartCard title="Active users (DAU)" eyebrow="Daily active" span={6} action={lineAction(d.dau[d.dau.length - 1] || 0, d.dau, 'today')}>
          <P.LineChart series={[{ data: d.dau, color: '#2ACF56', name: 'DAU' }]} labels={d.labels} height={190} />
        </P.ChartCard>
        <P.ChartCard title="Sessions / day" eyebrow="Volume" span={6} action={lineAction(d.sessions[d.sessions.length - 1] || 0, d.sessions, 'today')}>
          <P.LineChart series={[{ data: d.sessions, color: '#FE7E07', name: 'Sessions' }]} labels={d.labels} height={190} />
        </P.ChartCard>

        <P.ChartCard title="New users" eyebrow="Sign-ups per day" span={6} action={lineAction(d.newUsers.reduce((a, b) => a + b, 0), d.newUsers, 'total')}>
          <P.LineChart series={[{ data: d.newUsers, color: '#8676FF', name: 'New users' }]} labels={d.labels} height={190} />
        </P.ChartCard>
        <P.ChartCard title="Waterings / day" eyebrow="Engagement" span={6} action={lineAction(d.waterings[d.waterings.length - 1] || 0, d.waterings, 'today')}>
          <P.LineChart series={[{ data: d.waterings, color: '#2BB0A6', name: 'Waterings' }]} labels={d.labels} height={190} />
        </P.ChartCard>

        <P.ChartCard title="Users by country" eyebrow="Geography" span={12} action={<Badge variant="brand">{Object.keys(d.country.values).length} countries</Badge>}>
          {d.country.total === 0 ? <div style={{ padding: 24, color: 'var(--text-muted)' }}>No country data yet.</div> : <P.WorldMap values={d.country.values} top={d.country.top} />}
        </P.ChartCard>

        <P.ChartCard title="OS distribution" eyebrow="Platform · last 30 days active" span={4}>
          {osTotal === 0 ? <div style={{ padding: 24, color: 'var(--text-muted)' }}>No OS data yet.</div> :
            <P.DonutStat centerValue={fmt(osTotal)} centerLabel="DEVICES" data={[{ label: 'iOS', pct: op(d.os.ios), value: d.os.ios, color: '#8676FF' }, { label: 'Android', pct: op(d.os.android), value: d.os.android, color: '#22B8A6' }]} />}
        </P.ChartCard>
        <P.ChartCard title="Plant health" eyebrow="Current state" span={4}>
          {healthTotal === 0 ? <div style={{ padding: 24, color: 'var(--text-muted)' }}>No plant data yet.</div> :
            <P.DonutStat centerValue={fmt(healthTotal)} centerLabel="PLANTS" data={[
              { label: 'Happy', pct: hp(d.health.happy), value: d.health.happy, color: '#21A65A' },
              { label: 'Normal', pct: hp(d.health.normal), value: d.health.normal, color: '#5BD98C' },
              { label: 'Recovering', pct: hp(d.health.recovering), value: d.health.recovering, color: '#FFB020' },
              { label: 'Struggling', pct: hp(d.health.struggling), value: d.health.struggling, color: '#FF5C7A' },
            ]} />}
        </P.ChartCard>
        <P.ChartCard title="Retention" eyebrow="Active vs total users" span={4}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 18, paddingTop: 4 }}>
            <ProgressRing key={'ret' + rk} value={d.retRate} size={130} label="RETAINED" color="#2ACF56" />
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div><div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)' }}>Active</div><div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 18, color: 'var(--text-primary)' }}>{fmt(d.retained)}</div></div>
              <div><div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)' }}>Total</div><div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 18, color: 'var(--text-secondary)' }}>{fmt(d.totalSnap)}</div></div>
            </div>
          </div>
        </P.ChartCard>

        <P.ChartCard title="Add plant — method" eyebrow="Scan vs search" span={4}>
          {(d.addScan + d.addSearch) === 0 ? <div style={{ padding: 24, color: 'var(--text-muted)' }}>No data in range.</div> :
            <P.DonutStat centerValue={fmt(d.addScan + d.addSearch)} centerLabel="ADDED" data={[{ label: 'Scan', pct: Math.round(d.addScan / (d.addScan + d.addSearch) * 100), value: d.addScan, color: '#8676FF' }, { label: 'Search', pct: Math.round(d.addSearch / (d.addScan + d.addSearch) * 100), value: d.addSearch, color: '#2BB0A6' }]} />}
        </P.ChartCard>
        <P.ChartCard title="Onboarding add — method" eyebrow="First plant" span={4}>
          {(d.obScan + d.obSearch) === 0 ? <div style={{ padding: 24, color: 'var(--text-muted)' }}>No data in range.</div> :
            <P.DonutStat centerValue={fmt(d.obScan + d.obSearch)} centerLabel="ADDED" data={[{ label: 'Scan', pct: Math.round(d.obScan / (d.obScan + d.obSearch) * 100), value: d.obScan, color: '#FE7E07' }, { label: 'Search', pct: Math.round(d.obSearch / (d.obScan + d.obSearch) * 100), value: d.obSearch, color: '#5B8DEF' }]} />}
        </P.ChartCard>
        <P.ChartCard title="Page traffic" eyebrow="Today · screen views" span={4}>
          <P.DistBars color="#8676FF" items={d.traffic} />
        </P.ChartCard>

        <P.ChartCard title="Sessions by time of day" eyebrow="Day of week × 2 hours" span={6}>
          <P.HeatGrid data={d.heatSessions} color="#8676FF" />
        </P.ChartCard>
        <P.ChartCard title="Onboarding completions by time" eyebrow="Day of week × 2 hours" span={6}>
          <P.HeatGrid data={d.heatOnboarding} color="#8C9DF5" />
        </P.ChartCard>

        <CohortTable cohort={d.cohort} />

        <PendingCard title="Install channel mix" eyebrow="Acquisition" span={6} note="Install attribution arrives with the App Store Connect API." />
        <PendingCard title="Revenue" eyebrow="Sales report" span={6} note="Daily / monthly / yearly revenue arrives with the App Store Connect API." />
      </div>
    );
  }

  // formatNumber from the data layer (window.FlorioAPI may not be ready at definition time → lazy).
  function API_FMT(n) { return (window.FlorioAPI && window.FlorioAPI.formatNumber) ? window.FlorioAPI.formatNumber(n) : String(n ?? 0); }

  // Register under a Florio-owned namespace. NOTE: _ds_bundle.js itself defines
  // window.PAOverview/PAOnboarding/PAGamification (mock kit tabs), so we must NOT
  // key off those — an unbuilt tab would render the bundle's fake-data mock.
  (window.FlorioTabs = window.FlorioTabs || {}).overview = Overview;
})();
