<?php
$pageTitle = 'Ravencoin Testnet Assets';
$apiBase = '/api/v1';
$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$initialAsset = trim((string)($_GET['asset'] ?? $_GET['q'] ?? ''));
$initialAddress = trim((string)($_GET['address'] ?? ''));
if (preg_match('#^/assets/(.+)$#', $requestPath, $match)) {
    $initialAsset = rawurldecode($match[1]);
}
if (preg_match('#^/address/([^/]+)/assets/?$#', $requestPath, $match)) {
    $initialAddress = rawurldecode($match[1]);
    $initialAsset = '';
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></title>
<meta name="description" content="Interactive Ravencoin testnet asset explorer with asset search, balances, transactions, ownership and IPFS metadata.">
<meta name="theme-color" content="#06080d">
<style>
:root{--bg:#06080d;--panel:#0d121b;--panel2:#111827;--line:rgba(148,163,184,.2);--ink:#edf2f7;--muted:#9aa6b7;--blue:#38bdf8;--green:#22c55e;--amber:#f59e0b;--red:#ef4444}
*{box-sizing:border-box}html,body{max-width:100%;overflow-x:hidden}body{margin:0;background:linear-gradient(180deg,#06080d,#0a0f18 58%,#070a10);color:var(--ink);font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif}button,input,select{font:inherit}.shell{width:min(1440px,calc(100% - 28px));margin:0 auto;padding:14px 0 24px}.topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.brand{display:flex;align-items:center;gap:12px;color:var(--ink);text-decoration:none}.brand img{width:156px;max-width:46vw;height:auto;display:block}.brand-title{display:flex;flex-direction:column;gap:2px}.brand-title strong{font-size:18px}.brand-title span{font-size:12px;color:var(--muted)}.nav{display:flex;gap:8px;flex-wrap:wrap}.btn{min-height:42px;padding:0 14px;border:1px solid rgba(148,163,184,.28);border-radius:8px;background:linear-gradient(180deg,#2563eb,#0891b2);color:white;text-decoration:none;font-weight:800;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:7px}.btn.ghost{background:rgba(255,255,255,.035);color:#cbd5e1}.btn:disabled{opacity:.45;cursor:not-allowed}.btn:hover:not(:disabled){border-color:rgba(56,189,248,.72)}.card{min-width:0;background:linear-gradient(180deg,rgba(17,24,39,.94),rgba(10,15,23,.82));border:1px solid var(--line);border-radius:8px;box-shadow:0 14px 34px rgba(0,0,0,.32),0 1px 0 rgba(255,255,255,.04) inset}.hero{padding:16px;margin-bottom:12px}.eyebrow{display:flex;align-items:center;gap:8px;color:#dbe4ef;font-size:13px;margin-bottom:10px}.dot{width:8px;height:8px;border-radius:99px;background:var(--green);box-shadow:0 0 0 4px rgba(34,197,94,.13)}h1{margin:0;font-size:clamp(30px,5vw,54px);line-height:1.02;letter-spacing:0}.lead{margin:10px 0 0;color:var(--muted);max-width:760px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:12px 0}.metric{min-height:76px;padding:11px;border:1px solid var(--line);border-radius:8px;background:linear-gradient(180deg,rgba(17,24,39,.9),rgba(9,13,20,.72))}.metric div:first-child{font-size:12px;color:var(--muted);margin-bottom:5px}.metric .v{font-size:19px;font-weight:800;overflow-wrap:anywhere}.layout{display:grid;grid-template-columns:minmax(320px,.9fr) minmax(0,1.55fr);gap:12px}.panel{padding:12px}.head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;flex-wrap:wrap}.head h2,.head h3{margin:0;font-size:15px}.badge{border:1px solid rgba(148,163,184,.25);background:rgba(255,255,255,.035);border-radius:999px;color:#dbe4ef;font-size:12px;padding:5px 9px;white-space:nowrap}.badge.live{border-color:rgba(34,197,94,.35);color:#bbf7d0}.badge.warn{border-color:rgba(245,158,11,.4);color:#fde68a}.tools{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;margin-bottom:10px}.tools input{min-height:42px;border-radius:8px;border:1px solid rgba(148,163,184,.3);background:rgba(5,8,13,.86);color:var(--ink);padding:0 12px;outline:none}.tools input:focus{border-color:rgba(56,189,248,.85);box-shadow:0 0 0 3px rgba(56,189,248,.12)}.typebar{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:10px}.typebar button{min-height:32px;border-radius:999px;border:1px solid rgba(148,163,184,.22);background:rgba(255,255,255,.035);color:#cbd5e1;padding:0 10px;cursor:pointer;font-size:12px;font-weight:800}.typebar button.active{border-color:rgba(56,189,248,.72);background:rgba(56,189,248,.14);color:#e0f2fe}.asset-list{display:grid;gap:7px;max-height:720px;overflow:auto}.asset-row{border:1px solid rgba(148,163,184,.16);border-radius:8px;background:rgba(255,255,255,.025);padding:9px;cursor:pointer;color:var(--ink);text-align:left}.asset-row:hover,.asset-row.active{border-color:rgba(56,189,248,.62);background:rgba(56,189,248,.08)}.symbol{font-weight:900;overflow-wrap:anywhere}.muted{color:var(--muted)}.small{font-size:12px}.meta{display:flex;gap:8px;flex-wrap:wrap;color:var(--muted);font-size:12px;margin-top:4px}.pill{border:1px solid rgba(56,189,248,.32);background:rgba(56,189,248,.08);color:#bae6fd;border-radius:999px;padding:2px 7px;font-size:11px;font-weight:800;text-transform:uppercase}.pill.green{border-color:rgba(34,197,94,.35);background:rgba(34,197,94,.08);color:#bbf7d0}.pill.amber{border-color:rgba(245,158,11,.35);background:rgba(245,158,11,.08);color:#fde68a}.detail{display:grid;gap:10px}.box{border:1px solid rgba(148,163,184,.16);border-radius:8px;background:rgba(255,255,255,.025);padding:12px;overflow:hidden}.kv{display:grid;grid-template-columns:138px minmax(0,1fr);gap:8px;border-bottom:1px solid rgba(148,163,184,.12);padding:6px 0;font-size:14px}.kv:last-child{border-bottom:0}.kv span:first-child{color:var(--muted)}.kv strong{min-width:0;overflow-wrap:anywhere}.columns{display:grid;grid-template-columns:1fr 1fr;gap:10px}.event,.holder,.related-row{border:1px solid rgba(148,163,184,.15);border-radius:8px;background:rgba(255,255,255,.022);padding:8px;min-width:0}.event+.event,.holder+.holder,.related-row+.related-row{margin-top:7px}.holder-main{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}.inline{color:#9bdcff;text-decoration:none}.inline:hover{text-decoration:underline}.empty{border:1px dashed rgba(148,163,184,.28);border-radius:8px;color:var(--muted);padding:16px;text-align:center}.pager{display:flex;gap:8px;justify-content:flex-end;margin-top:10px;flex-wrap:wrap}.mini-actions{display:flex;gap:8px;flex-wrap:wrap}.ipfs-preview{margin-top:10px;border:1px solid rgba(148,163,184,.14);border-radius:8px;background:rgba(0,0,0,.16);padding:10px}.ipfs-preview img{display:block;width:100%;max-height:240px;object-fit:contain;border-radius:6px;background:#05070b}.footer{margin-top:12px;text-align:center;color:var(--muted);font-size:12px;display:flex;gap:8px;justify-content:center;flex-wrap:wrap}@media(max-width:1080px){.layout,.columns{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.shell{width:calc(100% - 18px)}.nav{width:100%}.nav .btn{flex:1}.tools,.metrics{grid-template-columns:1fr}.kv{grid-template-columns:1fr;gap:4px}.asset-list{max-height:none}.mini-actions .btn{width:100%}}
 .focus-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:12px}.focus-tile{border:1px solid rgba(148,163,184,.16);border-radius:8px;background:rgba(255,255,255,.026);padding:10px}.focus-tile span{display:block;color:var(--muted);font-size:12px;margin-bottom:4px}.focus-tile strong{font-size:16px;overflow-wrap:anywhere}.section-title{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}.chart{min-height:132px}.bars{display:flex;align-items:flex-end;gap:7px;height:94px;padding:8px 4px;border:1px solid rgba(148,163,184,.12);border-radius:8px;background:rgba(0,0,0,.14)}.bar{flex:1;min-width:8px;border-radius:5px 5px 2px 2px;background:linear-gradient(180deg,#38bdf8,#2563eb);box-shadow:0 0 18px rgba(56,189,248,.18)}.bar.alt{background:linear-gradient(180deg,#22c55e,#0f766e)}.chart-labels{display:flex;justify-content:space-between;gap:6px;color:var(--muted);font-size:11px;margin-top:7px}.tree{display:grid;gap:8px}.tree-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.tree-node{border:1px solid rgba(56,189,248,.26);background:rgba(56,189,248,.07);border-radius:8px;padding:8px 10px;color:#dff6ff;font-weight:800;overflow-wrap:anywhere}.tree-branch{height:1px;min-width:26px;background:rgba(148,163,184,.35)}.portfolio-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.portfolio-row{border:1px solid rgba(148,163,184,.15);border-radius:8px;background:rgba(255,255,255,.024);padding:10px}.media-grid{display:grid;grid-template-columns:minmax(190px,.46fr) minmax(0,1fr);gap:10px;align-items:start}.media-frame{border:1px solid rgba(148,163,184,.16);border-radius:8px;background:rgba(0,0,0,.18);padding:8px;min-height:110px;display:flex;align-items:center;justify-content:center;color:var(--muted);overflow:hidden}.media-frame img,.media-frame video{max-width:100%;max-height:260px;border-radius:6px}.json-view{white-space:pre-wrap;max-height:220px;overflow:auto;font-size:12px;color:#cbd5e1;background:rgba(0,0,0,.18);border:1px solid rgba(148,163,184,.12);border-radius:8px;padding:10px}.mode-note{margin-top:8px;color:var(--muted);font-size:12px}.address-short{overflow-wrap:anywhere}.infinite-sentinel{min-height:42px;border:1px dashed rgba(148,163,184,.24);border-radius:8px;color:var(--muted);display:flex;align-items:center;justify-content:center;font-size:12px;margin-top:8px;background:rgba(255,255,255,.018)}.asset-list .infinite-sentinel{margin-top:0}@media(max-width:1080px){.portfolio-grid,.media-grid,.focus-strip{grid-template-columns:1fr}}
</style>
</head>
<body>
<main class="shell">
  <nav class="topbar">
    <a class="brand" href="/">
      <img src="/ravencoin-wordmark.png?v=20260521-white2" alt="Ravencoin">
      <span class="brand-title"><strong>Assets</strong><span>Testnet asset explorer</span></span>
    </a>
    <div class="nav">
      <a class="btn ghost" href="/">Explorer</a>
      <a class="btn ghost" href="/richlist.html">Rich List</a>
    </div>
  </nav>

  <section class="card hero">
    <div class="eyebrow"><span class="dot"></span><span id="statusText">Connecting to Ravencoin testnet</span></div>
    <h1>Ravencoin testnet assets</h1>
    <p class="lead">Search assets, inspect balances, follow issue/transfer events and open IPFS metadata without touching the mainnet explorer.</p>
    <div class="focus-strip">
      <div class="focus-tile"><span>Direct asset URLs</span><strong>/assets/ASSET</strong></div>
      <div class="focus-tile"><span>Address portfolios</span><strong>/address/.../assets</strong></div>
      <div class="focus-tile"><span>Metadata</span><strong>IPFS / JSON / media</strong></div>
    </div>
  </section>

  <section class="metrics">
    <div class="metric"><div>Assets</div><div id="metricAssets" class="v">-</div></div>
    <div class="metric"><div>Events</div><div id="metricEvents" class="v">-</div></div>
    <div class="metric"><div>Balances</div><div id="metricBalances" class="v">-</div></div>
    <div class="metric"><div>Indexed Height</div><div id="metricHeight" class="v">-</div></div>
  </section>

  <section class="layout">
    <div class="card panel">
      <div class="head"><h2>Asset Index</h2><span class="badge live" id="assetBadge">loading</span></div>
      <form class="tools" id="assetSearchForm">
        <input id="assetSearchInput" autocomplete="off" placeholder="Search asset name">
        <button class="btn ghost" type="submit">Search</button>
      </form>
      <form class="tools" id="addressSearchForm">
        <input id="addressSearchInput" autocomplete="off" placeholder="Open address asset portfolio">
        <button class="btn ghost" type="submit">Portfolio</button>
      </form>
      <div class="typebar" id="assetTypeBar" aria-label="Asset type filters">
        <button type="button" class="active" data-type="">All</button>
        <button type="button" data-type="root">Root</button>
        <button type="button" data-type="owner">Owner</button>
        <button type="button" data-type="sub">Sub</button>
        <button type="button" data-type="unique">Unique</button>
        <button type="button" data-type="restricted">Restricted</button>
        <button type="button" data-type="qualifier">Qualifier</button>
      </div>
      <div id="assetList" class="asset-list"><div class="empty">Loading assets</div></div>
      <div class="pager" id="assetPager"></div>
    </div>
    <div id="assetDetail" class="detail">
      <div class="card panel"><div class="empty">Select an asset to inspect holders, events and metadata.</div></div>
    </div>
  </section>

  <p class="footer"><span>RavencoinExplorer.com</span><span>·</span><span>Testnet assets</span></p>
</main>

<script>
(() => {
  const API_BASE = <?= json_encode($apiBase) ?>;
  const INITIAL_ASSET = <?= json_encode($initialAsset) ?>;
  const INITIAL_ADDRESS = <?= json_encode($initialAddress) ?>;
  const fmt = new Intl.NumberFormat(undefined);
  const $ = id => document.getElementById(id);
  const api = path => fetch(`${API_BASE}${path}`, {cache:"no-store"}).then(async r => {
    const j = await r.json().catch(() => ({}));
    if (!r.ok || j.ok === false) throw Object.assign(new Error(j.error?.message || `HTTP ${r.status}`), {payload:j, status:r.status});
    return j;
  });
  const safe = value => String(value ?? "").replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
  const time = (ts, iso) => iso || (ts ? new Date(ts * 1000).toISOString() : "-");
  let currentAsset = null;
  let currentType = "";
  let assetOffset = 0;
  let holderOffset = 0;
  let eventOffset = 0;
  let addressOffset = 0;
  let assetQuery = "";
  let currentAddress = "";
  let assetItems = [];
  let assetHasMore = false;
  let assetLoading = false;
  let currentHolderLimit = 20;
  let currentEventLimit = 30;
  let currentAddressLimit = 50;
  let holderHasMore = false;
  let eventHasMore = false;
  let addressHasMore = false;
  const observers = new Map();
  const assetLimit = 24;
  const holderLimit = 20;
  const eventLimit = 30;
  const addressAssetLimit = 50;

  function pathEncode(value){ return encodeURIComponent(value || ""); }
  function qty(value){
    if (value == null || value === "") return "-";
    const text = String(value);
    const parts = text.split(".");
    const whole = fmt.format(Number(parts[0] || 0));
    if (!parts[1]) return whole;
    const frac = parts[1].replace(/0+$/, "");
    return frac ? `${whole}.${frac}` : whole;
  }
  function numberValue(value){
    const n = Number(String(value ?? "0").replace(/,/g, ""));
    return Number.isFinite(n) ? n : 0;
  }
  function kv(label, value){ return `<div class="kv"><span>${label}</span><strong>${value ?? "-"}</strong></div>`; }
  function typeLabel(asset){ return asset?.symbol?.endsWith("!") ? "owner" : (asset?.asset_type || "asset"); }
  function assetUrl(symbol){ return `/assets/${pathEncode(symbol)}`; }
  function addressAssetsUrl(address){ return `/address/${pathEncode(address)}/assets`; }
  function searchUrl(){
    const params = new URLSearchParams();
    if (assetQuery) params.set("q", assetQuery);
    if (currentType) params.set("type", currentType);
    return `/assets${params.toString() ? `?${params}` : ""}`;
  }
  function explorerLink(value){ return `/?q=${encodeURIComponent(value || "")}`; }
  function assetRoot(symbol){
    let s = String(symbol || "").replace(/!$/, "");
    if (s.startsWith("$") || s.startsWith("#")) s = s.slice(1);
    return s.split(/[\/#]/)[0];
  }
  function ipfsPath(value){ return String(value || "").replace(/^ipfs:\/\//i, "").replace(/^\/ipfs\//i, ""); }
  function ipfsGateway(value){
    const path = ipfsPath(value).split("/").filter(Boolean).map(encodeURIComponent).join("/");
    return `https://ipfs.io/ipfs/${path}`;
  }
  function sameAsset(a, b){ return String(a || "").toUpperCase() === String(b || "").toUpperCase(); }
  function sentinelText(loading, hasMore){
    if (loading) return "Loading more";
    return hasMore ? "Scroll for more" : "End of results";
  }
  function observeSentinel(key, id, callback, root=null){
    if (observers.has(key)) observers.get(key).disconnect();
    const node = $(id);
    if (!node) return;
    const observer = new IntersectionObserver(entries => {
      if (entries.some(entry => entry.isIntersecting)) callback();
    }, {root, rootMargin:"160px 0px", threshold:0.01});
    observer.observe(node);
    observers.set(key, observer);
  }

  async function loadStatus(){
    try {
      const [status, backfill] = await Promise.all([api("/status"), api("/assets/backfill-status")]);
      $("statusText").textContent = status.data?.chain === "test" ? "Live on Ravencoin testnet" : "Connected";
      $("metricAssets").textContent = fmt.format(backfill.data?.asset_count || 0);
      $("metricEvents").textContent = fmt.format(backfill.data?.event_count || 0);
      $("metricBalances").textContent = fmt.format(backfill.data?.balance_count || 0);
      $("metricHeight").textContent = fmt.format(backfill.data?.current_height || status.data?.db_height || 0);
    } catch(e) {
      $("statusText").textContent = "Asset API temporarily unavailable";
    }
  }

  function renderList(items){
    const rows = items.length ? items.map(asset => `
      <button class="asset-row${asset.symbol === currentAsset ? " active" : ""}" data-asset="${safe(asset.symbol)}">
        <div class="symbol">${safe(asset.symbol)}</div>
        <div class="meta">
          <span class="pill">${safe(typeLabel(asset))}</span>
          <span>${qty(asset.total_issued)} issued</span>
          <span>${fmt.format(asset.holder_count || 0)} holders</span>
          <span>block ${asset.created_height ? fmt.format(asset.created_height) : "-"}</span>
        </div>
      </button>`).join("") : `<div class="empty">No assets found.</div>`;
    $("assetList").innerHTML = `${rows}<div id="assetListSentinel" class="infinite-sentinel">${sentinelText(assetLoading, assetHasMore)}</div>`;
    $("assetList").querySelectorAll("[data-asset]").forEach(btn => btn.addEventListener("click", () => openAsset(btn.dataset.asset)));
    observeSentinel("assets", "assetListSentinel", loadMoreAssets, $("assetList"));
  }

  async function loadAssets(reset=false){
    if (assetLoading) return;
    if (reset) {
      assetOffset = 0;
      assetItems = [];
      assetHasMore = false;
    }
    assetLoading = true;
    $("assetBadge").textContent = assetQuery ? "search" : "latest";
    if (reset) $("assetList").innerHTML = `<div class="empty">Loading assets.</div>`;
    else renderList(assetItems);
    try {
      const qs = new URLSearchParams({limit:String(assetLimit), offset:String(assetOffset)});
      if (assetQuery) qs.set("q", assetQuery);
      if (currentType) qs.set("asset_type", currentType);
      const j = await api(`/assets?${qs.toString()}`);
      const items = j.data?.items || [];
      assetItems = reset ? items : assetItems.concat(items);
      assetHasMore = !!j.meta?.has_more;
      assetLoading = false;
      renderList(assetItems);
      $("assetPager").innerHTML = "";
      if (reset && !currentAsset && assetItems[0]) {
        const exact = assetQuery ? assetItems.find(asset => asset.symbol.toUpperCase() === assetQuery.toUpperCase()) : null;
        openAsset((exact || items[0]).symbol);
      }
    } catch(err) {
      assetLoading = false;
      $("assetBadge").textContent = "error";
      $("assetList").innerHTML = `<div class="empty">${safe(err.message)}</div>`;
      $("assetPager").innerHTML = "";
    }
  }

  function loadMoreAssets(){
    if (!assetHasMore || assetLoading) return;
    assetOffset += assetLimit;
    loadAssets(false);
  }

  function overview(asset){
    const direct = `${location.origin}${assetUrl(asset.symbol || "")}`;
    const ipfs = asset.ipfs_hash ? `<a class="inline mono" href="${ipfsGateway(asset.ipfs_hash)}" target="_blank" rel="noopener">${safe(asset.ipfs_hash)}</a>` : "-";
    return `<div class="card panel"><div class="head"><h2>${safe(asset.symbol)}</h2><div class="mini-actions"><span class="badge live">${safe(typeLabel(asset))}</span><button class="btn ghost" type="button" id="copyAssetLink">Copy Link</button></div></div>
      ${kv("Quantity", qty(asset.total_issued))}
      ${kv("Units", asset.units ?? "-")}
      ${kv("Reissuable", asset.reissuable === true ? "yes" : asset.reissuable === false ? "no" : "-")}
      ${kv("Holders", fmt.format(asset.holder_count || 0))}
      ${kv("Events", fmt.format(asset.event_count || 0))}
      ${kv("Direct Link", `<a class="inline mono" href="${safe(direct)}">${safe(direct)}</a>`)}
      ${kv("Created", asset.created_height ? `<a class="inline" href="${explorerLink(asset.created_height)}">Block ${fmt.format(asset.created_height)}</a>` : "-")}
      ${kv("Issue TX", asset.created_txid ? `<a class="inline mono" href="${explorerLink(asset.created_txid)}">${safe(asset.created_txid)}</a>` : "-")}
      ${kv("Owner", asset.owner_address ? `<a class="inline mono" href="${addressAssetsUrl(asset.owner_address)}">${safe(asset.owner_address)}</a>` : "-")}
      ${kv("IPFS", ipfs)}
    </div>`;
  }

  function holders(items, meta){
    return `<div class="box"><div class="head"><h3>Asset Richlist</h3><span class="badge">${fmt.format(meta?.offset || 0)}-${fmt.format((meta?.offset || 0) + items.length)}</span></div>${items.length ? items.map(row => `
      <div class="holder"><div class="holder-main"><strong>${qty(row.qty)}</strong><span class="pill">balance</span></div><a class="inline mono" href="${addressAssetsUrl(row.address)}">${safe(row.address)}</a></div>`).join("") : `<div class="empty">No holder balances returned.</div>`}
      <div id="holderSentinel" class="infinite-sentinel">${sentinelText(false, !!meta?.has_more)}</div>
    </div>`;
  }

  function events(items, meta){
    return `<div class="box"><div class="head"><h3>Asset Events</h3><span class="badge">${fmt.format(meta?.offset || 0)}-${fmt.format((meta?.offset || 0) + items.length)}</span></div>${items.length ? items.map(event => `
      <div class="event">
        <div class="meta"><span class="pill amber">${safe(event.kind || "event")}</span><span>${qty(event.qty)}</span><span>block ${event.block_height ? fmt.format(event.block_height) : "-"}</span><span>${time(event.time,event.time_iso)}</span></div>
        <a class="inline mono" href="${explorerLink(event.txid)}">${safe(event.txid)}</a>
        ${event.from_address ? `<div class="small muted">from <a class="inline mono" href="${addressAssetsUrl(event.from_address)}">${safe(event.from_address)}</a></div>` : ""}
        ${event.to_address ? `<div class="small muted">to <a class="inline mono" href="${addressAssetsUrl(event.to_address)}">${safe(event.to_address)}</a></div>` : ""}
      </div>`).join("") : `<div class="empty">No asset events returned.</div>`}
      <div id="eventSentinel" class="infinite-sentinel">${sentinelText(false, !!meta?.has_more)}</div>
    </div>`;
  }

  function bars(values, alt=false){
    const max = Math.max(1, ...values.map(v => Number(v) || 0));
    return `<div class="bars">${values.map(v => `<div class="bar${alt ? " alt" : ""}" title="${safe(v)}" style="height:${Math.max(5, Math.round((Number(v || 0) / max) * 100))}%"></div>`).join("")}</div>`;
  }

  function analytics(asset, holdersList, eventsList){
    const holderValues = holdersList.slice(0, 12).map(row => numberValue(row.qty));
    const kindCounts = eventsList.reduce((acc, row) => {
      const key = String(row.kind || "event").toUpperCase();
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {});
    const heights = eventsList.map(row => Number(row.block_height || 0)).filter(Boolean);
    const minH = Math.min(...heights, 0);
    const maxH = Math.max(...heights, 0);
    const buckets = Array.from({length: 10}, () => 0);
    if (heights.length && maxH > minH) {
      heights.forEach(height => buckets[Math.min(9, Math.floor(((height - minH) / (maxH - minH + 1)) * 10))]++);
    } else if (heights.length) {
      buckets[9] = heights.length;
    }
    return `<div class="card panel">
      <div class="head"><h2>Asset Analytics</h2><span class="badge">${fmt.format(eventsList.length)} sampled events</span></div>
      <div class="columns">
        <div class="box chart"><div class="section-title"><h3>Holder Distribution</h3><span class="badge">${fmt.format(holdersList.length)} rows</span></div>${holderValues.length ? bars(holderValues, true) : `<div class="empty">No balance data yet.</div>`}<div class="chart-labels"><span>largest</span><span>top holders</span></div></div>
        <div class="box chart"><div class="section-title"><h3>Event Activity</h3><span class="badge">${fmt.format(asset.event_count || eventsList.length || 0)} total</span></div>${buckets.some(Boolean) ? bars(buckets) : `<div class="empty">No event activity yet.</div>`}<div class="chart-labels"><span>${minH ? fmt.format(minH) : "start"}</span><span>${maxH ? fmt.format(maxH) : "latest"}</span></div></div>
      </div>
      <div class="meta">${Object.keys(kindCounts).length ? Object.entries(kindCounts).map(([key,value]) => `<span class="pill">${safe(key)} ${fmt.format(value)}</span>`).join("") : `<span class="muted small">No event type split available.</span>`}</div>
    </div>`;
  }

  function familyTree(asset, items){
    const root = assetRoot(asset.symbol);
    const relatedItems = items.filter(item => !sameAsset(item.symbol, asset.symbol));
    const owner = relatedItems.find(item => item.symbol === `${root}!`);
    const subs = relatedItems.filter(item => String(item.symbol || "").startsWith(`${root}/`)).slice(0, 8);
    const uniques = relatedItems.filter(item => String(item.symbol || "").startsWith(`${root}#`)).slice(0, 8);
    return `<div class="card panel">
      <div class="head"><h2>Asset Family Tree</h2><span class="badge">${safe(root || asset.symbol)}</span></div>
      <div class="tree">
        <div class="tree-row"><span class="tree-node">${safe(root || asset.symbol)}</span>${owner ? `<span class="tree-branch"></span><button class="tree-node asset-row" type="button" data-related="${safe(owner.symbol)}">${safe(owner.symbol)}</button>` : ""}</div>
        <div class="tree-row"><span class="pill">sub</span>${subs.length ? subs.map(item => `<button class="tree-node asset-row" type="button" data-related="${safe(item.symbol)}">${safe(item.symbol)}</button>`).join("") : `<span class="muted small">No sub-assets in this sample.</span>`}</div>
        <div class="tree-row"><span class="pill">unique</span>${uniques.length ? uniques.map(item => `<button class="tree-node asset-row" type="button" data-related="${safe(item.symbol)}">${safe(item.symbol)}</button>`).join("") : `<span class="muted small">No unique assets in this sample.</span>`}</div>
      </div>
    </div>`;
  }

  function metadataPanel(asset){
    if (!asset.ipfs_hash) {
      return `<div class="card panel"><div class="head"><h2>IPFS Metadata</h2><span class="badge">none</span></div><div class="empty">This asset has no IPFS attachment.</div></div>`;
    }
    return `<div class="card panel"><div class="head"><h2>IPFS Metadata</h2><span class="badge live">attached</span></div>
      <div class="ipfs-preview">
        <div class="meta"><span class="pill green">IPFS</span><a class="inline" href="${ipfsGateway(asset.ipfs_hash)}" target="_blank" rel="noopener">Open gateway</a><a class="inline" href="https://${safe(ipfsPath(asset.ipfs_hash))}.ipfs.dweb.link/" target="_blank" rel="noopener">Open dweb</a></div>
        <div id="ipfsMetadata" class="media-grid"><div class="media-frame">Loading metadata</div><div class="json-view">Fetching ${safe(asset.ipfs_hash)}</div></div>
      </div>
    </div>`;
  }

  async function loadIpfsMetadata(asset){
    const target = $("ipfsMetadata");
    if (!target || !asset.ipfs_hash) return;
    const gateway = ipfsGateway(asset.ipfs_hash);
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 6000);
    try {
      const response = await fetch(gateway, {cache:"force-cache", signal: controller.signal, headers: {Accept: "application/json,text/plain,*/*"}});
      clearTimeout(timeout);
      const contentType = response.headers.get("content-type") || "";
      if (contentType.startsWith("image/")) {
        target.innerHTML = `<div class="media-frame"><img src="${gateway}" alt="${safe(asset.symbol)} IPFS media"></div><div class="json-view">${safe(contentType)}</div>`;
        return;
      }
      if (contentType.startsWith("video/")) {
        target.innerHTML = `<div class="media-frame"><video src="${gateway}" controls></video></div><div class="json-view">${safe(contentType)}</div>`;
        return;
      }
      const text = await response.text();
      let metadata = null;
      try { metadata = JSON.parse(text); } catch(e) {}
      if (!metadata) {
        target.innerHTML = `<div class="media-frame">No structured preview</div><div class="json-view">${safe(text.slice(0, 1200))}</div>`;
        return;
      }
      const media = metadata.image || metadata.image_url || metadata.animation_url || "";
      const mediaUrl = media ? ipfsGateway(media) : "";
      target.innerHTML = `<div class="media-frame">${mediaUrl ? `<img src="${mediaUrl}" alt="${safe(metadata.name || asset.symbol)} media" onerror="this.parentNode.textContent='Media preview unavailable'">` : "No media field"}</div>
        <div><h3>${safe(metadata.name || asset.symbol)}</h3><p class="muted">${safe(metadata.description || "No description field.")}</p><div class="json-view">${safe(JSON.stringify(metadata, null, 2).slice(0, 2500))}</div></div>`;
    } catch(e) {
      clearTimeout(timeout);
      target.innerHTML = `<div class="media-frame">Preview unavailable</div><div class="json-view">${safe(e.name === "AbortError" ? "IPFS gateway timed out." : e.message)}</div>`;
    }
  }

  function related(items, symbol){
    const filtered = items.filter(asset => asset.symbol !== symbol);
    return `<div class="card panel"><div class="head"><h2>Related Assets</h2><span class="badge">${safe(assetRoot(symbol))}</span></div>
      ${filtered.length ? filtered.slice(0, 12).map(asset => `
        <button class="related-row asset-row" type="button" data-related="${safe(asset.symbol)}">
          <div class="symbol">${safe(asset.symbol)}</div>
          <div class="meta"><span class="pill">${safe(typeLabel(asset))}</span><span>${qty(asset.total_issued)} issued</span><span>${fmt.format(asset.holder_count || 0)} holders</span></div>
        </button>`).join("") : `<div class="empty">No related assets found yet.</div>`}
    </div>`;
  }

  function bindDetailControls(asset, holderMeta, eventMeta){
    const copyButton = $("copyAssetLink");
    if (copyButton) copyButton.onclick = async () => {
      const link = `${location.origin}${assetUrl(asset.symbol || "")}`;
      try {
        await navigator.clipboard.writeText(link);
        copyButton.textContent = "Copied";
        setTimeout(() => copyButton.textContent = "Copy Link", 1200);
      } catch(e) {
        location.href = link;
      }
    };
    holderHasMore = !!holderMeta?.has_more;
    eventHasMore = !!eventMeta?.has_more;
    observeSentinel("holders", "holderSentinel", () => {
      if (!holderHasMore || currentHolderLimit >= 100) return;
      currentHolderLimit = Math.min(100, currentHolderLimit + holderLimit);
      loadAssetDetail(asset.symbol);
    });
    observeSentinel("events", "eventSentinel", () => {
      if (!eventHasMore || currentEventLimit >= 100) return;
      currentEventLimit = Math.min(100, currentEventLimit + eventLimit);
      loadAssetDetail(asset.symbol);
    });
    document.querySelectorAll("[data-related]").forEach(btn => btn.addEventListener("click", () => openAsset(btn.dataset.related)));
  }

  async function loadAssetDetail(symbol){
    $("assetDetail").innerHTML = `<div class="card panel"><div class="empty">Loading ${safe(symbol)}.</div></div>`;
    try {
      const root = assetRoot(symbol);
      const [detail, holderRows, eventRows, relatedRows] = await Promise.all([
        api(`/asset?name=${encodeURIComponent(symbol)}`),
        api(`/asset/holders?name=${encodeURIComponent(symbol)}&limit=${currentHolderLimit}&offset=0`),
        api(`/asset/events?name=${encodeURIComponent(symbol)}&limit=${currentEventLimit}&offset=0`),
        api(`/assets?q=${encodeURIComponent(root)}&limit=30`)
      ]);
      const asset = detail.data || {};
      document.querySelectorAll(".asset-row").forEach(row => row.classList.toggle("active", row.dataset.asset === symbol));
      const holderItems = holderRows.data?.items || [];
      const eventItems = eventRows.data?.items || [];
      const relatedItems = relatedRows.data?.items || [];
      $("assetDetail").innerHTML = `${overview(asset)}${analytics(asset, holderItems, eventItems)}<div class="columns">${holders(holderItems, holderRows.meta)}${events(eventItems, eventRows.meta)}</div>${metadataPanel(asset)}${familyTree(asset, relatedItems)}${related(relatedItems, symbol)}`;
      $("assetBadge").textContent = `${fmt.format(asset.holder_count || 0)} holders`;
      bindDetailControls(asset, holderRows.meta, eventRows.meta);
      loadIpfsMetadata(asset);
    } catch(err) {
      $("assetDetail").innerHTML = `<div class="card panel"><div class="empty">${safe(err.message)}</div></div>`;
    }
  }

  async function openAsset(symbol){
    currentAsset = symbol;
    holderOffset = 0;
    eventOffset = 0;
    currentHolderLimit = holderLimit;
    currentEventLimit = eventLimit;
    currentAddress = "";
    history.replaceState(null, "", assetUrl(symbol));
    await loadAssetDetail(symbol);
  }

  function portfolioChart(items){
    const values = items.slice(0, 12).map(item => numberValue(item.qty));
    return `<div class="box chart"><div class="section-title"><h3>Portfolio Distribution</h3><span class="badge">${fmt.format(items.length)} assets</span></div>${values.length ? bars(values, true) : `<div class="empty">No asset balances.</div>`}<div class="chart-labels"><span>largest</span><span>top assets</span></div></div>`;
  }

  async function loadAddressPortfolio(address){
    currentAddress = address;
    currentAsset = null;
    $("addressSearchInput").value = address;
    $("assetDetail").innerHTML = `<div class="card panel"><div class="empty">Loading address asset portfolio.</div></div>`;
    history.replaceState(null, "", addressAssetsUrl(address));
    try {
      const [summary, portfolio] = await Promise.all([
        api(`/address/${encodeURIComponent(address)}?utxo_limit=0`),
        api(`/address/${encodeURIComponent(address)}/assets?limit=${currentAddressLimit}&offset=0`)
      ]);
      const items = portfolio.data?.items || [];
      addressHasMore = !!portfolio.meta?.has_more;
      $("assetBadge").textContent = `${fmt.format(items.length)} portfolio`;
      $("assetDetail").innerHTML = `<div class="card panel">
        <div class="head"><h2>Address Asset Portfolio</h2><span class="badge live">testnet</span></div>
        ${kv("Address", `<span class="mono address-short">${safe(address)}</span>`)}
        ${kv("RVN Balance", qty(summary.data?.balance_rvn || 0))}
        ${kv("Asset Rows", fmt.format(items.length))}
        ${kv("Direct Link", `<a class="inline mono" href="${addressAssetsUrl(address)}">${safe(`${location.origin}${addressAssetsUrl(address)}`)}</a>`)}
      </div>
      <div class="columns">
        ${portfolioChart(items)}
        <div class="box"><div class="head"><h3>Asset Balances</h3><span class="badge">${fmt.format(addressOffset)}-${fmt.format(addressOffset + items.length)}</span></div>
          <div class="portfolio-grid">${items.length ? items.map(item => `<button class="portfolio-row asset-row" type="button" data-related="${safe(item.symbol)}"><div class="holder-main"><strong>${safe(item.symbol)}</strong><span class="pill">${safe(typeLabel(item))}</span></div><div class="meta"><span>${qty(item.qty)}</span><span>${qty(item.total_issued)} issued</span></div></button>`).join("") : `<div class="empty">No asset balances found.</div>`}</div>
          <div id="addressSentinel" class="infinite-sentinel">${sentinelText(false, addressHasMore)}</div>
        </div>
      </div>`;
      document.querySelectorAll("[data-related]").forEach(btn => btn.addEventListener("click", () => openAsset(btn.dataset.related)));
      observeSentinel("address", "addressSentinel", () => {
        if (!addressHasMore || currentAddressLimit >= 100) return;
        currentAddressLimit = Math.min(100, currentAddressLimit + addressAssetLimit);
        loadAddressPortfolio(address);
      });
    } catch(err) {
      $("assetBadge").textContent = "address error";
      $("assetDetail").innerHTML = `<div class="card panel"><div class="empty">${safe(err.message)}</div></div>`;
    }
  }

  $("assetSearchForm").addEventListener("submit", e => {
    e.preventDefault();
    assetQuery = $("assetSearchInput").value.trim();
    currentAsset = null;
    currentAddress = "";
    history.replaceState(null, "", searchUrl());
    loadAssets(true);
  });

  $("addressSearchForm").addEventListener("submit", e => {
    e.preventDefault();
    const address = $("addressSearchInput").value.trim();
    if (!address) return;
    addressOffset = 0;
    currentAddressLimit = addressAssetLimit;
    loadAddressPortfolio(address);
  });

  $("assetTypeBar").querySelectorAll("[data-type]").forEach(btn => btn.addEventListener("click", () => {
    currentType = btn.dataset.type;
    $("assetTypeBar").querySelectorAll("[data-type]").forEach(item => item.classList.toggle("active", item === btn));
    currentAsset = null;
    loadAssets(true);
    history.replaceState(null, "", searchUrl());
  }));

  const params = new URLSearchParams(location.search);
  const q = INITIAL_ASSET || params.get("q");
  const address = INITIAL_ADDRESS || params.get("address");
  const type = params.get("type");
  if (q) {
    $("assetSearchInput").value = q;
    assetQuery = q;
  }
  if (type) {
    currentType = type;
    $("assetTypeBar").querySelectorAll("[data-type]").forEach(item => item.classList.toggle("active", item.dataset.type === currentType));
  }
  loadStatus();
  if (address) {
    currentAddressLimit = addressAssetLimit;
    loadAssets(true);
    loadAddressPortfolio(address);
  } else {
    loadAssets(true);
  }
})();
</script>
</body>
</html>
