// PromptOS app — tokens, data, shared atoms
const posThemes = {
  dark: {
    bg: '#06060b',
    panel: '#0b0b13',
    card: '#10101b',
    card2: '#161624',
    line: 'rgba(255,255,255,0.07)',
    line2: 'rgba(255,255,255,0.14)',
    hov: 'rgba(255,255,255,0.06)',
    text: '#f1f1f8',
    mut: '#9b9bb4',
    dim: '#666680',
    green: '#4ade80',
  },
  light: {
    bg: '#f3f3f8',
    panel: '#ffffff',
    card: '#ffffff',
    card2: '#f1f1f6',
    line: 'rgba(15,15,45,0.09)',
    line2: 'rgba(15,15,45,0.18)',
    hov: 'rgba(15,15,45,0.05)',
    text: '#15151f',
    mut: '#5b5b74',
    dim: '#8c8ca2',
    green: '#16a34a',
  },
};

// T bersifat mutable: App menukar nilainya lewat Object.assign(T, posThemes[mode])
// sebelum render sehingga seluruh komponen mengikuti tema aktif.
const T = {
  font: "'Helvetica Neue', Helvetica, Arial, sans-serif",
  vio: '#7c5cfa',
  vioSoft: 'rgba(124,92,250,0.16)',
  grad: 'linear-gradient(135deg, #8f63f9, #6347ee)',
  amber: '#f5b94a',
  ...posThemes.dark,
};

// paket langganan — dipakai landing pricing & halaman Plans di app
const posPlans = [
  {
    id: 'basic', name: 'Basic', priceLabel: 'Gratis', period: '', color: '#60a5fa',
    tagline: 'Untuk individu yang baru memulai',
    features: ['50 prompt tersimpan', '3 koleksi', 'AI Generator — metode Role-based', 'Prompt Builder drag & drop', 'Dukungan komunitas'],
  },
  {
    id: 'pro', name: 'Pro', priceLabel: 'Rp99.000', period: '/bln', color: '#7c5cfa', highlight: true,
    tagline: 'Untuk power user & kreator',
    features: ['Semua fitur Basic', 'Prompt & koleksi tak terbatas', 'AI Generator — semua 5 metode (RAG, CoT, Few-shot, ReAct)', 'AI Agents — buat & jalankan', 'Testing Lab — 5 model + output gambar & video', 'Dukungan prioritas'],
  },
  {
    id: 'plus', name: 'Plus', priceLabel: 'Rp299.000', period: '/bln', color: '#f472b6',
    tagline: 'Untuk tim & organisasi',
    features: ['Semua fitur Pro', 'Insights & Knowledge Graph', 'Testing Lab — semua model AI', 'Manajemen tim & RBAC', 'Koleksi bersama', 'Akses API + dukungan khusus & SLA'],
  },
];
const posPlanMap = {};
posPlans.forEach((p) => { posPlanMap[p.id] = p; });

// matriks perbandingan fitur antar paket (true/false atau teks) — selaras dengan fitur nyata aplikasi
const posPlanMatrix = [
  { label: 'Prompt tersimpan', basic: '50', pro: 'Tak terbatas', plus: 'Tak terbatas' },
  { label: 'Koleksi', basic: '3', pro: 'Tak terbatas', plus: 'Tak terbatas' },
  { label: 'AI Generator (metode prompting)', basic: 'Role-based', pro: 'Semua (5)', plus: 'Semua (5)' },
  { label: 'Prompt Builder (drag & drop)', basic: true, pro: true, plus: true },
  { label: 'AI Agents (buat & jalankan)', basic: false, pro: true, plus: true },
  { label: 'Testing Lab', basic: false, pro: '5 model', plus: 'Semua model' },
  { label: 'Output gambar & video (Testing Lab)', basic: false, pro: true, plus: true },
  { label: 'Insights & analitik', basic: false, pro: false, plus: true },
  { label: 'Knowledge Graph', basic: false, pro: false, plus: true },
  { label: 'Manajemen tim & RBAC', basic: false, pro: false, plus: true },
  { label: 'Koleksi bersama', basic: false, pro: false, plus: true },
  { label: 'Akses API', basic: false, pro: false, plus: true },
  { label: 'Dukungan', basic: 'Komunitas', pro: 'Prioritas', plus: 'Khusus + SLA' },
];

