← back to AbramsEgo

public/landing.html

532 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AbramsEgo · the dashboard that pays for itself</title>
<meta name="description" content="A self-hostable AI-agent command center with a self-funding P&L — fleet monitoring, canaries, token+energy cost accounting, and a grounded AI chat. A dashboard that pays for itself." />
<style>
  /* — reuse the command-center dark theme + CSS vars — */
  :root{
    --bg:#0a0e14; --panel:#111823; --panel2:#0d141d; --line:#1e2a3a; --ink:#dbe6f3;
    --dim:#7d8fa6; --accent:#4d9fff; --good:#3fd18b; --warn:#ffb454; --bad:#ff5d6c;
    --gold:#ffd166; --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  }
  *{box-sizing:border-box}
  body{margin:0;background:radial-gradient(1200px 600px at 70% -10%,#12203300,#0a0e14),var(--bg);
    color:var(--ink);font:14px/1.55 var(--mono);-webkit-font-smoothing:antialiased}
  a{color:var(--accent);text-decoration:none}
  header{display:flex;align-items:center;gap:14px;padding:12px 20px;border-bottom:1px solid var(--line);
    position:sticky;top:0;background:rgba(10,14,20,.9);backdrop-filter:blur(8px);z-index:5}
  .logo{font-weight:700;letter-spacing:.5px;font-size:16px}
  .logo b{color:var(--accent)}
  .nav{margin-left:auto;display:flex;gap:18px;align-items:center;font-size:12px}
  .nav a{color:var(--dim)}.nav a:hover{color:var(--ink)}
  .btn{display:inline-block;background:var(--accent);color:#04101f;font-weight:700;border:0;
    border-radius:9px;padding:11px 18px;cursor:pointer;font-family:var(--mono);font-size:13px;
    letter-spacing:.3px;transition:transform .08s ease,filter .12s ease}
  .btn:hover{filter:brightness(1.08)}.btn:active{transform:translateY(1px)}
  .btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--line)}
  .btn.gold{background:var(--gold);color:#241a00}
  main{max-width:1120px;margin:0 auto;padding:0 20px 64px}
  section{padding:52px 0;border-bottom:1px solid #ffffff08}
  .kicker{font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:14px}
  h1{font-size:clamp(30px,6vw,52px);line-height:1.08;margin:0 0 18px;letter-spacing:-.5px;font-weight:800}
  h1 .hl{color:var(--gold)}
  .lede{font-size:17px;color:var(--dim);max-width:640px;margin:0 0 26px}
  .hero-cta{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
  .hero-note{font-size:11px;color:var(--dim);margin-top:14px}
  h2{font-size:24px;margin:0 0 8px;letter-spacing:-.3px}
  .sub{color:var(--dim);margin:0 0 28px;font-size:14px}
  /* — self-funding proof strip — */
  .proof{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:34px}
  @media(max-width:760px){.proof{grid-template-columns:repeat(2,1fr)}}
  .stat{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
    border-radius:12px;padding:16px}
  .stat .k{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.6px}
  .stat .v{font-size:26px;font-weight:800;margin-top:6px}
  .stat .n{font-size:11px;color:var(--dim);margin-top:4px}
  /* — features — */
  .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
  @media(max-width:860px){.grid{grid-template-columns:1fr}}
  .card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
    border-radius:12px;padding:18px}
  .card .ico{font-size:22px}
  .card h3{margin:10px 0 6px;font-size:15px}
  .card p{margin:0;color:var(--dim);font-size:13px}
  /* — demo video — */
  .demo{margin-top:26px;text-align:center}
  .demo video{width:100%;max-width:920px;border:1px solid var(--line);border-radius:14px;
    box-shadow:0 20px 60px -30px #000;display:block;margin:0 auto;background:#0c1420}
  .demo .cap{color:var(--dim);font-size:11px;margin-top:10px}
  /* — pricing — */
  .tiers{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px}
  @media(max-width:860px){.tiers{grid-template-columns:1fr}}
  .tier{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
    border-radius:14px;padding:22px;display:flex;flex-direction:column}
  .tier.feat{border-color:#4d9fff66;box-shadow:0 0 0 1px #4d9fff22, 0 10px 40px -20px #4d9fff55}
  .tier .badge{align-self:flex-start;font-size:10px;letter-spacing:1px;text-transform:uppercase;
    color:var(--accent);border:1px solid #4d9fff44;background:#4d9fff0f;border-radius:20px;padding:3px 9px;margin-bottom:12px}
  .tier .name{font-size:16px;font-weight:700}
  .tier .price{font-size:34px;font-weight:800;margin:8px 0 2px}
  .tier .price small{font-size:13px;font-weight:600;color:var(--dim)}
  .tier .for{color:var(--dim);font-size:12px;margin-bottom:16px}
  .tier ul{list-style:none;padding:0;margin:0 0 20px;flex:1}
  .tier li{padding:6px 0;border-bottom:1px dashed #ffffff08;font-size:13px;color:var(--ink)}
  .tier li:before{content:"✓";color:var(--good);margin-right:8px}
  .tier li.no{color:var(--dim)}.tier li.no:before{content:"—";color:var(--dim)}
  .pricing-note{text-align:center;color:var(--dim);font-size:11px;margin-top:16px}
  /* — waitlist — */
  .wl{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
    border-radius:14px;padding:26px;max-width:560px;margin:0 auto;text-align:center}
  .wl form{display:flex;gap:10px;margin-top:16px}
  @media(max-width:520px){.wl form{flex-direction:column}}
  .wl input{flex:1;background:#0c1420;border:1px solid var(--line);color:var(--ink);
    border-radius:9px;padding:12px 14px;font-family:var(--mono);font-size:14px}
  .wl input:focus{outline:none;border-color:var(--accent)}
  .wl .msg{margin-top:12px;font-size:12px;min-height:16px}
  .wl .msg.ok{color:var(--good)}.wl .msg.err{color:var(--bad)}
  /* — licensing grid (engine 5) — sort + density per Steve's grid rules — */
  .lic-controls{display:flex;gap:22px;align-items:center;margin:0 0 18px;font-size:12px;color:var(--dim)}
  .lic-controls label{display:flex;gap:8px;align-items:center}
  .lic-controls select{background:#0c1420;border:1px solid var(--line);color:var(--ink);
    border-radius:8px;padding:7px 10px;font-family:var(--mono);font-size:12px}
  .lic-controls input[type=range]{accent-color:var(--accent);width:130px}
  .lic-grid{display:grid;grid-template-columns:repeat(var(--lic-cols,4),1fr);gap:14px}
  @media(max-width:760px){.lic-grid{grid-template-columns:repeat(2,1fr)}}
  .lic-card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
    border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
  .lic-card img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#0c1420}
  .lic-card .b{padding:12px 14px;display:flex;flex-direction:column;gap:6px;flex:1}
  .lic-card .t{font-size:13px;font-weight:700}
  .lic-card .d{font-size:11px;color:var(--dim);flex:1}
  .lic-card .tiers-line{font-size:10px;color:var(--gold)}
  .lic-card .row{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:4px}
  .lic-card .row a{font-size:11px}
  #lic-form select{background:#0c1420;border:1px solid var(--line);color:var(--ink);
    border-radius:9px;padding:12px 10px;font-family:var(--mono);font-size:13px}
  footer{max-width:1120px;margin:0 auto;padding:26px 20px;color:var(--dim);font-size:11px;
    display:flex;gap:14px;flex-wrap:wrap;align-items:center}
  .tag{font-size:10px;color:var(--warn);border:1px solid #ffb45444;background:#ffb4540f;
    border-radius:20px;padding:3px 9px}
</style>
</head>
<body>
<header>
  <div class="logo"><b>Abrams</b>Ego</div>
  <span class="tag" id="test-badge" hidden>TEST MODE — no real charges</span>
  <nav class="nav">
    <a href="#features">Features</a>
    <a href="#pricing">Pricing</a>
    <a href="#licensing">Licensing</a>
    <a href="/index.html">Live demo →</a>
    <a href="/api/report/sample">Sample report</a>
    <a class="btn ghost" href="#waitlist">Join waitlist</a>
  </nav>
</header>

<main>
  <!-- HERO -->
  <section id="hero">
    <div class="kicker">Self-hostable AI-agent command center</div>
    <h1>The dashboard that <span class="hl">pays for itself.</span></h1>
    <p class="lede">
      AbramsEgo watches your whole agent fleet — local + cloud — and keeps its own
      books. It accounts for every token and watt it burns, tracks the revenue your
      agents earn, and shows you one number: <b>how much of its own running cost it
      covers.</b> Most dashboards are pure cost. This one has a P&amp;L.
    </p>
    <div class="hero-cta">
      <a class="btn gold" href="#waitlist">Get early access</a>
      <a class="btn ghost" href="/index.html">See the live dashboard →</a>
    </div>
    <div class="hero-note">Read-only · Basic-auth gated · runs on your box · your data never leaves it.</div>

    <!-- self-funding proof strip -->
    <div class="proof">
      <div class="stat"><div class="k">Fleet watched</div><div class="v" id="s-fleet">—</div><div class="n">local + Kamatera procs</div></div>
      <div class="stat"><div class="k">Cost accounted · 30d</div><div class="v gold" id="s-cost">—</div><div class="n">tokens + energy est</div></div>
      <div class="stat"><div class="k">Self-funding today</div><div class="v" id="s-pct">—</div><div class="n">of run cost covered</div></div>
      <div class="stat"><div class="k">Canaries live</div><div class="v" id="s-can">—</div><div class="n">health heartbeats</div></div>
    </div>
  </section>

  <!-- FEATURES -->
  <section id="features">
    <div class="kicker">What's inside</div>
    <h2>Everything you need to run agents like a business</h2>
    <p class="sub">One page, zero external dependencies, deployed behind your own auth.</p>
    <div class="grid">
      <div class="card"><div class="ico">🛰</div><h3>Unified fleet view</h3>
        <p>Local pm2 + remote Kamatera processes in one board — up/down, restarts, memory, CPU. Stale-while-revalidate so it's always instant.</p></div>
      <div class="card"><div class="ico">🐤</div><h3>Canary board</h3>
        <p>Every health heartbeat your agents write gets surfaced with a verdict + age, so a silently-dead job can't hide for twelve days.</p></div>
      <div class="card"><div class="ico">💰</div><h3>Self-funding P&amp;L</h3>
        <p>Token cost <i>and</i> energy cost, per day and per window, against real dated income — with a break-even target and a live self-funding %.</p></div>
      <div class="card"><div class="ico">📈</div><h3>Cost trend + per-model</h3>
        <p>7-day daily cost trend and a 30-day per-model breakdown, straight from your cost ledger. Know what's actually expensive.</p></div>
      <div class="card"><div class="ico">🧠</div><h3>Model + session usage</h3>
        <p>Per-model calls, tokens and spend, plus active Claude sessions and the most recent sub-agent runs — each stamped with created date + time.</p></div>
      <div class="card"><div class="ico">🤖</div><h3>Grounded AI chat</h3>
        <p>Ask "is it paying for itself?" and get an answer grounded in the live snapshot — via your local Ollama, so the chat itself costs $0.</p></div>
      <div class="card"><div class="ico">📄</div><h3>Sellable Fleet Health Report</h3>
        <p>One click turns your snapshot into a polished, self-contained report — fleet status, canary verdicts, cost trend, P&amp;L. <a href="/api/report/sample">See a sample report →</a></p></div>
    </div>

    <!-- 50s silent tour of the live dashboard (autoplays muted when scrolled into view) -->
    <div class="demo">
      <video id="tour" muted loop playsinline preload="none"
             poster="/assets/demo/abramsego-tour-poster.jpg"
             src="/assets/demo/abramsego-tour.mp4"></video>
      <div class="cap">50-second tour of the live dashboard — P&amp;L, cost trend, fleet, canaries, officers, sessions and grounded chat.</div>
    </div>
    <script>
      (function(){
        var v = document.getElementById('tour');
        if (!('IntersectionObserver' in window)) { v.preload = 'metadata'; v.controls = true; return; }
        new IntersectionObserver(function(es, obs){
          es.forEach(function(e){
            if (e.isIntersecting) { v.play().catch(function(){ v.controls = true; }); obs.disconnect(); }
          });
        }, { threshold: 0.35 }).observe(v);
      })();
    </script>
  </section>

  <!-- PRICING -->
  <section id="pricing">
    <div class="kicker">Pricing</div>
    <h2>Start free. Pay when you want it hosted.</h2>
    <p class="sub">The full dashboard is yours to self-host at no cost. Paid tiers are for when you'd rather we run it.</p>
    <div class="tiers">
      <div class="tier">
        <div class="name">Self-host</div>
        <div class="price">$0</div>
        <div class="for">forever · your box, your data</div>
        <ul>
          <li>Full command-center dashboard</li>
          <li>Fleet + canaries + P&amp;L</li>
          <li>Local Ollama AI chat</li>
          <li>Basic-auth gate included</li>
          <li class="no">You host &amp; update it</li>
          <li class="no">Community support</li>
        </ul>
        <a class="btn ghost" href="#waitlist">Get the template</a>
      </div>
      <div class="tier feat">
        <div class="badge">Most popular</div>
        <div class="name">Hosted</div>
        <div class="price">$19<small>/mo</small></div>
        <div class="for">we run it · you just log in</div>
        <ul>
          <li>Everything in Self-host</li>
          <li>Managed hosting + updates</li>
          <li>Daily snapshot backups</li>
          <li>Email alerts on canary FAIL</li>
          <li>Hosted uptime for the dashboard</li>
          <li class="no">Single fleet</li>
        </ul>
        <a class="btn" href="#waitlist" data-tier="hosted">Join waitlist</a>
      </div>
      <div class="tier">
        <div class="name">Pro</div>
        <div class="price">$49<small>/mo</small></div>
        <div class="for">multi-fleet · teams</div>
        <ul>
          <li>Everything in Hosted</li>
          <li>Multiple fleets / machines</li>
          <li>Team logins &amp; roles</li>
          <li>Revenue engine integrations</li>
          <li>Priority support</li>
          <li>Custom cost-ledger sources</li>
        </ul>
        <a class="btn ghost" href="#waitlist" data-tier="pro">Talk to us</a>
      </div>
    </div>
    <p class="pricing-note" id="pricing-note">Prices indicative · billing not yet live — join the waitlist and you'll be first to know when checkout opens.</p>
  </section>

  <!-- LICENSING (engine 5) — original Wallpaper's Back designs available to
       license. Grid is fed by /api/licensing/catalog (a read-only cache of
       designs LIVE on wallpapersback.com); prices are DRAFT copy only and the
       inquiry CTA just posts to the waitlist — no sale happens here. -->
  <section id="licensing">
    <div class="kicker">Original designs · licensing</div>
    <h2>License our original designs</h2>
    <p class="sub">Every pattern below is an original, live on
      <a href="https://wallpapersback.com" target="_blank" rel="noopener noreferrer">Wallpaper's Back</a> —
      our AI-assisted, human-curated wallpaper studio. License them for your own products,
      print runs, or campaigns. <span class="tag">Pricing DRAFT — inquiries only</span></p>
    <div class="lic-controls">
      <label>Sort
        <select id="lic-sort">
          <option value="newest">Newest</option>
          <option value="color">Color</option>
          <option value="style">Style</option>
          <option value="sku">SKU A→Z</option>
          <option value="title">Title A→Z</option>
        </select>
      </label>
      <label>Density
        <input id="lic-density" type="range" min="2" max="6" step="1" value="4" />
      </label>
    </div>
    <div class="lic-grid" id="lic-grid"></div>
    <div class="wl" id="lic-inquiry" style="margin-top:30px">
      <div class="kicker" style="margin-bottom:8px">License inquiry</div>
      <h2 style="margin-bottom:6px;font-size:19px">Ask about a design</h2>
      <p class="sub" style="margin:0" id="lic-context">Pick a design above, or send a general licensing inquiry.</p>
      <form id="lic-form">
        <input id="lic-email" type="email" required autocomplete="email" placeholder="you@brand.com" />
        <select id="lic-tier">
          <option value="digital_single_use">Digital single-use</option>
          <option value="commercial_print" selected>Commercial print</option>
          <option value="exclusive">Exclusive</option>
        </select>
        <button class="btn gold" type="submit">Send inquiry</button>
      </form>
      <div class="msg" id="lic-msg"></div>
    </div>
  </section>

  <!-- WAITLIST -->
  <section id="waitlist">
    <div class="wl">
      <div class="kicker" style="margin-bottom:8px">Early access</div>
      <h2 style="margin-bottom:6px">Be first in line</h2>
      <p class="sub" style="margin:0">Drop your email and we'll reach out the moment hosted plans go live. No spam — one launch note.</p>
      <form id="wl-form">
        <input id="wl-email" type="email" required autocomplete="email" placeholder="you@company.com" />
        <button class="btn gold" type="submit">Notify me</button>
      </form>
      <div class="msg" id="wl-msg"></div>
    </div>
  </section>

  <!-- TOOLS WE RUN ON — affiliate strip. Hidden until the registry has at
       least one non-draft entry (see /api/affiliates), so nothing renders
       while every candidate is still a draft. -->
  <section id="tools-strip" hidden style="text-align:center;padding:26px 20px">
    <div class="kicker" style="margin-bottom:10px">Tools we run on</div>
    <div id="tools-links" style="display:flex;gap:18px;justify-content:center;flex-wrap:wrap;font-size:13px"></div>
  </section>

  <!-- AD SLOT (engine 4) — house ad by default. When /api/ads/config reports a
       pub id (Steve joined a network + set AD_NETWORK_PUB_ID, the gated
       switch), the house card hides and the generic network container below
       renders instead. -->
  <section id="ad-slot" style="padding:30px 20px;border-bottom:0">
    <div class="card" id="house-ad" style="max-width:620px;margin:0 auto;display:flex;gap:16px;align-items:center;flex-wrap:wrap">
      <div class="ico" style="font-size:26px">📄</div>
      <div style="flex:1;min-width:220px">
        <div style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--dim)">House ad</div>
        <h3 style="margin:4px 0 4px">Your fleet, as a report clients can read</h3>
        <p>The Fleet Health Report turns your live snapshot into a polished, sendable artifact.
           <a href="/api/report/sample">See a sample</a> — or <a href="#waitlist">join the waitlist</a> for hosted plans.</p>
      </div>
      <a class="btn ghost" href="/api/report/sample" style="flex-shrink:0">View sample →</a>
    </div>
    <!-- Generic network container. The pub id lands on data-pub-id; the
         network's loader <script> (e.g. AdSense adsbygoogle.js?client=<pub id>)
         is added here only at go-live — joining a network is Steve's gated
         switch, nothing in this repo signs up. -->
    <div id="network-ad" hidden style="max-width:728px;margin:0 auto;text-align:center"></div>
  </section>
</main>

<footer>
  <span><b style="color:var(--ink)">AbramsEgo</b> — self-funding AI-agent command center.</span>
  <span class="tag">Preview · publish gated</span>
  <span style="margin-left:auto"><a href="/index.html">Live dashboard →</a></span>
</footer>

<script>
const $ = (id)=>document.getElementById(id);
const usd = (n)=> n==null? '—' : '$'+Number(n).toFixed(n<1?4:2);

// Pull real numbers from the live snapshot so the landing is honest, not mocked.
(async function fillProof(){
  try{
    const d = await (await fetch('/api/data')).json();
    if(!d || !d.builtAt) return;
    const lf=d.localFleet||{}, k=d.kamatera||{};
    const totalFleet=(lf.total||0)+(k.total||0);
    $('s-fleet').textContent = totalFleet || '—';
    const c30=(d.cost&&d.cost.month)||{};
    $('s-cost').textContent = usd(c30.total);
    const pct=(d.pnl&&d.pnl.today&&d.pnl.today.selfFundingPct);
    const el=$('s-pct'); el.textContent=(pct==null?'0':pct)+'%';
    el.className='v '+((pct||0)>=100?'good':(pct||0)>0?'warn':'gold');
    const cans=(d.canaries&&d.canaries.count)||0;
    $('s-can').textContent = cans || '—';
  }catch(e){ /* snapshot not ready — leave dashes */ }
})();

// Tier CTAs ← GET /api/sell/config. When checkout is ready the Hosted/Pro
// buttons create Stripe Checkout Sessions; otherwise they keep their
// #waitlist anchor fallback. mode=test shows the header badge.
(async function wireCheckout(){
  const note = $('pricing-note');
  let cfg = null;
  try { cfg = await (await fetch('/api/sell/config')).json(); } catch(e){}
  if (!cfg || !cfg.checkoutReady) {
    note.textContent = 'Stripe test keys not yet installed — checkout opens once they land. Join the waitlist to be first.';
    return; // buttons keep their #waitlist hrefs
  }
  if (cfg.mode === 'test') {
    $('test-badge').hidden = false;
    note.textContent = 'Checkout is in Stripe TEST mode — card 4242 4242 4242 4242 works, no real charges.';
  }
  const checkoutPath = cfg.checkoutPath || '/api/checkout';
  document.querySelectorAll('[data-tier]').forEach((btn)=>{
    btn.textContent = 'Subscribe (test)';
    btn.addEventListener('click', async (e)=>{
      e.preventDefault();
      btn.textContent = '…creating session';
      try {
        const r = await fetch(checkoutPath,{method:'POST',headers:{'Content-Type':'application/json'},
          body:JSON.stringify({tier:btn.dataset.tier})});
        const j = await r.json();
        if (r.ok && j.url) { location.href = j.url; return; }
        note.textContent = j.error || 'Checkout unavailable — try again.';
      } catch(err){ note.textContent = 'Network error — try again.'; }
      btn.textContent = 'Subscribe (test)';
    });
  });
})();

// "Tools we run on" strip ← GET /api/affiliates. Empty (all drafts) means the
// section stays hidden and the page is unchanged.
(async function fillTools(){
  try{
    const j = await (await fetch('/api/affiliates')).json();
    const list = (j && j.affiliates) || [];
    if(!list.length) return;
    $('tools-links').innerHTML = list.map(a=>
      `<a href="/go/${encodeURIComponent(a.slug)}" rel="sponsored noopener" target="_blank" style="color:var(--dim)">${a.label}</a>`
    ).join('');
    $('tools-strip').hidden = false;
  }catch(e){ /* strip stays hidden */ }
})();

// Ad slot ← GET /api/ads/config. House ad by default; with a pub id the house
// card hides and the generic network unit renders (the network's loader script
// is added at go-live, alongside the real ads.txt entry — both Steve-gated).
(async function wireAdSlot(){
  try{
    const cfg = await (await fetch('/api/ads/config')).json();
    if(!cfg || !cfg.enabled || !cfg.pubId) return; // house ad stays
    $('house-ad').hidden = true;
    const slot = $('network-ad');
    slot.innerHTML = '<ins class="ad-network-unit" data-pub-id="'+cfg.pubId.replace(/[^\w-]/g,'')+'"'+
      ' style="display:block;min-height:90px;border:1px dashed var(--line);border-radius:12px"></ins>';
    slot.hidden = false;
  }catch(e){ /* house ad stays */ }
})();

// Licensing grid (engine 5) ← GET /api/licensing/catalog. Sort + density
// controls persist in localStorage (Steve's hard rule for grids >12 items).
// Inquiry posts to /api/waitlist with source:"licensing" — interest capture
// only; actual listings/outreach stay Steve-gated.
(async function licensing(){
  const sec=$('licensing'), grid=$('lic-grid');
  let designs=[];
  try{
    const j = await (await fetch('/api/licensing/catalog')).json();
    designs = (j && j.designs) || [];
  }catch(e){}
  if(!designs.length){ sec.hidden = true; return; }

  // color buckets for the Color sort: white → warm → green → blue → purple → dark
  function colorBucket(hex){
    if(!hex) return 9;
    const n=parseInt(hex.replace('#',''),16); if(isNaN(n)) return 9;
    const r=(n>>16&255)/255, g=(n>>8&255)/255, b=(n&255)/255;
    const mx=Math.max(r,g,b), mn=Math.min(r,g,b), l=(mx+mn)/2, d=mx-mn;
    if(l>0.86) return 0;                       // white/cream
    if(l<0.16) return 8;                       // near-black
    if(d<0.08) return 7;                       // gray/greige
    let h=0;
    if(mx===r) h=((g-b)/d)%6; else if(mx===g) h=(b-r)/d+2; else h=(r-g)/d+4;
    h=(h*60+360)%360;
    if(h<70||h>=330) return 1;                 // warm (red/orange/yellow)
    if(h<170) return 2;                        // green
    if(h<260) return 3;                        // blue
    return 4;                                  // purple
  }
  const sorters={
    newest:(a,b)=>new Date(b.createdAt)-new Date(a.createdAt),
    color:(a,b)=>colorBucket(a.dominantHex)-colorBucket(b.dominantHex)||a.id-b.id,
    style:(a,b)=>(a.category||'').localeCompare(b.category||'')||a.id-b.id,
    sku:(a,b)=>String(a.id).localeCompare(String(b.id),undefined,{numeric:true}),
    title:(a,b)=>(a.title||'').localeCompare(b.title||''),
  };
  const sortSel=$('lic-sort'), dens=$('lic-density');
  sortSel.value = localStorage.getItem('lic-sort') || 'newest';
  dens.value = localStorage.getItem('lic-cols') || '4';
  function usd0(n){ return '$'+Number(n).toLocaleString(); }
  let picked=null; // {id,title} from a card's Inquire button
  function render(){
    localStorage.setItem('lic-sort', sortSel.value);
    localStorage.setItem('lic-cols', dens.value);
    grid.style.setProperty('--lic-cols', dens.value);
    const list=[...designs].sort(sorters[sortSel.value]||sorters.newest);
    grid.innerHTML=list.map(d=>{
      const t=d.licenseTiers||[];
      const line=t.map(x=>x.label.replace(' license','')+' '+usd0(x.priceUsd)).join(' · ');
      return `<div class="lic-card">
        <a href="${d.wpbUrl}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="${d.img}" alt="${d.title}"></a>
        <div class="b">
          <div class="t">${d.title}</div>
          <div class="d">${d.styleLine}</div>
          <div class="tiers-line">DRAFT · ${line}</div>
          <div class="row">
            <a href="${d.wpbUrl}" target="_blank" rel="noopener noreferrer">View on WPB →</a>
            <a href="#lic-inquiry" data-lic-id="${d.id}" data-lic-title="${d.title}">Inquire</a>
          </div>
        </div>
      </div>`;
    }).join('');
    grid.querySelectorAll('[data-lic-id]').forEach(a=>a.addEventListener('click',()=>{
      picked={id:a.dataset.licId,title:a.dataset.licTitle};
      $('lic-context').textContent='Re: '+picked.title+' (#'+picked.id+')';
    }));
  }
  sortSel.addEventListener('change',render);
  dens.addEventListener('input',render);
  render();

  $('lic-form').addEventListener('submit', async (e)=>{
    e.preventDefault();
    const email=$('lic-email').value.trim(), msg=$('lic-msg');
    const tier=($('lic-tier').value+(picked?':'+picked.id:'')).slice(0,40);
    msg.className='msg'; msg.textContent='…sending';
    try{
      const r = await fetch('/api/waitlist',{method:'POST',headers:{'Content-Type':'application/json'},
        body:JSON.stringify({email, tier, source:'licensing'})});
      const j = await r.json();
      if(r.ok && j.ok){ msg.className='msg ok'; msg.textContent='Inquiry received — we’ll reach out with licensing details. 🎉'; $('lic-form').reset(); }
      else { msg.className='msg err'; msg.textContent=j.error||'Could not send — try again.'; }
    }catch(err){ msg.className='msg err'; msg.textContent='Network error — try again.'; }
  });
})();

// Waitlist capture → POST /api/waitlist
$('wl-form').addEventListener('submit', async (e)=>{
  e.preventDefault();
  const email=$('wl-email').value.trim();
  const msg=$('wl-msg');
  if(!email){ msg.className='msg err'; msg.textContent='Enter an email.'; return; }
  msg.className='msg'; msg.textContent='…adding you';
  try{
    const r = await fetch('/api/waitlist',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({email, tier:'hosted', source:'landing'})});
    const j = await r.json();
    if(r.ok && j.ok){ msg.className='msg ok'; msg.textContent='You’re on the list — we’ll be in touch. 🎉'; $('wl-form').reset(); }
    else { msg.className='msg err'; msg.textContent=j.error||'Could not add you — try again.'; }
  }catch(err){ msg.className='msg err'; msg.textContent='Network error — try again.'; }
});
</script>
</body>
</html>