PageSourceSearch

https://www.hankgreen.com/

html hankgreen.com collected 2026-10-02 06:31:01 UTC 95,808 bytes, 2,770 lines download raw bytes

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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
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        ' &rarr;</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) + ' &rarr;</a>';
1539      } else if (item.link) {
1540        html += '<a class="featured-item-cta" href="' + esc(item.link) + '" target="_blank">Learn more &rarr;</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">&times;</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 = '&times;';
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 = '&#8592;';
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 = '&#8594;';
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,'&quot;') + '" target="_blank">' + (a.linkText || 'Learn more') + ' &rarr;</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.