// batasan fungsional tiap paket (Infinity = tak terbatas)
const posPlanLimits = {
  basic: { prompts: 50, collections: 3, genMethods: ['role'], labModels: 1, testingLab: false, agents: false, insights: false, knowledgeGraph: false },
  pro: { prompts: Infinity, collections: Infinity, genMethods: ['role', 'rag', 'cot', 'fewshot', 'react'], labModels: 5, testingLab: true, agents: true, insights: false, knowledgeGraph: false },
  plus: { prompts: Infinity, collections: Infinity, genMethods: ['role', 'rag', 'cot', 'fewshot', 'react'], labModels: Infinity, testingLab: true, agents: true, insights: true, knowledgeGraph: true },
};
const posLimitsFor = (plan) => posPlanLimits[plan] || posPlanLimits.basic;

const posModelList = [
  { n: 'ChatGPT', c: '#19c37d', a: 'G' },
  { n: 'Claude', c: '#d97757', a: 'C' },
  { n: 'Gemini', c: '#4e8df6', a: 'G' },
  { n: 'Perplexity', c: '#22b8cf', a: 'P' },
  { n: 'Grok', c: '#e5e5ec', a: 'X' },
  { n: 'Veo', c: '#60a5fa', a: 'V' },
  { n: 'Runway', c: '#34d399', a: 'R' },
  { n: 'Kling', c: '#f472b6', a: 'K' },
  { n: 'Manus', c: '#f5a524', a: 'M' },
];
const posModelColor = {};
posModelList.forEach((m) => { posModelColor[m.n] = m.c; });

// logo resmi dari folder "AI logo" (disalin ke assets/ai/)
const posModelImages = {
  'ChatGPT': 'assets/ai/chatgpt.png',
  'Claude': 'assets/ai/claude.jpg',
  'Gemini': 'assets/ai/gemini.webp',
  'Perplexity': 'assets/ai/perplexity.webp',
  'Grok': 'assets/ai/grok.png',
  'Veo': 'assets/ai/veo.png',
  'Runway': 'assets/ai/runway.webp',
  'Kling': 'assets/ai/kling.jpg',
  'Manus': 'assets/ai/manus.webp',
};

// simplified brand-style logo marks, drawn in a 24x24 viewBox with currentColor
const posModelLogos = {
  'ChatGPT': (
    <g fill="currentColor">
      {[0, 60, 120, 180, 240, 300].map((a) => (
        <rect key={a} x="10.7" y="2.6" width="2.6" height="10.4" rx="1.3" transform={`rotate(${a} 12 12)`} />
      ))}
    </g>
  ),
  'Claude': (
    <g stroke="currentColor" strokeWidth="2.1" strokeLinecap="round">
      {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => (
        <line key={i} x1="12" y1={i % 2 ? 5.6 : 3.4} x2="12" y2="8.8" transform={`rotate(${i * 30} 12 12)`} />
      ))}
    </g>
  ),
  'Gemini': (
    <path fill="currentColor" d="M12 2 C12.8 7.5 16.5 11.2 22 12 C16.5 12.8 12.8 16.5 12 22 C11.2 16.5 7.5 12.8 2 12 C7.5 11.2 11.2 7.5 12 2 Z" />
  ),
  'Midjourney': (
    <g fill="currentColor">
      <path d="M14.5 2.5 C10.5 6.2 7.8 10.2 6.5 14.8 H14.5 Z" />
      <path d="M3.5 17 H20.5 L17.6 20.5 H6.4 Z" />
    </g>
  ),
  'Stable Diffusion': (
    <path fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"
      d="M16.5 6 C16.5 3.6 7.8 3.4 7.8 7.4 C7.8 11.5 16.8 11.6 16.8 16 C16.8 20.2 7.5 20 7.5 17.2" />
  ),
  'Perplexity': (
    <g fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinejoin="round" strokeLinecap="round">
      <path d="M12 2.5 V21.5" />
      <path d="M4.5 7 L12 12 L4.5 17" />
      <path d="M19.5 7 L12 12 L19.5 17" />
    </g>
  ),
  'Grok': (
    <g fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round">
      <path d="M5 19.5 L19 4.5" />
      <path d="M5 4.5 L10.4 10.4" />
      <path d="M13.6 13.6 L19 19.5" />
    </g>
  ),
  'Veo': (
    <path fill="currentColor" d="M3.5 4.5 H8.5 L12 13.5 L15.5 4.5 H20.5 L14.3 19.5 H9.7 Z" />
  ),
  'Runway': (
    <g fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
      <path d="M7 21 V3.5 H13.5 A4.8 4.8 0 0 1 13.5 13.1 H7" />
      <path d="M12.5 13.1 L17.5 21" />
    </g>
  ),
  'Kling': (
    <g fill="none" stroke="currentColor" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round">
      <path d="M7 3 V21" />
      <path d="M17.5 3.5 L7.5 13 L18 21" />
    </g>
  ),
  'Manus': (
    <path fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
      d="M4.5 20 V10 A3.7 3.7 0 0 1 11.9 10 V13 M11.9 10 A3.7 3.7 0 0 1 19.3 10 V20" />
  ),
};

