add animated peeking bufo from all four edges

isolate all bufo peeking logic into separate bufo-peek.js file, keeping
the silly whimsical behavior cleanly separated from core search functionality.

behavior:
- bufo randomly appears from one of four edges (top, right, bottom, left)
- rotates 90° between edges to always peek perpendicular to edge
- animates: peeks in → holds → peeks back out (6s cycle)
- moves to new random edge after each cycle
- hides permanently after first search

technical:
- new static/bufo-peek.js handles all positioning and animation logic
- CSS variables (--peek-start, --peek-in) drive smooth animations
- event-based communication (bufo-hide) for clean separation

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

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

Changed files
+84 -15
static
+43
static/bufo-peek.js
··· 1 + // bufo peeking animation logic 2 + // this file handles the silly but delightful bufo that peeks from random edges 3 + 4 + (function() { 5 + const peekingBufo = document.getElementById('peekingBufo'); 6 + let hasSearched = false; 7 + 8 + // animation cycle duration (must match CSS animation duration) 9 + const PEEK_CYCLE_MS = 6000; 10 + 11 + // function to set random bufo position 12 + function setRandomBufoPosition() { 13 + const positions = ['top', 'right', 'bottom', 'left']; 14 + 15 + // remove all position classes 16 + peekingBufo.classList.remove( 17 + 'peeking-bufo-top', 18 + 'peeking-bufo-right', 19 + 'peeking-bufo-bottom', 20 + 'peeking-bufo-left' 21 + ); 22 + 23 + // set new random position 24 + const position = positions[Math.floor(Math.random() * positions.length)]; 25 + peekingBufo.classList.add(`peeking-bufo-${position}`); 26 + } 27 + 28 + // set initial position 29 + setRandomBufoPosition(); 30 + 31 + // move to new position after each peek cycle 32 + setInterval(() => { 33 + if (!hasSearched) { 34 + setRandomBufoPosition(); 35 + } 36 + }, PEEK_CYCLE_MS); 37 + 38 + // hide bufo after first search 39 + window.addEventListener('bufo-hide', () => { 40 + hasSearched = true; 41 + peekingBufo.classList.add('hidden'); 42 + }); 43 + })();
+41 -15
static/index.html
··· 202 202 padding: 40px; 203 203 } 204 204 205 + @keyframes peek-in-out { 206 + 0% { 207 + transform: var(--peek-start); 208 + } 209 + 20% { 210 + transform: var(--peek-in); 211 + } 212 + 80% { 213 + transform: var(--peek-in); 214 + } 215 + 100% { 216 + transform: var(--peek-start); 217 + } 218 + } 219 + 205 220 .peeking-bufo { 206 221 position: fixed; 207 222 pointer-events: none; 208 223 z-index: 1000; 209 - top: 50%; 210 224 width: 200px; 211 225 height: auto; 212 - transition: opacity 0.3s ease-out; 226 + animation: peek-in-out 6s ease-in-out infinite; 213 227 } 214 228 215 229 .peeking-bufo.hidden { 216 230 opacity: 0; 217 231 pointer-events: none; 232 + animation: none; 218 233 } 219 234 220 235 .peeking-bufo-right { 221 - right: 0; 222 - transform: translateY(-50%); 236 + right: -200px; 237 + top: 50%; 238 + --peek-start: translateY(-50%); 239 + --peek-in: translateX(-200px) translateY(-50%); 240 + } 241 + 242 + .peeking-bufo-bottom { 243 + bottom: -200px; 244 + left: 50%; 245 + --peek-start: translateX(-50%) rotate(90deg); 246 + --peek-in: translateX(-50%) translateY(-200px) rotate(90deg); 223 247 } 224 248 225 249 .peeking-bufo-left { 226 - left: 0; 227 - transform: translateY(-50%) scaleX(-1); 250 + left: -200px; 251 + top: 50%; 252 + --peek-start: translateY(-50%) scaleX(-1); 253 + --peek-in: translateX(200px) translateY(-50%) scaleX(-1); 254 + } 255 + 256 + .peeking-bufo-top { 257 + top: -200px; 258 + left: 50%; 259 + --peek-start: translateX(-50%) rotate(-90deg); 260 + --peek-in: translateX(-50%) translateY(200px) rotate(-90deg); 228 261 } 229 262 230 263 @media (max-width: 1024px) { ··· 266 299 267 300 <img src="https://all-the.bufo.zone/bufo-just-checking.gif" alt="bufo peeking" class="peeking-bufo" id="peekingBufo"> 268 301 302 + <script src="/static/bufo-peek.js"></script> 269 303 <script> 270 304 const searchInput = document.getElementById('searchInput'); 271 305 const searchButton = document.getElementById('searchButton'); 272 306 const resultsDiv = document.getElementById('results'); 273 307 const loadingDiv = document.getElementById('loading'); 274 308 const errorDiv = document.getElementById('error'); 275 - const peekingBufo = document.getElementById('peekingBufo'); 276 - 277 - // randomly position bufo on left or right 278 - if (Math.random() < 0.5) { 279 - peekingBufo.classList.add('peeking-bufo-left'); 280 - } else { 281 - peekingBufo.classList.add('peeking-bufo-right'); 282 - } 283 309 284 310 let hasSearched = false; 285 311 ··· 289 315 290 316 // hide bufo after first search 291 317 if (!hasSearched) { 292 - peekingBufo.classList.add('hidden'); 318 + window.dispatchEvent(new Event('bufo-hide')); 293 319 hasSearched = true; 294 320 } 295 321