← 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&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&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&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&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&L</li>
<li>Local Ollama AI chat</li>
<li>Basic-auth gate included</li>
<li class="no">You host & 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 & 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>