// Shared icons, components, and API client for the Emulated.vip panel.

// ============ Icons (inline SVG, currentColor) ============
const Ico = {
  Dash: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>,
  Box: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>,
  Key: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zM17 8l3-3"/><path d="m14 7 3 3"/></svg>,
  Users: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
  Money: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>,
  Trophy: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>,
  List: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>,
  Code: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>,
  Shield: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>,
  Cpu: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>,
  Down: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>,
  Book: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
  Chat: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>,
  Bell: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>,
  Search: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>,
  Plus: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>,
  Settings: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
  Out: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>,
  Discord: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09a.1.1 0 0 0-.07-.03c-1.5.26-2.93.71-4.27 1.33a.07.07 0 0 0-.03.03C2.07 9.39 1.32 13.32 1.69 17.2c0 .02.01.04.03.05a16.2 16.2 0 0 0 4.9 2.48.1.1 0 0 0 .08-.03c.38-.52.72-1.07 1.01-1.65a.1.1 0 0 0-.05-.13 10.7 10.7 0 0 1-1.52-.72.1.1 0 0 1 0-.16c.1-.07.2-.15.3-.23a.1.1 0 0 1 .1-.01c3.18 1.45 6.62 1.45 9.76 0a.1.1 0 0 1 .1.01c.1.08.2.16.3.23a.1.1 0 0 1 0 .16c-.48.28-.98.52-1.52.72a.1.1 0 0 0-.05.13c.3.58.63 1.13 1 1.65.03.03.07.04.1.03a16.2 16.2 0 0 0 4.9-2.48.1.1 0 0 0 .04-.05c.45-4.48-.75-8.38-3.17-11.83a.07.07 0 0 0-.04-.04zM8.52 14.83c-.95 0-1.74-.87-1.74-1.94 0-1.07.77-1.94 1.74-1.94.98 0 1.76.88 1.74 1.94 0 1.07-.77 1.94-1.74 1.94zm6.98 0c-.95 0-1.74-.87-1.74-1.94 0-1.07.77-1.94 1.74-1.94.98 0 1.76.88 1.74 1.94 0 1.07-.76 1.94-1.74 1.94z"/></svg>,
  Eye: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>,
  Edit: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>,
  Trash: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><polyline points="3 6 5 6 21 6"/><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>,
  Copy: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>,
  Check: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" {...p}><polyline points="20 6 9 17 4 12"/></svg>,
  Up: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>,
  Zap: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
  Lock: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>,
  Arrow: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>,
  Refresh: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>,
  Crown: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M3 18l1.6-9 4.4 4 3-7 3 7 4.4-4L21 18H3zm0 2h18v2H3v-2z"/></svg>,
  Btc: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24zm5.5 10.2c-.2 1.4-1 2.1-2 2.3.7.4 1.4 1.1 1.4 2.5 0 1.9-1.4 2.9-3.5 3v2h-1.2v-2h-.9v2H10v-2H7v-1.3h.8c.3 0 .5-.2.5-.5v-5.7c0-.3-.2-.5-.5-.5H7V8.4h3V6.5h1.2v1.9h.9V6.5h1.2v2c1.8.1 3.2.8 3.2 2.6 0-1.5 1.4-3.1 5.5-3l.5.1zM12 11h1.5c.8 0 1.3-.3 1.3-1S14.3 9 13.5 9H12v2zm0 3.5h1.8c.9 0 1.5-.3 1.5-1.1s-.6-1.1-1.5-1.1H12v2.2z"/></svg>,
  Paypal: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M7.3 5.5h6.4c2.5 0 4.1 1.3 3.8 3.6-.4 2.7-2.4 4-5.1 4H9.6L8.7 19H6L7.3 5.5zM18 9.5c0 3.5-2.7 5.6-6 5.6h-1.4l-1 4.4H7l1.4-6h1.8c2.4 0 4.6-1 4.9-3.4.2-1.4-.5-2.4-2.1-2.4h-1l.3-1.6h2c2.7 0 4.4 1.3 3.7 3.4z"/></svg>,
  Stripe: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M13.5 7.5c-.5-.1-.9-.2-1.3-.2-.6 0-1 .2-1 .7 0 1.3 4.3.7 4.3 3.9 0 2-1.5 3-3.6 3-.9 0-1.8-.2-2.7-.5v-2.5c.9.5 1.9.8 2.8.8.7 0 1.1-.2 1.1-.7 0-1.4-4.4-.8-4.4-3.8 0-1.9 1.4-2.9 3.5-2.9.8 0 1.7.1 2.5.4l-.2 1.8z"/></svg>,
  Heart: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>,
  Globe: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>,
  Reset: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>,
  Filter: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>,
};

