// UI primitives. Linear-restrained.

// ---------- Card
function Card({ children, className = "", style, padding = "p", hover, onClick, ...rest }) {
  return <div onClick={onClick}
    className={`exos-card ${padding} ${hover ? "hover" : ""} ${className}`}
    style={style} {...rest}>{children}</div>;
}

// ---------- StatCard
function StatCard({ label, value, sub, tone = "default", icon, accent }) {
  const tones = {
    default: "var(--text)", gain: "var(--gain)", loss: "var(--loss)", warn: "var(--warn)", accent: "var(--accent)"
  };
  return (
    <Card className="exos-stat">
      <div className="exos-stat-head">
        <span className="exos-label">{label}</span>
        {icon && <span className="exos-stat-icon">{icon}</span>}
      </div>
      <div className="exos-stat-value mono" style={{ color: tones[tone] }}>{value}</div>
      {sub && <div className="exos-stat-sub">{sub}</div>}
      {accent && <div className="exos-stat-accent" style={{ background: accent }} />}
    </Card>
  );
}

// ---------- Button
function Button({ variant = "secondary", size = "md", icon, iconRight, children, className = "", ...rest }) {
  return (
    <button className={`exos-btn ${variant} ${size} ${className}`} {...rest}>
      {icon && <span className="exos-btn-icon">{icon}</span>}
      {children && <span>{children}</span>}
      {iconRight && <span className="exos-btn-icon right">{iconRight}</span>}
    </button>
  );
}

// ---------- Pill / Badge
function Pill({ tone = "default", children, dot, className = "", ...rest }) {
  return (
    <span className={`exos-pill ${tone} ${className}`} {...rest}>
      {dot && <span className="exos-pill-dot" />}
      {children}
    </span>
  );
}

// ---------- Section header
function SectionHeader({ kicker, title, sub, right }) {
  return (
    <div className="exos-section-head">
      <div>
        {kicker && <div className="exos-kicker mono">{kicker}</div>}
        <h2 className="exos-h2">{title}</h2>
        {sub && <div className="exos-sub">{sub}</div>}
      </div>
      {right && <div className="exos-section-right">{right}</div>}
    </div>
  );
}

// ---------- Number ticker
function Ticker({ value, format = (v) => v.toFixed(2), flash = true }) {
  const prev = React.useRef(value);
  const [dir, setDir] = React.useState(0);
  React.useEffect(() => {
    if (prev.current !== value) {
      setDir(value > prev.current ? 1 : -1);
      prev.current = value;
      const id = setTimeout(() => setDir(0), 700);
      return () => clearTimeout(id);
    }
  }, [value]);
  const color = !flash || dir === 0 ? undefined : dir > 0 ? "var(--gain)" : "var(--loss)";
  return <span className="mono" style={{ color, transition: "color .5s" }}>{format(value)}</span>;
}

// ---------- Coin icon · tries real logo CDNs, falls back to letter circle
const ICON_OVERRIDES = {
  // map symbol -> CDN slug when the symbol differs from the icon file name
  POL: "matic",
  WBTC: "btc",
};
const __exosFailedIcons = new Set();

function getCoinIconUrls(meta) {
  const out = [];
  if (meta.image) out.push(meta.image);
  const sym = (meta.symbol || "").toLowerCase().replace(/[^a-z0-9]/g, "");
  const override = ICON_OVERRIDES[meta.symbol]?.toLowerCase();
  const slug = override || sym;
  if (slug) {
    out.push(`https://cdn.jsdelivr.net/gh/spothq/cryptocurrency-icons@master/svg/color/${slug}.svg`);
    out.push(`https://assets.coincap.io/assets/icons/${slug}@2x.png`);
  }
  return out.filter(u => !__exosFailedIcons.has(u));
}

