/* Slate — Custom dashboards (Datadog-style panel layouts)
   ===================================================
   Each dashboard is a JSON document of panels:
     { panels: [{id, type, config, size}] }

   Panel types:
     kpi         single big number with prev-period delta + sparkline
     line        time-series chart (impressions, scans, scan_rate, revenue)
     bar         categorical bar chart (top campaigns, top species)
     table       generic data table (top campaigns, top species, screens)
     funnel      conversion funnel (visit → engage → cta → sale)
     engagement  passive engagement table (avg dwell + hold rate)
     text        markdown/heading for context (no data fetch)

   Each panel fetches its OWN data so the layout system stays
   decoupled. Panels accept the dashboard-wide range as a prop and
   re-fetch when it changes. Sizes map to a 12-col grid:
     sm   = 4 cols (1/3)
     md   = 6 cols (1/2)
     lg   = 8 cols (2/3)
     full = 12 cols (full width)

   Public exports (window.*):
     DashboardScreen   — single dashboard at #dashboards/:id
     DashboardsList    — list view at #dashboards (no id) */

(function () {
  /* ─── Panel size → grid columns ──────────────────────────────── */
  const SIZE_COLS = { sm: 4, md: 6, lg: 8, full: 12 };

  /* ─── Helpers ─────────────────────────────────────────────────── */
  const _fmtN  = typeof fmtN  === 'function' ? fmtN  : (n) => String(Math.round(Number(n) || 0));
  const _fmtPct = typeof fmtPct === 'function' ? fmtPct : (n, d = 1) => (Number(n) || 0).toFixed(d) + '%';
  const _fmtCurrency = typeof fmtCurrency === 'function' ? fmtCurrency : (c) => '£' + ((Number(c) || 0) / 100).toFixed(0);
  const _fmtDelta    = typeof fmtDelta === 'function' ? fmtDelta : (d, suffix) => { const v = Number(d) || 0; if (v === 0) return '— flat'; return (v > 0 ? '▲ ' : '▼ ') + Math.abs(v).toFixed(1) + (suffix || '%'); };
  const _Sparkline   = typeof Sparkline === 'function' ? Sparkline : null;

  function rangeToDates(rangeId) {
    const map = { '24h': 24, '7d': 24 * 7, '30d': 24 * 30, '90d': 24 * 90 };
    const hours = map[rangeId] || map['7d'];
    const to = new Date();
    const from = new Date(to.getTime() - hours * 3600 * 1000);
    return { from: from.toISOString(), to: to.toISOString() };
  }

  /* ─── KPI panel ───────────────────────────────────────────────── */
  function KpiPanel({ config, range }) {
    const metric = config.metric || 'impressions';
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const { from, to } = rangeToDates(range);
      apiFetch(`/api/analytics/metrics?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`)
        .then((r) => { setData(r); setLoading(false); })
        .catch(() => setLoading(false));
    }, [range]);
    const m = data && data[metric];
    function display(value) {
      if (metric === 'revenue')   return _fmtCurrency(value);
      if (metric === 'scan_rate') return _fmtPct(value, 2);
      return _fmtN(value);
    }
    const value = m ? (metric === 'revenue' ? m.cents : m.value) : null;
    const delta = m ? (m.delta_pp != null ? m.delta_pp : m.delta_pct) : null;
    const suffix = m && m.delta_pp != null ? 'pp' : '%';
    return (
      <div style={{ padding: '16px 18px' }}>
        <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600 }}>
          {config.label || metric}
        </div>
        <div style={{ fontSize: 28, fontWeight: 600, marginTop: 4, fontFamily: 'var(--aq-ff-display, inherit)' }}>
          {loading ? '—' : display(value)}
        </div>
        {delta != null && (
          <div style={{ marginTop: 4, fontSize: 12, color: delta > 0 ? 'oklch(0.78 0.13 160)' : (delta < 0 ? 'var(--aqos-danger)' : 'var(--aqos-text-faint)') }}>
            {_fmtDelta(delta, suffix)}
          </div>
        )}
        {_Sparkline && m && m.sparkline && m.sparkline.length > 0 && (
          <_Sparkline data={m.sparkline} color="oklch(0.66 0.16 270)" height={28} />
        )}
      </div>
    );
  }

  /* ─── Line chart panel ────────────────────────────────────────── */
  function LinePanel({ config, range }) {
    const metric = config.metric || 'impressions';
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const { from, to } = rangeToDates(range);
      /* /api/analytics/metrics returns a sparkline array per metric.
         We re-purpose it as the line data — daily buckets. */
      apiFetch(`/api/analytics/metrics?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`)
        .then((r) => {
          const m = r && r[metric];
          setData((m && m.sparkline) || []);
          setLoading(false);
        })
        .catch(() => setLoading(false));
    }, [range, metric]);
    const max = Math.max(1, ...data);
    return (
      <div style={{ padding: '14px 18px 18px' }}>
        <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600, marginBottom: 8 }}>
          {config.label || metric} — over time
        </div>
        {loading && <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</div>}
        {!loading && data.length === 0 && (
          <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12, padding: '20px 0', textAlign: 'center' }}>No data</div>
        )}
        {!loading && data.length > 0 && (
          <svg viewBox="0 0 200 60" preserveAspectRatio="none" style={{ width: '100%', height: 60, display: 'block' }}>
            <polyline
              fill="none"
              stroke={config.color || 'oklch(0.66 0.16 270)'}
              strokeWidth="1.5"
              points={data.map((v, i) => {
                const x = (i / Math.max(1, data.length - 1)) * 200;
                const y = 60 - (v / max) * 56 - 2;
                return `${x.toFixed(1)},${y.toFixed(1)}`;
              }).join(' ')}
            />
          </svg>
        )}
      </div>
    );
  }

  /* ─── Bar chart panel ─────────────────────────────────────────── */
  function BarPanel({ config, range }) {
    const source = config.source || 'campaigns'; // 'campaigns' | 'species'
    const limit = Math.max(3, Math.min(10, config.limit || 5));
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const { from, to } = rangeToDates(range);
      const url = source === 'species'
        ? `/api/analytics/species?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=${limit}`
        : `/api/analytics/campaigns?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=${limit}`;
      apiFetch(url).then((r) => {
        const rows = source === 'species' ? (r && r.ranking) || [] : (r && r.campaigns) || [];
        setData(rows);
        setLoading(false);
      }).catch(() => setLoading(false));
    }, [range, source, limit]);
    const valueKey = source === 'species' ? 'views' : 'impressions';
    const labelKey = source === 'species' ? 'common_name' : 'campaign_name';
    const max = Math.max(1, ...data.map(r => Number(r[valueKey] || r.view_count || 0)));
    return (
      <div style={{ padding: '14px 18px 18px' }}>
        <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600, marginBottom: 10 }}>
          Top {source}
        </div>
        {loading && <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</div>}
        {!loading && data.length === 0 && <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12 }}>No data</div>}
        <div style={{ display: 'grid', gap: 6 }}>
          {data.map((r, i) => {
            const v = Number(r[valueKey] || r.view_count || 0);
            const w = (v / max) * 100;
            return (
              <div key={r.id || r.campaign_id || i} style={{ display: 'grid', gridTemplateColumns: '1fr 60px', gap: 8, alignItems: 'center' }}>
                <div style={{ position: 'relative', height: 18 }}>
                  <div style={{ position: 'absolute', inset: 0, background: 'var(--aqos-surface-2)', borderRadius: 3 }} />
                  <div style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: w + '%', background: 'oklch(0.66 0.16 270)', borderRadius: 3, opacity: 0.85 }} />
                  <div style={{ position: 'absolute', inset: 0, padding: '0 6px', fontSize: 11, color: 'var(--aqos-text)', display: 'flex', alignItems: 'center', overflow: 'hidden', whiteSpace: 'nowrap' }}>
                    {r[labelKey] || r.scientific_name || r.name || '—'}
                  </div>
                </div>
                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
                  {_fmtN(v)}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  /* ─── Table panel ─────────────────────────────────────────────── */
  function TablePanel({ config, range }) {
    const source = config.source || 'campaigns'; // 'campaigns' | 'species' | 'screens'
    const limit = Math.max(3, Math.min(20, config.limit || 8));
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const { from, to } = rangeToDates(range);
      const url = source === 'species'
        ? `/api/analytics/species?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=${limit}`
        : source === 'screens'
        ? `/api/analytics/screens?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=${limit}`
        : `/api/analytics/campaigns?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=${limit}`;
      apiFetch(url).then((r) => {
        const rows = source === 'species' ? (r && r.ranking) || []
                  : source === 'screens'  ? (r && r.screens) || []
                  : (r && r.campaigns) || [];
        setData(rows);
        setLoading(false);
      }).catch(() => setLoading(false));
    }, [range, source, limit]);
    return (
      <div>
        <div style={{ padding: '14px 18px 8px', fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600 }}>
          {config.label || ('Top ' + source)}
        </div>
        <table className="x-analytics-table">
          <thead>
            <tr>
              <th style={{ paddingLeft: 18 }}>Name</th>
              {source === 'campaigns' && <th style={{ textAlign: 'right' }}>Impr.</th>}
              {source === 'campaigns' && <th style={{ textAlign: 'right' }}>CTR</th>}
              {source === 'species'   && <th style={{ textAlign: 'right' }}>Views</th>}
              {source === 'screens'   && <th style={{ textAlign: 'right' }}>Events</th>}
            </tr>
          </thead>
          <tbody>
            {loading && <tr><td colSpan={3} style={{ padding: 18, color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</td></tr>}
            {!loading && data.length === 0 && <tr><td colSpan={3} style={{ padding: 18, color: 'var(--aqos-text-faint)', fontSize: 12, textAlign: 'center' }}>No data</td></tr>}
            {data.map((r, i) => (
              <tr key={r.id || r.campaign_id || i}>
                <td style={{ paddingLeft: 18 }}>
                  {(r.emoji || '') + ' '}
                  <strong>{r.campaign_name || r.common_name || r.name || r.screen_code || '—'}</strong>
                </td>
                {source === 'campaigns' && <td className="x-analytics-table-num">{_fmtN(r.impressions)}</td>}
                {source === 'campaigns' && <td className="x-analytics-table-num">{_fmtPct(r.ctr_pct, 1)}</td>}
                {source === 'species'   && <td className="x-analytics-table-num">{_fmtN(r.views || r.view_count || 0)}</td>}
                {source === 'screens'   && <td className="x-analytics-table-num">{_fmtN(r.event_count || r.total_events || 0)}</td>}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  /* ─── Funnel panel ────────────────────────────────────────────── */
  function FunnelPanel({ range }) {
    const [steps, setSteps] = useState([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const { from, to } = rangeToDates(range);
      apiFetch(`/api/analytics/funnel?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`)
        .then((r) => { setSteps((r && r.steps) || []); setLoading(false); })
        .catch(() => setLoading(false));
    }, [range]);
    const top = steps[0] ? Number(steps[0].count) || 0 : 0;
    const palette = ['oklch(0.78 0.13 160)', 'oklch(0.66 0.16 270)', 'oklch(0.70 0.16 25)', 'oklch(0.78 0.14 75)'];
    return (
      <div style={{ padding: '14px 18px 18px' }}>
        <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600, marginBottom: 10 }}>
          Conversion funnel
        </div>
        {loading && <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</div>}
        {!loading && steps.length === 0 && <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12 }}>No data</div>}
        {steps.map((s, i) => {
          const w = top > 0 ? (Number(s.count) / top) * 100 : 0;
          return (
            <div key={s.key} style={{ marginBottom: 8 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 3 }}>
                <span>{s.label}</span>
                <span><strong style={{ color: 'var(--aqos-text)' }}>{_fmtN(s.count)}</strong> · {_fmtPct(s.pct, 1)}</span>
              </div>
              <div style={{ height: 14, background: 'var(--aqos-surface-2)', borderRadius: 3, position: 'relative', overflow: 'hidden' }}>
                <div style={{ position: 'absolute', inset: 0, width: w + '%', background: palette[i % palette.length], opacity: 0.85 }} />
              </div>
            </div>
          );
        })}
      </div>
    );
  }

  /* ─── Engagement panel ────────────────────────────────────────── */
  function EngagementPanel({ range }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const { from, to } = rangeToDates(range);
      apiFetch(`/api/analytics/engagement?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=8`)
        .then((r) => { setData(r); setLoading(false); })
        .catch(() => setLoading(false));
    }, [range]);
    function fmtMs(ms) {
      const v = Number(ms) || 0;
      if (v < 1000) return v + ' ms';
      if (v < 60000) return (v / 1000).toFixed(1) + 's';
      return Math.floor(v / 60000) + 'm';
    }
    const t = data && data.totals;
    return (
      <div>
        <div style={{ padding: '14px 18px 4px' }}>
          <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600 }}>
            🤚 Passive engagement
          </div>
          {t && t.exposures > 0 && (
            <div style={{ marginTop: 4, fontSize: 12, color: 'var(--aqos-text-dim)' }}>
              {_fmtN(t.exposures)} exposures · {_fmtN(t.holds)} holds · {_fmtPct(t.hold_rate_pct, 1)} hold rate
            </div>
          )}
        </div>
        <table className="x-analytics-table">
          <thead>
            <tr>
              <th style={{ paddingLeft: 18 }}>Species</th>
              <th style={{ textAlign: 'right' }}>Avg dwell</th>
              <th style={{ textAlign: 'right' }}>Hold rate</th>
            </tr>
          </thead>
          <tbody>
            {loading && <tr><td colSpan={3} style={{ padding: 18, color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</td></tr>}
            {!loading && (!data || !data.species || data.species.length === 0) && (
              <tr><td colSpan={3} style={{ padding: 18, color: 'var(--aqos-text-faint)', fontSize: 12, textAlign: 'center' }}>No engagement data yet</td></tr>
            )}
            {data && (data.species || []).map(s => (
              <tr key={s.species_id}>
                <td style={{ paddingLeft: 18 }}>{(s.emoji || '') + ' '}<strong>{s.common_name}</strong></td>
                <td className="x-analytics-table-num">{fmtMs(s.avg_exposure_ms)}</td>
                <td className="x-analytics-table-num"><strong style={{ color: 'oklch(0.78 0.13 160)' }}>{_fmtPct(s.hold_rate_pct, 1)}</strong></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  /* ─── Text/markdown panel (no data fetch) ─────────────────────── */
  function TextPanel({ config }) {
    return (
      <div style={{ padding: '14px 18px 18px' }}>
        {config.heading && (
          <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 6, fontFamily: 'var(--aq-ff-display, inherit)' }}>
            {config.heading}
          </div>
        )}
        <div style={{ color: 'var(--aqos-text-dim)', fontSize: 13, lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>
          {config.body || ''}
        </div>
      </div>
    );
  }

  /* ─── Panel registry ──────────────────────────────────────────── */
  const PANEL_TYPES = [
    { id: 'kpi',         label: 'KPI tile',          component: KpiPanel,        defaultSize: 'sm' },
    { id: 'line',        label: 'Line chart',        component: LinePanel,       defaultSize: 'md' },
    { id: 'bar',         label: 'Bar chart',         component: BarPanel,        defaultSize: 'md' },
    { id: 'table',       label: 'Table',             component: TablePanel,      defaultSize: 'md' },
    { id: 'funnel',      label: 'Conversion funnel', component: FunnelPanel,     defaultSize: 'md' },
    { id: 'engagement',  label: 'Engagement',        component: EngagementPanel, defaultSize: 'lg' },
    { id: 'text',        label: 'Text / heading',    component: TextPanel,       defaultSize: 'full' },
  ];
  function panelTypeById(id) { return PANEL_TYPES.find(p => p.id === id) || PANEL_TYPES[0]; }

  /* Expose for Phase M.3 (edit mode adds to this) and the screen below. */
  window.AQUAOS_PANEL_TYPES = PANEL_TYPES;
  window.AQUAOS_PANEL_BY_ID = panelTypeById;
  window.AQUAOS_PANEL_SIZE_COLS = SIZE_COLS;

  /* ─── Single panel wrapper ────────────────────────────────────── */
  function PanelWrapper({ panel, range, editMode, onRemove, onResize, onMove, isFirst, isLast }) {
    const meta = panelTypeById(panel.type);
    const Component = meta.component;
    const cols = SIZE_COLS[panel.size] || SIZE_COLS[meta.defaultSize] || 6;
    return (
      <div style={{
        gridColumn: 'span ' + cols,
        background: 'var(--aqos-surface)',
        border: '1px solid var(--aqos-border)',
        borderRadius: 8,
        position: 'relative',
        overflow: 'hidden',
      }}>
        {editMode && (
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            padding: '4px 8px', background: 'var(--aqos-surface-2)',
            borderBottom: '1px solid var(--aqos-border)', fontSize: 11,
          }}>
            <span style={{ color: 'var(--aqos-text-dim)' }}>
              {meta.label}{panel.config && panel.config.label ? ' · ' + panel.config.label : ''}
            </span>
            <span style={{ display: 'flex', gap: 8 }}>
              <select value={panel.size || meta.defaultSize} onChange={(e) => onResize(panel.id, e.target.value)}
                      style={{ background: 'var(--aqos-surface)', color: 'var(--aqos-text)', border: '1px solid var(--aqos-border)', borderRadius: 4, fontSize: 11, padding: '2px 4px' }}>
                <option value="sm">Small</option>
                <option value="md">Medium</option>
                <option value="lg">Large</option>
                <option value="full">Full</option>
              </select>
              <a onClick={() => onMove(panel.id, -1)} style={{ cursor: isFirst ? 'default' : 'pointer', color: isFirst ? 'var(--aqos-text-faint)' : 'var(--aqos-text-dim)' }} title="Move up">▲</a>
              <a onClick={() => onMove(panel.id,  1)} style={{ cursor: isLast  ? 'default' : 'pointer', color: isLast  ? 'var(--aqos-text-faint)' : 'var(--aqos-text-dim)' }} title="Move down">▼</a>
              <a onClick={() => onRemove(panel.id)} style={{ cursor: 'pointer', color: 'var(--aqos-danger)' }} title="Remove panel">✕</a>
            </span>
          </div>
        )}
        <Component config={panel.config || {}} range={range} />
      </div>
    );
  }
  window.AQUAOS_PanelWrapper = PanelWrapper;

  /* ─── Share modal — public link toggle ────────────────────────── */
  function ShareModal({ dashboardId, onClose }) {
    const [busy, setBusy] = useState(false);
    const [info, setInfo] = useState(null); // { is_public, share_url }

    /* Probe current state on mount — read the dashboard row to find
       out if it's already public + the existing token if any. */
    useEffect(() => {
      apiFetch('/api/analytics/dashboards/' + encodeURIComponent(dashboardId))
        .then((r) => {
          const d = r && r.dashboard;
          if (!d) return;
          const baseUrl = window.location.origin;
          setInfo({
            is_public: !!d.is_public,
            share_url: d.share_token ? baseUrl + '/public/d/' + d.share_token : null,
          });
        })
        .catch(() => {});
    }, [dashboardId]);

    function toggle(enable) {
      setBusy(true);
      apiFetch('/api/analytics/dashboards/' + encodeURIComponent(dashboardId) + '/share', {
        method: 'POST', body: JSON.stringify({ enable }),
      }).then((r) => {
        setBusy(false);
        setInfo({ is_public: !!r.is_public, share_url: r.share_url });
      }).catch((e) => { setBusy(false); window.toast && window.toast(e.message, 'error'); });
    }

    function copyLink() {
      if (!info || !info.share_url) return;
      navigator.clipboard?.writeText(info.share_url).then(
        () => window.toast && window.toast('Link copied'),
        () => window.prompt('Copy this link:', info.share_url)
      );
    }

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onClose}>
        <div onClick={(e) => e.stopPropagation()} style={{
          width: 480, background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 4 }}>Share dashboard</h2>
          <p style={{ fontSize: 12, color: 'var(--aqos-text-faint)', marginBottom: 14 }}>
            Publishing creates a token-gated public URL anyone can view without signing in.
            Only safe panel types render publicly (KPIs / bar charts / engagement summary / text).
            Revenue is intentionally omitted.
          </p>

          {info ? (
            <Fragment>
              <div style={{
                padding: '10px 12px', borderRadius: 6,
                background: info.is_public ? 'color-mix(in srgb, oklch(0.78 0.13 160) 14%, transparent)' : 'var(--aqos-surface-2)',
                marginBottom: 14, fontSize: 12.5,
              }}>
                <strong style={{ color: info.is_public ? 'oklch(0.78 0.13 160)' : 'var(--aqos-text-dim)' }}>
                  {info.is_public ? '● Public' : '○ Private'}
                </strong>
                <span style={{ marginLeft: 8, color: 'var(--aqos-text-dim)' }}>
                  {info.is_public ? 'Anyone with the link can view this dashboard.' : 'Only you and your team can see this dashboard.'}
                </span>
              </div>

              {info.is_public && info.share_url && (
                <Fragment>
                  <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Public link</div>
                  <div style={{
                    padding: '8px 10px', fontSize: 12, fontFamily: 'var(--aq-ff-mono, ui-monospace, monospace)',
                    background: 'var(--aqos-surface-2)', borderRadius: 6,
                    border: '1px solid var(--aqos-border)', wordBreak: 'break-all',
                    marginBottom: 8,
                  }}>{info.share_url}</div>
                  <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
                    <a onClick={copyLink} style={{ fontSize: 12, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>📋 Copy link</a>
                    <a href={info.share_url} target="_blank" rel="noopener noreferrer" style={{ fontSize: 12, color: 'var(--aqos-text-dim)' }}>↗ Open in new tab</a>
                  </div>
                </Fragment>
              )}

              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
                <button type="button" className="x-btn ghost" onClick={onClose}>Close</button>
                {info.is_public
                  ? <button type="button" className="x-btn" onClick={() => toggle(false)} disabled={busy} style={{ background: 'var(--aqos-danger)' }}>
                      {busy ? '…' : 'Stop sharing'}
                    </button>
                  : <button type="button" className="x-btn" onClick={() => toggle(true)} disabled={busy}>
                      {busy ? '…' : 'Make public'}
                    </button>
                }
              </div>
            </Fragment>
          ) : (
            <div style={{ color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</div>
          )}
        </div>
      </div>
    );
  }

  /* ─── Dashboard templates — starter layouts for new dashboards ──
     Each template is a {name, description, layout} the create flow
     can prefill so users don't stare at an empty grid. Picked from
     the most common analytics workflows we've seen.

     Adding a template is just one entry — the panels reference
     existing panel types from PANEL_TYPES, so no registry update
     needed. */
  const DASHBOARD_TEMPLATES = [
    {
      id: 'blank',
      name: 'Blank',
      description: 'Start from scratch',
      icon: '☐',
      layout: { panels: [] },
    },
    {
      id: 'operations',
      name: 'Operations',
      description: 'Live status check — screens online, recent activity, top campaigns',
      icon: '⚙️',
      layout: {
        panels: [
          { id: 'p_ops_1', type: 'kpi',  size: 'sm', config: { metric: 'screens_online', label: 'Screens online' } },
          { id: 'p_ops_2', type: 'kpi',  size: 'sm', config: { metric: 'impressions', label: 'Impressions' } },
          { id: 'p_ops_3', type: 'kpi',  size: 'sm', config: { metric: 'active_alerts', label: 'Active alerts' } },
          { id: 'p_ops_4', type: 'line', size: 'lg', config: { metric: 'impressions', label: 'Impressions over time' } },
          { id: 'p_ops_5', type: 'table', size: 'md', config: { source: 'screens', label: 'Top screens' } },
          { id: 'p_ops_6', type: 'engagement', size: 'lg', config: {} },
        ],
      },
    },
    {
      id: 'marketing',
      name: 'Marketing',
      description: 'Campaign focus — impressions, scans, CTR, conversion funnel',
      icon: '📣',
      layout: {
        panels: [
          { id: 'p_mkt_1', type: 'kpi', size: 'sm', config: { metric: 'impressions', label: 'Impressions' } },
          { id: 'p_mkt_2', type: 'kpi', size: 'sm', config: { metric: 'scans', label: 'QR scans' } },
          { id: 'p_mkt_3', type: 'kpi', size: 'sm', config: { metric: 'scan_rate', label: 'Scan rate' } },
          { id: 'p_mkt_4', type: 'bar', size: 'md', config: { source: 'campaigns', limit: 5 } },
          { id: 'p_mkt_5', type: 'funnel', size: 'md', config: {} },
          { id: 'p_mkt_6', type: 'table', size: 'full', config: { source: 'campaigns', limit: 10, label: 'All campaigns' } },
        ],
      },
    },
    {
      id: 'executive',
      name: 'Executive',
      description: 'High-level KPIs for board / leadership readouts',
      icon: '📈',
      layout: {
        panels: [
          { id: 'p_exec_1', type: 'text', size: 'full', config: { heading: 'Weekly summary', body: 'Edit this text panel to add a context paragraph for your board readout.' } },
          { id: 'p_exec_2', type: 'kpi', size: 'sm', config: { metric: 'impressions', label: 'Impressions' } },
          { id: 'p_exec_3', type: 'kpi', size: 'sm', config: { metric: 'scans', label: 'Scans' } },
          { id: 'p_exec_4', type: 'kpi', size: 'sm', config: { metric: 'revenue', label: 'Revenue' } },
          { id: 'p_exec_5', type: 'line', size: 'full', config: { metric: 'impressions', label: 'Impressions trend' } },
          { id: 'p_exec_6', type: 'bar', size: 'md', config: { source: 'campaigns', limit: 5 } },
          { id: 'p_exec_7', type: 'bar', size: 'md', config: { source: 'species', limit: 5 } },
        ],
      },
    },
    {
      id: 'engagement',
      name: 'Engagement',
      description: 'Passive-viewing focus — hold rate, dwell, top engaging species',
      icon: '🤚',
      layout: {
        panels: [
          { id: 'p_eng_1', type: 'engagement', size: 'full', config: {} },
          { id: 'p_eng_2', type: 'bar', size: 'md', config: { source: 'species', limit: 8 } },
          { id: 'p_eng_3', type: 'line', size: 'md', config: { metric: 'impressions', label: 'Exposure volume' } },
          { id: 'p_eng_4', type: 'text', size: 'full', config: { heading: 'How to read this', body: 'Hold rate = % of species exposures where a visitor pressed and held to pause. High hold rates indicate strong engagement; low hold rates suggest the species is being passed over.' } },
        ],
      },
    },
  ];
  window.AQUAOS_DASHBOARD_TEMPLATES = DASHBOARD_TEMPLATES;

  /* ─── Add Panel modal ─────────────────────────────────────────── */
  function AddPanelModal({ onAdd, onCancel }) {
    const [type, setType] = useState('kpi');
    const [config, setConfig] = useState({ metric: 'impressions', label: '' });
    const meta = panelTypeById(type);

    function changeType(t) {
      setType(t);
      /* Reset config to a sensible default per type. */
      if (t === 'kpi' || t === 'line') setConfig({ metric: 'impressions', label: '' });
      else if (t === 'bar' || t === 'table') setConfig({ source: 'campaigns', label: '' });
      else if (t === 'text') setConfig({ heading: 'Section heading', body: '' });
      else setConfig({});
    }

    function submit(e) {
      if (e) e.preventDefault();
      onAdd({ type, config, size: meta.defaultSize });
    }

    const inputStyle = {
      width: '100%', padding: '8px 10px', fontSize: 13,
      background: 'var(--aqos-surface-2)', color: 'var(--aqos-text)',
      border: '1px solid var(--aqos-border)', borderRadius: 6,
      boxSizing: 'border-box',
    };

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onCancel}>
        <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{
          width: 460, background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 14 }}>Add panel</h2>

          <label style={{ display: 'block', marginBottom: 12 }}>
            <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Panel type</div>
            <select value={type} onChange={(e) => changeType(e.target.value)} style={inputStyle}>
              {PANEL_TYPES.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
            </select>
          </label>

          {(type === 'kpi' || type === 'line') && (
            <label style={{ display: 'block', marginBottom: 12 }}>
              <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Metric</div>
              <select value={config.metric || 'impressions'} onChange={(e) => setConfig({ ...config, metric: e.target.value })} style={inputStyle}>
                <option value="impressions">Impressions</option>
                <option value="scans">QR scans</option>
                <option value="scan_rate">Scan rate</option>
                <option value="revenue">Attributed revenue</option>
                <option value="screens_online">Screens online</option>
                <option value="active_alerts">Active alerts</option>
              </select>
            </label>
          )}
          {(type === 'bar' || type === 'table') && (
            <label style={{ display: 'block', marginBottom: 12 }}>
              <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Data source</div>
              <select value={config.source || 'campaigns'} onChange={(e) => setConfig({ ...config, source: e.target.value })} style={inputStyle}>
                <option value="campaigns">Top campaigns</option>
                <option value="species">Top species</option>
                {type === 'table' && <option value="screens">Top screens</option>}
              </select>
            </label>
          )}
          {type === 'text' && (
            <Fragment>
              <label style={{ display: 'block', marginBottom: 12 }}>
                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Heading</div>
                <input value={config.heading || ''} onChange={(e) => setConfig({ ...config, heading: e.target.value })} style={inputStyle} />
              </label>
              <label style={{ display: 'block', marginBottom: 12 }}>
                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Body</div>
                <textarea value={config.body || ''} onChange={(e) => setConfig({ ...config, body: e.target.value })}
                          style={{ ...inputStyle, minHeight: 80, resize: 'vertical' }} />
              </label>
            </Fragment>
          )}
          {type !== 'funnel' && type !== 'engagement' && type !== 'text' && (
            <label style={{ display: 'block', marginBottom: 12 }}>
              <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Custom label (optional)</div>
              <input value={config.label || ''} onChange={(e) => setConfig({ ...config, label: e.target.value })} style={inputStyle} />
            </label>
          )}

          <div style={{ marginTop: 8, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
            <button type="button" className="x-btn ghost" onClick={onCancel}>Cancel</button>
            <button type="submit" className="x-btn">Add panel</button>
          </div>
        </form>
      </div>
    );
  }

  /* ─── Single dashboard screen ─────────────────────────────────── */
  function DashboardScreen({ param }) {
    const canView = !!(Auth.canViewAnalytics && Auth.canViewAnalytics());
    const canManage = !!(Auth.canManageAnalyticsAlerts && Auth.canManageAnalyticsAlerts());
    /* param looks like "id?range=7d" — slice off the query for the id. */
    const id = (param || '').split('?')[0];
    const [dashboard, setDashboard] = useState(null);
    const [layout, setLayout] = useState({ panels: [] });
    const [loading, setLoading] = useState(true);
    const [err, setErr] = useState(null);
    const [editMode, setEditMode] = useState(false);
    const [showAdd, setShowAdd] = useState(false);
    const [showShare, setShowShare] = useState(false);
    const [range, setRange] = useState('7d');
    const [dirty, setDirty] = useState(false);

    useEffect(() => {
      if (!canView || !id) return;
      apiFetch('/api/analytics/dashboards/' + encodeURIComponent(id))
        .then((r) => {
          const d = r && r.dashboard;
          setDashboard(d);
          setLayout(d && d.layout ? d.layout : { panels: [] });
          setLoading(false);
        })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }, [id, canView]);

    function save() {
      apiFetch('/api/analytics/dashboards/' + encodeURIComponent(id), {
        method: 'PATCH',
        body: JSON.stringify({ layout }),
      }).then(() => {
        setDirty(false);
        window.toast && window.toast('Dashboard saved');
      }).catch((e) => window.toast && window.toast('Save failed: ' + e.message, 'error'));
    }
    function addPanel(p) {
      const id = 'p_' + Math.random().toString(36).slice(2, 10);
      setLayout({ ...layout, panels: [...(layout.panels || []), { id, ...p }] });
      setShowAdd(false); setDirty(true);
    }
    function removePanel(pid) {
      setLayout({ ...layout, panels: layout.panels.filter(p => p.id !== pid) });
      setDirty(true);
    }
    function resizePanel(pid, size) {
      setLayout({ ...layout, panels: layout.panels.map(p => p.id === pid ? { ...p, size } : p) });
      setDirty(true);
    }
    function movePanel(pid, delta) {
      const idx = layout.panels.findIndex(p => p.id === pid);
      if (idx < 0) return;
      const newIdx = idx + delta;
      if (newIdx < 0 || newIdx >= layout.panels.length) return;
      const arr = [...layout.panels];
      const [removed] = arr.splice(idx, 1);
      arr.splice(newIdx, 0, removed);
      setLayout({ ...layout, panels: arr });
      setDirty(true);
    }

    if (!canView) {
      return <div className="aq-content"><div className="aq-content-inner" style={{ padding: 60 }}>
        <p style={{ color: 'var(--aqos-text-dim)' }}>You don't have permission to view dashboards.</p>
      </div></div>;
    }
    if (loading) return <div className="aq-content"><div className="aq-content-inner" style={{ padding: 60 }}>Loading…</div></div>;
    if (err) return <div className="aq-content"><div className="aq-content-inner" style={{ padding: 60 }}>
      <p style={{ color: 'var(--aqos-danger)' }}>Couldn't load dashboard: {err}</p>
    </div></div>;
    if (!dashboard) return null;

    return (
      <div className="aq-content">
        <div className="aq-content-inner" style={{ maxWidth: 1400 }}>
          <header className="x-analytics-head" style={{ marginBottom: 14 }}>
            <div>
              <a onClick={() => { window.location.hash = '#dashboards'; }} className="x-analytics-meta" style={{ cursor: 'pointer', display: 'inline-block', marginBottom: 4 }}>
                ← All dashboards
              </a>
              <h1>{dashboard.name}</h1>
              {dashboard.description && (
                <div className="x-analytics-meta">{dashboard.description}</div>
              )}
            </div>
            <div className="x-analytics-filters" style={{ marginBottom: 0, padding: 0, border: 0 }}>
              {[
                { id: '24h', label: '24h' },
                { id: '7d', label: '7d' },
                { id: '30d', label: '30d' },
                { id: '90d', label: '90d' },
              ].map((r) => (
                <span key={r.id} className={'x-analytics-chip' + (range === r.id ? ' is-active' : '')}
                      onClick={() => setRange(r.id)} style={{ cursor: 'pointer' }}>{r.label}</span>
              ))}
              {canManage && (
                <Fragment>
                  <span className="x-analytics-chip" onClick={() => setEditMode(!editMode)} style={{ cursor: 'pointer' }}>
                    {editMode ? '✓ Done' : '✎ Edit'}
                  </span>
                  {editMode && (
                    <Fragment>
                      <span className="x-analytics-chip" onClick={() => setShowAdd(true)} style={{ cursor: 'pointer' }}>+ Add panel</span>
                      <span className="x-analytics-chip" onClick={() => setShowShare(true)} style={{ cursor: 'pointer' }}>↗ Share</span>
                      {dirty && <span className="x-analytics-chip is-active" onClick={save} style={{ cursor: 'pointer' }}>💾 Save</span>}
                    </Fragment>
                  )}
                </Fragment>
              )}
            </div>
          </header>

          {/* Panel grid — 12-col CSS grid on desktop, collapses to 1 col
              on phones (see analytics.css media block). Class-based so the
              media query can override; inline grid would beat any rule. */}
          <div className="x-dashboard-grid">
            {(layout.panels || []).map((p, i) => (
              <PanelWrapper
                key={p.id}
                panel={p}
                range={range}
                editMode={editMode}
                onRemove={removePanel}
                onResize={resizePanel}
                onMove={movePanel}
                isFirst={i === 0}
                isLast={i === layout.panels.length - 1}
              />
            ))}
            {(layout.panels || []).length === 0 && (
              <div style={{ gridColumn: 'span 12', padding: 40, textAlign: 'center', color: 'var(--aqos-text-faint)', background: 'var(--aqos-surface)', border: '1px dashed var(--aqos-border)', borderRadius: 8 }}>
                No panels yet.
                {canManage && <div style={{ marginTop: 10 }}><a onClick={() => { setEditMode(true); setShowAdd(true); }} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Add your first panel →</a></div>}
              </div>
            )}
          </div>

          {showAdd && (
            <AddPanelModal onAdd={addPanel} onCancel={() => setShowAdd(false)} />
          )}
          {showShare && (
            <ShareModal dashboardId={id} onClose={() => setShowShare(false)} />
          )}
        </div>
      </div>
    );
  }

  /* ─── Dashboards list (index) ─────────────────────────────────── */
  function DashboardsList({ param }) {
    /* Branch: if a dashboard id is in the URL, render the single
       dashboard view instead of the index. Keeps the router happy
       with a single 'dashboards' route head. */
    if (param) return <DashboardScreen param={param} />;
    const canView = !!(Auth.canViewAnalytics && Auth.canViewAnalytics());
    const canManage = !!(Auth.canManageAnalyticsAlerts && Auth.canManageAnalyticsAlerts());
    const [dashboards, setDashboards] = useState([]);
    const [loading, setLoading] = useState(true);
    const [showCreate, setShowCreate] = useState(false);
    const [name, setName] = useState('');
    const [busy, setBusy] = useState(false);

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/dashboards')
        .then((r) => { setDashboards((r && r.dashboards) || []); setLoading(false); })
        .catch(() => setLoading(false));
    }
    useEffect(() => { if (canView) reload(); }, [canView]);

    const [pickedTemplate, setPickedTemplate] = useState('blank');
    function create() {
      if (!name.trim() || busy) return;
      setBusy(true);
      const tpl = DASHBOARD_TEMPLATES.find(t => t.id === pickedTemplate) || DASHBOARD_TEMPLATES[0];
      /* Re-stamp template panel ids with fresh random suffixes so two
         dashboards from the same template don't collide on panel id
         (the layout doesn't actually require globally-unique ids, but
         it's tidier and protects against any code that assumes it). */
      const layout = {
        panels: (tpl.layout.panels || []).map((p) => ({
          ...p,
          id: 'p_' + Math.random().toString(36).slice(2, 10),
        })),
      };
      apiFetch('/api/analytics/dashboards', {
        method: 'POST',
        body: JSON.stringify({ name: name.trim(), layout }),
      }).then((r) => {
        setBusy(false); setShowCreate(false); setName(''); setPickedTemplate('blank');
        if (r && r.id) window.location.hash = '#dashboards/' + r.id;
      }).catch((e) => { setBusy(false); window.toast && window.toast('Create failed: ' + e.message, 'error'); });
    }
    function deleteDashboard(d) {
      if (!window.confirm(`Delete dashboard "${d.name}"?`)) return;
      apiFetch('/api/analytics/dashboards/' + encodeURIComponent(d.id), { method: 'DELETE' })
        .then(reload)
        .catch((e) => window.toast && window.toast(e.message, 'error'));
    }

    if (!canView) {
      return <div className="aq-content"><div className="aq-content-inner" style={{ padding: 60 }}>
        <p style={{ color: 'var(--aqos-text-dim)' }}>You don't have permission to view dashboards.</p>
      </div></div>;
    }

    return (
      <div className="aq-content">
        <div className="aq-content-inner" style={{ maxWidth: 1100 }}>
          <header className="x-analytics-head" style={{ marginBottom: 14 }}>
            <div>
              <h1>Dashboards</h1>
              <div className="x-analytics-meta">Custom panel layouts — your own + your team's shared.</div>
            </div>
            {canManage && (
              <button className="x-btn" onClick={() => setShowCreate(true)}>+ New dashboard</button>
            )}
          </header>

          {loading && <div style={{ color: 'var(--aqos-text-faint)' }}>Loading…</div>}
          {!loading && dashboards.length === 0 && (
            <div style={{ padding: 36, textAlign: 'center', color: 'var(--aqos-text-faint)', background: 'var(--aqos-surface)', border: '1px dashed var(--aqos-border)', borderRadius: 8 }}>
              No dashboards yet. {canManage && <a onClick={() => setShowCreate(true)} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Create your first one →</a>}
            </div>
          )}

          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
            {dashboards.map((d) => (
              <div key={d.id} style={{
                padding: 16, background: 'var(--aqos-surface)',
                border: '1px solid var(--aqos-border)', borderRadius: 8,
                cursor: 'pointer',
              }} onClick={() => { window.location.hash = '#dashboards/' + d.id; }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
                  <strong style={{ fontSize: 14 }}>{d.name}</strong>
                  <span style={{ fontSize: 10, color: 'var(--aqos-text-faint)' }}>
                    {d.is_shared ? '👥 shared' : '🧍 personal'}
                  </span>
                </div>
                {d.description && (
                  <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12, marginBottom: 8 }}>{d.description}</div>
                )}
                <div style={{ color: 'var(--aqos-text-faint)', fontSize: 11 }}>
                  {(d.layout && d.layout.panels ? d.layout.panels.length : 0)} panel{(d.layout && d.layout.panels && d.layout.panels.length === 1) ? '' : 's'}
                  {' · updated '}
                  {d.updated_at ? new Date(d.updated_at).toLocaleDateString() : '—'}
                </div>
                {canManage && (
                  <div style={{ marginTop: 8 }}>
                    <a onClick={(e) => { e.stopPropagation(); deleteDashboard(d); }} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>Delete</a>
                  </div>
                )}
              </div>
            ))}
          </div>

          {showCreate && (
            <div style={{
              position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }} onClick={() => setShowCreate(false)}>
              <form onSubmit={(e) => { e.preventDefault(); create(); }} onClick={(e) => e.stopPropagation()} style={{
                width: 560, maxHeight: '90vh', overflowY: 'auto',
                background: 'var(--aqos-surface)', borderRadius: 10,
                border: '1px solid var(--aqos-border)', padding: 22,
                boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
              }}>
                <h2 style={{ fontSize: 15, fontWeight: 600, marginBottom: 4 }}>New dashboard</h2>
                <p style={{ fontSize: 12, color: 'var(--aqos-text-faint)', marginBottom: 14 }}>
                  Pick a starter template or build from scratch — you can always rearrange panels later.
                </p>

                <label style={{ display: 'block', marginBottom: 12 }}>
                  <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Name</div>
                  <input autoFocus value={name} onChange={(e) => setName(e.target.value)}
                         placeholder="e.g. Ops morning check"
                         style={{ width: '100%', padding: '8px 10px', fontSize: 13, background: 'var(--aqos-surface-2)', color: 'var(--aqos-text)', border: '1px solid var(--aqos-border)', borderRadius: 6, boxSizing: 'border-box' }} />
                </label>

                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 8 }}>Template</div>
                <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginBottom: 14 }}>
                  {DASHBOARD_TEMPLATES.map((tpl) => {
                    const picked = pickedTemplate === tpl.id;
                    return (
                      <div
                        key={tpl.id}
                        onClick={() => setPickedTemplate(tpl.id)}
                        style={{
                          padding: '10px 12px',
                          background: picked ? 'color-mix(in srgb, oklch(0.66 0.16 270) 14%, var(--aqos-surface-2))' : 'var(--aqos-surface-2)',
                          border: picked ? '1px solid oklch(0.66 0.16 270)' : '1px solid var(--aqos-border)',
                          borderRadius: 6,
                          cursor: 'pointer',
                        }}
                      >
                        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 2 }}>
                          <span style={{ fontSize: 14 }}>{tpl.icon}</span>
                          <strong style={{ fontSize: 12.5 }}>{tpl.name}</strong>
                          <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--aqos-text-faint)' }}>
                            {tpl.layout.panels.length} panel{tpl.layout.panels.length === 1 ? '' : 's'}
                          </span>
                        </div>
                        <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', lineHeight: 1.4 }}>{tpl.description}</div>
                      </div>
                    );
                  })}
                </div>

                <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
                  <button type="button" className="x-btn ghost" onClick={() => setShowCreate(false)}>Cancel</button>
                  <button type="submit" className="x-btn" disabled={busy || !name.trim()}>
                    {busy ? 'Creating…' : 'Create'}
                  </button>
                </div>
              </form>
            </div>
          )}
        </div>
      </div>
    );
  }

  window.DashboardScreen = DashboardScreen;
  window.DashboardsList = DashboardsList;
})();