// ============ API client ============
const api = {
  async req(method, path, body) {
    const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin' };
    if (body !== undefined) opts.body = JSON.stringify(body);
    const r = await fetch(path, opts);
    const ct = r.headers.get('content-type') || '';
    const data = ct.includes('json') ? await r.json() : await r.text();
    if (!r.ok) throw new Error((data && data.error) || `HTTP ${r.status}`);
    return data;
  },
  get: (p) => api.req('GET', p),
  post: (p,b) => api.req('POST', p, b||{}),
  put: (p,b) => api.req('PUT', p, b||{}),
  patch: (p,b) => api.req('PATCH', p, b||{}),
  del: (p) => api.req('DELETE', p),
  async upload(path, file, fields) {
    const fd = new FormData();
    fd.append('file', file);
    for (const [k,v] of Object.entries(fields||{})) fd.append(k, v);
    const r = await fetch(path, { method:'POST', body: fd, credentials: 'same-origin' });
    if (!r.ok) { const t = await r.text(); throw new Error(t || `HTTP ${r.status}`); }
    return await r.json();
  },
};

// ============ Hook: GET an endpoint with refresh + loading ============
function useApi(path, deps=[]) {
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => {
    if (!path) return;
    let cancelled = false;
    setLoading(true);
    api.get(path).then(d => { if(!cancelled){ setData(d); setError(null); setLoading(false); } })
      .catch(e => { if(!cancelled){ setError(e.message); setLoading(false); } });
    return () => { cancelled = true; };
  }, [path, tick, ...deps]);
  return { data, error, loading, refresh: () => setTick(t=>t+1), setData };
}

// ============ Toast (simple top-right) ============
const toastState = { listeners: [] };
function toast(msg, kind='ok') {
  toastState.listeners.forEach(fn => fn({ id: Date.now()+Math.random(), msg, kind }));
}
function ToastHost() {
  const [items, setItems] = React.useState([]);
  React.useEffect(() => {
    const fn = (t) => {
      setItems(prev => [...prev, t]);
      setTimeout(() => setItems(prev => prev.filter(x => x.id !== t.id)), 3800);
    };
    toastState.listeners.push(fn);
    return () => { toastState.listeners = toastState.listeners.filter(x => x !== fn); };
  }, []);
  return (
    <div style={{ position: 'fixed', top: 16, right: 16, zIndex: 9999, display: 'flex', flexDirection: 'column', gap: 8, pointerEvents: 'none' }}>
      {items.map(t => (
        <div key={t.id} style={{
          minWidth: 240, maxWidth: 360, padding: '12px 14px',
          background: t.kind === 'err' ? 'rgba(190,40,40,0.95)' : t.kind === 'warn' ? 'rgba(200,140,40,0.95)' : 'rgba(20,150,70,0.95)',
          color: '#fff', borderRadius: 10, fontSize: 13, fontWeight: 600,
          boxShadow: '0 12px 30px -8px rgba(0,0,0,0.5)', pointerEvents: 'auto',
        }}>{t.msg}</div>
      ))}
    </div>
  );
}

// ============ Decorative ring SVGs ============
function RingsBg({ density = 8, opacity = 0.45 }) {
  const items = [];
  const rng = (i) => Math.sin(i * 9301 + 49297) * 0.5 + 0.5;
  for (let i = 0; i < density; i++) {
    const x = rng(i) * 100;
    const y = rng(i + 100) * 100;
    const r = 80 + rng(i + 200) * 220;
    const rot = rng(i + 300) * 60 - 30;
    items.push(
      <svg key={i} style={{ left: `${x}%`, top: `${y}%`, transform: `translate(-50%,-50%) rotate(${rot}deg)` }} width={r * 2} height={r * 2} viewBox={`-${r} -${r} ${r*2} ${r*2}`}>
        <ellipse cx="0" cy="0" rx={r * 0.95} ry={r * 0.55} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.5"/>
        <ellipse cx="0" cy="0" rx={r * 0.75} ry={r * 0.42} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.4"/>
        <ellipse cx="0" cy="0" rx={r * 0.55} ry={r * 0.3} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.3"/>
        <ellipse cx="0" cy="0" rx={r * 0.35} ry={r * 0.2} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.2"/>
      </svg>
    );
  }
  return <div className="rings" style={{ color: 'var(--accent)', opacity }}>{items}</div>;
}