function MLogo({ name, size = 12 }) {
  const art = posModelLogos[name];
  if (!art) return null;
  return <svg width={size} height={size} viewBox="0 0 24 24" style={{ display: 'block' }}>{art}</svg>;
}

// Fallback offline saja — sumber data sebenarnya adalah PostgreSQL via /api/state
// (seed komprehensif ada di seed-data.js). Kosong agar tidak menampilkan dummy basi.
const posLibrary = [];

const posCoverOptions = [
  'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9',
  'pack-social', 'pack-style', 'pack-business',
  'cat-marketing', 'cat-image-generation', 'cat-coding', 'cat-research', 'cat-video',
].map((n) => `assets/covers/${n}.svg`);

const posCatCovers = {
  'Marketing': 'assets/covers/cat-marketing.svg',
  'Image Generation': 'assets/covers/cat-image-generation.svg',
  'Coding': 'assets/covers/cat-coding.svg',
  'Research': 'assets/covers/cat-research.svg',
  'Video': 'assets/covers/cat-video.svg',
};

const posAgents = [
  { t: 'Content Creator Agent', d: 'Research, write, review, and publish content', c: '#f472b6' },
  { t: 'Research Analyst Agent', d: 'Research, analyze, and create insights', c: '#60a5fa' },
  { t: 'Coding Assistant Agent', d: 'Code, debug, and optimize application', c: '#34d399' },
  { t: 'Business Analyst Agent', d: 'Analyze business data and create report', c: '#f5b94a' },
];

const posPacks = [
  { t: 'Prompt Pack: Social Media', by: 'MarketerPro', r: 4.9, s: '1.2K sales', p: '$12', hue: 25, img: 'assets/covers/pack-social.svg' },
  { t: 'Midjourney Style Pack', by: 'AICreator', r: 4.8, s: '890 sales', p: '$15', hue: 265, img: 'assets/covers/pack-style.svg' },
  { t: 'Business Proposal Pack', by: 'BizExpert', r: 4.9, s: '756 sales', p: '$10', hue: 210, img: 'assets/covers/pack-business.svg' },
];

