redesign: hero → map → security explanation

- Hero: 'Your vault. Wherever you want it.'
- Map immediately under hero with 22 region dots
- Three cards: self-hosted / Zürich HQ / closest POP
- Security model: problem (AI access) → sealed/agent layers → why Zürich
- Added /geo endpoint to main.go for IP-based location fallback
- Geo: browser geolocation first, IP fallback on deny
- Closest POP card wired to deeplink /signup?region=<id>
This commit is contained in:
James 2026-03-02 03:55:47 -05:00
parent e9a1efeede
commit 27c0ad6d9b
3 changed files with 223 additions and 128 deletions

View File

@ -50,119 +50,184 @@
</div>
</nav>
<div class="pt-20">
<div class="text-center py-16 px-6">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">vault<span class="text-accent font-mono">1984</span> Hosted</h1>
<p class="text-gray-400 text-lg max-w-xl mx-auto">We run it. You own it. Your L2 keys never leave your device.</p>
<!-- Hero -->
<div style="height:80px"></div>
<section class="pb-12" style="padding-top:0">
<div class="max-w-7xl mx-auto px-6 text-center">
<p class="text-green-400 text-xs font-mono uppercase tracking-widest mb-4">Hosting</p>
<h1 class="text-4xl md:text-6xl font-bold text-white mb-5">Your vault. Wherever you want it.</h1>
<p class="text-gray-400 text-xl max-w-2xl mx-auto">We run it. You own it. Pick your region — your data stays there.</p>
</div>
</section>
<!-- World Map — Infrastructure -->
<section class="py-8 border-t border-white/5">
<!-- Map -->
<section class="pb-10">
<div class="max-w-7xl mx-auto px-6">
<h2 class="text-3xl md:text-4xl font-bold text-white text-center mb-4">Your vault. Deployed close to you.</h2>
<p class="text-gray-400 text-center mb-4 max-w-2xl mx-auto">Hosted on Hostkey TIER III infrastructure. Pick your region at signup.</p>
<p class="text-gray-400 text-center mb-3 max-w-2xl mx-auto">Or run it yourself. We embrace that too. No account, no payment, no questions asked.</p>
<div class="mx-auto mb-4 rounded-2xl border border-green-500/20 bg-green-500/5 p-6 text-center">
<p class="text-green-400 text-xs font-mono uppercase tracking-widest mb-4">The security model</p>
<h2 class="text-2xl md:text-3xl font-bold text-white mb-4">Location is latency. Not security.</h2>
<p class="text-gray-300 leading-relaxed">
Your L2 encryption key is derived client-side from your Touch ID or security key. It never leaves your device. Our servers store ciphertext they cannot decrypt — regardless of who owns the rack, the building, or the country it sits in.
</p>
<p class="text-gray-400 mt-4 leading-relaxed">
Pick your region for speed. Pick it for compliance if your organisation requires it. But your private fields are safe in any of them.
</p>
</div>
<div class="relative mb-4 rounded-2xl overflow-hidden border border-white/5">
<div class="relative mb-6 rounded-2xl overflow-hidden border border-white/5">
<svg id="worldmap" viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg" class="w-full" style="display:block;background:#0a1628;">
<image href="/worldmap.svg" x="0" y="0" width="1000" height="460"/>
<circle cx="284.7" cy="143.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.00s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.00s" repeatCount="indefinite"/></circle>
<circle cx="284.7" cy="143.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.80s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="0.80s" repeatCount="indefinite"/></circle>
<circle cx="284.7" cy="143.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="284.7" y="155.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Washington D.C.</text>
<circle cx="160.0" cy="143.1" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.08s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.08s" repeatCount="indefinite"/></circle>
<circle cx="160.0" cy="143.1" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/></circle>
<circle cx="160.0" cy="143.1" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="160.0" y="135.1" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">San Francisco</text>
<circle cx="295.6" cy="122.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.16s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.16s" repeatCount="indefinite"/></circle>
<circle cx="295.6" cy="122.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/></circle>
<circle cx="295.6" cy="122.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="295.6" y="114.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Montréal</text>
<circle cx="224.7" cy="187.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.24s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.24s" repeatCount="indefinite"/></circle>
<circle cx="224.7" cy="187.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/></circle>
<circle cx="224.7" cy="187.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.32s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.32s" repeatCount="indefinite"/></circle>
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/></circle>
<text x="224.7" y="199.0" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Mexico City</text>
<circle cx="294.2" cy="219.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.32s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.32s" repeatCount="indefinite"/></circle>
<circle cx="294.2" cy="219.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/></circle>
<circle cx="294.2" cy="219.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="294.2" y="231.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Bogotá</text>
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.40s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.40s" repeatCount="indefinite"/></circle>
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/></circle>
<circle cx="370.6" cy="282.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.40s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.40s" repeatCount="indefinite"/></circle>
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/></circle>
<text x="370.6" y="294.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">São Paulo</text>
<circle cx="303.9" cy="306.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.48s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.48s" repeatCount="indefinite"/></circle>
<circle cx="303.9" cy="306.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/></circle>
<circle cx="303.9" cy="306.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="303.9" y="318.0" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Santiago</text>
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.56s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.56s" repeatCount="indefinite"/></circle>
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/></circle>
<circle cx="499.7" cy="106.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="506.4" cy="113.4" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.48s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.48s" repeatCount="indefinite"/></circle>
<circle cx="506.4" cy="113.4" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/></circle>
<circle cx="506.4" cy="113.4" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="524.2" cy="110.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.56s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.56s" repeatCount="indefinite"/></circle>
<circle cx="524.2" cy="110.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/></circle>
<circle cx="524.2" cy="110.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="499.7" y="98.0" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">London</text>
<circle cx="523.6" cy="117.6" r="5" fill="none" stroke="#D4AF37" stroke-width="2"><animate attributeName="r" values="5;18;5" dur="2.4s" begin="0.64s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.64s" repeatCount="indefinite"/></circle>
<circle cx="523.6" cy="117.6" r="5" fill="none" stroke="#D4AF37" stroke-width="1.5"><animate attributeName="r" values="5;18;5" dur="2.4s" begin="1.44s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.44s" repeatCount="indefinite"/></circle>
<circle cx="523.6" cy="117.6" r="6" fill="#D4AF37" stroke="#0a1628" stroke-width="2"/>
<circle cx="523.6" cy="117.6" r="3" fill="#0a1628"/>
<text x="523.6" y="109.6" font-family="Inter,sans-serif" font-size="8.5" fill="#D4AF37" text-anchor="middle" opacity="0.85">Zürich</text>
<circle cx="489.7" cy="136.4" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.72s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.72s" repeatCount="indefinite"/></circle>
<circle cx="489.7" cy="136.4" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/></circle>
<circle cx="489.7" cy="136.4" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="489.7" y="128.4" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Madrid</text>
<circle cx="550.3" cy="82.1" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.80s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.80s" repeatCount="indefinite"/></circle>
<circle cx="550.3" cy="82.1" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/></circle>
<circle cx="550.3" cy="82.1" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.88s" repeatCount="indefinite"/></circle>
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/></circle>
<text x="550.3" y="74.1" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Stockholm</text>
<circle cx="580.3" cy="134.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.88s" repeatCount="indefinite"/></circle>
<circle cx="580.3" cy="134.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/></circle>
<circle cx="580.3" cy="134.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="580.3" y="126.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Istanbul</text>
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.96s" repeatCount="indefinite"/></circle>
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.76s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.76s" repeatCount="indefinite"/></circle>
<circle cx="653.6" cy="173.6" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="596.7" cy="157.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.96s" repeatCount="indefinite"/></circle>
<circle cx="596.7" cy="157.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.76s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.76s" repeatCount="indefinite"/></circle>
<circle cx="596.7" cy="157.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.04s" repeatCount="indefinite"/></circle>
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.84s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.84s" repeatCount="indefinite"/></circle>
<text x="653.6" y="165.6" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Dubai</text>
<circle cx="509.4" cy="215.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.04s" repeatCount="indefinite"/></circle>
<circle cx="509.4" cy="215.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.84s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.84s" repeatCount="indefinite"/></circle>
<circle cx="509.4" cy="215.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="509.4" y="227.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Lagos</text>
<circle cx="602.2" cy="232.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.12s" repeatCount="indefinite"/></circle>
<circle cx="602.2" cy="232.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.92s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.92s" repeatCount="indefinite"/></circle>
<circle cx="602.2" cy="232.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="602.2" y="244.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Nairobi</text>
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.20s" repeatCount="indefinite"/></circle>
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.00s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.00s" repeatCount="indefinite"/></circle>
<circle cx="551.1" cy="307.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.12s" repeatCount="indefinite"/></circle>
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.92s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.92s" repeatCount="indefinite"/></circle>
<text x="551.1" y="319.2" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Cape Town</text>
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.28s" repeatCount="indefinite"/></circle>
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.08s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.08s" repeatCount="indefinite"/></circle>
<circle cx="702.5" cy="187.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.20s" repeatCount="indefinite"/></circle>
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.00s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.00s" repeatCount="indefinite"/></circle>
<text x="702.5" y="179.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Mumbai</text>
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.36s" repeatCount="indefinite"/></circle>
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.16s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.16s" repeatCount="indefinite"/></circle>
<circle cx="788.3" cy="227.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="796.7" cy="243.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.28s" repeatCount="indefinite"/></circle>
<circle cx="796.7" cy="243.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.08s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.08s" repeatCount="indefinite"/></circle>
<circle cx="796.7" cy="243.6" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="782.5" cy="223.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.36s" repeatCount="indefinite"/></circle>
<circle cx="782.5" cy="223.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.16s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.16s" repeatCount="indefinite"/></circle>
<circle cx="782.5" cy="223.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="788.3" y="239.2" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Singapore</text>
<circle cx="920.0" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.44s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.44s" repeatCount="indefinite"/></circle>
<circle cx="920.0" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.24s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.24s" repeatCount="indefinite"/></circle>
<circle cx="920.0" cy="307.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.52s" repeatCount="indefinite"/></circle>
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.32s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.32s" repeatCount="indefinite"/></circle>
<text x="920.0" y="319.2" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Sydney</text>
<circle cx="888.1" cy="148.3" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.52s" repeatCount="indefinite"/></circle>
<circle cx="888.1" cy="148.3" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.32s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.32s" repeatCount="indefinite"/></circle>
<circle cx="888.1" cy="148.3" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="888.1" y="140.3" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Tokyo</text>
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.60s" repeatCount="indefinite"/></circle>
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.40s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.40s" repeatCount="indefinite"/></circle>
<circle cx="852.5" cy="143.6" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.60s" repeatCount="indefinite"/></circle>
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.40s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.40s" repeatCount="indefinite"/></circle>
<text x="852.5" y="135.6" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Seoul</text>
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.68s" repeatCount="indefinite"/></circle>
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.48s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.48s" repeatCount="indefinite"/></circle>
<circle cx="817.2" cy="180.3" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="817.2" y="172.3" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Hong Kong</text>
</svg>
</svg>
</div>
<div id="dc-grid" style="display:flex;gap:12px;width:100%;margin-bottom:2.5rem">
<div class="rounded-xl p-4 text-center card-hover" data-lon="-77.4" style="background:#0d1f10;border:1px solid rgba(34,197,94,0.25);flex:1;min-width:0">
<div class="text-2xl mb-1.5">🇺🇸</div>
<div class="text-white font-semibold text-sm">Virginia</div>
<div class="text-gray-500 text-xs mb-2">US East</div>
<div class="flex items-center justify-center gap-1.5 text-xs text-accent"><span class="w-1.5 h-1.5 rounded-full bg-accent inline-block"></span>Live</div>
<!-- Self-hosted -->
<div class="rounded-xl p-4 text-center card-hover" style="background:#1f0a0a;border:1px solid rgba(239,68,68,0.35);flex:1;min-width:0">
<div class="text-2xl mb-1.5">🖥️</div>
<div class="text-white font-semibold text-sm">Self-hosted</div>
<div class="text-gray-500 text-xs mb-2">Your machine. Your rules.</div>
<div class="flex items-center justify-center gap-1.5 text-xs mb-3" style="color:#EF4444"><span class="w-1.5 h-1.5 rounded-full inline-block" style="background:#EF4444"></span>Free forever</div>
<a href="/install" class="block w-full text-center text-xs font-semibold py-1.5 px-3 rounded-lg transition-colors" style="background:rgba(239,68,68,0.15);color:#EF4444;border:1px solid rgba(239,68,68,0.3)" onmouseover="this.style.background='rgba(239,68,68,0.25)'" onmouseout="this.style.background='rgba(239,68,68,0.15)'">Download now →</a>
</div>
<div class="rounded-xl p-4 text-center card-hover" data-lon="8.5" style="background:#3d2e00;border:1px solid rgba(212,175,55,0.5);flex:1;min-width:0">
<div class="text-2xl mb-2">🇨🇭</div>
<div class="text-white font-semibold text-sm">Zürich</div>
<div class="text-gray-500 text-xs mb-2">EU Central</div>
<div class="flex items-center justify-center gap-1.5 text-xs" style="color:#D4AF37"><span class="w-1.5 h-1.5 rounded-full inline-block" style="background:#D4AF37"></span>Live</div>
<!-- Zürich HQ -->
<div class="rounded-xl p-4 text-center card-hover" style="background:#3d2e00;border:1px solid rgba(212,175,55,0.5);flex:1;min-width:0">
<div class="text-2xl mb-1.5">🇨🇭</div>
<div class="text-white font-semibold text-sm">Zürich, Switzerland</div>
<div class="text-gray-500 text-xs mb-2">Capital of Security</div>
<div class="flex items-center justify-center gap-1.5 text-xs mb-3" style="color:#D4AF37"><span class="w-1.5 h-1.5 rounded-full inline-block" style="background:#D4AF37"></span>Headquarters</div>
<a href="/signup?region=eu-central-2" class="block w-full text-center text-xs font-semibold py-1.5 px-3 rounded-lg transition-colors" style="background:rgba(212,175,55,0.15);color:#D4AF37;border:1px solid rgba(212,175,55,0.3)" onmouseover="this.style.background='rgba(212,175,55,0.25)'" onmouseout="this.style.background='rgba(212,175,55,0.15)'">Buy now →</a>
</div>
<!-- Closest POP — populated by JS -->
<div id="closest-pop" class="rounded-xl p-4 text-center card-hover" style="background:#0d1f10;border:1px solid rgba(34,197,94,0.25);flex:1;min-width:0">
<div class="text-2xl mb-1.5">📍</div>
<div id="closest-name" class="text-white font-semibold text-sm">Nearest region</div>
<div id="closest-sub" class="text-gray-500 text-xs mb-2">Locating you…</div>
<div class="flex items-center justify-center gap-1.5 text-xs mb-3 text-accent"><span class="w-1.5 h-1.5 rounded-full bg-accent inline-block"></span>Closest to you</div>
<a id="closest-buy" href="/signup" class="block w-full text-center text-xs font-semibold py-1.5 px-3 rounded-lg transition-colors" style="background:rgba(34,197,94,0.15);color:#22C55E;border:1px solid rgba(34,197,94,0.3)" onmouseover="this.style.background='rgba(34,197,94,0.25)'" onmouseout="this.style.background='rgba(34,197,94,0.15)'">Buy now →</a>
</div>
</div>
</div>
</section>
<!-- Why it matters -->
<section class="py-16 border-t border-white/5">
<div class="max-w-7xl mx-auto px-6">
<div class="text-center mb-14 max-w-3xl mx-auto">
<p class="text-green-400 text-xs font-mono uppercase tracking-widest mb-3">The security model</p>
<h2 class="text-2xl md:text-3xl font-bold text-white mb-4">Your AI needs access.<br>Not to everything.</h2>
<p class="text-gray-400 text-lg leading-relaxed max-w-2xl mx-auto">A password manager that blocks AI agents is useless in 2025. But one that hands them everything is a liability. vault1984 solves this with two layers.</p>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-14 max-w-5xl mx-auto">
<div class="rounded-2xl p-6 border border-white/5" style="background:#0d1a0d">
<p class="text-green-400 text-xs font-mono uppercase tracking-widest mb-3">Sealed fields</p>
<h3 class="text-xl font-bold text-white mb-3">Only you. Only in person.</h3>
<p class="text-gray-400 leading-relaxed">Passwords and private notes are encrypted on your device with a key derived from your fingerprint or hardware token. We store a locked box. No key ever reaches our servers. Not a court order. Not your AI assistant. Sealed fields require your physical presence to unlock.</p>
</div>
<div class="rounded-2xl p-6 border border-white/5" style="background:#0d1627">
<p class="text-blue-400 text-xs font-mono uppercase tracking-widest mb-3">Agent fields</p>
<h3 class="text-xl font-bold text-white mb-3">Your AI, scoped and controlled.</h3>
<p class="text-gray-400 leading-relaxed">Fields you designate as agent-accessible are encrypted on our servers. You issue scoped tokens — Claude gets your GitHub token, nothing else. Revoke at any time. The agent never sees sealed fields, no matter what.</p>
</div>
</div>
<div>
<p class="text-xs font-mono uppercase tracking-widest mb-3" style="color:#D4AF37">Why Zürich</p>
<h2 class="text-2xl md:text-3xl font-bold text-white mb-4">Sealed fields: jurisdiction irrelevant.<br>Agent fields: it isn't.</h2>
<p class="text-gray-400 text-lg leading-relaxed mb-6">Sealed fields are protected by math — the server's location doesn't matter. But agent fields live on a server in a jurisdiction. A US server is subject to the CLOUD Act. A UK server to the Investigatory Powers Act. Zürich is subject to Swiss law — which does not cooperate with foreign government data requests. No backdoors. Both layers protected.</p>
<div class="grid md:grid-cols-3 gap-4 max-w-5xl mx-auto">
<div class="rounded-xl p-4 border border-white/5" style="background:#1a0a0a">
<p class="text-red-400 text-xs font-mono uppercase tracking-widest mb-2">Self-hosted · US</p>
<p class="text-gray-300 text-sm leading-relaxed">Your server, your rules — until a court says otherwise. CLOUD Act applies to US persons regardless of encryption.</p>
</div>
<div class="rounded-xl p-4 border border-white/5" style="background:#0d1627">
<p class="text-blue-400 text-xs font-mono uppercase tracking-widest mb-2">Self-hosted · anywhere</p>
<p class="text-gray-300 text-sm leading-relaxed">Full control. Your infrastructure, your jurisdiction. The right choice if you know what you're doing.</p>
</div>
<div class="rounded-xl p-4" style="background:#3d2e00;border:1px solid rgba(212,175,55,0.3)">
<p class="text-xs font-mono uppercase tracking-widest mb-2" style="color:#D4AF37">Hosted · Zürich, Switzerland</p>
<p class="text-gray-300 text-sm leading-relaxed">Swiss law. Swiss courts. No CLOUD Act. No backdoors. We handle the infrastructure — you get the protection.</p>
</div>
<div class="rounded-xl p-4 text-center card-hover" data-lon="151.2" style="background:#0d1f10;border:1px solid rgba(34,197,94,0.25);flex:1;min-width:0">
<div class="text-2xl mb-2">🇦🇺</div>
<div class="text-white font-semibold text-sm">Sydney</div>
<div class="text-gray-500 text-xs mb-2">Asia Pacific South</div>
<div class="flex items-center justify-center gap-1.5 text-xs text-accent"><span class="w-1.5 h-1.5 rounded-full bg-accent inline-block"></span>Live</div>
</div>
</div>
@ -424,67 +489,75 @@
svg.appendChild(label);
}
const POPS = [
{name:"Washington D.C.", region:"us-east-1", lat:37.5, lon:-77.5},
{name:"San Francisco", region:"us-west-1", lat:37.8, lon:-122.4},
{name:"Montréal", region:"ca-central-1", lat:45.5, lon:-73.6},
{name:"Mexico City", region:"mx-central-1", lat:19.4, lon:-99.1},
{name:"Bogotá", region:"sa-bogota", lat:4.7, lon:-74.1},
{name:"São Paulo", region:"sa-east-1", lat:-23.6, lon:-46.6},
{name:"Santiago", region:"sa-west-1", lat:-33.4, lon:-70.6},
{name:"London", region:"eu-west-2", lat:51.5, lon:-0.1},
{name:"Zürich", region:"eu-central-2", lat:47.4, lon:8.5},
{name:"Madrid", region:"eu-south-2", lat:40.4, lon:-3.7},
{name:"Stockholm", region:"eu-north-1", lat:59.3, lon:18.1},
{name:"Istanbul", region:"tr-west-1", lat:41.0, lon:28.9},
{name:"Dubai", region:"me-central-1", lat:25.2, lon:55.3},
{name:"Lagos", region:"af-west-1", lat:6.5, lon:3.4},
{name:"Nairobi", region:"af-east-1", lat:-1.3, lon:36.8},
{name:"Cape Town", region:"af-south-1", lat:-33.9, lon:18.4},
{name:"Mumbai", region:"ap-south-1", lat:19.1, lon:72.9},
{name:"Singapore", region:"ap-southeast-1", lat:1.3, lon:103.8},
{name:"Sydney", region:"ap-southeast-2", lat:-33.9, lon:151.2},
{name:"Tokyo", region:"ap-northeast-1", lat:35.7, lon:139.7},
{name:"Seoul", region:"ap-northeast-2", lat:37.6, lon:126.9},
{name:"Hong Kong", region:"ap-east-1", lat:22.3, lon:114.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 grid = document.getElementById('dc-grid');
if (!grid) return;
// Build visitor card
const flag = d.country_code ? d.country_code.toUpperCase().split('').map(c =>
String.fromCodePoint(c.charCodeAt(0) + 127397)).join('') : '📍';
const city = d.city || 'You';
const country = d.country_name || '';
const region = d.region || '';
const card = document.createElement('div');
card.className = 'rounded-xl p-4 text-center card-hover';
card.setAttribute('data-lon', d.longitude);
card.style.cssText = 'background:#1f0a0a;border:1px solid rgba(239,68,68,0.35);flex:1;min-width:0';
card.innerHTML = `
<div class="text-2xl mb-2">${flag}</div>
<div class="text-white font-semibold text-sm">${city}</div>
<div class="text-gray-500 text-xs">${country}</div>
<div class="text-gray-500 text-xs mb-2">${region}</div>
<div class="flex items-center justify-center gap-1.5 text-xs text-gray-400">
<span class="w-1.5 h-1.5 rounded-full inline-block" style="background:#EF4444;opacity:0.6"></span>You are here
</div>`;
// Insert at correct longitude position
const cards = [...grid.children];
const insertBefore = cards.find(c => parseFloat(c.getAttribute('data-lon')) > d.longitude);
if (insertBefore) grid.insertBefore(card, insertBefore);
else grid.appendChild(card);
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.name;
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);
} else if (d.private && navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude, lon = pos.coords.longitude;
// Reverse geocode via open-meteo's free geocoding isn't ideal;
// use bigdatacloud free reverse geocode — no key, no signup
fetch(`/geo?lat=${lat}&lon=${lon}`)
.then(r => r.json())
.then(g => handleGeoData({
latitude: lat, longitude: lon,
city: g.city || 'You',
region: g.region || '',
country_name: g.country_name || '',
country_code: g.country_code || ''
}))
.catch(() => handleGeoData({ latitude: lat, longitude: lon,
city: 'You', region: '', country_name: '', country_code: '' }));
}, () => {});
}
})
.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>
</body>

26
main.go
View File

@ -2,22 +2,44 @@ package main
import (
"embed"
"io"
"log"
"net/http"
"os"
"strings"
)
//go:embed *.html *.svg *.css
var static embed.FS
func geoHandler(w http.ResponseWriter, r *http.Request) {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.RemoteAddr
}
// Strip port
if i := strings.LastIndex(ip, ":"); i >= 0 {
ip = ip[:i]
}
ip = strings.Trim(ip, "[]")
resp, err := http.Get("https://ipapi.co/" + ip + "/json/")
if err != nil {
http.Error(w, `{"error":"geo failed"}`, 502)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
io.Copy(w, resp.Body)
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8099"
}
http.HandleFunc("/geo", geoHandler)
http.Handle("/", http.FileServer(http.FS(static)))
log.Printf("vault1984-web starting on :%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)

Binary file not shown.