// ============ Logo ============
function Logo({ size = 28 }) {
  return <img src="assets/logo.png" width={size} height={size} alt="Emulated.vip"
    onError={(e)=>{ e.currentTarget.style.display='none'; }}
    style={{ filter: 'drop-shadow(0 0 12px rgba(255,184,0,0.4))' }}/>;
}

// ============ Avatar ============
function Avatar({ name, size = 28 }) {
  const safe = name || '?';
  const letters = safe.split(/[\s_.-]/).filter(Boolean).slice(0, 2).map(s => s[0]).join("").toUpperCase() || '?';
  let h = 0; for (let i = 0; i < safe.length; i++) h = (h * 31 + safe.charCodeAt(i)) >>> 0;
  const hue = h % 360;
  const bg = `linear-gradient(135deg, oklch(60% 0.15 ${hue}), oklch(45% 0.12 ${(hue + 40) % 360}))`;
  return <div className="avatar" style={{ background: bg, width: size, height: size, fontSize: size * 0.4, color: '#fff' }}>{letters}</div>;
}

// ============ Spark ============
function Spark({ data, color = "var(--accent)", height = 60 }) {
  if (!data || !data.length) data = [0,0];
  const w = 200, h = height, pad = 6;
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const pts = data.map((v, i) => {
    const x = pad + (i / (data.length - 1)) * (w - pad * 2);
    const y = h - pad - ((v - min) / range) * (h - pad * 2);
    return `${x},${y}`;
  }).join(" ");
  const area = `${pad},${h} ${pts} ${w - pad},${h}`;
  const id = `g-${Math.random().toString(36).slice(2, 8)}`;
  return (
    <svg className="spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      <defs>
        <linearGradient id={id} x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.45"/>
          <stop offset="100%" stopColor={color} stopOpacity="0"/>
        </linearGradient>
      </defs>
      <polygon points={area} fill={`url(#${id})`}/>
      <polyline points={pts} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

// ============ Bar chart ============
function Bars({ data, color = "var(--accent)", height = 140, labels }) {
  if (!data || !data.length) return null;
  const max = Math.max(...data) || 1;
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height, padding: '0 4px' }}>
      {data.map((v, i) => (
        <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, height: '100%' }}>
          <div style={{
            width: '100%',
            height: `${(v / max) * 100}%`,
            background: `linear-gradient(180deg, ${color}, color-mix(in oklab, ${color} 30%, transparent))`,
            borderRadius: '3px 3px 0 0',
            boxShadow: `0 0 12px color-mix(in oklab, ${color} 40%, transparent)`,
            transition: 'height 0.3s'
          }}/>
          {labels && <span style={{ fontSize: 10, color: 'var(--text-dimmer)', fontFamily: 'JetBrains Mono, monospace' }}>{labels[i]}</span>}
        </div>
      ))}
    </div>
  );
}

// ============ Stat tile ============
function Stat({ label, value, delta, deltaDir = "up", icon, spark }) {
  return (
    <div className="stat">
      {icon && <div className="ico">{icon}</div>}
      <div className="label">{label}</div>
      <div className="value">{value}</div>
      {delta && <div className={`delta ${deltaDir}`}>{deltaDir === 'up' ? '↑' : '↓'} {delta}</div>}
      {spark && <div style={{ marginTop: 8 }}><Spark data={spark} height={36}/></div>}
    </div>
  );
}

// ============ Modal ============
function Modal({ open, onClose, title, children, width=520, closeOnBackdrop=true }) {
  const downOnBackdrop = React.useRef(false);
  if (!open) return null;
  return (
    <div
      role="presentation"
      onMouseDown={e => { downOnBackdrop.current = e.target === e.currentTarget; }}
      onClick={e => {
        if (closeOnBackdrop && downOnBackdrop.current && e.target === e.currentTarget) onClose?.();
        downOnBackdrop.current = false;
      }}
      style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
      zIndex: 1000, display: 'grid', placeItems: 'center', padding: 24,
      }}
    >
      <div onClick={e=>e.stopPropagation()} className="card" style={{ width: '100%', maxWidth: width, maxHeight: '90vh', overflow: 'auto' }}>
        <div className="card-head"><h3>{title}</h3><button className="btn btn-sm btn-ghost" onClick={onClose}>×</button></div>
        <div className="card-pad">{children}</div>
      </div>
    </div>
  );
}