// ---- ikon yang bisa dipilih untuk kategori & koleksi ----
const posIcons = {
  folder: ['M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z'],
  tag: ['M4 4h7l9 9-7 7-9-9Z', 'M8 8h.01'],
  star: ['M12 3l2.6 6 6.4.5-4.9 4.2 1.5 6.3-5.6-3.4-5.6 3.4 1.5-6.3-4.9-4.2 6.4-.5Z'],
  bolt: ['M13 3 5 13h6l-1 8 8-11h-6Z'],
  brush: ['M15 4l5 5-8 8-5-5Z', 'M7 12l-3 8 8-3'],
  code: ['M9 7l-5 5 5 5', 'M15 7l5 5-5 5'],
  chart: ['M4 20V10', 'M10 20V4', 'M16 20v-7', 'M3 20h18'],
  camera: ['M3 8a2 2 0 0 1 2-2h2l2-2h6l2 2h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z', 'M12 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z'],
  video: ['M3 7a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z', 'M16 10l5-3v10l-5-3'],
  doc: ['M6 3h8l4 4v14H6Z', 'M14 3v4h4', 'M9 12h6M9 16h4'],
  megaphone: ['M3 10v4a1 1 0 0 0 1 1h2l9 5V4l-9 5H4a1 1 0 0 0-1 1Z', 'M18 9a4 4 0 0 1 0 6'],
  bulb: ['M9 18h6M10 21h4', 'M12 3a6 6 0 0 1 4 10c-1 1-1 2-1 3H9c0-1 0-2-1-3a6 6 0 0 1 4-10Z'],
  rocket: ['M5 15c-1 2-1 4-1 4s2 0 4-1', 'M14 4c4 1 6 3 6 3s-1 5-4 8l-5 1-3-3 1-5c2-3 5-7 5-7Z', 'M14 9h.01'],
  heart: ['M12 20C6 15 3 12 3 8.5A4 4 0 0 1 12 6a4 4 0 0 1 9 2.5C21 12 18 15 12 20Z'],
  globe: ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z', 'M3 12h18', 'M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18'],
  briefcase: ['M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z', 'M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', 'M3 13h18'],
  sparkles: ['M12 3l1.5 5 5 1.5-5 1.5L12 16l-1.5-5-5-1.5 5-1.5Z'],
  flask: ['M9 3v6l-5 9a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1l-5-9V3', 'M8 3h8', 'M7 14h10'],
};
const posSwatches = ['#7c5cfa', '#f472b6', '#60a5fa', '#f5b94a', '#34d399', '#22b8cf', '#f87171', '#a78bfa', '#fb923c', '#e5e5ec'];

function IconGlyph({ name, size = 16, color = 'currentColor' }) {
  const paths = posIcons[name];
  if (!paths) return null;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8"
      strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block', flex: '0 0 auto' }}>
      {paths.map((d, i) => <path key={i} d={d} />)}
    </svg>
  );
}

function IconPicker({ value, onChange, color }) {
  const c = color || T.vio;
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
      {Object.keys(posIcons).map((n) => (
        <button key={n} type="button" onClick={() => onChange(n)} title={n} style={{
          width: 36, height: 36, borderRadius: 9, cursor: 'pointer',
          background: value === n ? c + '22' : T.card2,
          border: `1.5px solid ${value === n ? c : T.line}`,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          color: value === n ? c : T.mut,
        }}><IconGlyph name={n} size={18} color="currentColor" /></button>
      ))}
    </div>
  );
}

function ColorPicker({ value, onChange }) {
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
      {posSwatches.map((c) => (
        <button key={c} type="button" onClick={() => onChange(c)} title={c} style={{
          width: 26, height: 26, borderRadius: 8, cursor: 'pointer', background: c,
          border: `2px solid ${value === c ? T.text : 'transparent'}`,
        }}></button>
      ))}
    </div>
  );
}

// ---- atoms ----
function MChip({ name, size = 18 }) {
  const m = posModelList.find((x) => x.n === name) || { n: name, c: '#9b9bb4', a: (name || '?')[0] };
  const img = posModelImages[m.n];
  if (img) {
    return (
      <img src={img} alt={m.n} width={size} height={size} draggable={false} style={{
        width: size, height: size, borderRadius: size * 0.3, flex: '0 0 auto',
        objectFit: 'cover', display: 'inline-block', background: '#fff',
        border: `1px solid ${m.c}55`,
      }} />
    );
  }
  return (
    <span style={{
      width: size, height: size, borderRadius: size * 0.3, flex: '0 0 auto',
      background: m.c + '22', border: `1px solid ${m.c}55`,
      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      color: m.c, fontSize: size * 0.55, fontWeight: 700, lineHeight: 1,
    }}>{posModelLogos[m.n] ? <MLogo name={m.n} size={size * 0.64} /> : m.a}</span>
  );
}

function Stars({ r }) {
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
      <span style={{ color: T.amber, fontSize: 12, lineHeight: 1 }}>★</span>
      <span style={{ fontSize: 12, fontWeight: 700, color: T.text }}>{r}</span>
    </span>
  );
}

