Landing pricing: delete sovereign section, dynamic tier cards, FAQ fix

Pricing section overhaul:
- Delete the "Sovereign by default / Everything stays on your
  hardware" section (left + right panels weren't really telling
  one story, and the lede repeats what the hero already says).
- Tier intro: drop the dense self-license framing; keep just
  "Every Keysat instance ships at a highly functional Creator
  tier; pay only when you outgrow it."
- Drop the "No fake feature gates" trailing blurb.
- FAQ tier-difference answer: reflect Patron as a perpetual
  license + direct 1:1 support (not just "Pro with a badge").

Tier cards now render LIVE from the master Keysat:
- On page load, fetches /v1/products/keysat (for the entitlements
  catalog) and /v1/products/keysat/policies (for the tier list),
  then re-renders #tier-grid-live with the operator-configured
  data — prices, marketing bullets, hidden-entitlement filter,
  and any active featured (launch-special) discount applied.
- Matches the buy page's visual treatment: diagonal LAUNCH SPECIAL
  ribbon, struck-through original price, discounted headline
  price, "Limited: N of M remaining" meta line. Single source of
  truth between landing and /buy.
- Static markup stays as a graceful fallback when the fetch fails
  (offline, daemon down, CORS hiccup). The script only mutates
  the DOM on a successful response from both endpoints.
