// Florio Analytics — Onboarding tab (real data via window.FlorioAPI).
// Mirrors the data logic of js/pages/onboarding.js (funnel steps, paywall A/B,
// avg onboarding time, avg plant-scan time, notification permissions,
// completions trend) but renders it in the new design-kit visual language.
(function () {
  const NS = window.HyperChartsDesignSystem_e51a95;
  const { LineChart, Badge, TrendChip } = NS;
  const I = window.HyperIcons;
  const { useState, useEffect } = React;

  const rangeToDays = (r) => ({ Day: 1, Week: 7, Month: 30, Year: 365 }[r] || 30);

  // ── Onboarding step maps (copied 1:1 from js/pages/onboarding.js) ──────────────
  // The flow was rewritten on 2026-06-15; step numbers are positions in
  // SCREEN_FLOW so their meaning changed. Use the new map when the range includes
  // any on/after-cutover date, else the old map.
  const NEW_FLOW_CUTOVER = '2026-06-15';

  const OLD_STEPS = [
    { step: 1, label: 'Intro 1' }, { step: 2, label: 'Intro 2' }, { step: 3, label: 'Intro 3' },
    { step: 4, label: 'Name' }, { step: 5, label: 'Age' }, { step: 6, label: 'Plants killed' },
    { step: 7, label: 'Spending' }, { step: 8, label: 'Money wasted' }, { step: 9, label: 'Used car' },
    { step: 10, label: 'Good news' }, { step: 11, label: 'Social proof' }, { step: 12, label: 'Honesty' },
    { step: 13, label: 'Relationship' }, { step: 14, label: 'Barriers' }, { step: 15, label: 'Deeper patterns' },
    { step: 16, label: 'Goals' }, { step: 17, label: 'Personal meaning' }, { step: 18, label: 'Reflection' },
    { step: 19, label: 'Location' }, { step: 20, label: 'Care time' }, { step: 21, label: 'Profile loading' },
    { step: 22, label: 'Journey' }, { step: 23, label: 'Plant scan' }, { step: 24, label: 'Plant watered' },
    { step: 25, label: 'Plant name' }, { step: 26, label: 'Plant character' }, { step: 27, label: 'Commitment' },
    { step: 28, label: 'Social proof final' }, { step: 29, label: 'Paywall' }, { step: 30, label: 'Auth' },
  ];

  const NEW_STEPS = [
    { step: 1, label: 'Intro 1' }, { step: 2, label: 'Intro 2' }, { step: 3, label: 'Name' },
    { step: 4, label: 'Age' }, { step: 5, label: 'Plant count' }, { step: 6, label: 'Biggest struggle' },
    { step: 7, label: 'Forget frequency' }, { step: 8, label: 'Plants doing' }, { step: 9, label: 'Goals' },
    { step: 10, label: 'Reflection' }, { step: 11, label: 'Social proof' }, { step: 12, label: 'Location' },
    { step: 13, label: 'Care time' }, { step: 14, label: 'Profile loading' }, { step: 15, label: 'Plant scan' },
    // Sub-steps inside the single plant-scan screen (string keys written by
    // onboardingAnalytics.markStep): chose a method → loader shown → avatar revealed.
    { step: 'plant_calculating', label: 'Calculating' },
    { step: 'plant_avatar', label: 'Plant ready' },
    { step: 16, label: 'Name plant' }, { step: 17, label: 'Avatar reveal' }, { step: 18, label: 'First watering' },
    { step: 19, label: 'Widget' }, { step: 20, label: 'Commitment' }, { step: 21, label: 'Trial reminder' },
    { step: 22, label: 'Paywall' }, { step: 23, label: 'Auth' },
  ];

  function getStepsForDates(dates) {
    const hasNew = (dates || []).some((d) => d >= NEW_FLOW_CUTOVER);
    return hasNew ? NEW_STEPS : OLD_STEPS;
  }

  // Per-step counts — identical logic to getStepCounts() in js/pages/onboarding.js.
  function getStepCounts(data, steps, API) {
    const starts = API.sumDailyField(data, 'onboardingStarted');
    const completions = API.sumDailyField(data, 'onboardingCompleted');
    const stepsMap = API.mergeDailyMaps(data, 'onboardingSteps');
    const counts = steps.map((s) => {
      if (s.step === 1) return starts;
      if (s.label === 'Paywall') return completions; // Paywall = onboarding complete
      return stepsMap['step_' + s.step] || stepsMap[s.step] || 0;
    });
    return { starts, completions, counts };
  }

  // ── Avg onboarding time (start→complete). New dates: per-user onbDurations.
  //    Old dates: reconstruct from session start/complete event pairs. ───────────
  async function computeAvgOnboardingTime(dates, data, API) {
    const durations = [];
    const { oldDates, newDates } = API.splitOnboardingDates(dates);

    for (const d of newDates) {
      const arr = data[d] && data[d].onbDurations;
      if (Array.isArray(arr)) durations.push(...arr);
    }

    if (oldDates.length) {
      try {
        const events = await API.fetchRawEvents(oldDates[0], oldDates[oldDates.length - 1]);
        const sessions = {};
        for (const e of events) {
          if (e.event === 'onboarding_start' || e.event === 'onboarding_complete') {
            if (!sessions[e.sessionId]) sessions[e.sessionId] = {};
            const ts = e.timestamp && e.timestamp.toDate ? e.timestamp.toDate()
              : (e.timestamp && e.timestamp.seconds ? new Date(e.timestamp.seconds * 1000) : null);
            if (ts) sessions[e.sessionId][e.event] = ts;
          }
        }
        for (const s of Object.values(sessions)) {
          if (s['onboarding_start'] && s['onboarding_complete']) {
            const dur = Math.floor((s['onboarding_complete'] - s['onboarding_start']) / 1000);
            if (dur > 0 && dur < 3600) durations.push(dur);
          }
        }
      } catch (e) { console.error('[Onboarding] avg-time old events failed:', e); }
    }

    if (durations.length === 0) return null;
    const avg = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
    return { sec: avg, label: `${Math.floor(avg / 60)}m ${avg % 60}s` };
  }

  // ── Avg plant scan time (ms per user, scan → avatar ready) ─────────────────────
  function computeAvgScanTime(data) {
    const times = [];
    for (const d of Object.keys(data)) {
      const arr = data[d] && data[d].plantScanTimes;
      if (Array.isArray(arr)) times.push(...arr);
    }
    if (times.length === 0) return null;
    const avgMs = times.reduce((a, b) => a + b, 0) / times.length;
    return {
      n: times.length,
      label: avgMs >= 1000 ? `${(avgMs / 1000).toFixed(1)}s` : `${Math.round(avgMs)}ms`,
    };
  }

  // ── Paywall A/B (hard vs soft): sum per-date buckets into one totals object. ────
  function sumPaywall(data) {
    const out = { hard: { viewed: 0, trial: 0, free: 0 }, soft: { viewed: 0, trial: 0, free: 0 } };
    for (const date of Object.keys(data)) {
      const pw = data[date] && data[date].paywall;
      if (!pw) continue;
      for (const v of ['hard', 'soft']) {
        if (!pw[v]) continue;
        out[v].viewed += pw[v].viewed || 0;
        out[v].trial += pw[v].trial || 0;
        out[v].free += pw[v].free || 0;
      }
    }
    return out;
  }

  // ── Cohort segmentation by survey answer (Age / Plant count) ──────────────────
  // Attributes you can break the key metrics down by. `order` controls row order;
  // any answer value not listed still shows (appended, alphabetical).
  const SEGMENT_ATTRS = {
    ageRange: {
      label: 'Age',
      order: ['Under 18', '18-24', '25-34', '35-44', '45-54', '55+'],
    },
    plantsKilled: {
      label: 'Plant count',
      order: ['1 plant', '2–3 plants', '4–6 plants', '7–10 plants', "10+ plants (it's a jungle)"],
    },
  };

  // Group per-user rows by their answer to `attrKey`, tallying the key metrics.
  function computeSegments(users, attrKey, order) {
    const groups = {};
    for (const u of users || []) {
      const raw = u.answers && u.answers[attrKey];
      if (raw === undefined || raw === null || raw === '') continue;
      const key = String(raw);
      if (!groups[key]) groups[key] = { value: key, starts: 0, completed: 0, trial: 0, auth: 0 };
      const g = groups[key];
      g.starts += 1; // every doc with this answer is a user who started onboarding
      if (u.completed) g.completed += 1;
      if (u.trial) g.trial += 1;
      if (u.auth) g.auth += 1;
    }
    const ordered = (order || []).filter((k) => groups[k]);
    const extra = Object.keys(groups).filter((k) => !(order || []).includes(k)).sort();
    return [...ordered, ...extra].map((k) => groups[k]);
  }

  // One metric cell: big rate %, a subtle proportional fill bar, raw count beneath.
  function RateCell({ rate, count, accent }) {
    return (
      <div style={{ position: 'relative', overflow: 'hidden', borderRadius: 10, background: 'var(--surface-sunken)', padding: '9px 11px' }}>
        <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: Math.min(rate, 100).toFixed(1) + '%', background: `color-mix(in srgb, ${accent || 'var(--app-accent)'} 16%, transparent)` }} />
        <div style={{ position: 'relative' }}>
          <div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 15, color: 'var(--text-primary)' }}>{rate.toFixed(1)}%</div>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-muted)', marginTop: 1 }}>{API_FMT(count)}</div>
        </div>
      </div>
    );
  }

  const shortDate = (s) => { const d = new Date(s + 'T00:00:00'); return `${d.getDate()}/${d.getMonth() + 1}`; };
  const safePct = (n, d) => (d > 0 ? (n / d) * 100 : 0);
  function API_FMT(n) { return (window.FlorioAPI && window.FlorioAPI.formatNumber) ? window.FlorioAPI.formatNumber(n) : String(n ?? 0); }

  // ── Paywall A/B variant panel (kit visual language) ────────────────────────────
  function PaywallVariant({ name, t, leading }) {
    const trialRate = safePct(t.trial, t.viewed);
    const freeRate = safePct(t.free, t.viewed);
    return (
      <div style={{
        background: 'var(--surface-sunken)', borderRadius: 'var(--radius-lg)', padding: '18px 20px',
        border: leading ? '1px solid color-mix(in srgb, var(--app-accent) 55%, transparent)' : '1px solid var(--border-subtle)',
        display: 'flex', flexDirection: 'column', gap: 14, position: 'relative',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <span style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 15, color: 'var(--text-primary)', textTransform: 'capitalize' }}>{name}</span>
          {leading && <Badge variant="brand">Leading</Badge>}
        </div>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 9 }}>
          <span style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 30, letterSpacing: '-.02em', color: 'var(--text-primary)', lineHeight: 1 }}>{trialRate.toFixed(1)}%</span>
          <span style={{ fontFamily: 'var(--font-sans)', fontSize: 12.5, color: 'var(--text-muted)' }}>trial conversion</span>
        </div>
        <div style={{ height: 8, background: 'color-mix(in srgb, var(--app-accent) 12%, var(--surface-card))', borderRadius: 99, overflow: 'hidden' }}>
          <div style={{ width: Math.min(trialRate, 100).toFixed(1) + '%', height: '100%', borderRadius: 99, background: 'linear-gradient(90deg, #EE9E4F, #FBAE62)' }} />
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 10, marginTop: 2 }}>
          {[
            { v: API_FMT(t.viewed), l: 'viewers' },
            { v: API_FMT(t.trial), l: 'trials started' },
            { v: freeRate.toFixed(1) + '%', l: 'chose free' },
          ].map((s, i) => (
            <div key={i}>
              <div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 16, color: 'var(--text-primary)' }}>{s.v}</div>
              <div style={{ fontFamily: 'var(--font-sans)', fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{s.l}</div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  function Onboarding({ range }) {
    const P = window.PA;
    const [d, setD] = useState(null);
    const [mode, setMode] = useState('number'); // funnel pills default to numbers ('number' | 'percent')
    const [segAttr, setSegAttr] = useState('ageRange'); // attribute for the by-attribute breakdown

    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));

        // Merge old aggregates (< cutover) with the new per-user onboarding rollups
        // (≥ cutover). Same fetcher + logic as js/pages/onboarding.js.
        const data = await API.fetchMergedOnboarding(dates).catch((e) => { console.error('[Onboarding] fetchMergedOnboarding', e); return {}; });
        if (!alive) return;

        // Per-user rows (new-flow docs) for the by-attribute key-metric breakdown.
        const users = await API.fetchOnboardingUsers(dates).catch((e) => { console.error('[Onboarding] fetchOnboardingUsers', e); return []; });
        if (!alive) return;

        const steps = getStepsForDates(dates);
        const { starts, completions, counts } = getStepCounts(data, steps, API);

        const notifAllowed = API.sumDailyField(data, 'notificationsAllowed');
        const notifDenied = API.sumDailyField(data, 'notificationsDenied');

        // newUsers / newPlants live only on new-flow dates; sum is safe (0 on old).
        const accountsCreated = API.sumDailyField(data, 'newUsers');
        const plantsAdded = API.sumDailyField(data, 'newPlants');

        const paywall = sumPaywall(data);
        const trialsStarted = paywall.hard.trial + paywall.soft.trial;
        const paywallViews = paywall.hard.viewed + paywall.soft.viewed;
        const freeChosen = paywall.hard.free + paywall.soft.free;

        const avgTime = await computeAvgOnboardingTime(dates, data, API);
        if (!alive) return;
        const avgScan = computeAvgScanTime(data);

        // Completions-per-day trend series.
        const trend = dates.map((x) => (data[x] && data[x].onboardingCompleted) || 0);

        // Biggest step-to-step losses (for the DropList).
        const drops = [];
        for (let i = 0; i < steps.length - 1; i++) {
          const lost = Math.max(counts[i] - counts[i + 1], 0);
          if (lost > 0) drops.push({ label: steps[i + 1].label, value: lost });
        }
        drops.sort((a, b) => b.value - a.value);

        setD({
          dates, labels: dates.map(shortDate), steps, counts, starts, completions,
          notifAllowed, notifDenied,
          accountsCreated, plantsAdded,
          paywall, trialsStarted, paywallViews, freeChosen,
          avgTime, avgScan, trend, drops: drops.slice(0, 6),
          users,
        });
      })();
      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 rk = (range && range.key === 'custom') ? (range.start + '_' + range.end) : String((range && range.key) || range); // stable redraw key per range
    const completionRate = safePct(d.completions, d.starts);
    const notifTotal = d.notifAllowed + d.notifDenied;
    const notifAllowedPct = notifTotal > 0 ? Math.round((d.notifAllowed / notifTotal) * 100) : 0;

    // Paywall leader (only when both variants have viewers and rates differ).
    const hardRate = safePct(d.paywall.hard.trial, d.paywall.hard.viewed);
    const softRate = safePct(d.paywall.soft.trial, d.paywall.soft.viewed);
    let leader = null;
    if (d.paywall.hard.viewed > 0 && d.paywall.soft.viewed > 0 && hardRate !== softRate) {
      leader = hardRate > softRate ? 'hard' : 'soft';
    }

    // KPI tiles (StatTiles).
    const kpis = [
      { icon: I.sprout({ size: 18 }), label: 'Onboarding starts', value: fmt(d.starts), sub: 'In the selected range', accent: 'var(--app-accent)' },
      { icon: I.flame({ size: 18 }), label: 'Completions', value: fmt(d.completions), sub: `${completionRate.toFixed(1)}% completion rate`, accent: '#21A65A' },
      { icon: I.users({ size: 18 }), label: 'Accounts created', value: fmt(d.accountsCreated), sub: 'sign_up during onboarding', accent: '#B771F3' },
      { icon: I.rocket({ size: 18 }), label: 'Trials started', value: fmt(d.trialsStarted), sub: `${safePct(d.trialsStarted, d.paywallViews).toFixed(1)}% of paywall views`, accent: '#5B8DEF' },
      { icon: I.clock({ size: 18 }), label: 'Avg. onboarding time', value: d.avgTime ? d.avgTime.label : '--', sub: d.avgTime ? 'start → complete' : 'No completions yet', accent: '#2BB0A6' },
    ];

    // Conversion-rate grid (key ratios).
    const ratios = [
      { label: 'Start → Complete', v: completionRate },
      { label: 'Paywall → Trial', v: safePct(d.trialsStarted, d.paywallViews) },
      { label: 'Start → Account', v: safePct(d.accountsCreated, d.starts) },
      { label: 'Start → Trial', v: safePct(d.trialsStarted, d.starts) },
      { label: 'Notif. allowed', v: notifTotal > 0 ? notifAllowedPct : 0 },
      { label: 'Paywall → Free', v: safePct(d.freeChosen, d.paywallViews) },
    ];

    // Funnel steps in the kit widget's shape ({ name, value }).
    const funnelSteps = d.steps.map((s, i) => ({ name: s.label, value: d.counts[i] }));
    const funnelEmpty = d.counts.every((c) => c === 0);
    const modeToggle = (
      <div style={{ display: 'inline-flex', background: 'var(--surface-sunken)', borderRadius: 'var(--radius-md)', padding: 3, gap: 3 }}>
        {[{ k: 'number', l: '#' }, { k: 'percent', l: '%' }].map((m) => (
          <button key={m.k} onClick={() => setMode(m.k)}
            style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 700, padding: '5px 13px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: mode === m.k ? 'var(--surface-card)' : 'transparent', color: mode === m.k ? 'var(--text-primary)' : 'var(--text-muted)', boxShadow: mode === m.k ? 'var(--shadow-card)' : 'none' }}>{m.l}</button>
        ))}
      </div>
    );

    const trendTotal = d.trend.reduce((a, b) => a + b, 0);

    // By-attribute breakdown (Age / Plant count) — recomputed on toggle, no refetch.
    const segCfg = SEGMENT_ATTRS[segAttr];
    const segments = computeSegments(d.users, segAttr, segCfg.order);
    const segTotal = segments.reduce((a, s) => a + s.starts, 0);
    const segToggle = (
      <div style={{ display: 'inline-flex', background: 'var(--surface-sunken)', borderRadius: 'var(--radius-md)', padding: 3, gap: 3 }}>
        {Object.entries(SEGMENT_ATTRS).map(([k, cfg]) => (
          <button key={k} onClick={() => setSegAttr(k)}
            style={{ fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 700, padding: '5px 13px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: segAttr === k ? 'var(--surface-card)' : 'transparent', color: segAttr === k ? 'var(--text-primary)' : 'var(--text-muted)', boxShadow: segAttr === k ? 'var(--shadow-card)' : 'none' }}>{cfg.label}</button>
        ))}
      </div>
    );

    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, minmax(0,1fr))', gap: 20 }}>
        {/* KPI row */}
        <div style={{ gridColumn: 'span 12', display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 16 }}>
          {kpis.map((kp, i) => <P.StatTile key={i} {...kp} />)}
        </div>

        {/* Key metrics by attribute (Age / Plant count) */}
        <P.ChartCard
          title="Key metrics by attribute"
          eyebrow={`Broken down by ${segCfg.label.toLowerCase()} · ${fmt(segTotal)} users answered`}
          span={12}
          action={segToggle}
        >
          {segments.length === 0 ? (
            <div style={{ minHeight: 160, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No {segCfg.label.toLowerCase()} answers in this range</div>
          ) : (
            <div>
              {/* header row */}
              <div style={{ display: 'grid', gridTemplateColumns: '1.5fr repeat(4, 1fr)', gap: 10, padding: '0 2px 10px', borderBottom: '1px solid var(--border-subtle)' }}>
                {[segCfg.label, 'Started', 'Completed', 'Trial', 'Account'].map((h, i) => (
                  <div key={i} style={{ fontFamily: 'var(--font-sans)', fontSize: 11, fontWeight: 700, letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--text-muted)' }}>{h}</div>
                ))}
              </div>
              {/* one row per answer value */}
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 10 }}>
                {segments.map((s) => {
                  const compRate = safePct(s.completed, s.starts);
                  const trialRate = safePct(s.trial, s.starts);
                  const authRate = safePct(s.auth, s.starts);
                  const share = safePct(s.starts, segTotal);
                  return (
                    <div key={s.value} style={{ display: 'grid', gridTemplateColumns: '1.5fr repeat(4, 1fr)', gap: 10, alignItems: 'center' }}>
                      <div>
                        <div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 13.5, color: 'var(--text-primary)' }}>{s.value}</div>
                        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-muted)', marginTop: 1 }}>{share.toFixed(0)}% of cohort</div>
                      </div>
                      <div style={{ background: 'var(--surface-sunken)', borderRadius: 10, padding: '9px 11px' }}>
                        <div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 15, color: 'var(--text-primary)' }}>{fmt(s.starts)}</div>
                        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-muted)', marginTop: 1 }}>users</div>
                      </div>
                      <RateCell rate={compRate} count={s.completed} accent="#21A65A" />
                      <RateCell rate={trialRate} count={s.trial} accent="#5B8DEF" />
                      <RateCell rate={authRate} count={s.auth} accent="#B771F3" />
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </P.ChartCard>

        {/* Big step-by-step funnel — kit Funnel widget */}
        <P.ChartCard
          title="Onboarding funnel"
          eyebrow={`${d.steps.length} steps · start → complete`}
          span={12}
          action={<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}><Badge variant="neutral">{completionRate.toFixed(1)}% end-to-end</Badge>{modeToggle}</div>}
        >
          {funnelEmpty ? (
            <div style={{ minHeight: 220, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No onboarding data yet</div>
          ) : (
            <P.Funnel steps={funnelSteps} mode={mode} stepW={96} />
          )}
        </P.ChartCard>

        {/* Paywall A/B test (hard vs soft) */}
        <P.ChartCard
          title="Paywall A/B · Hard vs Soft"
          eyebrow="Trial conversion by variant"
          span={12}
          action={d.paywallViews > 0 ? <Badge variant="brand">{fmt(d.paywallViews)} saw a paywall</Badge> : null}
        >
          {d.paywallViews === 0 ? (
            <div style={{ minHeight: 120, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No paywall data yet</div>
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
              <PaywallVariant name="hard" t={d.paywall.hard} leading={leader === 'hard'} />
              <PaywallVariant name="soft" t={d.paywall.soft} leading={leader === 'soft'} />
            </div>
          )}
        </P.ChartCard>

        {/* Start → Trial → Paid sub-funnel (real data) */}
        <P.ChartCard title="Start → Trial → Paid" eyebrow="Monetization funnel" span={6}>
          {d.starts === 0 ? (
            <div style={{ minHeight: 200, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No data in range</div>
          ) : (
            <P.Funnel height={252} stepW={150} steps={[
              { name: 'Started', value: d.starts },
              { name: 'Trial started', value: d.trialsStarted },
              { name: 'Paid (kept)', value: Math.max(d.trialsStarted - d.freeChosen, 0) },
            ]} />
          )}
        </P.ChartCard>

        {/* Conversion rates grid */}
        <P.ChartCard title="Conversion rates" eyebrow="Key ratios" span={6}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            {ratios.map((c, i) => (
              <div key={i} style={{ background: 'var(--surface-sunken)', borderRadius: 'var(--radius-lg)', padding: '14px 16px' }}>
                <div style={{ fontFamily: 'var(--font-sans)', fontSize: 12.5, color: 'var(--text-secondary)', marginBottom: 6 }}>{c.label}</div>
                <span style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 22, color: 'var(--text-primary)', letterSpacing: '-.02em' }}>{c.v.toFixed(1)}%</span>
              </div>
            ))}
          </div>
        </P.ChartCard>

        {/* Notification permission + avg scan time */}
        <P.ChartCard title="Notification permission" eyebrow="Allow vs deny prompt" span={4}>
          {notifTotal === 0 ? (
            <div style={{ minHeight: 160, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No prompt data yet</div>
          ) : (
            <P.DonutStat centerValue={fmt(notifTotal)} centerLabel="PROMPTED" data={[
              { label: 'Allowed', pct: notifAllowedPct, value: d.notifAllowed, color: '#21A65A' },
              { label: 'Denied', pct: 100 - notifAllowedPct, value: d.notifDenied, color: '#FF5C7A' },
            ]} />
          )}
        </P.ChartCard>

        <P.ChartCard title="Avg. plant scan time" eyebrow="Scan → avatar ready" span={4}>
          <div style={{ minHeight: 160, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: 10, textAlign: 'center' }}>
            <span style={{ width: 56, height: 56, borderRadius: 16, display: 'grid', placeItems: 'center', background: 'color-mix(in srgb, var(--app-accent) 14%, transparent)', color: 'var(--app-accent)' }}>{I.sparkle({ size: 26 })}</span>
            <span style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 38, letterSpacing: '-.02em', color: 'var(--text-primary)', lineHeight: 1 }}>{d.avgScan ? d.avgScan.label : '--'}</span>
            <span style={{ fontFamily: 'var(--font-sans)', fontSize: 12.5, color: 'var(--text-muted)' }}>{d.avgScan ? `Across ${fmt(d.avgScan.n)} scans` : 'No plant scans yet'}</span>
          </div>
        </P.ChartCard>

        {/* Where users drop */}
        <P.ChartCard title="Where users drop" eyebrow="Biggest step losses" span={4}>
          {d.drops.length === 0 ? (
            <div style={{ minHeight: 160, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No drop-off yet</div>
          ) : (
            <P.DropList items={d.drops} />
          )}
        </P.ChartCard>

        {/* Completions per day */}
        <P.ChartCard
          title="Completions / day"
          eyebrow="Onboarding finished"
          span={12}
          action={<div style={{ textAlign: 'right' }}><div style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 24, letterSpacing: '-.02em', color: 'var(--text-primary)' }}>{fmt(trendTotal)}</div><div style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-muted)', marginTop: 2 }}>total</div></div>}
        >
          {trendTotal === 0 ? (
            <div style={{ minHeight: 156, display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontSize: 13 }}>No completions in range</div>
          ) : (
            <P.LineChart series={[{ data: d.trend, color: '#EE9E4F', name: 'Completions' }]} labels={d.labels} height={200} />
          )}
        </P.ChartCard>
      </div>
    );
  }

  // Register under the Florio-owned namespace. NOTE: _ds_bundle.js itself defines
  // a MOCK window.PAOnboarding — we must NOT key off it or the tab would render
  // the bundle's fake data.
  (window.FlorioTabs = window.FlorioTabs || {}).onboarding = Onboarding;
})();