function Cover({ hue, height = 120, img, label = 'cover art' }) {
  if (img) {
    return (
      <div style={{ height, borderRadius: 10, overflow: 'hidden' }}>
        <img src={img} alt="" draggable={false} style={{
          width: '100%', height: '100%', objectFit: 'cover', display: 'block',
        }} />
      </div>
    );
  }
  return (
    <div style={{
      height, borderRadius: 10, position: 'relative', overflow: 'hidden',
      background: `linear-gradient(140deg, oklch(0.35 0.12 ${hue} / 0.9), oklch(0.2 0.06 ${hue + 40}))`,
    }}>
      <div style={{
        position: 'absolute', inset: 0,
        background: 'repeating-linear-gradient(45deg, rgba(255,255,255,0.04) 0 8px, transparent 8px 16px)',
      }}></div>
      <span style={{
        position: 'absolute', right: 8, bottom: 6, fontFamily: 'monospace', fontSize: 9,
        color: 'rgba(255,255,255,0.45)', letterSpacing: '0.08em',
      }}>{label}</span>
    </div>
  );
}

function Card({ children, style = {}, hover, onClick, onDragOver, onDrop }) {
  const [h, setH] = React.useState(false);
  return (
    <div onClick={onClick} onDragOver={onDragOver} onDrop={onDrop}
      onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
      style={{
        background: T.card, border: `1px solid ${h && hover ? T.line2 : T.line}`, borderRadius: 14,
        transition: 'border-color .15s, transform .15s, box-shadow .15s',
        transform: h && hover ? 'translateY(-2px)' : 'none',
        boxShadow: h && hover ? '0 10px 30px rgba(0,0,0,0.45)' : 'none',
        cursor: onClick ? 'pointer' : 'default',
        ...style,
      }}>{children}</div>
  );
}

function VioBtn({ children, onClick, small, style = {} }) {
  const [h, setH] = React.useState(false);
  return (
    <button onClick={onClick}
      onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
      style={{
        fontFamily: T.font, fontSize: small ? 12.5 : 14, fontWeight: 700, color: '#fff',
        background: T.grad, border: 'none', borderRadius: 9, cursor: 'pointer',
        padding: small ? '8px 14px' : '11px 20px',
        filter: h ? 'brightness(1.15)' : 'none', transition: 'filter .15s',
        ...style,
      }}>{children}</button>
  );
}

function GhostBtn({ children, onClick, small, style = {} }) {
  const [h, setH] = React.useState(false);
  return (
    <button onClick={onClick}
      onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
      style={{
        fontFamily: T.font, fontSize: small ? 12.5 : 14, fontWeight: 600, color: T.text,
        background: h ? T.hov : 'transparent',
        border: `1px solid ${T.line2}`, borderRadius: 9, cursor: 'pointer',
        padding: small ? '8px 14px' : '11px 20px', transition: 'background .15s',
        ...style,
      }}>{children}</button>
  );
}

function Select({ value, options, onChange, style = {} }) {
  return (
    <select value={value} onChange={(e) => onChange && onChange(e.target.value)} style={{
      fontFamily: T.font, fontSize: 13, color: T.text, background: T.card2,
      border: `1px solid ${T.line}`, borderRadius: 9, padding: '10px 12px', width: '100%',
      appearance: 'auto', outline: 'none', ...style,
    }}>
      {options.map((o) => <option key={o} value={o}>{o}</option>)}
    </select>
  );
}

// ---- chart atoms (SVG murni, ikut tema lewat token T) ----
function Sparkline({ data, color = T.vio, w = 90, h = 26 }) {
  const max = Math.max(...data), min = Math.min(...data);
  const pts = data.map((v, i) =>
    `${(i / (data.length - 1)) * w},${h - ((v - min) / (max - min || 1)) * (h - 4) - 2}`).join(' ');
  return (
    <svg width={w} height={h} style={{ display: 'block' }}>
      <polygon points={`0,${h} ${pts} ${w},${h}`} fill={color + '22'}></polygon>
      <polyline points={pts} fill="none" stroke={color} strokeWidth="1.6" strokeLinejoin="round"></polyline>
    </svg>
  );
}

