interactive intro to open social at-me.zzstoatzz.io

feat: add neon glow animations and PDS hosting status card

Restore missing keyframes:
- neon-flicker animation for guestbook sign text
- pov-subtle-flicker animation for POV indicator
- Both with dark mode variants for enhanced glow effects

Add PDS hosting status card to identity sidebar:
- Detects Bluesky-hosted vs self-hosted PDSes
- Bluesky-hosted: shows encouragement to self-host with link to guide
- Self-hosted: acknowledges setup and fetches PDS version via /xrpc/_health
- Color-coded cards (blue for Bluesky, purple for self-hosted)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+157 -1
+24
src/view/detail.css
··· 407 407 } 408 408 } 409 409 410 + .ownership-box.self-hosted { 411 + background: rgba(156, 39, 176, 0.05); 412 + border-color: rgba(156, 39, 176, 0.3); 413 + } 414 + 415 + @media (prefers-color-scheme: dark) { 416 + .ownership-box.self-hosted { 417 + background: rgba(186, 104, 200, 0.08); 418 + border-color: rgba(186, 104, 200, 0.4); 419 + } 420 + } 421 + 422 + .ownership-box.bluesky-hosted { 423 + background: rgba(33, 150, 243, 0.05); 424 + border-color: rgba(33, 150, 243, 0.3); 425 + } 426 + 427 + @media (prefers-color-scheme: dark) { 428 + .ownership-box.bluesky-hosted { 429 + background: rgba(66, 165, 245, 0.08); 430 + border-color: rgba(66, 165, 245, 0.4); 431 + } 432 + } 433 + 410 434 .ownership-header { 411 435 font-size: 0.7rem; 412 436 font-weight: 600;
+72
src/view/guestbook.css
··· 553 553 color: #e66; 554 554 } 555 555 } 556 + 557 + /* Neon flicker animation for guestbook sign text */ 558 + @keyframes neon-flicker { 559 + 0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { 560 + opacity: 0.6; 561 + text-shadow: 0 0 4px currentColor; 562 + } 563 + 20%, 24%, 55% { 564 + opacity: 0.2; 565 + text-shadow: none; 566 + } 567 + } 568 + 569 + /* POV indicator flicker - subtle 37 second loop, flickers TO brightness */ 570 + @keyframes pov-subtle-flicker { 571 + 0%, 98% { 572 + opacity: 0.4; 573 + text-shadow: 0 0 3px currentColor; 574 + } 575 + 17%, 17.3%, 17.6% { 576 + opacity: 0.75; 577 + text-shadow: 0 0 8px currentColor, 0 0 12px currentColor; 578 + } 579 + 17.15%, 17.45% { 580 + opacity: 0.5; 581 + text-shadow: 0 0 4px currentColor; 582 + } 583 + 71%, 71.2% { 584 + opacity: 0.8; 585 + text-shadow: 0 0 10px currentColor, 0 0 15px currentColor; 586 + } 587 + 71.1% { 588 + opacity: 0.45; 589 + text-shadow: 0 0 3px currentColor; 590 + } 591 + } 592 + 593 + @media (prefers-color-scheme: dark) { 594 + @keyframes neon-flicker { 595 + 0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { 596 + opacity: 0.5; 597 + text-shadow: 0 0 6px currentColor, 0 0 12px rgba(255, 107, 157, 0.3); 598 + } 599 + 20%, 24%, 55% { 600 + opacity: 0.15; 601 + text-shadow: 0 0 2px currentColor; 602 + } 603 + } 604 + 605 + @keyframes pov-subtle-flicker { 606 + 0%, 98% { 607 + opacity: 0.35; 608 + text-shadow: 0 0 4px currentColor, 0 0 8px rgba(138, 180, 248, 0.2); 609 + } 610 + 17%, 17.3%, 17.6% { 611 + opacity: 0.75; 612 + text-shadow: 0 0 12px currentColor, 0 0 20px rgba(138, 180, 248, 0.6); 613 + } 614 + 17.15%, 17.45% { 615 + opacity: 0.45; 616 + text-shadow: 0 0 6px currentColor, 0 0 10px rgba(138, 180, 248, 0.3); 617 + } 618 + 71%, 71.2% { 619 + opacity: 0.8; 620 + text-shadow: 0 0 15px currentColor, 0 0 25px rgba(138, 180, 248, 0.7); 621 + } 622 + 71.1% { 623 + opacity: 0.4; 624 + text-shadow: 0 0 5px currentColor, 0 0 9px rgba(138, 180, 248, 0.25); 625 + } 626 + } 627 + }
+61 -1
src/view/visualization.js
··· 126 126 }); 127 127 } 128 128 129 + // Check if PDS is hosted by Bluesky 130 + function isBlueskyHostedPds(pdsUrl) { 131 + const host = pdsUrl.replace('https://', '').replace('http://', '').toLowerCase(); 132 + return host.includes('.host.bsky.network') || 133 + host.includes('bsky.social') || 134 + host.includes('bsky.network') || 135 + host.endsWith('.bsky.app'); 136 + } 137 + 138 + // Fetch PDS health/version info 139 + async function fetchPdsHealth(pdsUrl) { 140 + try { 141 + const response = await fetch(`${pdsUrl}/xrpc/_health`, { 142 + signal: AbortSignal.timeout(5000) 143 + }); 144 + if (response.ok) { 145 + return await response.json(); 146 + } 147 + } catch (e) { 148 + // Health endpoint not available or errored 149 + } 150 + return null; 151 + } 152 + 129 153 function setupIdentityClickHandler(allCollections, appCount, profile) { 130 154 const pdsHost = state.globalPds.replace('https://', '').replace('http://', ''); 155 + const isSelfHosted = !isBlueskyHostedPds(state.globalPds); 131 156 132 - document.querySelector('.identity').addEventListener('click', () => { 157 + document.querySelector('.identity').addEventListener('click', async () => { 133 158 const detail = document.getElementById('detail'); 134 159 160 + // Build the hosting status card based on whether self-hosted or Bluesky-hosted 161 + let hostingStatusCard; 162 + if (isSelfHosted) { 163 + hostingStatusCard = ` 164 + <div class="ownership-box self-hosted"> 165 + <div class="ownership-header">🛠️ self-hosted pds</div> 166 + <div class="ownership-text">you're running your own PDS at <strong>${pdsHost}</strong> - nice! you have full control over your data and identity.</div> 167 + <div id="pdsVersionInfo" class="pds-version-info" style="margin-top: 0.5rem; font-size: 0.65rem; color: var(--text-lighter);">checking version...</div> 168 + </div> 169 + `; 170 + } else { 171 + hostingStatusCard = ` 172 + <div class="ownership-box bluesky-hosted"> 173 + <div class="ownership-header">☁️ bluesky-hosted pds</div> 174 + <div class="ownership-text">your data is hosted on bluesky's infrastructure. this is the default and works great! but if you want more control, you can <a href="https://atproto.com/guides/self-hosting" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline; font-weight: 500;">self-host your own PDS</a>.</div> 175 + <div style="margin-top: 0.5rem; font-size: 0.6rem; color: var(--text-lighter);">self-hosting means you control the server where your posts, likes, and follows live. you can migrate your data anytime.</div> 176 + </div> 177 + `; 178 + } 179 + 135 180 detail.innerHTML = ` 136 181 <button class="detail-close" id="detailClose">x</button> 137 182 <h3>your personal data server</h3> ··· 153 198 <div class="ownership-text">your <a href="https://atproto.com/guides/data-repos" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data Server</a> is hosted at <a href="${state.globalPds}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;"><strong>${pdsHost}</strong></a>. all your posts, likes, and follows are stored here. apps like bluesky just connect to it.</div> 154 199 </div> 155 200 201 + ${hostingStatusCard} 202 + 156 203 <div class="ownership-box"> 157 204 <div class="ownership-header">explore your data</div> 158 205 <div class="ownership-text">want to see everything stored on your PDS? check out <a href="https://pdsls.dev/${pdsHost}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">pdsls.dev/${pdsHost}</a> - a tool for browsing all the records in your repository.</div> ··· 192 239 e.stopPropagation(); 193 240 detail.classList.remove('visible'); 194 241 }); 242 + 243 + // Fetch PDS version info asynchronously for self-hosted PDSes 244 + if (isSelfHosted) { 245 + const healthInfo = await fetchPdsHealth(state.globalPds); 246 + const versionEl = document.getElementById('pdsVersionInfo'); 247 + if (versionEl) { 248 + if (healthInfo?.version) { 249 + versionEl.textContent = `pds version: ${healthInfo.version}`; 250 + } else { 251 + versionEl.textContent = 'version info not available'; 252 + } 253 + } 254 + } 195 255 }); 196 256 } 197 257