// ============ Helpers ============
const fmtMoney = (n) => '$' + (Number(n)||0).toFixed(2);
const fmtTime = (sec) => {
  if (!sec) return '—';
  const d = new Date(sec*1000);
  const now = Date.now();
  const diff = (now - d.getTime())/1000;
  if (diff < 60) return 'just now';
  if (diff < 3600) return `${Math.floor(diff/60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff/3600)}h ago`;
  if (diff < 86400*7) return `${Math.floor(diff/86400)}d ago`;
  return d.toISOString().slice(0,10);
};
const fmtDate = (sec) => sec ? new Date(sec*1000).toISOString().slice(0,10) : '—';

const RANK_COLORS = { Diamond: '#b9f2ff', Platinum: '#90e0ef', Gold: '#FFB800', Silver: '#c0c0c0', Bronze: '#cd7f32' };
const DURATIONS = ["1 Day", "1 Week", "1 Month", "Lifetime"];

// ============ Copy button (clipboard with toast) ============
function CopyBtn({ value, label='Copy', size='sm', icon=true, kind='', successMsg='Copied' }) {
  const [done, setDone] = React.useState(false);
  const copy = async (e) => {
    e?.stopPropagation();
    try { await navigator.clipboard.writeText(value); setDone(true); setTimeout(()=>setDone(false), 1200); toast(successMsg); }
    catch { toast('Copy failed','err'); }
  };
  return (
    <button className={`btn btn-${size} ${kind}`} onClick={copy} title="Copy" type="button">
      {icon && (done ? <Ico.Check className="icon" width={12} height={12}/> : <Ico.Copy className="icon" width={12} height={12}/>)} {label}
    </button>
  );
}

// ============ Keys reveal modal (post-generation) ============
function KeysReveal({ open, batchId, keys, cost, balanceAfter, productName, duration, onClose }) {
  if (!open) return null;
  const text = (keys||[]).join('\n');
  const csv = (keys||[]).join('\n');
  const download = (ext='txt') => {
    const blob = new Blob([ext==='csv'?csv:text], { type: ext==='csv'?'text/csv':'text/plain' });
    const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `batch-${batchId||Date.now()}.${ext}`; a.click();
    URL.revokeObjectURL(a.href);
  };
  return (
    <Modal open onClose={onClose} title={`${keys.length} key${keys.length===1?'':'s'} generated`} width={620}>
      <div className="flex-col gap-12">
        <div className="flex gap-8" style={{ flexWrap:'wrap', alignItems: 'center' }}>
          <span className="pill green"><span className="dot"/>success</span>
          {productName && <span className="pill">{productName}</span>}
          {duration && <span className="tag">{duration}</span>}
          {batchId && <span className="mono" style={{ fontSize: 11, color: 'var(--text-dim)' }}>BATCH-{String(batchId).padStart(5,'0')}</span>}
          <div style={{ flex: 1 }}/>
          {cost !== undefined && <span className="mono" style={{ fontSize: 13, color: 'var(--text-dim)' }}>cost <b style={{color:'var(--accent)'}}>{fmtMoney(cost)}</b></span>}
          {balanceAfter !== undefined && <span className="mono" style={{ fontSize: 13, color: 'var(--text-dim)' }}>balance <b style={{color:'var(--accent)'}}>{fmtMoney(balanceAfter)}</b></span>}
        </div>

        <div style={{ maxHeight: 320, overflow: 'auto', border: '1px solid var(--border)', borderRadius: 'var(--r-md)', background: '#0a0a0a' }}>
          {(keys||[]).map((k, i) => (
            <div key={i} style={{ display:'flex', alignItems:'center', gap: 8, padding: '8px 12px', borderBottom: i<keys.length-1?'1px solid var(--border)':'none' }}>
              <span style={{ minWidth: 28, fontSize: 10, color: 'var(--text-dimmer)', fontFamily: 'var(--mono, monospace)' }}>{String(i+1).padStart(3,'0')}</span>
              <span className="mono" style={{ flex: 1, fontSize: 12.5, color: 'var(--text)', wordBreak:'break-all', userSelect: 'all' }}>{k}</span>
              <CopyBtn value={k} label="" size="sm" kind="btn-ghost" successMsg="Key copied"/>
            </div>
          ))}
        </div>

        <div className="flex gap-8" style={{ justifyContent: 'space-between', flexWrap:'wrap' }}>
          <div className="flex gap-8">
            <CopyBtn value={text} label={`Copy all (${keys.length})`} size="" kind="btn-primary" successMsg={`${keys.length} keys copied`}/>
            <button className="btn" onClick={()=>download('txt')}><Ico.Down className="icon"/> .txt</button>
            <button className="btn" onClick={()=>download('csv')}><Ico.Down className="icon"/> .csv</button>
          </div>
          <button className="btn" onClick={onClose}>Done</button>
        </div>
        <div style={{ fontSize: 11, color: 'var(--text-dimmer)', lineHeight: 1.5 }}>
          Keep these safe — anyone with a key can activate it. You can always get them back from the batch row.
        </div>
      </div>
    </Modal>
  );
}

