···
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
* { margin: 0; padding: 0; box-sizing: border-box; }
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
+
background: radial-gradient(ellipse at center, #0a0a0f 0%, #000000 100%);
+
transform-style: preserve-3d;
+
animation: rotate 120s infinite linear;
+
from { transform: rotateY(0deg); }
+
to { transform: rotateY(360deg); }
+
justify-content: center;
+
transition: all 0.3s ease;
+
backdrop-filter: blur(4px);
+
transform: scale(1.2) !important;
+
color: rgba(255, 255, 255, 0.9);
+
background: rgba(10, 10, 15, 0.95);
+
border: 1px solid rgba(255, 255, 255, 0.1);
+
padding: 0.5rem 0.75rem;
+
transition: opacity 0.2s;
+
.app-orb:hover .app-tooltip {
+
justify-content: center;
+
background: rgba(10, 10, 15, 0.8);
+
border: 1px solid rgba(255, 255, 255, 0.1);
+
backdrop-filter: blur(10px);
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+
letter-spacing: 0.05em;
+
color: rgba(255, 255, 255, 0.5);
+
background: rgba(255, 255, 255, 0.05);
+
border: 1px solid rgba(255, 255, 255, 0.1);
+
border-color: rgba(255, 255, 255, 0.3);
+
background: rgba(255, 255, 255, 0.08);
+
color: rgba(255, 255, 255, 0.3);
+
background: rgba(255, 255, 255, 0.1);
+
border: 1px solid rgba(255, 255, 255, 0.2);
+
background: rgba(255, 255, 255, 0.15);
+
border-color: rgba(255, 255, 255, 0.3);
.hidden { display: none; }
+
.loading { color: rgba(255, 255, 255, 0.5); font-size: 0.9rem; }
+
transform: translateX(-50%);
+
color: rgba(255, 255, 255, 0.3);
+
color: rgba(255, 255, 255, 0.5);
+
transition: color 0.2s;
+
color: rgba(255, 255, 255, 0.8);
+
<div class="atmosphere" id="atmosphere"></div>
+
<div class="login-card">
+
<div id="restoring" class="loading hidden">restoring session...</div>
+
<form id="loginForm" method="post" action="/login">
+
<div class="subtitle">explore the atmosphere</div>
+
<input type="text" name="handle" placeholder="handle.bsky.social" required autofocus>
+
<button type="submit">enter</button>
+
by <a href="https://bsky.app/profile/zzstoatzz.io" target="_blank" rel="noopener noreferrer">@zzstoatzz.io</a>
+
// Check for saved session
const savedDid = localStorage.getItem('atme_did');
document.getElementById('loginForm').classList.add('hidden');
···
document.getElementById('restoring').classList.add('hidden');
+
// Fetch and cache atmosphere data
+
async function fetchAtmosphere() {
+
const CACHE_KEY = 'atme_atmosphere';
+
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
+
const cached = localStorage.getItem(CACHE_KEY);
+
const { data, timestamp } = JSON.parse(cached);
+
if (Date.now() - timestamp < CACHE_DURATION) {
+
const response = await fetch('https://ufos-api.microcosm.blue/collections?order=dids-estimate&limit=50');
+
const json = await response.json();
+
// Group by namespace (first two segments)
+
json.collections.forEach(col => {
+
const parts = col.nsid.split('.');
+
if (parts.length >= 2) {
+
const ns = `${parts[0]}.${parts[1]}`;
+
namespaces[ns].dids_total += col.dids_estimate;
+
namespaces[ns].records_total += col.creates;
+
namespaces[ns].collections.push(col.nsid);
+
const data = Object.values(namespaces).sort((a, b) => b.dids_total - a.dids_total).slice(0, 30);
+
localStorage.setItem(CACHE_KEY, JSON.stringify({
+
console.error('Failed to fetch atmosphere data:', e);
+
// Try to fetch app avatar
+
async function fetchAppAvatar(namespace) {
+
const reversed = namespace.split('.').reverse().join('.');
+
const handles = [reversed, `${reversed}.bsky.social`];
+
for (const handle of handles) {
+
const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`);
+
if (!didRes.ok) continue;
+
const { did } = await didRes.json();
+
const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`);
+
if (!profileRes.ok) continue;
+
const profile = await profileRes.json();
+
if (profile.avatar) return profile.avatar;
+
async function renderAtmosphere() {
+
const data = await fetchAtmosphere();
+
if (!data.length) return;
+
const atmosphere = document.getElementById('atmosphere');
+
const maxSize = Math.max(...data.map(d => d.dids_total));
+
data.forEach((app, i) => {
+
const orb = document.createElement('div');
+
orb.className = 'app-orb';
+
// Size based on user count (20-80px)
+
const size = 20 + (app.dids_total / maxSize) * 60;
+
// Position in 3D space
+
const angle = (i / data.length) * Math.PI * 2;
+
const radius = 250 + (i % 3) * 100;
+
const y = (i % 5) * 80 - 160;
+
const x = Math.cos(angle) * radius;
+
const z = Math.sin(angle) * radius;
+
orb.style.width = `${size}px`;
+
orb.style.height = `${size}px`;
+
orb.style.left = `calc(50% + ${x}px)`;
+
orb.style.top = `calc(50% + ${y}px)`;
+
orb.style.transform = `translateZ(${z}px) translate(-50%, -50%)`;
+
orb.style.background = `radial-gradient(circle, rgba(255,255,255,0.1), rgba(255,255,255,0.02))`;
+
orb.style.border = '1px solid rgba(255,255,255,0.1)';
+
orb.style.boxShadow = '0 0 20px rgba(255,255,255,0.1)';
+
const letter = app.namespace.split('.')[1]?.[0]?.toUpperCase() || app.namespace[0].toUpperCase();
+
orb.innerHTML = `<div class="fallback">${letter}</div>`;
+
const tooltip = document.createElement('div');
+
tooltip.className = 'app-tooltip';
+
const users = app.dids_total >= 1000000
+
? `${(app.dids_total / 1000000).toFixed(1)}M users`
+
: `${(app.dids_total / 1000).toFixed(0)}K users`;
+
tooltip.textContent = `${app.namespace} • ${users}`;
+
orb.appendChild(tooltip);
+
atmosphere.appendChild(orb);
+
// Fetch and apply avatar
+
fetchAppAvatar(app.namespace).then(avatarUrl => {
+
orb.innerHTML = `<img src="${avatarUrl}" alt="${app.namespace}" />`;
+
orb.appendChild(tooltip);