This commit is contained in:
Grant
2026-05-11 17:54:06 -05:00
parent d36de2c501
commit a8a0e39fe0
+200 -41
View File
@@ -334,6 +334,32 @@ pre.code .p { color:rgba(245,241,232,0.55); }
content:'\2713'; position:absolute; left:0; top:0;
color:var(--gold-700); font-weight:700;
}
/* Launch-special discount treatment on the live tier cards. Matches
the buy page's diagonal ribbon + slashed-price visual so the
landing page and the buy page tell the same pricing story. */
.tier-card.has-launch {
position:relative;
clip-path:polygon(0 -20px, 100% -20px, 100% 100%, 0 100%);
}
.tier-launch-ribbon {
position:absolute; top:14px; right:-44px;
background:var(--gold-500); color:var(--navy-950);
font-family:var(--font-display); font-weight:700; font-size:10.5px;
letter-spacing:0.14em; text-transform:uppercase;
padding:4px 50px; transform:rotate(35deg);
box-shadow:0 2px 6px rgba(14,31,51,0.15);
z-index:2; pointer-events:none;
}
.tier-launch-meta {
font-size:11.5px; color:var(--gold-700); font-weight:600;
margin-bottom:6px;
}
.tier-price-original {
font-family:var(--font-display); font-weight:500; font-size:14px;
color:var(--ink-500); margin:0 0 4px;
text-decoration:line-through; text-decoration-color:rgba(14,31,51,0.4);
}
.tier-card .tier-features-list { margin-top:4px; }
/* ---------- Install ---------- */
.install-grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; }
@@ -859,47 +885,23 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
</div>
</section>
<section class="block" id="sovereign">
<div class="wrap">
<div class="section-head">
<span class="eyebrow">Sovereign by default</span>
<h2>Everything stays on your hardware.</h2>
<p>Migrate Start9 boxes &mdash; Keysat goes with you. If the Keysat project disappears tomorrow, your already-issued licenses keep verifying: the public key is embedded in your software, the private key is on your machine.</p>
</div>
<div class="sov">
<div class="panel">
<h3>What you keep</h3>
<div class="sub">On your Start9, in your normal backups.</div>
<ul>
<li>Signing keypair</li>
<li>Customer email &middot; npub list</li>
<li>Sale records</li>
<li>Audit log</li>
<li>BTCPay invoice history</li>
<li>Webhook subscribers</li>
<li>Bitcoin (your wallet)</li>
</ul>
<p class="footnote">Backed up automatically by StartOS as part of your normal backup routine.</p>
</div>
<div class="panel dark">
<h3>Or accept fiat, on your terms</h3>
<div class="sub">Coming soon: opt-in card payments via Zaprite.</div>
<p style="font-size:14px; line-height:1.55; color:rgba(245,241,232,0.85); margin:0 0 16px;">If your customers prefer paying with credit cards over Bitcoin, you&rsquo;ll be able to plug Zaprite into Keysat as an alternative payment provider. Same Keysat license-issuance flow, but the payment can come through Stripe-via-Zaprite for cards or any of Zaprite&rsquo;s Bitcoin rails (BTCPay, Strike, Unchained). Trades off some sovereignty &mdash; cards mean Stripe KYC and customer PII flowing through Zaprite &mdash; in exchange for a much wider addressable audience.</p>
<p style="font-size:14px; line-height:1.55; color:rgba(245,241,232,0.85); margin:0 0 16px;">Keysat stays sovereign-by-default. Card payments are something you opt into per Keysat install if your business needs them. Shipping in v0.3.</p>
<p class="footnote">Source-available license &middot; pay your operator-style trade-offs deliberately, never by default.</p>
</div>
</div>
</div>
</section>
<section class="block" id="tiers">
<div class="wrap">
<div class="section-head">
<span class="eyebrow">Pricing</span>
<h2>Three tiers. Free forever for solo creators.</h2>
<p>Keysat&rsquo;s gating happens at the operator&rsquo;s self-license, not at the buyer&rsquo;s. Every Keysat instance ships at Creator (free); pay only when you outgrow it.</p>
<p>Every Keysat instance ships at a highly functional Creator tier; pay only when you outgrow it.</p>
</div>
<div class="tier-grid">
<!--
Tier cards. Static markup is the FALLBACK shape (rendered on
every page load, kept conservative so it stays correct even when
offline). On load, `loadLiveTiers()` below fetches the master
Keysat's product + policies and re-renders this grid with the
live prices, marketing bullets, hidden-entitlement filtering, and
any active launch-special featured discount applied — matching
what the buy page renders.
-->
<div class="tier-grid" id="tier-grid-live">
<div class="tier-card">
<div class="tier-cap">Creator</div>
<div class="tier-price"><span class="price-num">Free</span><span class="price-sub">forever</span></div>
@@ -928,19 +930,17 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
<div class="tier-card">
<div class="tier-cap">Patron</div>
<div class="tier-price"><span class="price-num">500k sats</span><span class="price-sub">/ year</span></div>
<p class="tier-pitch">Honest supporter tier. Same features as Pro; you&rsquo;re funding Keysat&rsquo;s development.</p>
<p class="tier-pitch">Perpetual license + direct one-on-one support, for creators who want Keysat to keep getting better.</p>
<ul>
<li>Everything in Pro</li>
<li>Perpetual license (one-time, never renews)</li>
<li>Direct one-on-one support</li>
<li>&ldquo;Patron&rdquo; badge in your admin dashboard</li>
<li>Listed on the Patrons page on keysat.xyz</li>
<li>Early access to release-candidate builds</li>
<li>Direct support line</li>
</ul>
</div>
</div>
<p style="margin-top:28px; text-align:center; font-size:13.5px; color:var(--ink-500);">
No fake feature gates: Patron is feature-identical to Pro. Pricing is a flat annual fee &mdash; we don&rsquo;t take a percentage of your sales.
</p>
</div>
</section>
@@ -995,7 +995,7 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
</div>
<div>
<h3>What&rsquo;s the difference between the three tiers?</h3>
<p>Creator is free forever, capped at 5 products / 5 policies per product / 10 active discount codes &mdash; plenty for a solo creator selling one-time or perpetual licenses. Pro lifts every cap and unlocks recurring subscriptions plus the Zaprite payment gateway (cards, Apple Pay, bank transfers, in addition to Bitcoin). Patron is Pro with a public supporter badge; same features, you&rsquo;re funding development. See the tier comparison above for the full breakdown.</p>
<p>Creator is free forever, capped at 5 products / 5 policies per product / 10 active discount codes &mdash; plenty for a solo creator selling one-time or perpetual licenses. Pro lifts every cap and unlocks recurring subscriptions plus the Zaprite payment gateway (cards, Apple Pay, bank transfers, in addition to Bitcoin), billed annually. Patron is everything in Pro, sold as a one-time perpetual license instead of an annual subscription, plus direct one-on-one support and a public supporter badge &mdash; you&rsquo;re funding development. See the tier cards above for the full breakdown.</p>
</div>
<div>
<h3>Do I have to use Bitcoin?</h3>
@@ -1123,5 +1123,164 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
start();
})();
</script>
<!--
Live tier-card render. Fetches the master Keysat's product + policies
on page load and re-renders the #tier-grid-live grid with whatever
the operator configured — prices, marketing bullets, hidden
entitlements, and any active featured (launch-special) discount.
Mirrors the buy page's tier-picker treatment so the landing page
and /buy/keysat tell exactly the same pricing story.
Failure mode: if the fetch errors (offline visitor, daemon down,
CORS hiccup), the static fallback grid renders as it does today.
This script only mutates the DOM on success.
-->
<script>
(function () {
const KEYSAT_API = 'https://licensing.keysat.xyz';
const PRODUCT_SLUG = 'keysat';
const fmtSats = (n) => {
if (n === 0) return 'Free';
if (n >= 1_000_000 && n % 100_000 === 0) return (n / 1_000_000) + 'M sats';
if (n >= 1_000 && n % 1_000 === 0) return (n / 1_000) + 'k sats';
return Number(n).toLocaleString('en-US') + ' sats';
};
const cadenceSuffix = (p) => {
if (!p.is_recurring) return '';
const d = p.renewal_period_days || 0;
if (d === 7) return '/ wk';
if (d === 30) return '/ mo';
if (d === 90) return '/ qtr';
if (d === 180) return '/ 6mo';
if (d === 365) return '/ year';
if (d > 0) return '/ ' + d + 'd';
return '';
};
const discountLabel = (fd) => {
if (!fd) return '';
if (fd.kind === 'percent') return Math.round(fd.amount / 100) + '% OFF';
if (fd.kind === 'free_license') return 'FREE';
if (fd.kind === 'set_price') return 'LIMITED PRICE';
return 'LAUNCH SPECIAL';
};
function renderCard(pol, catalog) {
const card = document.createElement('div');
card.className = 'tier-card' + (pol.highlighted ? ' featured' : '');
const fd = pol.featured_discount;
if (fd) card.classList.add('has-launch');
const cap = document.createElement('div');
cap.className = 'tier-cap' + (pol.highlighted ? ' gold' : '');
cap.textContent = pol.name;
card.appendChild(cap);
if (fd) {
const ribbon = document.createElement('div');
ribbon.className = 'tier-launch-ribbon';
ribbon.textContent = discountLabel(fd);
card.appendChild(ribbon);
const remaining = fd.remaining_uses;
if (typeof remaining === 'number' && remaining > 0) {
const meta = document.createElement('div');
meta.className = 'tier-launch-meta';
meta.textContent = 'Limited: ' + remaining + ' of ' + (fd.max_uses || 0) + ' remaining';
card.appendChild(meta);
}
const orig = document.createElement('div');
orig.className = 'tier-price-original';
orig.textContent = fmtSats(pol.price_sats);
card.appendChild(orig);
}
// Headline price. Free policies just say "Free"; paid policies
// show the discounted price when a featured discount applies.
const priceWrap = document.createElement('div');
priceWrap.className = 'tier-price';
const priceNum = document.createElement('span');
priceNum.className = 'price-num';
const priceSub = document.createElement('span');
priceSub.className = 'price-sub';
if (pol.price_sats === 0) {
priceNum.textContent = 'Free';
priceSub.textContent = 'forever';
} else {
const effective = fd ? fd.discounted_price_sats : pol.price_sats;
priceNum.textContent = fmtSats(effective);
const suffix = cadenceSuffix(pol);
priceSub.textContent = pol.is_recurring && suffix ? suffix : '';
if (!pol.is_recurring) priceSub.textContent = '';
}
priceWrap.appendChild(priceNum);
if (priceSub.textContent) priceWrap.appendChild(priceSub);
card.appendChild(priceWrap);
if (pol.description) {
const pitch = document.createElement('p');
pitch.className = 'tier-pitch';
pitch.textContent = pol.description;
card.appendChild(pitch);
}
// Merge marketing bullets + (visible) entitlements into a single
// <ul> in the operator-controlled order. Identical pattern to the
// buy page's tier-features list.
const hidden = new Set(pol.hidden_entitlements || []);
const entItems = (pol.entitlements || [])
.filter((slug) => !hidden.has(slug))
.map((slug) => {
const entry = catalog[slug];
return entry && entry.name ? entry.name : slug;
});
const bullets = pol.marketing_bullets || [];
const order = (pol.marketing_bullets_position === 'below')
? entItems.concat(bullets)
: bullets.concat(entItems);
if (order.length > 0) {
const list = document.createElement('ul');
list.className = 'tier-features-list';
order.forEach((text) => {
const li = document.createElement('li');
li.textContent = text;
list.appendChild(li);
});
card.appendChild(list);
}
return card;
}
async function loadLiveTiers() {
const grid = document.getElementById('tier-grid-live');
if (!grid) return;
try {
const [productR, policiesR] = await Promise.all([
fetch(KEYSAT_API + '/v1/products/' + PRODUCT_SLUG, { cache: 'no-cache' }),
fetch(KEYSAT_API + '/v1/products/' + PRODUCT_SLUG + '/policies', { cache: 'no-cache' }),
]);
if (!productR.ok || !policiesR.ok) return; // leave static fallback
const productJ = await productR.json();
const policiesJ = await policiesR.json();
const product = productJ.product || productJ;
const catalog = {};
(product.entitlements_catalog || []).forEach((e) => { catalog[e.slug] = e; });
const policies = (policiesJ.policies || []).filter((p) => !p.is_trial);
if (policies.length === 0) return;
// Empty the grid + render the live cards.
while (grid.firstChild) grid.removeChild(grid.firstChild);
policies.forEach((p) => grid.appendChild(renderCard(p, catalog)));
} catch (_) {
// Silent fallback. The static cards stay rendered.
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadLiveTiers);
} else {
loadLiveTiers();
}
})();
</script>
</body>
</html>