1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>Hank Green</title> 7<link rel="icon" type="image/x-icon" href="favicon.ico"> 8<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png"> 9<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png"> 10<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
11<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-0578044100560991" 12 crossorigin="anonymous"></script>
12 13<style> 14 /* ===== RESET & BASE ===== */ 15 *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } 16 17 :root { 18 /* 4Ã3 / Smush blue-on-paper palette ("Bluefish") */ 19 --bg: #faf7f1; /* warm paper */ 20 --text: #2b2620; /* ink */ 21 --text-light: #7a7264; 22 /* NOTE: still named --orange for historical reasons; primary accent is now 4Ã3 blue */ 23 --orange: #5b8def; 24 --orange-hover:#3a6fd8; 25 --orange-dark: #3a6fd8; 26 /* The four game colors (+ readable-on-paper variants for small text) */ 27 --c-blue: #5b8def; 28 --c-green: #56b870; 29 --c-gold: #e7b416; 30 --c-purple: #a06ee1; 31 --c-green-ink: #3f9a5c; 32 --c-gold-ink: #c39310; 33 --c-purple-ink:#8f5fd6; 34 --card: #ffffff; 35 --line: #d8d2c6; 36 /* Depth palette: blue â green â gold â purple â coral â mint â deep */ 37 --fn-bg-1: #EDF2FD; /* blue tint */ 38 --fn-bg-2: #E7F4EB; /* green tint */ 39 --fn-bg-3: #FAF1D4; /* gold tint */ 40 --fn-bg-4: #F0E8FA; /* purple tint */ 41 --fn-bg-5: #FBE7E4; /* soft coral */ 42 --fn-bg-6: #DCEFEA; /* cool mint */ 43 --fn-bg-7: #2D2A3A; /* deep slate */ 44 } 45 46 html { 47 font-size: 18px; 48 scroll-behavior: smooth; 49 } 50 51 body { 52 font-family: Georgia, 'Times New Roman', 'Palatino Linotype', serif; 53 /* faint paper-grain noise over a whisper of warm fade, borrowed from 4Ã3 */ 54 background: 55 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.04'/%3E%3C/svg%3E") fixed, 56 var(--bg) linear-gradient(180deg,#fdfbf6 0%,var(--bg) 45%,#f5efe3 100%) fixed; 57 color: var(--text); 58 line-height: 1.75; 59 min-height: 100vh; 60 -webkit-font-smoothing: antialiased; 61 } 62 63 /* ===== SITE PHOTO (bottom-right) ===== */ 64 .site-photo { 65 position: fixed; 66 bottom: 0; 67 right: 0; 68 max-height: 95vh; 69 width: auto; 70 max-width: 48vw; 71 z-index: 0; 72 pointer-events: none; 73 user-select: none; 74 opacity: 1; 75 } 76 .bg-image { 77 position: fixed; 78 bottom: 0; 79 right: 0; 80 width: 480px; 81 max-width: 45vw; 82 opacity: 0.32; 83 pointer-events: none; 84 z-index: 0; 85 user-select: none; 86 } 87 .bg-glow { 88 display: none; 89 } 90 91 /* ===== CONTENT AREA ===== */ 92 .content { 93 position: relative; 94 z-index: 1; 95 max-width: 620px; 96 padding: 3.5rem 2rem 8rem; 97 margin: 0 auto; 98 } 99 @media (min-width: 1100px) { 100 .content { 101 margin-left: max(8%, calc(50% - 480px)); 102 } 103 .featured-box { 104 position: absolute; 105 left: calc(100% + 2.5rem); 106 top: 6rem; /* default, overridden by JS to align with first paragraph */ 107 width: 250px; 108 margin-bottom: 0; 109 } 110 .accolades-btn { 111 margin-bottom: 0.6rem; 112 } 113 } 114 115 /* ===== MOBILE HERO HEADING ===== */ 116 .mobile-hero { 117 display: none; 118 } 119 @media (max-width: 600px) { 120 .mobile-hero { 121 display: block; 122 font-family: Georgia, 'Times New Roman', 'Palatino Linotype', serif; 123 font-size: 2.2rem; 124 font-weight: 700; 125 line-height: 1.15; 126 color: var(--text); 127 margin-bottom: 1.2rem; 128 } 129 .mobile-hero .fn-ref { font-size: inherit; } 130 .mobile-hero .fn-num { font-size: 0.35em; } 131 .greeting-inline { display: none; } 132 } 133 134 /* ===== PARAGRAPHS ===== */ 135 .paragraph-block { 136 margin-bottom: 1.6rem; 137 } 138 .paragraph-block p { 139 text-indent: 0; 140 } 141 142 /* ===== FOOTNOTE REFERENCES ===== */ 143 .fn-ref { 144 cursor: pointer; 145 position: relative; 146 transition: color 0.15s; 147 } 148 .fn-ref:hover, .fn-ref:focus-visible { color: var(--orange); } 149 .fn-ref.active { color: var(--orange); } 150 .fn-ref:focus { outline: none; } 151 .fn-ref:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; border-radius: 2px; } 152 .fn-ref[data-fn="name"] { color: var(--orange); font-weight: 600; } 153 154 .fn-num { 155 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 156 font-size: 0.6em; 157 font-weight: 700; 158 color: var(--orange); 159 margin-left: 1px; 160 vertical-align: super; 161 line-height: 1; 162 transition: color 0.15s, transform 0.15s; 163 display: inline-block; 164 } 165 .fn-ref:hover .fn-num { 166 color: var(--orange-hover); 167 transform: scale(1.15); 168 } 169 170 /* ===== POPOVER BUBBLES ===== */ 171 .fn-popover { 172 position: absolute; 173 width: 380px; 174 max-height: 340px; 175 overflow: hidden; 176 display: flex; 177 flex-direction: column; 178 border-radius: 12px; 179 border: 1px solid rgba(42,32,24,0.08); 180 box-shadow: 0 8px 32px rgba(42,32,24,0.16), 0 2px 8px rgba(42,32,24,0.08); 181 padding: 0; 182 animation: popIn 0.2s cubic-bezier(0.34,1.56,0.64,1); 183 } 184 .fn-popover.closing { 185 opacity: 0; 186 transform: scale(0.94); 187 transition: opacity 0.15s ease, transform 0.15s ease; 188 pointer-events: none; 189 } 190 .fn-popover.depth-1 { background: var(--fn-bg-1); } 191 .fn-popover.depth-2 { background: var(--fn-bg-2); } 192 .fn-popover.depth-3 { background: var(--fn-bg-3); } 193 .fn-popover.depth-4 { background: var(--fn-bg-4); } 194 .fn-popover.depth-5 { background: var(--fn-bg-5); } 195 .fn-popover.depth-6 { background: var(--fn-bg-6); } 196 .fn-popover.depth-7 { 197 background: var(--fn-bg-7); 198 border-color: rgba(255,255,255,0.1); 199 box-shadow: 0 10px 40px rgba(0,0,0,0.3), 0 2px 10px rgba(0,0,0,0.15); 200 } 201 .fn-popover.depth-7 .fn-pop-header { border-bottom-color: rgba(255,255,255,0.08); } 202 .fn-popover.depth-7 .fn-label { color: #C4B0F0; background: rgba(160,130,220,0.2); } 203 .fn-popover.depth-7 .fn-close { color: #C4B0F0; } 204 .fn-popover.depth-7 .fn-close:hover { background: #C4B0F0; color: var(--fn-bg-7); } 205 .fn-popover.depth-7 .fn-pop-body { color: #E8E4F0; } 206 .fn-popover.depth-7 .fn-pop-body a { color: #A8D0C8; } 207 .fn-popover.depth-7 .fn-pop-body a:hover { color: #C8F0E8; } 208 .fn-popover.depth-7 .fn-arrow { border-color: rgba(255,255,255,0.1); }
209 .fn-popover.depth-7 .fn-ref { color: #E8E4F0; } 210 .fn-popover.depth-7 .fn-ref:hover { color: #C4B0F0; } 211 .fn-popover.depth-7 .fn-num { color: #C4B0F0; } 212 213 214 .fn-pop-body::-webkit-scrollbar { width: 5px; } 215 .fn-pop-body::-webkit-scrollbar-thumb { background: rgba(91,141,239,0.25); border-radius: 3px; } 216 .fn-pop-body::-webkit-scrollbar-track { background: transparent; } 217 218 @keyframes popIn { 219 from { opacity: 0; transform: scale(0.9); } 220 to { opacity: 1; transform: scale(1); } 221 } 222 223 /* Arrow */ 224 .fn-arrow { 225 position: absolute; 226 width: 14px; 227 height: 14px; 228 background: inherit; 229 border: 1px solid rgba(42,32,24,0.08); 230 transform: rotate(45deg); 231 z-index: -1; 232 } 233 .fn-arrow.arrow-down { 234 bottom: -7px; 235 border-top: none; 236 border-left: none; 237 box-shadow: 3px 3px 4px rgba(42,32,24,0.06); 238 } 239 .fn-arrow.arrow-up { 240 top: -7px; 241 border-bottom: none; 242 border-right: none; 243 box-shadow: -1px -1px 3px rgba(42,32,24,0.04); 244 } 245 246 /* Popover header & body */ 247 .fn-pop-header { 248 display: flex; 249 align-items: center; 250 justify-content: space-between; 251 padding: 0.55rem 0.8rem 0.35rem; 252 border-bottom: 1px solid rgba(42,32,24,0.06); 253 background: inherit; 254 border-radius: 12px 12px 0 0; 255 z-index: 1; 256 flex-shrink: 0; 257 } 258 .fn-label { 259 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 260 font-size: 0.65rem; 261 font-weight: 700; 262 color: var(--orange); 263 background: rgba(91,141,239,0.1); 264 padding: 0.08rem 0.4rem; 265 border-radius: 3px; 266 letter-spacing: 0.03em; 267 } 268 .fn-close { 269 background: none; 270 border: none; 271 color: var(--orange); 272 font-size: 1.2rem; 273 font-weight: 700; 274 cursor: pointer; 275 line-height: 1; 276 padding: 0.1rem 0.3rem; 277 border-radius: 4px; 278 transition: background 0.12s, color 0.12s; 279 } 280 .fn-close:hover, .fn-close:focus-visible { 281 background: var(--orange); 282 color: #fff; 283 } 284 .fn-close:focus { outline: none; } 285 .fn-close:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; } 286 287 .fn-pop-body { 288 padding: 0.55rem 0.85rem 0.75rem; 289 font-size: 0.88rem; 290 line-height: 1.6; 291 color: var(--text); 292 overflow-y: auto; 293 flex: 1; 294 min-height: 0; 295 overscroll-behavior: contain; 296 } 297 .fn-pop-body a { 298 color: var(--orange-dark); 299 text-decoration: underline; 300 text-underline-offset: 2px; 301 transition: color 0.12s; 302 }
303 .fn-pop-body a:hover, .fn-pop-body a:focus-visible { color: var(--orange); } 304 .fn-pop-body a:focus { outline: none; } 305 .fn-pop-body a:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; border-radius: 2px; } 306 .fn-pop-body em { font-style: italic; } 307 .fn-pop-body img { max-width: 100%; margin: 0.4rem 0; border-radius: 4px; } 308 309 /* ===== MOBILE ===== */ 310 @media (max-width: 600px) { 311 html { font-size: 16px; } 312 .content { padding: 2.5rem 1.3rem 4rem; } 313 .bg-image { display: none; } 314 .site-photo { display: none; } 315 .socials-row { margin-top: 2rem; justify-content: center; } 316 .featured-box { font-size: 0.85rem; margin-top: 2rem; margin-bottom: 0; } 317 .achv-toast { max-width: 260px; } 318 .socials-panel { 319 left: auto; 320 right: 0; 321 min-width: 0; 322 max-width: 80vw; 323 } 324 325 .fn-popover { 326 width: min(340px, 90vw); 327 } 328 .fn-ref { 329 padding: 2px 1px; 330 } 331 .fn-num { 332 font-size: 0.7em; 333 padding: 4px 2px; 334 } 335 } 336 337 /* ===== SOCIALS BUTTON & PANEL ===== */ 338 .socials-row { 339 display: flex; 340 gap: 0.5rem; 341 flex-wrap: wrap; 342 margin-top: 2.5rem; 343 } 344 .socials-wrap { 345 position: relative; 346 display: inline-block; 347 } 348 .socials-btn { 349 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 350 font-size: 0.72rem; 351 font-weight: 500; 352 color: var(--text-light); 353 background: rgba(91,141,239,0.07); 354 border: none; 355 border-radius: 6px; 356 padding: 0.3rem 0.55rem; 357 cursor: pointer; 358 letter-spacing: 0.01em; 359 transition: color 0.15s, background 0.15s; 360 } 361 .socials-btn:hover, .socials-btn:focus-visible { color: var(--orange); background: rgba(91,141,239,0.10); } 362 .socials-btn:active { transform: scale(0.97); } 363 .socials-btn:focus { outline: none; } 364 .socials-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } 365 .socials-btn.open { background: rgba(91,141,239,0.14); } 366 .socials-panel { 367 position: absolute; 368 bottom: calc(100% + 8px); 369 left: 0; 370 background: var(--bg); 371 border: 1px solid rgba(42,32,24,0.1); 372 border-radius: 10px; 373 box-shadow: 0 6px 24px rgba(42,32,24,0.15); 374 padding: 0.5rem 0; 375 min-width: 200px; 376 z-index: 10; 377 animation: panelIn 0.2s cubic-bezier(0.34,1.56,0.64,1); 378 } 379 @keyframes panelIn { 380 from { opacity: 0; transform: translateY(6px) scale(0.95); } 381 to { opacity: 1; transform: translateY(0) scale(1); } 382 } 383 .socials-panel a { 384 display: block; 385 padding: 0.3rem 0.85rem; 386 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 387 font-size: 0.75rem; 388 font-weight: 500; 389 color: var(--text); 390 text-decoration: none; 391 transition: background 0.12s, color 0.12s; 392 white-space: nowrap; 393 } 394 .socials-panel a:hover, .socials-panel a:focus-visible { 395 background: rgba(91,141,239,0.07); 396 color: var(--orange); 397 } 398 .socials-panel a:focus { outline: none; } 399 .socials-panel a:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; } 400 401 /* ===== FEATURED BOX ===== */ 402 .featured-box { 403 background: rgba(91,141,239,0.05); 404 border: 1px solid rgba(91,141,239,0.18); 405 border-radius: 12px; 406 padding: 1rem 1.2rem 1.1rem; 407 margin-bottom: 2rem; 408 font-size: 0.84rem; 409 line-height: 1.55; 410 } 411 .featured-label { 412 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 413 font-size: 0.55rem; 414 font-weight: 700; 415 color: var(--orange); 416 text-transform: uppercase; 417 letter-spacing: 0.08em; 418 display: block; 419 margin-bottom: 0.6rem; 420 } 421 .featured-item { 422 margin-bottom: 0.9rem; 423 padding-bottom: 0.9rem; 424 border-bottom: 1px solid rgba(91,141,239,0.08); 425 } 426 .featured-item:last-of-type { 427 margin-bottom: 0; 428 padding-bottom: 0; 429 border-bottom: none; 430 } 431 .featured-item-img { 432 width: 100%; 433 border-radius: 6px; 434 margin-bottom: 0.5rem; 435 display: block; 436 } 437 .featured-item-title { 438 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 439 font-size: 0.82rem; 440 font-weight: 700; 441 color: var(--text); 442 margin-bottom: 0.2rem; 443 line-height: 1.3; 444 } 445 .featured-item-text { 446 color: var(--text-light); 447 font-size: 0.78rem; 448 line-height: 1.5; 449 margin-bottom: 0.35rem; 450 } 451 .featured-item-cta { 452 display: inline-block; 453 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 454 font-size: 0.7rem; 455 font-weight: 600; 456 color: var(--orange); 457 text-decoration: none; 458 border-bottom: 1px solid rgba(91,141,239,0.25); 459 transition: color 0.15s; 460 padding-bottom: 1px; 461 } 462 .featured-item-cta:hover, .featured-item-cta:focus-visible { 463 color: var(--orange-hover); 464 } 465 .featured-item-cta:focus { outline: none; } 466 .featured-item-cta:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } 467 .featured-box a { 468 color: var(--orange-dark); 469 font-weight: 600; 470 text-decoration: none; 471 border-bottom: 1px solid rgba(200,72,0,0.25); 472 transition: color 0.15s; 473 } 474 .featured-box a:hover { 475 color: var(--orange); 476 } 477 .newsletter-link { 478 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 479 font-size: 0.7rem; 480 line-height: 1.5; 481 color: var(--text-light); 482 margin-bottom: 0.75rem; 483 padding-bottom: 0.75rem; 484 border-bottom: 1px dashed rgba(91,141,239,0.15); 485 } 486 .newsletter-link a { 487 color: var(--orange); 488 font-weight: 600; 489 text-decoration: none; 490 transition: color 0.15s; 491 } 492 .newsletter-link a:hover { 493 color: var(--orange-hover); 494 } 495
496 /* ===== LITTLE TOUCHES ===== */ 497 ::selection { background: rgba(91,141,239,0.2); } 498 499 @keyframes fadeUp { 500 from { opacity: 0; transform: translateY(12px); } 501 to { opacity: 1; transform: translateY(0); } 502 } 503 504 /* Subtle scrim when popovers are open */ 505 .popover-scrim { 506 display: none; 507 position: fixed; 508 inset: 0; 509 background: rgba(42,32,24,0.06); 510 z-index: 49; 511 transition: opacity 0.2s; 512 } 513 .popover-scrim.active { 514 display: block; 515 } 516 517 /* ===== EASTER EGG GAME ===== */ 518 .game-sprite { 519 position: fixed; 520 z-index: 1000; 521 cursor: pointer; 522 user-select: none; 523 opacity: 0.82; 524 border-radius: 8px; 525 transition: transform 0.15s ease; 526 } 527 .game-sprite:hover { 528 transform: scale(1.12); 529 opacity: 1; 530 } 531 .game-toast { 532 position: fixed; 533 top: 50%; 534 left: 50%; 535 transform: translate(-50%,-50%) scale(0.8); 536 background: #2A2018; 537 color: #fff; 538 padding: 1rem 2rem; 539 border-radius: 12px; 540 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 541 font-size: 1.1rem; 542 font-weight: 700; 543 z-index: 1001; 544 box-shadow: 0 8px 32px rgba(0,0,0,0.3); 545 opacity: 0; 546 animation: gameToastIn 0.4s ease forwards; 547 } 548 @keyframes gameToastIn { 549 to { opacity: 1; transform: translate(-50%,-50%) scale(1); } 550 } 551 552 /* ===== ACHIEVEMENTS ===== */ 553 .achv-toast { 554 position: fixed; 555 top: 50%; 556 left: 50%; 557 transform: translate(-50%, -50%); 558 background: #2A2018; 559 color: #fff; 560 border-radius: 12px; 561 padding: 0.75rem 1.3rem 0.7rem; 562 z-index: 9000; 563 box-shadow: 0 8px 32px rgba(0,0,0,0.25); 564 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 565 max-width: 320px; 566 pointer-events: none; 567 text-align: center; 568 animation: achvIn 0.35s cubic-bezier(0.34,1.56,0.64,1) forwards; 569 overflow: visible; 570 } 571 .achv-toast.out { 572 animation: achvOut 0.4s ease forwards; 573 } 574 .achv-title { 575 font-size: 0.82rem; 576 font-weight: 700; 577 color: var(--orange-hover); 578 display: inline; 579 vertical-align: middle; 580 } 581 .achv-desc { 582 font-size: 0.72rem; 583 color: rgba(255,255,255,0.7); 584 margin-top: 0.2rem; 585 line-height: 1.45; 586 } 587 @keyframes achvIn { 588 from { opacity: 0; transform: translate(-50%, -50%) scale(0.85); } 589 to { opacity: 1; transform: translate(-50%, -50%) scale(1); } 590 } 591 @keyframes achvOut { 592 from { opacity: 1; transform: translate(-50%, -50%) scale(1); } 593 to { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 594 } 595 .confetti-piece { 596 position: fixed; 597 width: 8px; 598 height: 8px; 599 border-radius: 2px; 600 z-index: 9001; 601 pointer-events: none; 602 } 603 604 /* ===== META CELEBRATION ===== */ 605 @keyframes pageDance { 606 0% { transform: rotate(0deg) translateX(0); } 607 10% { transform: rotate(-2deg) translateX(-8px); } 608 20% { transform: rotate(2deg) translateX(8px); } 609 30% { transform: rotate(-1.5deg) translateX(-5px) translateY(-3px); } 610 40% { transform: rotate(1.5deg) translateX(5px) translateY(3px); } 611 50% { transform: rotate(-2.5deg) translateX(-6px); } 612 60% { transform: rotate(2.5deg) translateX(6px); } 613 70% { transform: rotate(-1deg) translateX(-4px) translateY(-2px); } 614 80% { transform: rotate(1deg) translateX(4px) translateY(2px); } 615 90% { transform: rotate(-0.5deg) translateX(-2px); } 616 100% { transform: rotate(0deg) translateX(0); } 617 } 618 body.dancing .content, 619 body.dancing .featured-box, 620 body.dancing .geo-toggle, 621 body.dancing .site-photo { 622 animation: pageDance 0.6s ease-in-out infinite; 623 } 624 body.dancing .featured-box { animation-delay: 0.1s; } 625 body.dancing .geo-toggle { animation-delay: 0.2s; } 626 .bean { 627 position: fixed; 628 z-index: 8999; 629 pointer-events: none; 630 user-select: none; 631 } 632 633 /* ===== ACHIEVEMENT TRACKER ===== */ 634 .achv-tracker { 635 display: none; 636 flex-wrap: wrap; 637 gap: 0.25rem; 638 margin-top: 1rem; 639 } 640 .achv-tracker.visible { 641 display: flex; 642 } 643 .achv-slot { 644 position: relative; 645 width: 1.5rem; 646 height: 1.5rem; 647 border-radius: 5px; 648 background: rgba(91,141,239,0.06); 649 border: 1px solid rgba(91,141,239,0.2); 650 display: none;
651 align-items: center; 652 justify-content: center; 653 font-size: 0.7rem; 654 transition: transform 0.2s; 655 cursor: default; 656 } 657 .achv-slot.unlocked { 658 display: flex; 659 } 660 .achv-slot:hover { transform: scale(1.15); } 661 662 /* ===== PALM TREE / GEOCITIES MODE ===== */ 663 .geo-toggle { 664 position: fixed; 665 /* clears the Web Things bar; --drawer-push moves it down in sync 666 with the menu drawer sliding open */ 667 top: calc(3.1rem + var(--drawer-push, 0px)); 668 right: 1rem; 669 z-index: 9999; 670 background: none; 671 border: none; 672 font-size: 1.6rem; 673 cursor: pointer; 674 opacity: 0.55; 675 transition: opacity 0.2s, transform 0.2s, 676 top 0.5s cubic-bezier(0.3,1.36,0.5,1); 677 padding: 0.3rem; 678 line-height: 1; 679 } 680 .geo-toggle:hover, .geo-toggle:focus-visible { opacity: 1; transform: scale(1.15) rotate(-10deg); } 681 .geo-toggle:focus { outline: none; } 682 .geo-toggle:focus-visible { outline: 2px solid var(--orange); outline-offset: 3px; border-radius: 8px; } 683 body.geocities .geo-toggle { opacity: 1; } 684 685 body.geocities { 686 --bg: #000033; 687 --text: #00ff00; 688 --text-light: #00cc00; 689 --orange: #ffff00; 690 --orange-hover: #ff00ff; 691 --orange-dark: #ff6600; 692 --fn-bg-1: #ffff00; 693 --fn-bg-2: #ff69b4; 694 --fn-bg-3: #00ffff; 695 --fn-bg-4: #00ff00; 696 --fn-bg-5: #ff8c00; 697 --fn-bg-6: #da70d6; 698 --fn-bg-7: #ff0000; 699 background-color: #000033; 700 background-image: 701 radial-gradient(circle at 15% 25%, #fff 0.5px, transparent 0.5px), 702 radial-gradient(circle at 35% 65%, #aaf 0.5px, transparent 0.5px), 703 radial-gradient(circle at 55% 15%, #fff 0.7px, transparent 0.7px), 704 radial-gradient(circle at 75% 45%, #ccf 0.5px, transparent 0.5px), 705 radial-gradient(circle at 90% 80%, #fff 0.6px, transparent 0.6px), 706 radial-gradient(circle at 45% 85%, #aaf 0.4px, transparent 0.4px), 707 radial-gradient(circle at 8% 70%, #fff 0.5px, transparent 0.5px), 708 radial-gradient(circle at 65% 35%, #ccf 0.6px, transparent 0.6px); 709 background-size: 250px 250px; 710 font-family: 'Comic Sans MS', 'Comic Sans', 'Chalkboard SE', cursive; 711 } 712 713 body.geocities .site-photo { 714 image-rendering: pixelated; 715 border: 3px solid #ff00ff; 716 border-radius: 0; 717 opacity: 0.6; 718 } 719 720 body.geocities .fn-num { 721 animation: geoBlink 1s step-end infinite; 722 color: #ff0000; 723 } 724 725 body.geocities .fn-popover { 726 border: 3px outset #999; 727 border-radius: 0; 728 box-shadow: 5px 5px 0 rgba(0,0,0,0.5); 729 font-family: 'Comic Sans MS', cursive; 730 } 731 body.geocities .fn-popover .fn-pop-header { border-radius: 0; } 732 body.geocities .fn-popover .fn-pop-body { color: #000; } 733 body.geocities .fn-popover .fn-pop-body a { color: #0000EE; text-decoration: underline; } 734 body.geocities .fn-popover .fn-pop-body a:hover { color: #ff00ff; } 735 body.geocities .fn-popover .fn-label { 736 background: #000; color: #0f0; font-family: 'Courier New', monospace; border-radius: 0; 737 } 738 body.geocities .fn-popover .fn-close:hover { background: #ff0000; color: #fff; } 739 body.geocities .fn-popover .fn-arrow { display: none; } 740 body.geocities .fn-popover.depth-7 .fn-pop-body { color: #ffff00; } 741 body.geocities .fn-popover.depth-7 .fn-label { background: #800000; color: #ffff00; } 742 743 body.geocities .featured-box { 744 background: #000066; border: 2px outset #888; border-radius: 0; color: #00ffff; 745 } 746 body.geocities .featured-label { color: #ff00ff; } 747 body.geocities .featured-item { border-bottom-color: #333399; } 748 body.geocities .featured-item-title { color: #ffff00; } 749 body.geocities .featured-item-text { color: #00ffff; } 750 body.geocities .featured-item-cta { color: #00ff00; border-bottom-color: #00ff00; } 751 body.geocities .featured-box a { color: #ffff00; border-bottom-color: #ffff00; } 752 753 body.geocities .socials-btn { background: #000066; border: 2px outset #888; border-radius: 0; color: #00ffff; font-family: 'Comic Sans MS', cursive; } 754 body.geocities .socials-btn:hover { background: #000099; } 755 body.geocities .socials-panel { background: #000066; border: 2px outset #888; border-radius: 0; } 756 body.geocities .socials-panel a { color: #00ffff; font-family: 'Comic Sans MS', cursive; } 757 body.geocities .socials-panel a:hover { background: #000033; color: #ff00ff; } 758 759 body.geocities ::selection { background: #ff00ff; color: #fff; } 760 761 .geo-marquee { 762 position: fixed; top: 0; left: 0; right: 0; 763 background: linear-gradient(90deg, #ff0000, #ff8800, #ffff00, #00ff00, #0088ff, #8800ff, #ff0000); 764 color: #fff; text-shadow: 1px 1px 0 #000; 765 font-family: 'Comic Sans MS', cursive; font-size: 0.85rem; font-weight: 700; 766 padding: 0.3rem 0; z-index: 9998; overflow: hidden; white-space: nowrap; 767 } 768 .geo-marquee span { 769 display: inline-block; padding-left: 100%; 770 animation: geoScroll 14s linear infinite; 771 } 772 @keyframes geoScroll { 773 from { transform: translateX(0); } 774 to { transform: translateX(-100%); } 775 } 776 777 .geo-counter { 778 position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); 779 font-family: 'Comic Sans MS', cursive; font-size: 0.75rem; color: #888; 780 z-index: 9998; text-align: center; 781 } 782 .geo-counter .counter-num { 783 font-family: 'Courier New', monospace; background: #000; color: #0f0; 784 padding: 0.15rem 0.5rem;
784 border: 1px inset #555; letter-spacing: 2px; 785 } 786 .geo-counter a { color: #00ccff; } 787 788 .geo-construction { 789 text-align: center; color: #ffff00; font-size: 0.85rem; font-weight: 700; 790 padding: 0.5rem; font-family: 'Comic Sans MS', cursive; 791 animation: geoBlink 1s step-end infinite; 792 } 793 @keyframes geoBlink { 794 0%, 49% { opacity: 1; } 795 50%, 100% { opacity: 0; } 796 } 797 798 /* Guestbook */ 799 .geo-gb-overlay { 800 display: none; position: fixed; inset: 0; 801 background: rgba(0,0,51,0.7); z-index: 10000; 802 align-items: center; justify-content: center; 803 } 804 .geo-gb-overlay.active { display: flex; } 805 .geo-gb-popup { 806 background: #c0c0c0; border: 3px outset #fff; 807 width: 420px; max-width: 92vw; max-height: 80vh; 808 display: flex; flex-direction: column; 809 font-family: 'Comic Sans MS', cursive; font-size: 0.82rem; 810 } 811 .geo-gb-titlebar { 812 background: linear-gradient(90deg, #000080, #1084d0); 813 color: #fff; font-weight: 700; font-size: 0.78rem; 814 padding: 3px 6px; display: flex; align-items: center; justify-content: space-between; 815 } 816 .geo-gb-close { 817 background: #c0c0c0; border: 2px outset #fff; 818 font-size: 0.7rem; font-weight: 700; cursor: pointer; 819 padding: 0 4px; line-height: 1.3; color: #000; 820 } 821 .geo-gb-body { padding: 0.6rem; overflow-y: auto; flex: 1; } 822 .geo-gb-entry { 823 border-bottom: 1px solid #999; padding: 0.4rem 0; color: #000; 824 } 825 .geo-gb-entry .gb-name { color: #0000EE; font-weight: 700; } 826 .geo-gb-entry .gb-date { color: #666; font-size: 0.7rem; } 827 .geo-gb-entry .gb-msg { margin-top: 2px; } 828 .geo-gb-form { border-top: 2px inset #999; padding: 0.5rem; background: #d4d0c8; } 829 .geo-gb-form label { display: block; font-weight: 700; font-size: 0.72rem; margin-top: 0.3rem; color: #000; } 830 .geo-gb-form input, .geo-gb-form textarea { 831 width: 100%; border: 2px inset #888; padding: 3px 5px; 832 font-family: 'Comic Sans MS', cursive; font-size: 0.8rem; background: #fff; 833 } 834 .geo-gb-form textarea { height: 45px; resize: none; } 835 .geo-gb-form button { 836 margin-top: 0.4rem; background: #c0c0c0; border: 2px outset #fff; 837 padding: 3px 16px; font-family: 'Comic Sans MS', cursive; 838 font-weight: 700; cursor: pointer; font-size: 0.78rem; 839 } 840 .geo-gb-form button:active { border-style: inset; } 841 .geo-gb-thanks { padding: 0.5rem; text-align: center; color: #000; font-size: 0.78rem; line-height: 1.5; } 842 .geo-gb-thanks b { color: #ff00ff; } 843 844 @media (max-width: 600px) { 845 body.geocities .fn-popover { border-radius: 0 !important; } 846 body.geocities .fn-popover .fn-pop-header { border-radius: 0 !important; } 847 } 848 849 /* ===== CLASSIC MODE ===== */ 850 body.classic { 851 --bg: #ffffff; 852 --text: #000000; 853 --text-light: #333333; 854 --orange: #0000EE; 855 --orange-hover: #0000EE; 856 --orange-dark: #551A8B; 857 --fn-bg-1: #ffffee; 858 --fn-bg-2: #eeffee; 859 --fn-bg-3: #eeeeff; 860 --fn-bg-4: #ffeeee; 861 --fn-bg-5: #fff0ee; 862 --fn-bg-6: #eeffff; 863 --fn-bg-7: #2A2A3A; 864 } 865 body.classic .fn-ref:hover { color: #0000EE; } 866 body.classic .fn-popover { border-radius: 0; border-color: #999; box-shadow: 2px 2px 4px rgba(0,0,0,0.2); } 867 body.classic .fn-popover .fn-pop-header { border-radius: 0; background: #ddd; } 868 body.classic .fn-popover .fn-label { color: #333; background: rgba(0,0,0,0.1); } 869 body.classic .fn-popover .fn-close { color: #333; } 870 body.classic .fn-popover .fn-close:hover { background: #cc0000; color: #fff; } 871 body.classic .fn-popover .fn-arrow { display: none; } 872 body.classic .fn-popover .fn-pop-body a { color: #0000EE; text-decoration: underline; } 873 body.classic .fn-popover .fn-pop-body a:hover { color: #cc0000; } 874 body.classic .socials-btn { border-radius: 0; border: 1px solid #999; background: #eee; color: #000; } 875 body.classic .socials-btn:hover { background: #ddd; color: #0000EE; } 876 body.classic .socials-btn.open { background: #ddd; } 877 body.classic .socials-panel { border-radius: 0; border: 1px solid #999; box-shadow: 2px 2px 4px rgba(0,0,0,0.15); } 878 body.classic .socials-panel a { color: #0000EE; } 879 body.classic .socials-panel a:hover { color: #cc0000; background: #ffffcc; } 880 body.classic .featured-box { border-radius: 0; border: 1px solid #ccc; background: #fafafa; } 881 body.classic .featured-label { color: #333; } 882 body.classic .featured-item-title { color: #000; } 883 body.classic .featured-item-text { color: #333; } 884 body.classic .featured-item-cta { color: #0000EE; border-bottom-color: #0000EE; } 885 body.classic .featured-box a { color: #0000EE; } 886 body.classic .newsletter-link a { color: #0000EE; } 887 body.classic .site-photo { filter: none; } 888 body.classic .bg-image { opacity: 0; } 889 body.classic .accolades-btn { border-radius: 0; border: 1px solid #999; color: #333; background: #eee; } 890 body.classic .accolades-btn:hover { color: #0000EE; border-color: #0000EE; } 891 body.classic .accolades-card { border-radius: 0; border: 1px solid #999; } 892 body.classic .accolades-title { color: #000; } 893 body.classic .accolades-detail { color: #333; } 894 body.classic .accolades-arrow { border-radius: 0; border: 1px solid #999; color: #333; } 895 body.classic .accolades-arrow:hover { color: #0000EE; border-color: #0000EE; } 896 body.classic .accolades-dot.active { background: #0000EE; }
897 body.classic .achv-toast { border-radius: 0; } 898 body.classic ::selection { background: #3366cc; color: #fff; } 899 900 .classic-toggle { 901 position: fixed; 902 top: 1rem; 903 left: 1rem; 904 z-index: 9999; 905 background: none; 906 border: none; 907 font-family: -apple-system, system-ui, sans-serif; 908 font-size: 0.65rem; 909 cursor: pointer; 910 opacity: 0.35; 911 transition: opacity 0.2s; 912 padding: 0.25rem 0.4rem; 913 color: var(--text-light); 914 } 915 .classic-toggle:hover { opacity: 0.8; } 916 body.classic .classic-toggle { opacity: 0.7; color: #0000EE; } 917 918 /* ===== ACCOLADES SLIDESHOW ===== */ 919 .accolades-btn { 920 display: inline-block; 921 margin-bottom: 1.2rem; 922 padding: 0.5rem 1.3rem; 923 background: rgba(91,141,239,0.03); 924 border: 1px dashed rgba(91,141,239,0.15); 925 border-radius: 6px; 926 cursor: pointer; 927 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 928 font-size: 0.82rem; 929 font-weight: 600; 930 color: rgba(90,74,58,0.45); 931 opacity: 0.7; 932 letter-spacing: 0.04em; 933 text-transform: uppercase; 934 transition: color 0.15s, border-color 0.15s, background 0.15s, opacity 0.15s; 935 } 936 .accolades-btn:hover, .accolades-btn:focus-visible { 937 color: var(--orange); 938 border-color: var(--orange); 939 background: rgba(91,141,239,0.10); 940 opacity: 1; 941 } 942 .accolades-btn:focus { outline: none; } 943 .accolades-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } 944 945 .accolades-overlay { 946 display: none; 947 position: fixed; 948 inset: 0; 949 background: rgba(42,32,24,0.55); 950 z-index: 8000; 951 align-items: center; 952 justify-content: center; 953 animation: accoladesFadeIn 0.25s ease; 954 } 955 .accolades-overlay.active { 956 display: flex; 957 } 958 @keyframes accoladesFadeIn { 959 from { opacity: 0; } 960 to { opacity: 1; } 961 } 962 963 .accolades-card { 964 position: relative; 965 background: var(--bg); 966 border: 1px solid rgba(42,32,24,0.10); 967 border-radius: 14px; 968 box-shadow: 0 12px 48px rgba(42,32,24,0.20), 0 2px 10px rgba(42,32,24,0.08); 969 width: 420px; 970 max-width: 90vw; 971 padding: 2rem 2rem 1.5rem; 972 text-align: center; 973 user-select: none; 974 overflow: hidden; 975 } 976 977 .accolades-close { 978 position: absolute; 979 top: 0.6rem; 980 right: 0.7rem; 981 background: none; 982 border: none; 983 font-size: 1.3rem; 984 font-weight: 700; 985 color: var(--text-light); 986 cursor: pointer; 987 padding: 0.15rem 0.4rem; 988 border-radius: 5px; 989 line-height: 1; 990 transition: background 0.12s, color 0.12s; 991 } 992 .accolades-close:hover, .accolades-close:focus-visible { 993 background: rgba(91,141,239,0.10); 994 color: var(--orange); 995 } 996 .accolades-close:focus { outline: none; } 997 .accolades-close:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; } 998 999 .accolades-label { 1000 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 1001 font-size: 0.58rem; 1002 font-weight: 700; 1003 color: var(--orange); 1004 text-transform: uppercase; 1005 letter-spacing: 0.1em; 1006 margin-bottom: 1rem; 1007 display: block; 1008 } 1009 1010 .accolades-slide { 1011 height: 180px; 1012 display: flex; 1013 flex-direction: column; 1014 align-items: center; 1015 justify-content: center; 1016 } 1017 1018 .accolades-year { 1019 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 1020 font-size: 0.65rem; 1021 font-weight: 600; 1022 color: var(--orange); 1023 background: rgba(91,141,239,0.08); 1024 padding: 0.1rem 0.5rem; 1025 border-radius: 10px; 1026 margin-bottom: 0.65rem; 1027 display: inline-block; 1028 } 1029 1030 .accolades-title { 1031 font-family: Georgia, 'Times New Roman', 'Palatino Linotype', serif; 1032 font-size: 1.3rem; 1033 font-weight: 700; 1034 color: var(--text); 1035 line-height: 1.3; 1036 margin-bottom: 0.5rem; 1037 } 1038 1039 .accolades-detail { 1040 font-family: Georgia, 'Times New Roman', 'Palatino Linotype', serif; 1041 font-size: 0.85rem; 1042 color: var(--text-light); 1043 line-height: 1.55; 1044 max-width: 340px; 1045 margin: 0 auto; 1046 } 1047 1048 .accolades-link { 1049 display: inline-block; 1050 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 1051 font-size: 0.78rem; 1052 font-weight: 600; 1053 color: var(--orange); 1054 text-decoration: none; 1055 margin-top: 0.6rem; 1056 transition: color 0.15s; 1057 } 1058 .accolades-link:hover { 1059 color: var(--orange-hover); 1060 } 1061 body.geocities .accolades-link { color: #00ff00; } 1062 body.geocities .accolades-link:hover { color: #ff00ff; } 1063 1064 .accolades-nav { 1065 display: flex; 1066 align-items: center; 1067 justify-content: center; 1068 gap: 0.75rem; 1069 margin-top: 1.3rem; 1070 } 1071 1072 .accolades-arrow { 1073 background: none; 1074 border: 1px solid rgba(42,32,24,0.12); 1075 border-radius: 50%; 1076 width: 32px; 1077 height: 32px; 1078 display: flex; 1079 align-items: center; 1080 justify-content: center; 1081 cursor: pointer; 1082 font-size: 0.85rem; 1083 color: var(--text-light); 1084 transition: border-color 0.15s, color 0.15s, background 0.15s; 1085 flex-shrink: 0; 1086 } 1087 .accolades-arrow:hover, .accolades-arrow:focus-visible { 1088 border-color: var(--orange); 1089 color: var(--orange); 1090 background: rgba(91,141,239,0.05); 1091 } 1092 .accolades-arrow:focus { outline: none; } 1093 .accolades-arrow:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } 1094 1095 .accolades-dots { 1096 display: flex; 1097 gap: 0.35rem;
1098 align-items: center; 1099 } 1100 .accolades-dot { 1101 width: 6px; 1102 height: 6px; 1103 border-radius: 50%; 1104 background: rgba(42,32,24,0.15); 1105 transition: background 0.2s, transform 0.2s; 1106 cursor: pointer; 1107 border: none; 1108 padding: 0; 1109 } 1110 .accolades-dot.active { 1111 background: var(--orange); 1112 transform: scale(1.3); 1113 } 1114 .accolades-dot:hover, .accolades-dot:focus-visible { 1115 background: var(--orange-hover); 1116 } 1117 .accolades-dot:focus { outline: none; } 1118 .accolades-dot:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } 1119 1120 /* Slide transition */ 1121 .accolades-slide { 1122 opacity: 1; 1123 transform: translateX(0); 1124 transition: opacity 0.25s ease, transform 0.25s ease; 1125 } 1126 .accolades-slide.slide-out-left { 1127 opacity: 0; 1128 transform: translateX(-30px); 1129 } 1130 .accolades-slide.slide-out-right { 1131 opacity: 0; 1132 transform: translateX(30px); 1133 } 1134 .accolades-slide.slide-in { 1135 opacity: 0; 1136 transform: translateX(0); 1137 } 1138 1139 /* Geocities mode override */ 1140 body.geocities .accolades-card { 1141 background: #000066; 1142 border: 3px outset #888; 1143 border-radius: 0; 1144 box-shadow: 5px 5px 0 rgba(0,0,0,0.5); 1145 color: #00ff00; 1146 } 1147 body.geocities .accolades-title { color: #ffff00; } 1148 body.geocities .accolades-detail { color: #00ffff; } 1149 body.geocities .accolades-year { background: #000033; color: #ff00ff; } 1150 body.geocities .accolades-label { color: #ff00ff; } 1151 body.geocities .accolades-close { color: #00ffff; } 1152 body.geocities .accolades-close:hover { background: #ff0000; color: #fff; } 1153 body.geocities .accolades-arrow { border-color: #333399; color: #00ffff; } 1154 body.geocities .accolades-arrow:hover { border-color: #ff00ff; color: #ff00ff; background: #000033; } 1155 body.geocities .accolades-dot { background: #333399; } 1156 body.geocities .accolades-dot.active { background: #ff00ff; } 1157 body.geocities .accolades-btn { background: #000066; border-color: #333399; color: #00ffff; border-radius: 0; } 1158 body.geocities .accolades-btn:hover { color: #ff00ff; border-color: #ff00ff; } 1159 1160 @media (max-width: 600px) { 1161 .accolades-card { 1162 padding: 1.5rem 1.3rem 1.2rem; 1163 } 1164 .accolades-title { 1165 font-size: 1.1rem; 1166 } 1167 } 1168 1169 1170 /* ===== TOP MENU BAR (game-tile tabs) ===== */ 1171 .code-bar { 1172 position: relative; 1173 z-index: 20; 1174 padding-top: 4px; 1175 background: linear-gradient(180deg, #fdfbf6, var(--bg)); 1176 border-bottom: 1px solid var(--line); 1177 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 1178 } 1179 /* accent strip: all five category colors when closed... */ 1180 .code-bar::before { 1181 content: ''; 1182 position: absolute; 1183 top: 0; left: 0; right: 0; 1184 height: 4px; 1185 background: linear-gradient(90deg, 1186 #5b8def 0 20%, #3f9a5c 20% 40%, #8f5fd6 40% 60%, 1187 #c39310 60% 80%, #d05b5b 80% 100%); 1188 } 1189 /* ...fading to the open category's color */ 1190 .code-bar::after { 1191 content: ''; 1192 position: absolute; 1193 top: 0; left: 0; right: 0; 1194 height: 4px; 1195 background: var(--bar-accent, var(--c-blue)); 1196 opacity: 0; 1197 transition: opacity 0.3s ease, background 0.35s ease; 1198 } 1199 .code-bar.open::after { opacity: 1; } 1200 .code-bar-tabs { 1201 display: flex; 1202 gap: 0.4rem; 1203 padding: 0.5rem 0.75rem 0.55rem; 1204 overflow-x: auto; 1205 scrollbar-width: none; 1206 } 1207 .code-bar-tabs::-webkit-scrollbar { display: none; } 1208 /* center when they fit, scroll when they don't */ 1209 .code-bar-tab:first-child { margin-left: auto; } 1210 .code-bar-tab:last-child { margin-right: auto; } 1211 .code-bar-tab { 1212 position: relative; 1213 flex: 0 0 auto; 1214 background: var(--card); 1215 border: 1.5px solid var(--line); 1216 border-radius: 9px; 1217 box-shadow: 0 2px 0 var(--line); 1218 cursor: pointer; 1219 padding: 0.32rem 0.7rem 0.36rem; 1220 font-family: inherit; 1221 font-size: 0.64rem; 1222 font-weight: 700; 1223 letter-spacing: 0.12em; 1224 text-transform: uppercase; 1225 color: var(--text-light); 1226 white-space: nowrap; 1227 transition: color 0.15s, background 0.2s, border-color 0.2s, 1228 transform 0.2s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.2s; 1229 } 1230 .code-bar-tab:hover { 1231 color: var(--tc); 1232 border-color: var(--tc); 1233 transform: translateY(-2px); 1234 box-shadow: 0 4px 0 color-mix(in srgb, var(--tc) 45%, transparent); 1235 } 1236 .code-bar-tab:active { transform: translateY(0); } 1237 .code-bar-tab:focus { outline: none; } 1238 .code-bar-tab:focus-visible { outline: 2px solid var(--tc); outline-offset: 2px; } 1239 .code-bar-tab.active { 1240 color: #fff; 1241 background: var(--tc); 1242 border-color: var(--tc); 1243 transform: none; 1244 box-shadow: 0 2px 0 color-mix(in srgb, var(--tc) 60%, #2b2620); 1245 } 1246 .code-bar-tab.active:hover { transform: none; } 1247 .code-bar-tab .chev { 1248 display: inline-block; 1249 font-size: 0.6rem; 1250 margin-left: 2px; 1251 opacity: 0.7; 1252 transition: transform 0.4s cubic-bezier(0.34,1.4,0.64,1); 1253 } 1254 .code-bar-tab.active .chev { transform: rotate(180deg); opacity: 1; } 1255 .code-bar-drawer { 1256 /* height is animated in px by JS so the rubbery overshoot actually 1257 shows (fr-based grid animation clamps at content height, which 1258 killed the bounce and left the pelican out of sync) */ 1259 height: 0; 1260 overflow: hidden; 1261 /* a whisper of the open category's color washes the drawer, 1262 darkened a touch so it reads as recessed into the page */ 1263 background: color-mix(in srgb, var(--bar-accent, var(--c-blue)) 8%, 1264 color-mix(in srgb, #2b2620 4%, var(--bg))); 1265 transition: height 0.5s cubic-bezier(0.3,1.36,0.5,1), background 0.35s ease; 1266 /* sunken well: soft shadows cast onto the drawer from all edges.
1267 They persist while the drawer slides shut â invisible at 0 height. */ 1268 box-shadow: 1269 inset 0 10px 14px -10px rgba(42,32,24,0.35), 1270 inset 0 -9px 12px -10px rgba(42,32,24,0.22), 1271 inset 12px 0 14px -14px rgba(42,32,24,0.20), 1272 inset -12px 0 14px -14px rgba(42,32,24,0.20), 1273 inset 0 1px 0 rgba(42,32,24,0.10); 1274 } 1275 .code-carousel { 1276 display: none; 1277 gap: 1rem; 1278 padding: 1.1rem 1.2rem 1.35rem; 1279 overflow-x: auto; 1280 scroll-snap-type: x proximity; 1281 -webkit-overflow-scrolling: touch; 1282 } 1283 .code-carousel.active { display: flex; } 1284 .code-carousel.slide-r { animation: panelR 0.3s ease; } 1285 .code-carousel.slide-l { animation: panelL 0.3s ease; } 1286 @keyframes panelR { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } } 1287 @keyframes panelL { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: none; } } 1288 /* center the row when it fits, stay scrollable when it doesn't */ 1289 .code-card:first-child { margin-left: auto; } 1290 .code-card:last-child { margin-right: auto; } 1291 .code-carousel { scrollbar-width: none; } 1292 .code-carousel::-webkit-scrollbar { display: none; } 1293 .code-card { 1294 display: block; 1295 flex: 0 0 205px; 1296 scroll-snap-align: center; 1297 background: var(--card); 1298 border: 1.5px solid var(--line); 1299 border-radius: 12px; 1300 overflow: hidden; 1301 text-decoration: none; 1302 color: var(--text); 1303 box-shadow: 0 2px 0 var(--line); 1304 transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.25s, border-color 0.25s; 1305 } 1306 .code-card:hover, .code-card:focus-visible { 1307 transform: translateY(-4px); 1308 border-color: var(--cc); 1309 box-shadow: 0 6px 0 var(--cc), 0 10px 24px rgba(42,32,24,0.12); 1310 } 1311 .code-card:focus { outline: none; } 1312 .code-card:focus-visible { outline: 2px solid var(--cc); outline-offset: 2px; } 1313 .code-card-thumb { 1314 display: block; 1315 height: 108px; 1316 background: color-mix(in srgb, var(--cc) 14%, #ffffff); 1317 border-bottom: 1.5px solid var(--line); 1318 overflow: hidden; 1319 } 1320 .code-card-thumb img { 1321 width: 100%; 1322 height: 100%; 1323 object-fit: cover; 1324 display: block; 1325 transition: transform 0.25s ease-out; 1326 } 1327 .code-card-thumb img.contain { object-fit: contain; padding: 8px; } 1328 .code-card:hover .code-card-thumb img { transform: scale(1.05); } 1329 .code-card-body { display: block; padding: 0.6rem 0.75rem 0.7rem; } 1330 .code-card-title { 1331 display: block; 1332 font-size: 0.8rem; 1333 font-weight: 800; 1334 line-height: 1.2; 1335 color: var(--cc); 1336 } 1337 .code-card-tagline { 1338 display: block; 1339 font-family: Georgia, 'Times New Roman', serif; 1340 font-style: italic; 1341 font-size: 0.68rem; 1342 color: var(--text-light); 1343 line-height: 1.4; 1344 margin-top: 0.15rem; 1345 } 1346 @media (max-width: 600px) { 1347 .code-bar-tab { padding: 0.45rem 0.6rem 0.5rem; letter-spacing: 0.1em; } 1348 .code-card { flex-basis: 175px; } 1349 .code-card-thumb { height: 92px; } 1350 } 1351 body.geocities .code-bar { display: none; } 1352 1353</style> 1354</head> 1355<body> 1356 1357<!-- Top menu bar (Web Things / Video / Podcasts / ...) is built from 1358 siteContent.topMenu by the main script below, after content.js loads. --> 1359 1360<div class="bg-glow"></div> 1361<div class="popover-scrim" id="scrim"></div> 1362 1363<main class="content" id="site-content"></main> 1364 1365 1366<button class="geo-toggle" id="geo-toggle" title="Geocities mode"><img src="pelican-blue.png" alt="" style="width:28px;height:28px;object-fit:contain;"></button> 1367
1368<script src="content.js"></script>
1368 1369
1370<script> 1371(function() { 1372 'use strict'; 1373 1374 const container = document.getElementById('site-content'); 1375 const scrim = document.getElementById('scrim'); 1376 const FN_REGEX = /\{\{fn:([^}]+)\}\}([\s\S]*?)\{\{\/fn\}\}/g; 1377 1378 // ââ Footnote number assignment ââ 1379 // Each popup gets its own numbering (1, 2, 3...) 1380 const fnNumbers = {}; 1381 1382 function assignChildNumbers(text) { 1383 const re = new RegExp(FN_REGEX.source, 'g'); 1384 let match; 1385 let localCounter = 0; 1386 while ((match = re.exec(text)) !== null) { 1387 const id = match[1]; 1388 if (!fnNumbers[id]) { 1389 fnNumbers[id] = ++localCounter; 1390 const fn = siteContent.footnotes[id]; 1391 if (fn) assignChildNumbers(fn.content); 1392 } 1393 } 1394 } 1395 (function() { 1396 let topCounter = 0; 1397 siteContent.paragraphs.forEach(function(p) { 1398 const re = new RegExp(FN_REGEX.source, 'g'); 1399 let match; 1400 while ((match = re.exec(p.text)) !== null) { 1401 const id = match[1]; 1402 if (!fnNumbers[id]) { 1403 fnNumbers[id] = ++topCounter; 1404 const fn = siteContent.footnotes[id]; 1405 if (fn) assignChildNumbers(fn.content); 1406 } 1407 } 1408 }); 1409 })(); 1410 1411 // ââ HTML escape ââ 1412 function esc(s) { 1413 return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 1414 } 1415 1416 // ââ Parse markers into clickable refs ââ 1417 function parseMarkers(text, isHtml) { 1418 if (!isHtml) { 1419 var parts = [], lastIdx = 0; 1420 var re = new RegExp(FN_REGEX.source, 'g'), m; 1421 while ((m = re.exec(text)) !== null) { 1422 if (m.index > lastIdx) parts.push(esc(text.slice(lastIdx, m.index))); 1423 var id = m[1], display = esc(m[2]), num = fnNumbers[id] || '?'; 1424 parts.push('<span class="fn-ref" data-fn="' + id + '" tabindex="0" role="button">' + display + 1425 '<sup class="fn-num">' + num + '</sup></span>'); 1426 lastIdx = m.index + m[0].length; 1427 } 1428 if (lastIdx < text.length) parts.push(esc(text.slice(lastIdx))); 1429 return parts.join(''); 1430 } else { 1431 return text.replace(new RegExp(FN_REGEX.source, 'g'), function(_, id, display) { 1432 var num = fnNumbers[id] || '?'; 1433 return '<span class="fn-ref" data-fn="' + id + '" tabindex="0" role="button">' + display + 1434 '<sup class="fn-num">' + num + '</sup></span>'; 1435 }); 1436 } 1437 } 1438 1439 // ââ Photo (bottom-right, or fallback to decorative bg) ââ 1440 // DISABLED for now (removed the headshot). To bring it back, un-comment 1441 // this block; it reads siteContent.photo ("hank-bw.png" in content.js). 1442 /* 1443 (function() { 1444 var src = siteContent.photo || null; 1445 if (src) { 1446 var img = document.createElement('img'); 1447 img.className = 'site-photo'; 1448 img.src = src; 1449 img.alt = ''; 1450 img.onerror = function() { this.style.display = 'none'; }; 1451 document.body.appendChild(img); 1452 } else { 1453 // Fallback decorative bg image 1454 var bg = document.createElement('img'); 1455 bg.className = 'bg-image'; 1456 bg.src = 'image.webp'; 1457 bg.alt = ''; 1458 bg.onerror = function() { this.style.display = 'none'; }; 1459 document.body.appendChild(bg); 1460 } 1461 })(); 1462 */ 1463 1464 // ââ Mobile hero heading ââ 1465 // Pull "Hello, I'm Hank." into a big heading for narrow screens 1466 (function() { 1467 var firstText = siteContent.paragraphs[0] && siteContent.paragraphs[0].text || ''; 1468 var greetMatch = firstText.match(/^(.*?\{\{fn:name\}\}.*?\{\{\/fn\}\}\.)\s*/); 1469 if (greetMatch) { 1470 var hero = document.createElement('h1'); 1471 hero.className = 'mobile-hero'; 1472 hero.innerHTML = parseMarkers(greetMatch[1], false); 1473 container.appendChild(hero); 1474 } 1475 })(); 1476 1477 // ââ Build paragraphs ââ 1478 siteContent.paragraphs.forEach(function(p, idx) { 1479 var block = document.createElement('div'); 1480 block.className = 'paragraph-block'; 1481 var para = document.createElement('p'); 1482 var text = p.text; 1483 // On the first paragraph, wrap the greeting portion so we can hide it on mobile 1484 if (idx === 0) { 1485 var greetMatch = text.match(/^(.*?\{\{fn:name\}\}.*?\{\{\/fn\}\}\.)\s*/); 1486 if (greetMatch) { 1487 var greetHtml = parseMarkers(greetMatch[1], false); 1488 var restHtml = parseMarkers(text.slice(greetMatch[0].length), false); 1489 para.innerHTML = '<span class="greeting-inline">' + greetHtml + ' </span>' + restHtml; 1490 } else { 1491 para.innerHTML = parseMarkers(text, false); 1492 } 1493 } else { 1494 para.innerHTML = parseMarkers(text, false); 1495 } 1496 block.appendChild(para); 1497 container.appendChild(block); 1498 }); 1499 1500 // ââ Featured box (optional) ââ
1501 // Supports both old format { text, link } and new format [{ title, text, cta, link, image }] 1502 // Rendered after paragraphs so it appears below text on narrow screens; 1503 // on wide screens, position:absolute pulls it to the right sidebar. 1504 var featItems = null; 1505 var featEl = null; 1506 if (siteContent.featured) { 1507 if (Array.isArray(siteContent.featured)) { 1508 featItems = siteContent.featured; 1509 } else if (siteContent.featured.text) { 1510 featItems = [siteContent.featured]; 1511 } 1512 } 1513 if (featItems && featItems.length) { 1514 var feat = document.createElement('div'); 1515 feat.className = 'featured-box'; 1516 // Newsletter link (above featured items) 1517 var nlHtml = ''; 1518 if (siteContent.newsletter && siteContent.newsletter.url) { 1519 nlHtml = '<div class="newsletter-link"><a href="' + esc(siteContent.newsletter.url) + 1520 '" target="_blank">' + (siteContent.newsletter.text || 'Subscribe to our newsletter') + 1521 ' →</a></div>'; 1522 } 1523 feat.innerHTML = nlHtml + '<span class="featured-label">Featured</span>'; 1524 featItems.forEach(function(item) { 1525 var div = document.createElement('div'); 1526 div.className = 'featured-item'; 1527 var html = ''; 1528 if (item.image) { 1529 html += '<img class="featured-item-img" src="' + esc(item.image) + '" alt="">'; 1530 } 1531 if (item.title) { 1532 html += '<div class="featured-item-title">' + item.title + '</div>'; 1533 } 1534 if (item.text) { 1535 html += '<div class="featured-item-text">' + parseMarkers(item.text, false) + '</div>'; 1536 } 1537 if (item.cta && item.link) { 1538 html += '<a class="featured-item-cta" href="' + esc(item.link) + '" target="_blank">' + esc(item.cta) + ' →</a>'; 1539 } else if (item.link) { 1540 html += '<a class="featured-item-cta" href="' + esc(item.link) + '" target="_blank">Learn more →</a>'; 1541 } 1542 div.innerHTML = html; 1543 feat.appendChild(div); 1544 }); 1545 container.appendChild(feat); 1546 featEl = feat; 1547 } 1548 1549 // ââ Align featured box with first paragraph on wide screens ââ 1550 // (called after all DOM insertions, including TL;DR button) 1551 function alignFeatured() { 1552 if (!featEl) return; 1553 var firstPara = container.querySelector('.paragraph-block'); 1554 if (firstPara && window.innerWidth >= 1100) { 1555 featEl.style.top = firstPara.offsetTop + 'px'; 1556 } else { 1557 featEl.style.top = ''; 1558 } 1559 } 1560 window.addEventListener('resize', alignFeatured); 1561 1562 // ââ Achievement tracker (inside featured box) ââ 1563 var achvTracker = document.createElement('div'); 1564 achvTracker.className = 'achv-tracker'; 1565 if (featEl) { 1566 featEl.appendChild(achvTracker); 1567 } else { 1568 container.appendChild(achvTracker); 1569 } 1570 1571 // ââ Link buttons (Socials, YouTube, Podcasts, Companies, Books) ââ 1572 var addMuseumButton = null; 1573 if (siteContent.socials && siteContent.socials.length) { 1574 var platformNames = { 1575 youtube: 'YouTube', instagram: 'Instagram', tiktok: 'TikTok', 1576 bluesky: 'Bluesky', threads: 'Threads', twitter: 'Twitter', 1577 x: 'X', mastodon: 'Mastodon', facebook: 'Facebook', linkedin: 'LinkedIn', 1578 podcasts: 'Podcasts', companies: 'Companies', books: 'Books' 1579 }; 1580 // Split into groups: dropdown buttons vs plain socials 1581 var buttonOrder = ['youtube', 'podcasts', 'companies', 'books']; 1582 var dropdownEntries = {}; 1583 var plainSocials = []; 1584 siteContent.socials.forEach(function(s) { 1585 if (buttonOrder.indexOf(s.platform) >= 0 && s.sublinks) { 1586 dropdownEntries[s.platform] = s; 1587 } else if (!s.sublinks) { 1588 plainSocials.push(s); 1589 } 1590 }); 1591 1592 var row = document.createElement('div'); 1593 row.className = 'socials-row'; 1594 var activePanel = null; 1595 var activeBtn = null; 1596 1597 var clickedBtns = {}; 1598 var totalButtons = 1 + buttonOrder.filter(function(k) { return dropdownEntries[k]; }).length; 1599 1600 function closePanel() { 1601 if (activeBtn) { activeBtn.setAttribute('aria-expanded', 'false'); } 1602 if (activePanel) { activePanel.remove(); activePanel = null; } 1603 if (activeBtn) { activeBtn.classList.remove('open'); activeBtn = null; } 1604 } 1605 1606 function makeButton(label, links) { 1607 var wrap = document.createElement('div'); 1608 wrap.className = 'socials-wrap'; 1609 var btn = document.createElement('button'); 1610 btn.className = 'socials-btn'; 1611 btn.textContent = label; 1612 btn.setAttribute('aria-expanded', 'false'); 1613 btn.setAttribute('aria-haspopup', 'true'); 1614 wrap.appendChild(btn); 1615 btn.addEventListener('click', function(e) { 1616 e.stopPropagation(); 1617 clickedBtns[label] = true; 1618 var allClicked = Object.keys(clickedBtns).length >= totalButtons; 1619 if (allClicked) unlockAchievement('a-fan'); 1620 if (activeBtn === btn) { closePanel(); btn.focus(); return; } 1621 closePanel(); 1622 var panel = document.createElement('div'); 1623 panel.className = 'socials-panel'; 1624 panel.setAttribute('role', 'menu'); 1625 var panelLinks = []; 1626 links.forEach(function(lnk) { 1627 var a = document.createElement('a'); 1628 a.href = lnk.url; 1629 a.target = '_blank'; 1630 a.rel = 'noopener'; 1631 a.textContent = lnk.label; 1632 a.setAttribute('role', 'menuitem'); 1633 panel.appendChild(a); 1634 panelLinks.push(a); 1635 }); 1636 // Arrow key navigation within panel 1637 panel.addEventListener('keydown', function(ev) { 1638 var idx = panelLinks.indexOf(document.activeElement); 1639 if (ev.key === 'ArrowDown' || ev.key === 'ArrowRight') { 1640 ev.preventDefault(); 1641 var next = (idx + 1) % panelLinks.length; 1642 panelLinks[next].focus(); 1643 } else if (ev.key === 'ArrowUp' || ev.key === 'ArrowLeft') { 1644 ev.preventDefault(); 1645 var prev = (idx - 1 + panelLinks.length) % panelLinks.length; 1646 panelLinks[prev].focus(); 1647 } else if (ev.key === 'Escape') { 1648 ev.preventDefault(); 1649 ev.stopPropagation(); 1650 closePanel(); 1651 btn.focus(); 1652 } 1653 }); 1654 wrap.appendChild(panel); 1655 activePanel = panel; 1656 activeBtn = btn; 1657 btn.classList.add('open'); 1658 btn.setAttribute('aria-expanded', 'true'); 1659 // Focus first link in panel 1660 if (panelLinks.length) panelLinks[0].focus(); 1661 }); 1662 return wrap; 1663 } 1664 1665 // "Socials" button â plain social platforms 1666 if (plainSocials.length) { 1667 var socialLinks = plainSocials.map(function(s) { 1668 return { label: platformNames[s.platform] || s.platform, url: s.url }; 1669 }); 1670 row.appendChild(makeButton('Socials', socialLinks)); 1671 } 1672 // Dropdown buttons in order 1673 buttonOrder.forEach(function(key) { 1674 var entry = dropdownEntries[key]; 1675 if (entry) { 1676 row.appendChild(makeButton(platformNames[key] || key, entry.sublinks)); 1677 } 1678 }); 1679 1680 // Museum of Former Projects â appears after "A Fan!" achievement 1681 var museumLinks = siteContent.museum || []; 1682 var museumAdded = false;
1683 addMuseumButton = function() { 1684 if (museumAdded) return; 1685 museumAdded = true; 1686 var wrap = makeButton('Museum of Former Projects', museumLinks); 1687 row.appendChild(wrap); 1688 }; 1689 1690 document.addEventListener('click', closePanel); 1691 container.appendChild(row); 1692 } 1693 1694 // âââââââââââââââââââââââââââââââââââââââââââ 1695 // POPOVER SYSTEM 1696 // âââââââââââââââââââââââââââââââââââââââââââ 1697 1698 var openPopovers = []; // { id, el, parentId } 1699 var nextZ = 50; 1700 1701 function findPopover(fnId) { 1702 for (var i = 0; i < openPopovers.length; i++) { 1703 if (openPopovers[i].id === fnId) return openPopovers[i]; 1704 } 1705 return null; 1706 } 1707 1708 function closePopover(fnId, animate) { 1709 // Gather this + all descendants 1710 var toClose = []; 1711 var rootEntry = findPopover(fnId); 1712 var returnFocusTo = rootEntry ? rootEntry.triggerRef : null; 1713 function gather(id) { 1714 toClose.push(id); 1715 openPopovers.forEach(function(p) { 1716 if (p.parentId === id) gather(p.id); 1717 }); 1718 } 1719 gather(fnId); 1720 1721 toClose.forEach(function(id) { 1722 var entry = findPopover(id); 1723 if (!entry) return; 1724 // Unmark ref 1725 document.querySelectorAll('.fn-ref[data-fn="' + id + '"]').forEach(function(r) { 1726 r.classList.remove('active'); 1727 }); 1728 if (animate !== false) { 1729 entry.el.classList.add('closing'); 1730 setTimeout(function() { entry.el.remove(); }, 160); 1731 } else { 1732 entry.el.remove(); 1733 } 1734 var idx = openPopovers.indexOf(entry); 1735 if (idx >= 0) openPopovers.splice(idx, 1); 1736 }); 1737 1738 updateScrim(); 1739 1740 // Return focus to the element that triggered the closed popover 1741 if (returnFocusTo && document.body.contains(returnFocusTo)) { 1742 returnFocusTo.focus(); 1743 } 1744 } 1745 1746 function closeAll(animate) { 1747 var ids = openPopovers.map(function(p) { return p.id; }); 1748 ids.forEach(function(id) { closePopover(id, animate); }); 1749 } 1750 1751 function updateScrim() { 1752 scrim.classList.toggle('active', openPopovers.length > 0); 1753 } 1754 1755 function openPopover(ref, fnId) { 1756 var footnote = siteContent.footnotes[fnId]; 1757 if (!footnote) return; 1758 1759 // Is this ref inside an existing popover? 1760 var parentPop = ref.closest('.fn-popover'); 1761 var parentId = parentPop ? parentPop.dataset.fn : null; 1762 1763 // If top-level (not inside a popover), close all existing 1764 if (!parentId) { 1765 closeAll(true); 1766 } 1767 1768 // Calculate depth 1769 var depth = 1; 1770 if (parentId) { 1771 var parentEntry = findPopover(parentId); 1772 depth = parentEntry ? (parseInt(parentEntry.el.dataset.depth) + 1) : 2; 1773 } 1774 var depthClass = 'depth-' + (((depth-1)%7)+1); 1775 1776 // Build popover 1777 var pop = document.createElement('div'); 1778 pop.className = 'fn-popover ' + depthClass; 1779 pop.dataset.fn = fnId; 1780 pop.dataset.depth = depth; 1781 pop.style.zIndex = nextZ++; 1782 pop.setAttribute('role', 'dialog'); 1783 pop.setAttribute('aria-label', 'Footnote ' + (fnNumbers[fnId] || fnId)); 1784 1785 var num = fnNumbers[fnId] || '?'; 1786 1787 var arrow = document.createElement('div'); 1788 arrow.className = 'fn-arrow'; 1789 1790 pop.innerHTML = 1791 '<div class="fn-pop-header">' + 1792 '<span class="fn-label">' + num + '</span>' + 1793 '<button class="fn-close" aria-label="Close">×</button>' + 1794 '</div>' + 1795 '<div class="fn-pop-body">' + parseMarkers(footnote.content, true) + '</div>'; 1796 1797 pop.insertBefore(arrow, pop.firstChild); 1798 1799 // Add to DOM hidden, measure, then position 1800 pop.style.visibility = 'hidden'; 1801 pop.style.left = '0px'; 1802 pop.style.top = '0px'; 1803 document.body.appendChild(pop); 1804 1805 var popW = pop.offsetWidth; 1806 var popH = pop.offsetHeight; 1807 var refRect = ref.getBoundingClientRect(); 1808 var scrollT = window.pageYOffset || document.documentElement.scrollTop; 1809 var scrollL = window.pageXOffset || document.documentElement.scrollLeft; 1810 var vpW = window.innerWidth; 1811 1812 var gap = 12; 1813 var above = true; 1814 1815 // Try above 1816 var top = refRect.top + scrollT - popH - gap; 1817 if (top < scrollT + 10) { 1818 // Not enough room above, go below 1819 top = refRect.bottom + scrollT + gap; 1820 above = false;
1821 } 1822 // If below also off-screen, just go below anyway 1823 var left = refRect.left + scrollL + (refRect.width / 2) - (popW / 2); 1824 left = Math.max(scrollL + 8, Math.min(left, scrollL + vpW - popW - 8)); 1825 1826 // Position arrow 1827 var arrowLeftPx = refRect.left + scrollL + (refRect.width / 2) - left - 7; 1828 arrowLeftPx = Math.max(14, Math.min(arrowLeftPx, popW - 28)); 1829 arrow.style.left = arrowLeftPx + 'px'; 1830 1831 if (above) { 1832 arrow.classList.add('arrow-down'); 1833 arrow.style.bottom = '-7px'; 1834 } else { 1835 arrow.classList.add('arrow-up'); 1836 arrow.style.top = '-7px'; 1837 } 1838 1839 // Set transform-origin for animation 1840 pop.style.transformOrigin = arrowLeftPx + 'px ' + (above ? 'bottom' : 'top'); 1841 1842 pop.style.top = top + 'px'; 1843 pop.style.left = left + 'px'; 1844 pop.style.visibility = 'visible'; 1845 1846 // Mark ref 1847 ref.classList.add('active'); 1848 1849 // Track 1850 openPopovers.push({ id: fnId, el: pop, parentId: parentId, triggerRef: ref }); 1851 updateScrim(); 1852 1853 // Move focus to close button for keyboard users 1854 var closeButton = pop.querySelector('.fn-close'); 1855 if (closeButton) closeButton.focus(); 1856 1857 // Easter egg trigger 1858 if (fnId === 'game1') { 1859 startEasterEggGame(); 1860 unlockAchievement('warned-you'); 1861 } 1862 1863 // Achievement check 1864 checkAchievements(fnId, depth); 1865 } 1866 1867 // ââ X-click tip tracker ââ 1868 var xClicks = 0; 1869 var xTipShown = false; 1870 var xClickTimer = null; 1871 1872 // ââ Global click handler ââ 1873 document.addEventListener('click', function(e) { 1874 // Close button inside popover 1875 if (e.target.closest('.fn-close')) { 1876 var pop = e.target.closest('.fn-popover'); 1877 if (pop) { 1878 e.stopPropagation(); 1879 var remainAfter = openPopovers.length - 1; 1880 closePopover(pop.dataset.fn, true); 1881 // Track consecutive X clicks â only tip if more popovers remain 1882 if (!xTipShown) { 1883 xClicks++; 1884 clearTimeout(xClickTimer); 1885 xClickTimer = setTimeout(function() { xClicks = 0; }, 3000); 1886 if (xClicks >= 2 && remainAfter > 0) { 1887 xTipShown = true; 1888 var tip = document.createElement('div'); 1889 tip.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' + 1890 'background:#2A2018;color:#fff;border-radius:12px;padding:0.8rem 1.3rem;z-index:9000;' + 1891 'box-shadow:0 8px 32px rgba(0,0,0,0.25);font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;' + 1892 'font-size:0.82rem;text-align:center;max-width:300px;line-height:1.5;' + 1893 'animation:achvIn 0.35s cubic-bezier(0.34,1.56,0.64,1) forwards'; 1894 tip.textContent = 'Hey! FYI, you don\u2019t have to keep clicking the Xs \u2014 just click anywhere that isn\u2019t a footnote and they\u2019ll all go away.'; 1895 document.body.appendChild(tip); 1896 setTimeout(function() { 1897 tip.style.opacity = '0'; 1898 tip.style.transition = 'opacity 0.4s'; 1899 setTimeout(function() { tip.remove(); }, 400); 1900 }, 4000); 1901 } 1902 } 1903 } 1904 return; 1905 } 1906 1907 // Footnote ref clicked 1908 var ref = e.target.closest('.fn-ref'); 1909 if (ref) { 1910 e.preventDefault(); 1911 e.stopPropagation(); 1912 var fnId = ref.dataset.fn; 1913 // Toggle if already open 1914 if (findPopover(fnId)) { 1915 closePopover(fnId, true); 1916 } else { 1917 openPopover(ref, fnId); 1918 } 1919 return; 1920 } 1921 1922 // Click on scrim or outside everything â close all 1923 if (!e.target.closest('.fn-popover')) { 1924 closeAll(true); 1925 } 1926 }); 1927 1928 // Keyboard: Enter/Space activates footnote refs 1929 document.addEventListener('keydown', function(e) { 1930 if (e.key === 'Enter' || e.key === ' ') { 1931 var ref = e.target.closest('.fn-ref'); 1932 if (ref) { 1933 e.preventDefault(); 1934 e.stopPropagation(); 1935 var fnId = ref.dataset.fn; 1936 if (findPopover(fnId)) { 1937 closePopover(fnId, true); 1938 ref.focus(); 1939 } else { 1940 openPopover(ref, fnId); 1941 } 1942 } 1943 } 1944 }); 1945 1946 // Escape key closes topmost popover (focus return handled by closePopover) 1947 document.addEventListener('keydown', function(e) { 1948 if (e.key === 'Escape' && openPopovers.length) { 1949 var last = openPopovers[openPopovers.length - 1]; 1950 closePopover(last.id, true); 1951 } 1952 }); 1953 1954 // âââââââââââââââââââââââââââââââââââââââââââ 1955 // EASTER EGG GAME 1956 // âââââââââââââââââââââââââââââââââââââââââââ 1957 var eggGame = { active: false, sprites: [], clicks: 0, animId: null }; 1958 1959 function startEasterEggGame() { 1960 if (eggGame.active) return; 1961 eggGame.active = true; 1962 eggGame.clicks = 0;
1963 eggGame.sprites = []; 1964 var src = 'hankhair.png'; 1965 for (var i = 0; i < 30; i++) spawnGameSprite(src); 1966 eggGame.animId = requestAnimationFrame(tickGame); 1967 } 1968 1969 function spawnGameSprite(src) { 1970 var img = document.createElement('img'); 1971 img.src = src; 1972 img.className = 'game-sprite'; 1973 var w = 100 + Math.floor(Math.random() * 201); 1974 img.style.width = w + 'px'; 1975 img.style.height = 'auto'; 1976 1977 var x = Math.random() * (window.innerWidth - w); 1978 var y = Math.random() * (window.innerHeight - w); 1979 var angle = Math.random() * Math.PI * 2; 1980 var speed = 0.3 + Math.random() * 0.5; 1981 1982 var rotSpeed = (Math.random() - 0.5) * 1.2; // degrees per frame 1983 var sprite = { 1984 el: img, x: x, y: y, 1985 vx: Math.cos(angle) * speed, 1986 vy: Math.sin(angle) * speed, 1987 w: w, rot: Math.random() * 360, rotSpeed: rotSpeed 1988 }; 1989 1990 img.style.left = x + 'px'; 1991 img.style.top = y + 'px'; 1992 1993 img.addEventListener('click', function(e) { 1994 e.preventDefault(); 1995 e.stopPropagation(); 1996 clickGameSprite(sprite); 1997 }); 1998 1999 document.body.appendChild(img); 2000 eggGame.sprites.push(sprite); 2001 } 2002 2003 function clickGameSprite(sprite) { 2004 sprite.el.style.transition = 'transform 0.3s ease, opacity 0.3s ease'; 2005 sprite.el.style.transform = 'rotate(' + sprite.rot + 'deg) scale(0)'; 2006 sprite.el.style.opacity = '0'; 2007 setTimeout(function() { sprite.el.remove(); }, 300); 2008 2009 var idx = eggGame.sprites.indexOf(sprite); 2010 if (idx >= 0) eggGame.sprites.splice(idx, 1); 2011 2012 eggGame.clicks++; 2013 2014 // Every 2 clicks, spawn 1 new one (if sprites remain) 2015 if (eggGame.clicks % 2 === 0 && eggGame.sprites.length > 0) { 2016 spawnGameSprite('hankhair.png'); 2017 } 2018 2019 // Win condition 2020 if (eggGame.sprites.length === 0) { 2021 eggGame.active = false; 2022 cancelAnimationFrame(eggGame.animId); 2023 unlockAchievement('hank-destroyer'); 2024 } 2025 } 2026 2027 function tickGame() { 2028 if (!eggGame.active) return; 2029 var vw = window.innerWidth; 2030 var vh = window.innerHeight; 2031 2032 for (var i = 0;
2032 i < eggGame.sprites.length; i++) { 2033 var s = eggGame.sprites[i]; 2034 s.x += s.vx; 2035 s.y += s.vy; 2036 2037 // Bounce off edges 2038 if (s.x <= 0) { s.x = 0; s.vx = Math.abs(s.vx); } 2039 if (s.x >= vw - s.w) { s.x = vw - s.w; s.vx = -Math.abs(s.vx); } 2040 if (s.y <= 0) { s.y = 0; s.vy = Math.abs(s.vy); } 2041 if (s.y >= vh - s.w) { s.y = vh - s.w; s.vy = -Math.abs(s.vy); } 2042 2043 s.rot += s.rotSpeed; 2044 s.el.style.left = s.x + 'px'; 2045 s.el.style.top = s.y + 'px'; 2046 s.el.style.transform = 'rotate(' + s.rot + 'deg)'; 2047 } 2048 2049 eggGame.animId = requestAnimationFrame(tickGame); 2050 } 2051 2052 // âââââââââââââââââââââââââââââââââââââââââââ 2053 // ACHIEVEMENT SYSTEM 2054 // âââââââââââââââââââââââââââââââââââââââââââ 2055 var achv = { 2056 unlocked: {}, 2057 openedSet: {}, // unique footnote IDs opened 2058 openedCount: 0, 2059 lastOpenTime: 0, 2060 totalFootnotes: Object.keys(siteContent.footnotes).length, 2061 2062 defs: [ 2063 { id: 'rabbit-hole', emoji: '\uD83D\uDC07', title: 'Rabbit Hole', 2064 desc: 'Most people stop at two. You are not most people.' }, 2065 { id: 'deep-state', emoji: '\uD83D\uDD75\uFE0F', title: 'Deep State', 2066 desc: 'OK, you\u2019ve got TWO achievements now. Are there more?' }, 2067 { id: 'speed-clicker', emoji: '\u26A1', title: 'Speed Clicker', 2068 desc: 'There\u2019s no way you read that footnote that fast.' }, 2069 { id: 'warned-you', emoji: '\uD83D\uDEA8', title: 'Don\u2019t Say I Didn\u2019t Warn You', 2070 desc: 'Look, I think we can all agree you did this to yourself.' }, 2071 { id: 'collector', emoji: '\uD83D\uDCE6', title: 'Collector', 2072 desc: 'Fifteen footnotes. You\u2019re building a collection.' }, 2073 { id: 'archivist', emoji: '\uD83D\uDCDC', title: 'Archivist', 2074 desc: 'I am both impressed and a little bit honored...' }, 2075 { id: 'chaos-agent', emoji: '\uD83D\uDD25', title: 'Chaos Agent', 2076 desc: 'YEAH!! GETTIN MESSY!!&!' }, 2077 { id: 'completionist', emoji: '\uD83C\uDFC6', title: 'Completionist', 2078 desc: 'You found them all. Go outside. Tell someone what you\u2019ve done.' }, 2079 { id: 'hank-destroyer', emoji: '\uD83D\uDCA5', title: 'Hank Destroyer', 2080 desc: 'Your destructive tendencies have been noted, catalogued, and sent to the authorities.' }, 2081 { id: 'geocities-activate', emoji: '\uD83E\uDDA9', title: 'GEOCITIES ACTIVATE!', 2082 desc: 'You took the time to click the little pelican and this is your reward.' }, 2083 { id: 'a-fan', emoji: '\uD83D\uDC4B', title: 'A Fan!', 2084 desc: 'Thanks for being interested in what I\u2019m up to.' }, 2085 { id: 'meta', emoji: '\uD83E\uDD2F', title: 'Now That\u2019s What I Call Meta', 2086 desc: 'You just got an achievement for getting all of the achievements.' } 2087 ], 2088 2089 queue: [], 2090 showing: false 2091 }; 2092 2093 // Populate achievement tracker slots 2094 achv.defs.forEach(function(def) { 2095 var slot = document.createElement('div'); 2096 slot.className = 'achv-slot'; 2097 slot.setAttribute('data-achv', def.id); 2098 slot.textContent = def.emoji; 2099 achvTracker.appendChild(slot); 2100 }); 2101 2102 function unlockAchievement(id) { 2103 if (achv.unlocked[id]) return; 2104 achv.unlocked[id] = true; 2105 var def = achv.defs.find(function(d) { return d.id === id; }); 2106 if (!def) return; 2107 // Show tracker on first unlock, light up the slot 2108 achvTracker.classList.add('visible'); 2109 var slot = achvTracker.querySelector('[data-achv="' + id + '"]'); 2110 if (slot) slot.classList.add('unlocked'); 2111 achv.queue.push(def); 2112 if (!achv.showing) showNextAchievement(); 2113 // Show Museum of Former Projects after A Fan! unlocks 2114 if (id === 'a-fan' && addMuseumButton) addMuseumButton(); 2115 // Trigger celebration on meta unlock 2116 if (id === 'meta') metaCelebration(); 2117 // Meta: check if all non-meta achievements are unlocked 2118 if (id !== 'meta') { 2119 var allOthers = achv.defs.every(function(d) { 2120 return d.id === 'meta' || achv.unlocked[d.id]; 2121 }); 2122 if (allOthers) unlockAchievement('meta'); 2123 } 2124 } 2125 2126 function showNextAchievement() { 2127 if (!achv.queue.length) { achv.showing = false; return; } 2128 achv.showing = true; 2129 var def = achv.queue.shift(); 2130 2131 var toast = document.createElement('div'); 2132 toast.className = 'achv-toast'; 2133 toast.innerHTML = 2134 '<div><span class="achv-title">' + def.title + '</span></div>' + 2135 '<div class="achv-desc">' + def.desc + '</div>'; 2136 document.body.appendChild(toast); 2137 2138 // Confetti burst from the toast 2139 spawnConfetti(toast); 2140 2141 setTimeout(function() { 2142 toast.classList.add('out'); 2143 setTimeout(function() { 2144 toast.remove(); 2145 showNextAchievement(); 2146 }, 400); 2147 }, 2800); 2148 } 2149 2150 function spawnConfetti(anchor) { 2151 var rect = anchor.getBoundingClientRect(); 2152 var cx = rect.left + rect.width / 2; 2153 var cy = rect.top + rect.height / 2; 2154 var colors = ['#5b8def','#56b870','#e7b416','#a06ee1','#FF4081','#00BCD4','#8BC34A']; 2155 var pieces = []; 2156 2157 for (var i = 0; i < 40; i++) { 2158 var el = document.createElement('div'); 2159 el.className = 'confetti-piece'; 2160 var color = colors[Math.floor(Math.random() * colors.length)]; 2161 var w = 4 + Math.random() * 6; 2162 var h = 4 + Math.random() * 8; 2163 el.style.width = w + 'px'; 2164 el.style.height = h + 'px'; 2165 el.style.background = color; 2166 el.style.borderRadius = Math.random() > 0.5 ? '50%' : '2px'; 2167 el.style.left = cx + 'px'; 2168 el.style.top = cy + 'px'; 2169 document.body.appendChild(el); 2170 2171 var angle = (Math.PI * 2 * i / 40) + (Math.random() - 0.5) * 0.8; 2172 var speed = 3 + Math.random() * 5; 2173 pieces.push({ 2174 el: el, x: cx, y: cy, 2175 vx: Math.cos(angle) * speed, 2176 vy: Math.sin(angle) * speed - 2 - Math.random() * 3, 2177 rot: Math.random() * 360, 2178 rotV: (Math.random() - 0.5) * 15, 2179 opacity: 1 2180 }); 2181 } 2182 2183 var start = performance.now(); 2184 function tick(now) { 2185 var dt = Math.min((now - start) / 16, 3); 2186 start = now; 2187 var alive = false;
2188 for (var j = 0; j < pieces.length; j++) { 2189 var p = pieces[j]; 2190 if (p.opacity <= 0) continue; 2191 alive = true; 2192 p.vy += 0.15 * dt; // gravity 2193 p.x += p.vx * dt; 2194 p.y += p.vy * dt; 2195 p.rot += p.rotV * dt; 2196 p.opacity -= 0.012 * dt; 2197 if (p.opacity < 0) p.opacity = 0; 2198 p.el.style.left = p.x + 'px'; 2199 p.el.style.top = p.y + 'px'; 2200 p.el.style.opacity = p.opacity; 2201 p.el.style.transform = 'translate(-50%,-50%) rotate(' + p.rot + 'deg)'; 2202 } 2203 if (alive) { 2204 requestAnimationFrame(tick); 2205 } else { 2206 pieces.forEach(function(pp) { pp.el.remove(); }); 2207 } 2208 } 2209 requestAnimationFrame(tick); 2210 } 2211 2212 // ââ Meta celebration: page dance + bean rain ââ 2213 function metaCelebration() { 2214 document.body.classList.add('dancing'); 2215 var beans = []; 2216 var beanInterval = setInterval(function() { 2217 var el = document.createElement('img'); 2218 el.src = 'small-beab.png'; 2219 el.className = 'bean'; 2220 var size = 20 + Math.random() * 25; 2221 el.style.width = size + 'px'; 2222 el.style.height = 'auto'; 2223 var startX = Math.random() * window.innerWidth; 2224 el.style.left = startX + 'px'; 2225 el.style.top = '-40px'; 2226 document.body.appendChild(el); 2227 beans.push({ 2228 el: el, 2229 x: startX, 2230 y: -40, 2231 vy: 1.5 + Math.random() * 2.5, 2232 vx: (Math.random() - 0.5) * 1.5, 2233 rot: Math.random() * 360, 2234 rotV: (Math.random() - 0.5) * 8 2235 }); 2236 }, 40); 2237 2238 var beanAnimId; 2239 function tickBeans() { 2240 for (var i = beans.length - 1; i >= 0; i--) { 2241 var b = beans[i]; 2242 b.y += b.vy; 2243 b.vy += 0.08; 2244 b.x += b.vx; 2245 b.rot += b.rotV; 2246 b.el.style.top = b.y + 'px'; 2247 b.el.style.left = b.x + 'px'; 2248 b.el.style.transform = 'rotate(' + b.rot + 'deg)'; 2249 if (b.y > window.innerHeight + 50) { 2250 b.el.remove(); 2251 beans.splice(i, 1); 2252 } 2253 } 2254 beanAnimId = requestAnimationFrame(tickBeans); 2255 } 2256 beanAnimId = requestAnimationFrame(tickBeans); 2257 2258 // Stop after 5 seconds 2259 setTimeout(function() { 2260 clearInterval(beanInterval); 2261 document.body.classList.remove('dancing'); 2262 setTimeout(function() { 2263 cancelAnimationFrame(beanAnimId); 2264 beans.forEach(function(b) { b.el.remove(); }); 2265 beans = []; 2266 }, 3000); 2267 }, 5000); 2268 } 2269 2270 // ââ Secret code: type 132435 to trigger meta ââ 2271 var cheatCode = '132435'; 2272 var cheatBuf = ''; 2273 var cheatTimer = null; 2274 document.addEventListener('keydown', function(e) { 2275 if (e.key >= '0' && e.key <= '9') { 2276 cheatBuf += e.key; 2277 clearTimeout(cheatTimer); 2278 cheatTimer = setTimeout(function() { cheatBuf = ''; }, 3000); 2279 if (cheatBuf === cheatCode) { 2280 cheatBuf = ''; 2281 // Unlock all non-meta achievements silently, then trigger meta 2282 achv.defs.forEach(function(d) { 2283 if (d.id !== 'meta') { 2284 if (!achv.unlocked[d.id]) { 2285 achv.unlocked[d.id] = true; 2286 achvTracker.classList.add('visible'); 2287 var slot = achvTracker.querySelector('[data-achv="' + d.id + '"]'); 2288 if (slot) slot.classList.add('unlocked'); 2289 } 2290 } 2291 }); 2292 unlockAchievement('meta'); 2293 } 2294 if (cheatBuf.length > cheatCode.length) cheatBuf = cheatBuf.slice(-cheatCode.length); 2295 } 2296 }); 2297 2298 function checkAchievements(fnId, depth) { 2299 var now = performance.now(); 2300 2301 // Track unique opens 2302 if (!achv.openedSet[fnId]) { 2303 achv.openedSet[fnId] = true; 2304 achv.openedCount++; 2305 } 2306 2307 // Rabbit Hole: depth 3 2308 if (depth >= 3) unlockAchievement('rabbit-hole'); 2309 2310 // Deep State: depth 6 2311 if (depth >= 6) unlockAchievement('deep-state'); 2312 2313 // Speed Clicker: < 400ms between opens 2314 if (achv.lastOpenTime && (now - achv.lastOpenTime) < 800) { 2315 unlockAchievement('speed-clicker'); 2316 } 2317 achv.lastOpenTime = now; 2318 2319 // Collector: 15 unique 2320 if (achv.openedCount >= 15) unlockAchievement('collector'); 2321
2322 // Archivist: 40 unique 2323 if (achv.openedCount >= 40) unlockAchievement('archivist'); 2324 2325 // Chaos Agent: 8+ popovers open at once 2326 if (openPopovers.length >= 8) unlockAchievement('chaos-agent'); 2327 2328 // Completionist: every footnote opened 2329 if (achv.openedCount >= achv.totalFootnotes) unlockAchievement('completionist'); 2330 } 2331 2332 // âââââââââââââââââââââââââââââââââââââââââââ 2333 // ACCOLADES SLIDESHOW 2334 // âââââââââââââââââââââââââââââââââââââââââââ 2335 (function() { 2336 if (!siteContent.accolades || !siteContent.accolades.length) return; 2337 2338 var accolades = siteContent.accolades; 2339 var currentSlide = 0; 2340 var transitioning = false; 2341 2342 // Build overlay 2343 var overlay = document.createElement('div'); 2344 overlay.className = 'accolades-overlay'; 2345 overlay.id = 'accolades-overlay'; 2346 overlay.setAttribute('role', 'dialog'); 2347 overlay.setAttribute('aria-modal', 'true'); 2348 overlay.setAttribute('aria-label', 'Accolades'); 2349 2350 var card = document.createElement('div'); 2351 card.className = 'accolades-card'; 2352 2353 var closeBtn = document.createElement('button'); 2354 closeBtn.className = 'accolades-close'; 2355 closeBtn.innerHTML = '×'; 2356 closeBtn.setAttribute('aria-label', 'Close'); 2357 2358 var label = document.createElement('span'); 2359 label.className = 'accolades-label'; 2360 label.textContent = 'TL;DR'; 2361 2362 var slide = document.createElement('div'); 2363 slide.className = 'accolades-slide'; 2364 slide.id = 'accolades-slide'; 2365 2366 var nav = document.createElement('div'); 2367 nav.className = 'accolades-nav'; 2368 2369 var prevBtn = document.createElement('button'); 2370 prevBtn.className = 'accolades-arrow'; 2371 prevBtn.innerHTML = '←'; 2372 prevBtn.setAttribute('aria-label', 'Previous'); 2373 2374 var dotsWrap = document.createElement('div'); 2375 dotsWrap.className = 'accolades-dots'; 2376 2377 var nextBtn = document.createElement('button'); 2378 nextBtn.className = 'accolades-arrow'; 2379 nextBtn.innerHTML = '→'; 2380 nextBtn.setAttribute('aria-label', 'Next'); 2381 2382 // Build dots 2383 var dots = []; 2384 accolades.forEach(function(_, i) { 2385 var dot = document.createElement('button'); 2386 dot.className = 'accolades-dot' + (i === 0 ? ' active' : ''); 2387 dot.setAttribute('aria-label', 'Go to slide ' + (i + 1)); 2388 dot.addEventListener('click', function() { goToSlide(i); }); 2389 dotsWrap.appendChild(dot); 2390 dots.push(dot); 2391 }); 2392 2393 nav.appendChild(prevBtn); 2394 nav.appendChild(dotsWrap); 2395 nav.appendChild(nextBtn); 2396 2397 card.appendChild(closeBtn); 2398 card.appendChild(label); 2399 card.appendChild(slide); 2400 card.appendChild(nav); 2401 overlay.appendChild(card); 2402 document.body.appendChild(overlay); 2403
2404 function renderSlide(index) { 2405 var a = accolades[index]; 2406 slide.innerHTML = 2407 (a.year ? '<span class="accolades-year">' + a.year + '</span>' : '') + 2408 '<div class="accolades-title">' + a.title + '</div>' + 2409 '<div class="accolades-detail">' + a.detail + '</div>' + 2410 (a.link ? '<a class="accolades-link" href="' + a.link.replace(/"/g,'"') + '" target="_blank">' + (a.linkText || 'Learn more') + ' →</a>' : ''); 2411 dots.forEach(function(d, i) { 2412 d.classList.toggle('active', i === index); 2413 }); 2414 } 2415 2416 function goToSlide(index, direction) { 2417 if (transitioning || index === currentSlide) return; 2418 transitioning = true; 2419 2420 var dir = direction || (index > currentSlide ? 'left' : 'right'); 2421 slide.classList.add(dir === 'left' ? 'slide-out-left' : 'slide-out-right'); 2422 2423 setTimeout(function() { 2424 currentSlide = index; 2425 renderSlide(currentSlide); 2426 slide.classList.remove('slide-out-left', 'slide-out-right'); 2427 slide.classList.add('slide-in'); 2428 2429 // Force reflow, then let CSS defaults (opacity:1, translateX(0)) take over 2430 void slide.offsetWidth; 2431 slide.classList.remove('slide-in'); 2432 2433 setTimeout(function() { transitioning = false; }, 50); 2434 }, 220); 2435 } 2436 2437 prevBtn.addEventListener('click', function() { 2438 var prev = (currentSlide - 1 + accolades.length) % accolades.length; 2439 goToSlide(prev, 'right'); 2440 }); 2441 2442 nextBtn.addEventListener('click', function() { 2443 var next = (currentSlide + 1) % accolades.length; 2444 goToSlide(next, 'left'); 2445 }); 2446 2447 // Keyboard navigation 2448 overlay.addEventListener('keydown', function(e) { 2449 if (e.key === 'ArrowLeft') { prevBtn.click(); e.preventDefault(); } 2450 if (e.key === 'ArrowRight') { nextBtn.click(); e.preventDefault(); } 2451 if (e.key === 'Escape') { closeOverlay(); e.preventDefault(); } 2452 }); 2453 2454 // Touch/swipe support 2455 var touchStartX = 0; 2456 var touchStartY = 0; 2457 card.addEventListener('touchstart', function(e) { 2458 touchStartX = e.touches[0].clientX; 2459 touchStartY = e.touches[0].clientY; 2460 }, { passive: true }); 2461 card.addEventListener('touchend', function(e) { 2462 var dx = e.changedTouches[0].clientX - touchStartX; 2463 var dy = e.changedTouches[0].clientY - touchStartY; 2464 if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) { 2465 if (dx < 0) nextBtn.click(); 2466 else prevBtn.click(); 2467 } 2468 }, { passive: true }); 2469 2470 // Close overlay 2471 function closeOverlay() { 2472 overlay.classList.remove('active'); 2473 if (accoladesBtn) accoladesBtn.focus(); 2474 } 2475 closeBtn.addEventListener('click', closeOverlay); 2476 overlay.addEventListener('click', function(e) { 2477 if (e.target === overlay) closeOverlay(); 2478 }); 2479 2480 // Focus trap within overlay 2481 overlay.addEventListener('keydown', function(e) { 2482 if (e.key !== 'Tab') return; 2483 var focusable = overlay.querySelectorAll('button, a[href], [tabindex]:not([tabindex="-1"])'); 2484 if (!focusable.length) return; 2485 var first = focusable[0]; 2486 var last = focusable[focusable.length - 1]; 2487 if (e.shiftKey) { 2488 if (document.activeElement === first) { e.preventDefault(); last.focus(); } 2489 } else { 2490 if (document.activeElement === last) { e.preventDefault(); first.focus(); } 2491 } 2492 }); 2493 2494 // Open overlay 2495 function openOverlay() { 2496 currentSlide = 0; 2497 renderSlide(0); 2498 overlay.classList.add('active'); 2499 closeBtn.focus(); 2500 } 2501 2502 // Add "TL;DR" button above the bio text 2503 var accoladesBtn = document.createElement('button'); 2504 accoladesBtn.className = 'accolades-btn'; 2505 accoladesBtn.textContent = 'TL;DR'; 2506 accoladesBtn.addEventListener('click', openOverlay); 2507 2508 // Insert before the first paragraph block 2509 var firstPara = container.querySelector('.paragraph-block'); 2510 if (firstPara) { 2511 container.insertBefore(accoladesBtn, firstPara); 2512 } else { 2513 container.insertBefore(accoladesBtn, container.firstChild); 2514 } 2515 2516 // Now that the button is in the DOM, align the featured sidebar
2517 alignFeatured(); 2518 2519 // Also close on Escape globally 2520 document.addEventListener('keydown', function(e) { 2521 if (e.key === 'Escape' && overlay.classList.contains('active')) { 2522 closeOverlay(); 2523 } 2524 }); 2525 2526 renderSlide(0); 2527 })(); 2528 2529 2530 // ======================================= 2531 // TOP MENU BAR (Web Things / Video / ...) 2532 // ======================================= 2533 if (siteContent.topMenu && siteContent.topMenu.length) { 2534 var codeBar = document.createElement('nav'); 2535 codeBar.className = 'code-bar'; 2536 codeBar.id = 'code-bar'; 2537 codeBar.setAttribute('aria-label', 'Things Hank makes'); 2538 2539 var tabsRow = document.createElement('div'); 2540 tabsRow.className = 'code-bar-tabs'; 2541 var drawer = document.createElement('div'); 2542 drawer.className = 'code-bar-drawer'; 2543 drawer.id = 'code-bar-drawer'; 2544 var drawerInner = document.createElement('div'); 2545 drawerInner.className = 'code-bar-drawer-inner'; 2546 drawer.appendChild(drawerInner); 2547 2548 var menuTabs = [], menuPanels = []; 2549 var openIdx = -1; 2550 2551 function setMenu(idx) { 2552 // idx === -1 closes the drawer 2553 var prev = openIdx; 2554 openIdx = idx; 2555 codeBar.classList.toggle('open', idx !== -1); 2556 menuTabs.forEach(function(t, i) { 2557 t.classList.toggle('active', i === idx); 2558 t.setAttribute('aria-expanded', i === idx ? 'true' : 'false'); 2559 }); 2560 if (idx !== -1) { 2561 menuPanels.forEach(function(p, i) { 2562 p.classList.remove('slide-l', 'slide-r'); 2563 p.classList.toggle('active', i === idx); 2564 }); 2565 codeBar.style.setProperty('--bar-accent', siteContent.topMenu[idx].color); 2566 if (prev !== -1 && prev !== idx) { 2567 menuPanels[idx].classList.add(idx > prev ? 'slide-r' : 'slide-l'); 2568 } 2569 } 2570 // On close we leave the current panel visible so the drawer has 2571 // content to slide shut around (it's clipped by the animated height). 2572 2573 // Animate the drawer height in px (same curve as the pelican, so the 2574 // rubbery overshoot matches exactly and nothing drifts apart). 2575 var push = (idx !== -1) ? drawerInner.scrollHeight : 0; 2576 drawer.style.height = push + 'px'; 2577 document.documentElement.style.setProperty('--drawer-push', push + 'px'); 2578 } 2579 2580 // Keep an open drawer sized correctly if the window is resized 2581 window.addEventListener('resize', function() { 2582 if (openIdx !== -1) { 2583 var h = drawerInner.scrollHeight; 2584 drawer.style.height = h + 'px'; 2585 document.documentElement.style.setProperty('--drawer-push', h + 'px'); 2586 } 2587 }); 2588 2589 siteContent.topMenu.forEach(function(cat, i) { 2590 var tab = document.createElement('button'); 2591 tab.className = 'code-bar-tab'; 2592 tab.style.setProperty('--tc', cat.color); 2593 tab.setAttribute('aria-expanded', 'false'); 2594 tab.setAttribute('aria-controls', 'code-bar-drawer'); 2595 tab.innerHTML = cat.label + ' <span class="chev">â¾</span>'; 2596 tab.addEventListener('click', function(e) { 2597 e.stopPropagation(); 2598 setMenu(openIdx === i ? -1 : i); 2599 }); 2600 tabsRow.appendChild(tab); 2601 menuTabs.push(tab); 2602 2603 var panel = document.createElement('div'); 2604 panel.className = 'code-carousel'; 2605 cat.items.forEach(function(item) { 2606 var card = document.createElement('a'); 2607 card.className = 'code-card'; 2608 card.style.setProperty('--cc', item.color); 2609 card.href = item.link; 2610 if (item.link.indexOf('://') !== -1) { 2611 card.target = '_blank'; 2612 card.rel = 'noopener'; 2613 } 2614 var thumb = '<span class="code-card-thumb">' + 2615 (item.art ? '<img' + (item.fit === 'contain' ? ' class="contain"' : '') + 2616 ' src="' + item.art + '" alt="" loading="lazy" onerror="this.remove()">' : '') + 2617 '</span>'; 2618 card.innerHTML = thumb + 2619 '<span class="code-card-body"><span class="code-card-title">' + item.title + '</span>' + 2620 '<span class="code-card-tagline">' + item.tagline + '</span></span>'; 2621 panel.appendChild(card); 2622 }); 2623 drawerInner.appendChild(panel); 2624 menuPanels.push(panel); 2625 }); 2626 2627 codeBar.appendChild(tabsRow); 2628 codeBar.appendChild(drawer); 2629 document.body.insertBefore(codeBar, document.body.firstChild); 2630 2631 document.addEventListener('click', function(e) { 2632 if (openIdx !== -1 && !codeBar.contains(e.target)) setMenu(-1); 2633 }); 2634 document.addEventListener('keydown', function(e) { 2635 if (e.key === 'Escape' && openIdx !== -1) { 2636 var t = menuTabs[openIdx]; 2637 setMenu(-1); 2638 t.focus(); 2639 } 2640 }); 2641 } 2642 2643 // (The dark "Ongoing Projects" gallery that used to render here was 2644 // retired in July 2026 â the top menu bar covers everything now. 2645 // siteContent.projects still exists in content.js if it's ever wanted back.) 2646 2647 // âââââââââââââââââââââââââââââââââââââââââââ
2648 // GEOCITIES MODE 2649 // âââââââââââââââââââââââââââââââââââââââââââ 2650 var geoActive = false; 2651 var geoElements = []; 2652 2653 // Guestbook entries 2654 var gbEntries = [ 2655 { name: 'Dave', date: 'Jan 1, 1999', msg: 'First!!!! This is my favorite website on the entire World Wide Web.' }, 2656 { name: 'MarsGirl2001', date: 'Feb 3, 1999', msg: 'Hank - loved your Mars page!! I bookmarked it. Keep up the good work!' }, 2657 { name: 'xXDarkAngelXx', date: 'Mar 18, 1999', msg: 'cool page!! PLEASE SIGN MY GUESTBOOK TOO!! www.geocities.com/TimesSquare/4892' }, 2658 { name: 'Dave', date: 'Apr 2, 1999', msg: 'Dave again. Still checking in every day. Updated my bookmarks folder to put you at the top.' }, 2659 { name: 'anonymous', date: 'Apr 20, 1999', msg: 'nice page but the star background makes it hard to read. also your visitor counter seems broken, it said 47 yesterday and now it says 12,847??' }, 2660 { name: 'AstroNerd', date: 'May 15, 1999', msg: 'Found this through AltaVista searching for mars exploration. Not disappointed!' }, 2661 { name: 'Tommy, age 11', date: 'Jun 22, 1999', msg: 'i think this is the best website ive ever seen and ive been on the internet for almost a whole year now' }, 2662 { name: 'Dave', date: 'Jul 4, 1999', msg: 'Happy 4th everyone! Hank if you ever need someone to help moderate this guestbook I am available 24/7. I have DSL now so I am always online.' }, 2663 { name: 'SurfDude99', date: 'Aug 9, 1999', msg: 'Found this through the webring. Radical!!! Added you to my links page.' }, 2664 { name: 'Dave', date: 'Dec 31, 1999', msg: 'Stocking up on canned goods for Y2K but wanted to sign one more time just in case the internet goes away forever. It has been an honor. -Dave' } 2665 ]; 2666 2667 function buildGuestbook() { 2668 var overlay = document.createElement('div'); 2669 overlay.className = 'geo-gb-overlay'; 2670 overlay.id = 'geo-guestbook'; 2671 var popup = document.createElement('div'); 2672 popup.className = 'geo-gb-popup'; 2673 // Title bar 2674 popup.innerHTML = '<div class="geo-gb-titlebar"><span>HANK\'S GUESTBOOK</span><button class="geo-gb-close" id="geo-gb-close">X</button></div>'; 2675 // Body with entries 2676 var body = document.createElement('div'); 2677 body.className = 'geo-gb-body'; 2678 body.id = 'geo-gb-body'; 2679 gbEntries.forEach(function(e) { 2680 var entry = document.createElement('div'); 2681 entry.className = 'geo-gb-entry'; 2682 entry.innerHTML = '<span class="gb-name">' + e.name + '</span> <span class="gb-date">' + e.date + '</span><div class="gb-msg">' + e.msg + '</div>'; 2683 body.appendChild(entry); 2684 }); 2685 popup.appendChild(body); 2686 // Form 2687 var form = document.createElement('div'); 2688 form.className = 'geo-gb-form'; 2689 form.id = 'geo-gb-form'; 2690 form.innerHTML = '<div style="padding:0.5rem;text-align:center;color:#cc0000;font-weight:700;font-size:0.78rem;line-height:1.6;">' + 2691 '<img src="https://web.archive.org/web/2009/http://www.geocities.com/SiliconValley/Campus/8757/construct2.gif" alt="" style="width:32px;height:32px;vertical-align:middle;image-rendering:pixelated;" onerror="this.style.display=\'none\'">' + 2692 ' GUESTBOOK INPUT CURRENTLY BROKEN <img src="https://web.archive.org/web/2009/http://www.geocities.com/SiliconValley/Campus/8757/construct2.gif" alt="" style="width:32px;height:32px;vertical-align:middle;image-rendering:pixelated;" onerror="this.style.display=\'none\'"><br>' + 2693 '<span style="font-size:0.68rem;color:#666;font-weight:400;">My friend Dave is working on fixing it. Please check back soon!<br>' + 2694 'In the meantime you can email your guestbook entry to [email protected]</span></div>'; 2695 popup.appendChild(form); 2696 overlay.appendChild(popup); 2697 document.body.appendChild(overlay); 2698 // Close button 2699 document.getElementById('geo-gb-close').addEventListener('click', function() { 2700 overlay.classList.remove('active'); 2701 }); 2702 overlay.addEventListener('click', function(ev) { 2703 if (ev.target === overlay) overlay.classList.remove('active'); 2704 }); 2705 return overlay; 2706 } 2707 2708 document.getElementById('geo-toggle').addEventListener('click', function(e) { 2709 e.stopPropagation(); 2710 geoActive = !geoActive; 2711 document.body.classList.toggle('geocities', geoActive); 2712 if (geoActive) unlockAchievement('geocities-activate'); 2713 2714 if (geoActive) { 2715 // Swap site photo to hankhair.png 2716 var sitePhotoEl = document.querySelector('.site-photo'); 2717 if (sitePhotoEl) { 2718 sitePhotoEl._origSrc = sitePhotoEl.src; 2719 sitePhotoEl.src = 'hankhair.png'; 2720 } 2721 2722 // Rainbow marquee 2723 var marquee = document.createElement('div'); 2724 marquee.className = 'geo-marquee'; 2725 marquee.innerHTML = '<span>~*~*~ Welcome to HANK\'S HOMEPAGE ~*~*~ Best viewed in Netscape Navigator 4.0 at 800x600 ~*~*~ Last updated: January 15, 1999 ~*~*~ DFTBA!! ~*~*~</span>'; 2726 document.body.appendChild(marquee); 2727 geoElements.push(marquee); 2728 2729 // Under construction (inserted at top of content) 2730 var construction = document.createElement('div'); 2731 construction.className = 'geo-construction'; 2732 construction.textContent = '\uD83D\uDEA7 !! UNDER CONSTRUCTION !! \uD83D\uDEA7'; 2733 container.insertBefore(construction, container.firstChild); 2734 geoElements.push(construction); 2735 2736 // Visitor counter + guestbook link 2737 var counter = document.createElement('div'); 2738 counter.className = 'geo-counter'; 2739 var num = 10000 + Math.floor(Math.random() * 89999); 2740 counter.innerHTML = 'You are visitor number <span class="counter-num">' + num + 2741 '</span><br><a href="#" id="geo-gb-link">Sign my guestbook!</a>'; 2742 document.body.appendChild(counter); 2743 geoElements.push(counter); 2744 2745 // Build guestbook 2746 var gb = buildGuestbook(); 2747 geoElements.push(gb); 2748
2749 // Wire link 2750 document.getElementById('geo-gb-link').addEventListener('click', function(ev) { 2751 ev.preventDefault(); 2752 document.getElementById('geo-guestbook').classList.add('active'); 2753 }); 2754 } else { 2755 // Restore site photo 2756 var sitePhotoEl = document.querySelector('.site-photo'); 2757 if (sitePhotoEl && sitePhotoEl._origSrc) sitePhotoEl.src = sitePhotoEl._origSrc; 2758 2759 geoElements.forEach(function(el) { el.remove(); }); 2760 geoElements = []; 2761 } 2762 }); 2763 2764 2765})(); 2766</script>
vendor: 1 bytes, line 2766
2766
2767<script defer src="/_vercel/insights/script.js"></script>
vendor: 1 bytes, line 2767
2767
2768<script defer src="/_vercel/speed-insights/script.js"></script>
2768 2769</body> 2770</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.