function CoinIcon({ coinId, size = 28 }) {
  const meta = getCoinMeta(coinId);
  const urls = React.useMemo(() => getCoinIconUrls(meta), [meta.id, meta.image, meta.symbol]);
  const [idx, setIdx] = React.useState(0);
  React.useEffect(() => { setIdx(0); }, [meta.id]);

  const url = urls[idx];
  if (url) {
    return (
      <img
        src={url}
        alt={meta.symbol}
        className="exos-coin-icon-img"
        style={{ width: size, height: size }}
        onError={() => {
          __exosFailedIcons.add(url);
          setIdx(i => i + 1);
        }}
      />
    );
  }
  // letter circle fallback
  return (
    <div className="exos-coin-icon" style={{
      width: size, height: size, background: meta.color,
      fontSize: size * 0.42,
    }}>
      <span>{(meta.symbol || "?").slice(0, 2)}</span>
    </div>
  );
}

// ---------- Input
function TextInput({ value, onChange, prefix, suffix, type = "text", placeholder, className = "", ...rest }) {
  return (
    <div className={`exos-input ${className}`}>
      {prefix && <span className="exos-input-aff">{prefix}</span>}
      <input value={value ?? ""} onChange={e => onChange?.(e.target.value)} type={type} placeholder={placeholder} {...rest} />
      {suffix && <span className="exos-input-aff right">{suffix}</span>}
    </div>
  );
}

// ---------- Number input
function NumInput({ value, onChange, step = 1, min, max, prefix, suffix, placeholder, className = "" }) {
  const [local, setLocal] = React.useState(value ?? "");
  React.useEffect(() => { setLocal(value ?? ""); }, [value]);
  return (
    <div className={`exos-input ${className}`}>
      {prefix && <span className="exos-input-aff">{prefix}</span>}
      <input className="mono" inputMode="decimal" value={local}
        placeholder={placeholder}
        onChange={e => {
          const v = e.target.value;
          setLocal(v);
          if (v === "" || v === "-") { onChange?.(undefined); return; }
          const n = parseFloat(v);
          if (!isNaN(n)) onChange?.(n);
        }}
        onBlur={() => { if (local === "" || isNaN(parseFloat(local))) setLocal(value ?? ""); }}
      />
      {suffix && <span className="exos-input-aff right">{suffix}</span>}
    </div>
  );
}

// ---------- Modal
function Modal({ open, onClose, title, children, width = 520, footer }) {
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose?.(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div className="exos-modal-root" onClick={onClose}>
      <div className="exos-modal" style={{ maxWidth: width }} onClick={e => e.stopPropagation()}>
        <div className="exos-modal-head">
          <div className="exos-modal-title">{title}</div>
          <button className="exos-icon-btn" onClick={onClose}><Icons.Close size={14} /></button>
        </div>
        <div className="exos-modal-body">{children}</div>
        {footer && <div className="exos-modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

// ---------- Empty state
function EmptyState({ title, sub, action, icon }) {
  return (
    <div className="exos-empty">
      {icon && <div className="exos-empty-icon">{icon}</div>}
      <div className="exos-empty-title">{title}</div>
      {sub && <div className="exos-empty-sub">{sub}</div>}
      {action && <div style={{ marginTop: 16 }}>{action}</div>}
    </div>
  );
}

// ---------- Allocation bar
function AllocationBar({ rows }) {
  return (
    <div className="exos-alloc">
      {rows.map((r, i) => (
        <div key={r.id} className="exos-alloc-seg" title={`${r.meta.symbol} · ${r.alloc.toFixed(1)}%`}
          style={{ width: `${r.alloc}%`, background: r.meta.color }} />
      ))}
    </div>
  );
}

// ---------- Slider
function Slider({ value, onChange, min = 0, max = 100, step = 1, format }) {
  return (
    <div className="exos-slider-wrap">
      <input type="range" className="exos-slider" min={min} max={max} step={step}
        value={value} onChange={e => onChange?.(parseFloat(e.target.value))} />
      <div className="exos-slider-val mono">{format ? format(value) : value}</div>
    </div>
  );
}

// ---------- Tab nav
function Tabs({ tabs, value, onChange }) {
  return (
    <div className="exos-tabs">
      {tabs.map(t => (
        <button key={t.id} className={`exos-tab ${value === t.id ? "active" : ""}`} onClick={() => onChange(t.id)}>
          {t.label}
        </button>
      ))}
    </div>
  );
}

// ---------- Toggle
function Toggle({ checked, onChange, label }) {
  return (
    <label className="exos-toggle">
      <span className="exos-toggle-track" data-on={checked}>
        <span className="exos-toggle-thumb" />
      </span>
      <input type="checkbox" checked={checked} onChange={e => onChange?.(e.target.checked)} style={{ display: "none" }} />
      {label && <span>{label}</span>}
    </label>
  );
}

// ---------- Donut chart (simple SVG)
function Donut({ data, size = 140, thickness = 16 }) {
  const total = data.reduce((a, d) => a + d.value, 0) || 1;
  const r = size / 2 - thickness / 2;
  const cx = size / 2, cy = size / 2;
  let acc = 0;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
      <circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--line)" strokeWidth={thickness} opacity={0.5} />
      {data.map((d, i) => {
        const frac = d.value / total;
        const start = acc;
        acc += frac;
        const C = 2 * Math.PI * r;
        const dash = frac * C;
        const offset = -start * C;
        return (
          <circle key={i} cx={cx} cy={cy} r={r} fill="none"
            stroke={d.color} strokeWidth={thickness}
            strokeDasharray={`${dash} ${C - dash}`}
            strokeDashoffset={offset}
            transform={`rotate(-90 ${cx} ${cy})`}
            strokeLinecap="butt"
          />
        );
      })}
    </svg>
  );
}

// ---------- Sparkline
function Sparkline({ values, width = 80, height = 24, color = "var(--text-mute)", positive }) {
  if (!values?.length) return null;
  const min = Math.min(...values), max = Math.max(...values);
  const range = max - min || 1;
  const step = width / (values.length - 1 || 1);
  const points = values.map((v, i) => `${i * step},${height - ((v - min) / range) * height}`).join(" ");
  const stroke = positive == null ? color : positive ? "var(--gain)" : "var(--loss)";
  return (
    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ overflow: "visible" }}>
      <polyline points={points} fill="none" stroke={stroke} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  );
}

