291 lines
15 KiB
Cheetah
291 lines
15 KiB
Cheetah
{{define "hosted"}}
|
|
<!-- Hero -->
|
|
<div class="hero container">
|
|
<p class="label accent mb-4"><span class="vaultname">clavitor</span> hosted</p>
|
|
<h1>Zero cache. Every request hits the vault.</h1>
|
|
<p class="lead">Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run {{len .Pops}} regions across every continent. Your data lives where you choose. <s>$20</s> $12/yr.</p>
|
|
</div>
|
|
|
|
<!-- Map -->
|
|
<div class="container">
|
|
<div class="map-wrap">
|
|
<svg id="worldmap" viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
|
|
<image href="/worldmap.svg" x="0" y="0" width="1000" height="460"/>
|
|
<text x="500" y="440" font-family="Figtree,sans-serif" font-size="18" font-weight="700" fill="#0A0A0A" text-anchor="middle" opacity="0.35" letter-spacing="0.3em">CLAVITOR GLOBAL PRESENCE</text>
|
|
</svg>
|
|
</div>
|
|
<div class="mt-12"></div>
|
|
<div id="dc-grid" class="mb-8">
|
|
<!-- Self-hosted -->
|
|
<div class="dc-card red" data-lon="-999">
|
|
<div class="dc-icon">🖥️</div>
|
|
<div class="dc-name">Self-hosted</div>
|
|
<div class="dc-sub">Your machine. Your rules.</div>
|
|
<div class="dc-status"><span class="dc-dot"></span>Free forever</div>
|
|
<a href="/install" class="btn btn-red btn-block">Download now →</a>
|
|
</div>
|
|
<!-- Zürich HQ -->
|
|
<div class="dc-card gold" data-lon="8.5">
|
|
<div class="dc-icon">🇨🇭</div>
|
|
<div class="dc-name">Zürich, Switzerland</div>
|
|
<div class="dc-sub">Capital of Privacy</div>
|
|
<div class="dc-status"><span class="dc-dot"></span>Headquarters</div>
|
|
<a href="/signup?region=eu-central-2" class="btn btn-gold btn-block">Buy now →</a>
|
|
</div>
|
|
<!-- Closest POP — populated by JS -->
|
|
<div id="closest-pop" class="dc-card" data-lon="999">
|
|
<div class="dc-icon">📍</div>
|
|
<div id="closest-name" class="dc-name">Nearest region</div>
|
|
<div id="closest-sub" class="dc-sub">Locating you…</div>
|
|
<div class="dc-status"><span class="dc-dot"></span>Closest to you</div>
|
|
<a id="closest-buy" href="/signup" class="btn btn-accent btn-block">Buy now →</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- Why encryption, not jurisdiction -->
|
|
<div class="section container">
|
|
<p class="label accent mb-3">Three-tier encryption</p>
|
|
<h2 class="mb-4">Jurisdiction is irrelevant.<br>Math is not.</h2>
|
|
<p class="lead mb-8">Your vault is encrypted at rest. Your credentials are encrypted per-field. Your identity fields are encrypted client-side with a key that never leaves your device. No server — ours or anyone's — can read what it doesn't have the key to. That's the real protection. Zürich is the belt to that suspenders: a jurisdiction where nobody will even try to force open what mathematics already guarantees they can't.</p>
|
|
<div class="grid-3">
|
|
<div class="card">
|
|
<p class="label mb-2">Vault Encryption</p>
|
|
<p>Entire vault encrypted at rest with AES-256-GCM. The baseline. Every password manager does this.</p>
|
|
</div>
|
|
<div class="card">
|
|
<p class="label accent mb-2">Credential Encryption</p>
|
|
<p>Per-field encryption. Your AI agent can read the API key it needs — but not the credit card number in the same entry.</p>
|
|
</div>
|
|
<div class="card red">
|
|
<p class="label red mb-2">Identity Encryption</p>
|
|
<p>Client-side. WebAuthn PRF. The key is derived from your WebAuthn authenticator — fingerprint, face, or hardware key — and never leaves your device. We cannot decrypt it. Period.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- What hosted adds -->
|
|
<div class="section container">
|
|
<p class="label accent mb-3">What hosted adds</p>
|
|
<h2 class="mb-8">Everything in self-hosted, plus</h2>
|
|
<div class="grid-3">
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Managed infrastructure</h3>
|
|
<p>We run it, monitor it, and keep it up. You just use it.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Daily encrypted backups</h3>
|
|
<p>Automatic daily backups. Encrypted at rest. Restorable on request.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">{{len .Pops}} regions</h3>
|
|
<p>Pick your region at signup. Your data stays there. Every continent covered.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Automatic updates</h3>
|
|
<p>Security patches and new features deployed automatically. No downtime.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">TLS included</h3>
|
|
<p>HTTPS out of the box. No Caddy, no certbot, no renewal headaches.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Email support</h3>
|
|
<p>Real human support. Not a chatbot. Not a forum post into the void.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- CTA -->
|
|
<div class="section container">
|
|
<h2 class="mb-4">Ready?</h2>
|
|
<p class="lead mb-6"><s>$20</s> $12/yr. 7-day money-back. Every feature included.</p>
|
|
<div class="btn-row">
|
|
<a href="/signup" class="btn btn-primary">Get started</a>
|
|
<a href="/pricing" class="btn btn-ghost">Compare plans →</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "hosted-script"}}
|
|
<script>
|
|
(function() {
|
|
const W = 1000, H = 460;
|
|
const ns = 'http://www.w3.org/2000/svg';
|
|
|
|
function project(lon, lat) {
|
|
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
|
|
const miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
|
|
const maxMiller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*80*Math.PI/180));
|
|
const x = (lon + 180) / 360 * W;
|
|
const y = H/2 - (miller / (2*maxMiller)) * H;
|
|
return [Math.round(x*10)/10, Math.round(y*10)/10];
|
|
}
|
|
|
|
function addPopDot(svg, pop, delay) {
|
|
const [x, y] = project(pop.lon, pop.lat);
|
|
const isHQ = pop.city === 'Zürich';
|
|
const isLive = pop.status === 'live';
|
|
const dotColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
|
|
const textColor = isHQ ? '#0A0A0A' : isLive ? '#B91C1C' : '#777777';
|
|
const pulseColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
|
|
const dotSize = isHQ ? 11 : 9;
|
|
const pulseR = isHQ ? 5 : 4;
|
|
const pulseMax = isHQ ? 18 : 13;
|
|
const tooltip = isLive ? pop.city + ' · Live' : pop.city + ' · Planned · Q2 2026';
|
|
|
|
// Pulse ring 1
|
|
const r1 = document.createElementNS(ns, 'circle');
|
|
r1.setAttribute('cx', x); r1.setAttribute('cy', y);
|
|
r1.setAttribute('r', pulseR); r1.setAttribute('fill', 'none');
|
|
r1.setAttribute('stroke', pulseColor); r1.setAttribute('stroke-width', isHQ ? '2' : '1.5');
|
|
const a1 = document.createElementNS(ns, 'animate');
|
|
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
|
|
a1.setAttribute('dur', '2.4s'); a1.setAttribute('begin', delay+'s'); a1.setAttribute('repeatCount', 'indefinite');
|
|
const a2 = document.createElementNS(ns, 'animate');
|
|
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.6;0;0.6');
|
|
a2.setAttribute('dur', '2.4s'); a2.setAttribute('begin', delay+'s'); a2.setAttribute('repeatCount', 'indefinite');
|
|
r1.appendChild(a1); r1.appendChild(a2);
|
|
|
|
// Pulse ring 2
|
|
const r2 = document.createElementNS(ns, 'circle');
|
|
r2.setAttribute('cx', x); r2.setAttribute('cy', y);
|
|
r2.setAttribute('r', pulseR); r2.setAttribute('fill', 'none');
|
|
r2.setAttribute('stroke', pulseColor); r2.setAttribute('stroke-width', isHQ ? '1.5' : '1');
|
|
const a3 = document.createElementNS(ns, 'animate');
|
|
a3.setAttribute('attributeName', 'r'); a3.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
|
|
a3.setAttribute('dur', '2.4s'); a3.setAttribute('begin', (delay+0.8)+'s'); a3.setAttribute('repeatCount', 'indefinite');
|
|
const a4 = document.createElementNS(ns, 'animate');
|
|
a4.setAttribute('attributeName', 'stroke-opacity'); a4.setAttribute('values', '0.4;0;0.4');
|
|
a4.setAttribute('dur', '2.4s'); a4.setAttribute('begin', (delay+0.8)+'s'); a4.setAttribute('repeatCount', 'indefinite');
|
|
r2.appendChild(a3); r2.appendChild(a4);
|
|
|
|
// Square dot
|
|
const half = dotSize / 2;
|
|
const dot = document.createElementNS(ns, 'rect');
|
|
dot.setAttribute('x', x - half); dot.setAttribute('y', y - half);
|
|
dot.setAttribute('width', dotSize); dot.setAttribute('height', dotSize);
|
|
dot.setAttribute('fill', dotColor); dot.setAttribute('stroke', '#F5F5F5'); dot.setAttribute('stroke-width', '1.5');
|
|
const title = document.createElementNS(ns, 'title');
|
|
title.textContent = tooltip;
|
|
dot.appendChild(title);
|
|
|
|
// Label
|
|
const label = document.createElementNS(ns, 'text');
|
|
label.setAttribute('x', x); label.setAttribute('y', y - half - 4);
|
|
label.setAttribute('font-family', 'Inter,sans-serif');
|
|
label.setAttribute('font-size', '8.5');
|
|
label.setAttribute('fill', textColor);
|
|
label.setAttribute('text-anchor', 'middle');
|
|
label.setAttribute('opacity', '0.85');
|
|
label.textContent = pop.city;
|
|
const labelTitle = document.createElementNS(ns, 'title');
|
|
labelTitle.textContent = tooltip;
|
|
label.appendChild(labelTitle);
|
|
|
|
svg.appendChild(r1);
|
|
svg.appendChild(r2);
|
|
svg.appendChild(dot);
|
|
svg.appendChild(label);
|
|
}
|
|
|
|
function addVisitorDot(lat, lon, city) {
|
|
const svg = document.getElementById('worldmap');
|
|
if (!svg) return;
|
|
const [x, y] = project(lon, lat);
|
|
|
|
const ring = document.createElementNS(ns, 'circle');
|
|
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
|
|
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
|
|
ring.setAttribute('stroke', '#0A0A0A'); ring.setAttribute('stroke-width', '1.5');
|
|
const a1 = document.createElementNS(ns, 'animate');
|
|
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
|
|
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
|
|
const a2 = document.createElementNS(ns, 'animate');
|
|
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.8;0;0.8');
|
|
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
|
|
ring.appendChild(a1); ring.appendChild(a2);
|
|
|
|
const dot = document.createElementNS(ns, 'circle');
|
|
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
|
|
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#0A0A0A');
|
|
dot.setAttribute('stroke', '#ffffff'); dot.setAttribute('stroke-width', '1.5');
|
|
|
|
const label = document.createElementNS(ns, 'text');
|
|
label.setAttribute('x', x); label.setAttribute('y', y + 15);
|
|
label.setAttribute('font-family', 'Inter,sans-serif');
|
|
label.setAttribute('font-size', '10');
|
|
label.setAttribute('fill', '#0A0A0A');
|
|
label.setAttribute('text-anchor', 'middle');
|
|
label.setAttribute('font-weight', '500');
|
|
label.textContent = city || 'You';
|
|
|
|
svg.appendChild(ring);
|
|
svg.appendChild(dot);
|
|
svg.appendChild(label);
|
|
}
|
|
|
|
const POPS = [{{range .Pops}}
|
|
{city:"{{.City}}", region:"{{.RegionName}}", lat:{{.Lat}}, lon:{{.Lon}}, status:"{{.Status}}", provider:"{{.Provider}}"},{{end}}
|
|
];
|
|
|
|
// Render all POP dots from DB data
|
|
const svg = document.getElementById('worldmap');
|
|
if (svg) {
|
|
POPS.forEach((pop, i) => addPopDot(svg, pop, (i * 0.08).toFixed(2)));
|
|
}
|
|
|
|
function findClosestPop(lat, lon) {
|
|
return POPS.reduce((best, p) => {
|
|
const d = (lat-p.lat)**2 + (lon-p.lon)**2;
|
|
const bd = (lat-best.lat)**2 + (lon-best.lon)**2;
|
|
return d < bd ? p : best;
|
|
});
|
|
}
|
|
|
|
function handleGeoData(d) {
|
|
if (!d.latitude || !d.longitude) return;
|
|
addVisitorDot(d.latitude, d.longitude, d.city || 'You');
|
|
|
|
const closest = findClosestPop(d.latitude, d.longitude);
|
|
const nameEl = document.getElementById('closest-name');
|
|
const subEl = document.getElementById('closest-sub');
|
|
const buyEl = document.getElementById('closest-buy');
|
|
if (nameEl) nameEl.textContent = closest.city;
|
|
if (subEl) subEl.textContent = d.city ? `~${d.city}` : 'Your region';
|
|
if (buyEl) buyEl.href = `/signup?region=${closest.region}`;
|
|
}
|
|
|
|
// Ask browser geolocation first (accurate, triggers permission prompt)
|
|
// Fall back to server-side IP lookup if denied or unavailable
|
|
function tryIPGeo() {
|
|
fetch('/geo')
|
|
.then(r => r.json())
|
|
.then(d => { if (d.latitude) handleGeoData(d); })
|
|
.catch(() => {});
|
|
}
|
|
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
pos => {
|
|
handleGeoData({
|
|
latitude: pos.coords.latitude,
|
|
longitude: pos.coords.longitude,
|
|
city: '', region: '', country_name: '', country_code: ''
|
|
});
|
|
},
|
|
() => tryIPGeo() // denied — fall back to IP
|
|
);
|
|
} else {
|
|
tryIPGeo();
|
|
}
|
|
})();
|
|
</script>
|
|
{{end}}
|