// ============ Empty state ============
function EmptyState({ icon, title, desc, action }) {
  return (
    <div style={{ textAlign:'center', padding: '48px 24px', color: 'var(--text-dim)' }}>
      {icon && <div style={{ display: 'inline-grid', placeItems: 'center', width: 56, height: 56, borderRadius: 14, background: 'var(--accent-soft)', color: 'var(--accent)', marginBottom: 14 }}>{React.cloneElement(icon, { width: 24, height: 24 })}</div>}
      <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text)', marginBottom: 4 }}>{title}</div>
      {desc && <div style={{ fontSize: 13, maxWidth: 360, margin: '0 auto 16px' }}>{desc}</div>}
      {action}
    </div>
  );
}

// ============ NumberInput — stepper + presets, no fiddly browser arrows ============
function NumberInput({ value, onChange, min, max, step=1, presets=[], prefix, suffix, placeholder, style={}, disabled }) {
  const clamp = (v) => {
    let n = +v;
    if (isNaN(n)) n = min ?? 0;
    if (min !== undefined && n < min) n = min;
    if (max !== undefined && n > max) n = max;
    // Snap to step for ints
    if (step >= 1 && Number.isInteger(step)) n = Math.round(n/step)*step;
    return n;
  };
  const bump = (d) => onChange(clamp((+value||0) + d*step));
  return (
    <div className="flex-col gap-8" style={style}>
      <div className="flex gap-0" style={{ alignItems: 'stretch' }}>
        <button type="button" className="btn btn-sm" disabled={disabled || (min !== undefined && +value <= min)} onClick={()=>bump(-1)} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0, padding: '0 12px', fontSize: 18, fontWeight: 700, minWidth: 36 }}>−</button>
        <div style={{ position: 'relative', flex: 1 }}>
          {prefix && <span className="mono" style={{ position:'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color:'var(--text-dimmer)', pointerEvents:'none' }}>{prefix}</span>}
          <input
            className="input mono"
            type="text"
            inputMode="decimal"
            disabled={disabled}
            value={value}
            placeholder={placeholder}
            onChange={e=>{
              const raw = e.target.value.replace(/[^0-9.\-]/g,'');
              onChange(raw === '' ? '' : (step < 1 ? +raw : (raw === '-' ? '-' : +raw)));
            }}
            onBlur={()=>onChange(clamp(value))}
            style={{ borderRadius: 0, textAlign: 'center', fontSize: 15, fontWeight: 700, paddingLeft: prefix?22:10, paddingRight: suffix?28:10 }}
          />
          {suffix && <span className="mono" style={{ position:'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', color:'var(--text-dimmer)', pointerEvents:'none' }}>{suffix}</span>}
        </div>
        <button type="button" className="btn btn-sm" disabled={disabled || (max !== undefined && +value >= max)} onClick={()=>bump(1)} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, padding: '0 12px', fontSize: 18, fontWeight: 700, minWidth: 36 }}>+</button>
      </div>
      {presets.length > 0 && (
        <div className="flex gap-4" style={{ flexWrap: 'wrap' }}>
          {presets.map(p => (
            <span key={p} className={`pill ${+value===+p?'gold':''}`} style={{ cursor: disabled?'not-allowed':'pointer', padding: '4px 10px', fontSize: 11 }} onClick={()=>!disabled && onChange(p)}>{prefix||''}{p}{suffix||''}</span>
          ))}
        </div>
      )}
    </div>
  );
}

// Make available globally
Object.assign(window, {
  Ico, RingsBg, Logo, Avatar, Spark, Bars, Stat, Modal, ToastHost, CopyBtn, KeysReveal, EmptyState, NumberInput,
  api, useApi, toast, fmtMoney, fmtTime, fmtDate, RANK_COLORS, DURATIONS,
});