// ---------- Global CSS that complements the design tokens
const STYLES = `
.exos-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius-lg); position: relative; }
.exos-card.p { padding: calc(18px * var(--density)); }
.exos-card.p-sm { padding: calc(12px * var(--density)); }
.exos-card.p-none { padding: 0; }
.exos-card.hover { transition: background .15s, border-color .15s, transform .15s; cursor: pointer; }
.exos-card.hover:hover { background: var(--bg-elev-2); border-color: var(--line-strong); }

.exos-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-mute); font-weight: 500; }
.exos-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--accent); font-weight: 500; margin-bottom: 8px; }
.exos-sub { font-size: 13px; color: var(--text-mute); }

.exos-stat { display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
.exos-stat-head { display: flex; justify-content: space-between; align-items: center; }
.exos-stat-icon { color: var(--text-mute); }
.exos-stat-value { font-size: 26px; font-weight: 500; letter-spacing: -0.01em; line-height: 1; }
.exos-stat-sub { font-size: 12px; color: var(--text-mute); }
.exos-stat-accent { position: absolute; top: 0; left: 0; right: 0; height: 2px; opacity: 0.8; }

.exos-section-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin-bottom: 18px; }
.exos-h2 { margin: 0; font-size: 22px; font-weight: 500; letter-spacing: -0.015em; }
.exos-section-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.exos-btn {
  display: inline-flex; align-items: center; gap: 6px;
  border-radius: 8px; cursor: pointer; transition: background .15s, border-color .15s, color .15s, transform .1s;
  border: 1px solid var(--line); background: var(--bg-elev); color: var(--text);
  font-weight: 500; user-select: none; white-space: nowrap;
}
.exos-btn.sm { padding: 5px 9px; font-size: 12px; }
.exos-btn.md { padding: 7px 12px; font-size: 13px; }
.exos-btn.lg { padding: 10px 16px; font-size: 14px; }
.exos-btn:hover { background: var(--bg-hover); border-color: var(--line-strong); }
.exos-btn:active { transform: translateY(0.5px); }
.exos-btn.primary { background: var(--accent); border-color: var(--accent); color: white; }
:root[data-theme="light"] .exos-btn.primary { color: white; }
.exos-btn.primary:hover { filter: brightness(1.06); }
.exos-btn.ghost { background: transparent; border-color: transparent; color: var(--text-mute); }
.exos-btn.ghost:hover { color: var(--text); background: var(--bg-hover); }
.exos-btn.danger { color: var(--loss); }
.exos-btn.danger:hover { background: var(--loss-soft); border-color: var(--loss); }
.exos-btn-icon { display: inline-flex; }
.exos-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.exos-icon-btn {
  width: 28px; height: 28px; display: inline-grid; place-items: center; cursor: pointer;
  background: transparent; border: 1px solid transparent; border-radius: 6px; color: var(--text-mute);
}
.exos-icon-btn:hover { background: var(--bg-hover); color: var(--text); }

.exos-pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; font-size: 11px; font-weight: 500;
  border-radius: 999px; border: 1px solid var(--line);
  color: var(--text-mute); background: var(--bg-elev);
  letter-spacing: 0.01em;
}
.exos-pill-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.exos-pill.gain { color: var(--gain); background: var(--gain-soft); border-color: transparent; }
.exos-pill.loss { color: var(--loss); background: var(--loss-soft); border-color: transparent; }
.exos-pill.warn { color: var(--warn); background: var(--warn-soft); border-color: transparent; }
.exos-pill.accent { color: var(--accent); background: var(--accent-soft); border-color: transparent; }

.exos-coin-icon {
  border-radius: 50%; display: grid; place-items: center;
  color: white; font-weight: 600; font-family: "Geist", sans-serif;
  letter-spacing: -0.02em; flex-shrink: 0;
  box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.12);
}
.exos-coin-icon-img {
  border-radius: 50%; flex-shrink: 0;
  background: var(--bg-elev-2);
  object-fit: contain;
}
:root[data-theme="light"] .exos-coin-icon-img { background: white; }

.exos-input {
  display: flex; align-items: center; gap: 6px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  padding: 0 10px; height: 38px;
  transition: border-color .15s, box-shadow .15s;
}
.exos-input:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.exos-input input { width: 100%; background: transparent; border: 0; outline: 0; color: var(--text); font: inherit; font-size: 14px; }
.exos-input input::placeholder { color: var(--text-dim); }
@media (max-width: 880px) {
  .exos-input { height: 44px; }
  .exos-input input { font-size: 16px; } /* prevent iOS zoom */
}
.exos-input-aff { color: var(--text-mute); font-size: 12px; flex-shrink: 0; }
.exos-input-aff.right { margin-left: auto; }

.exos-modal-root {
  position: fixed; inset: 0; background: oklch(0 0 0 / 0.55);
  display: grid; place-items: center; z-index: 100; padding: 20px;
  animation: fadein .15s ease;
  backdrop-filter: blur(4px);
}
@media (max-width: 540px) {
  .exos-modal-root { padding: 12px; align-items: flex-end; }
  .exos-modal {
    max-height: 88vh; overflow-y: auto;
    margin-bottom: env(safe-area-inset-bottom);
    -webkit-overflow-scrolling: touch;
  }
  .exos-modal-body { padding: 14px; }
  .exos-modal-foot { padding: 10px 14px; }
}
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
.exos-modal {
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: var(--radius-lg); width: 100%;
  box-shadow: var(--shadow-lg); overflow: hidden;
  animation: rise .2s cubic-bezier(.16,1,.3,1);
}
@keyframes rise { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
.exos-modal-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--line); }
.exos-modal-title { font-weight: 500; font-size: 14px; }
.exos-modal-body { padding: 18px; }
.exos-modal-foot { padding: 12px 18px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; gap: 8px; background: var(--bg-elev-2); }

.exos-empty { padding: 40px 24px; text-align: center; color: var(--text-mute); }
.exos-empty-icon { display: inline-flex; padding: 12px; border-radius: 14px; background: var(--bg-elev-2); margin-bottom: 14px; color: var(--text-mute); }
.exos-empty-title { color: var(--text); font-size: 16px; font-weight: 500; margin-bottom: 4px; }
.exos-empty-sub { font-size: 13px; max-width: 400px; margin: 0 auto; }

.exos-alloc { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--line); }
.exos-alloc-seg { transition: width .4s ease; }
.exos-alloc-seg + .exos-alloc-seg { box-shadow: -1px 0 0 var(--bg); }

.exos-slider-wrap { display: flex; align-items: center; gap: 12px; }
.exos-slider { flex: 1; -webkit-appearance: none; appearance: none; height: 4px; background: var(--line); border-radius: 4px; outline: none; }
.exos-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: var(--accent); border-radius: 50%; cursor: pointer; transition: transform .12s; }
.exos-slider::-webkit-slider-thumb:hover { transform: scale(1.1); }
.exos-slider::-moz-range-thumb { width: 16px; height: 16px; background: var(--accent); border-radius: 50%; border: none; cursor: pointer; }
.exos-slider-val { font-size: 12px; color: var(--text-mute); min-width: 40px; text-align: right; }

.exos-tabs { display: inline-flex; gap: 2px; background: var(--bg-elev-2); padding: 3px; border-radius: 8px; border: 1px solid var(--line); }
.exos-tab { padding: 5px 11px; font-size: 12px; font-weight: 500; color: var(--text-mute); border: 0; background: transparent; border-radius: 6px; cursor: pointer; transition: background .15s, color .15s; }
.exos-tab:hover { color: var(--text); }
.exos-tab.active { background: var(--bg); color: var(--text); box-shadow: var(--shadow-sm); }

.exos-toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; font-size: 13px; color: var(--text); }
.exos-toggle-track { width: 32px; height: 18px; border-radius: 12px; background: var(--line); position: relative; transition: background .2s; }
.exos-toggle-track[data-on="true"] { background: var(--accent); }
.exos-toggle-thumb { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: white; transition: transform .2s; }
.exos-toggle-track[data-on="true"] .exos-toggle-thumb { transform: translateX(14px); }

/* tables */
.exos-table { width: 100%; border-collapse: collapse; }
.exos-table th, .exos-table td { padding: 11px 14px; text-align: left; font-size: 13px; }
.exos-table th { color: var(--text-mute); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; border-bottom: 1px solid var(--line); }
.exos-table tbody tr { border-bottom: 1px solid var(--line); transition: background .12s; }
.exos-table tbody tr:hover { background: var(--bg-elev-2); }
.exos-table tbody tr:last-child { border-bottom: 0; }
.exos-table .right { text-align: right; }
.exos-table .mono { font-variant-numeric: tabular-nums; }

/* layout helpers */
.exos-grid { display: grid; gap: 14px; }
.exos-grid > * { min-width: 0; }
.exos-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.exos-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.exos-grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1100px) { .exos-grid.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 980px) { .exos-grid.cols-2 { grid-template-columns: 1fr; } }
@media (max-width: 760px) { .exos-grid.cols-3 { grid-template-columns: 1fr; } }

.exos-row { display: flex; gap: 12px; align-items: center; }
.exos-row.wrap { flex-wrap: wrap; }
.exos-col { display: flex; flex-direction: column; gap: 12px; }
.exos-spacer { flex: 1; }

/* page padding */
.exos-page { padding: calc(28px * var(--density)) calc(32px * var(--density)); max-width: 1400px; margin: 0 auto; }
@media (max-width: 760px) {
  .exos-page {
    padding: 20px 16px;
    padding-bottom: max(40px, env(safe-area-inset-bottom));
  }
}

.exos-h1 { margin: 0; font-size: 28px; font-weight: 500; letter-spacing: -0.02em; }
.exos-page-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 14px; margin-bottom: 24px; flex-wrap: wrap; }
.exos-page-head-sub { color: var(--text-mute); font-size: 14px; margin-top: 4px; max-width: 560px; }
`;

(function injectStyles(){
  if (document.getElementById("exos-styles")) return;
  const tag = document.createElement("style");
  tag.id = "exos-styles";
  tag.textContent = STYLES;
  document.head.appendChild(tag);
})();

Object.assign(window, {
  Card, StatCard, Button, Pill, SectionHeader, Ticker,
  CoinIcon, TextInput, NumInput, Modal, EmptyState,
  AllocationBar, Slider, Tabs, Toggle, Donut, Sparkline,
});