function VBars({ data, labels, color = T.vio, h = 84 }) {
  const max = Math.max(...data);
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: h }}>
        {data.map((v, i) => (
          <div key={i} title={String(v)} style={{
            flex: 1, height: `${(v / max) * 100}%`, borderRadius: '4px 4px 2px 2px',
            background: i === data.length - 1 ? T.grad : color + '55',
          }}></div>
        ))}
      </div>
      {labels && (
        <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
          {labels.map((l, i) => (
            <span key={i} style={{ flex: 1, textAlign: 'center', fontSize: 9.5, color: T.dim }}>{l}</span>
          ))}
        </div>
      )}
    </div>
  );
}

function HBarList({ rows, max }) {
  const m = max || Math.max(...rows.map((r) => r.v));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
      {rows.map((r) => (
        <div key={r.label} style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
          <span style={{ display: 'flex', alignItems: 'center', fontSize: 11.5 }}>
            <span style={{ color: T.mut, display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
              {r.icon}{r.label}
            </span>
            <span style={{ marginLeft: 'auto', color: T.text, fontWeight: 700, paddingLeft: 8 }}>
              {r.right != null ? r.right : r.v}
            </span>
          </span>
          <span style={{ height: 5, borderRadius: 3, background: T.line, overflow: 'hidden' }}>
            <span style={{ display: 'block', height: '100%', width: `${(r.v / m) * 100}%`, borderRadius: 3, background: r.c || T.grad }}></span>
          </span>
        </div>
      ))}
    </div>
  );
}

function Donut({ segs, size = 116, thick = 13, title, sub }) {
  const r = (size - thick) / 2;
  const C = 2 * Math.PI * r;
  const total = segs.reduce((s, x) => s + x.v, 0) || 1;
  let acc = 0;
  return (
    <div style={{ position: 'relative', width: size, height: size, flex: '0 0 auto' }}>
      <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={T.line} strokeWidth={thick}></circle>
        {segs.map((s, i) => {
          const frac = s.v / total;
          const off = acc; acc += frac;
          return (
            <circle key={i} cx={size / 2} cy={size / 2} r={r} fill="none" stroke={s.c} strokeWidth={thick}
              strokeDasharray={`${Math.max(frac * C - 2.5, 0.1)} ${C}`} strokeDashoffset={-off * C}></circle>
          );
        })}
      </svg>
      <span style={{
        position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center', lineHeight: 1.25,
      }}>
        <span style={{ fontSize: 17, fontWeight: 800, color: T.text }}>{title}</span>
        {sub && <span style={{ fontSize: 9.5, color: T.dim }}>{sub}</span>}
      </span>
    </div>
  );
}

function Badge({ children, color }) {
  const c = color || T.green;
  return (
    <span style={{
      fontSize: 10.5, fontWeight: 700, color: c, background: c + '1c',
      border: `1px solid ${c}44`, borderRadius: 99, padding: '3px 9px',
      flex: '0 0 auto', lineHeight: 1.2,
    }}>{children}</span>
  );
}

function SectionTitle({ children, right }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
      <span style={{ fontSize: 16, fontWeight: 700, color: T.text }}>{children}</span>
      {right && <span style={{ marginLeft: 'auto' }}>{right}</span>}
    </div>
  );
}

function AppLogo({ small }) {
  const s = small ? 26 : 30;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <img src="assets/logo.png" alt="PromptOS" width={s} height={s}
        style={{ display: 'block', flex: '0 0 auto', objectFit: 'contain' }} />
      <span style={{ fontWeight: 800, fontSize: small ? 15 : 17, letterSpacing: '-0.02em', color: T.text }}>
        Prompt<span style={{ color: T.vio }}>OS</span>
      </span>
    </span>
  );
}

Object.assign(window, {
  T, posThemes, posPlans, posPlanMap, posPlanMatrix, posPlanLimits, posLimitsFor, posModelList, posModelColor, posModelLogos, posModelImages, posLibrary, posAgents, posPacks, posCatCovers, posCoverOptions,
  MChip, MLogo, Stars, Cover, Card, VioBtn, GhostBtn, Select, SectionTitle, AppLogo,
  Sparkline, VBars, HBarList, Donut, Badge,
  posIcons, posSwatches, IconGlyph, IconPicker, ColorPicker,
});
