PageSourceSearch

https://www.edthesock.com/sock-drawer/

html edthesock.com collected 2026-10-02 10:32:16 UTC 68,580 bytes, 1,599 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">
6  <title>The Sock Drawer — Ed The Sock</title>
7  <link rel="stylesheet" href="/assets/wp-blocks.css?v=6dddaf47">
8  <link rel="preconnect" href="https://fonts.googleapis.com">
9  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10  <!-- Roboto 400/500 + Oxanium 400/600 — hero captions/attribution (config/site-fonts.json) -->
11  <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Oxanium:wght@400;600&family=Roboto:wght@400;500&family=Source+Sans+3:wght@400&display=swap">
12  <link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@400;600&family=Roboto:wght@400;500&family=Source+Sans+3:wght@400&display=swap" rel="stylesheet">
13  
13<script>
14  (function () {
15    if (!document.fonts || !document.fonts.load) return;
16    ["Roboto", "Oxanium", "Source Sans 3"].forEach(function (name) {
17      document.fonts.load('400 16px "' + name + '"').catch(function () {});
18      document.fonts.load('500 16px "' + name + '"').catch(function () {});
19      document.fonts.load('600 16px "' + name + '"').catch(function () {});
20    });
21  })();
22  </script>
22
23</head>
24<body class="page-no-title">
25<link rel="preconnect" href="https://fonts.googleapis.com">
26<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
27<!-- Nav font: the original uses "InterFace" (Dalton Maag, commercial, served
28     via the Squarespace Adobe Fonts kit — can't be rehosted). Source Sans 3
29     measured closest of 12 Google candidates: "The Sock Drawer" @19.2px is
30     134.4px vs InterFace 131.4px (Roboto was 148, Inter 157). -->
31<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400&display=swap" rel="stylesheet">
32<style>
33/* Shared header height for page-content offsets (hero, page titles).
34   Measured live: desktop header 119px, mobile 94px. */
35:root {
36    --eds-header-h: 119px;
37}
38
39@media (max-width: 900px) {
40    :root {
41        --eds-header-h: 94px;
42    }
43}
44
45/* Site body background = mint brick (brick + 15% green), like the original.
46   Pages cover it with their own cover blocks; it shows in the strip behind
47   the fixed frosted header, which blurs it into the sage tint that keeps
48   the ED THE SOCK.COM logo readable (on the original this is the first
49   section's background extending under the header). */
50body {
51    background-color: #b9cfc2;
52    background-image: linear-gradient(rgba(32, 157, 80, 0.15), rgba(32, 157, 80, 0.15)),
53        url("/assets/images/site-background.webp");
54    background-size: cover;
55    background-position: center top;
56    background-attachment: fixed;
57    background-repeat: no-repeat;
58}
59
60/* Theme gives cover blocks a default 16px padding all around; original
61   Squarespace sections have no top inset (page offsets via --eds-header-h
62   margins assume content starts at the cover's top edge) and a 26px side
63   gutter for the fluid grid. */
64.wp-block-cover.alignfull {
65    padding: 0 26px 16px;
66}
67
68/* Squarespace-style frosted fixed header: content scrolls underneath.
69   Original: header-blur-background blur(12px) + header-background-solid
70   rgba(32,157,80,0.09) @ .95 + drop shadow; logo 307x116 at ~26px left. */
71.eds-site-header {
72    position: fixed;
73    top: 0;
74    left: 0;
75    right: 0;
76    z-index: 100;
77    background: transparent !important;
78}
79
80/* The frosted tint lives on a pseudo-element, NOT the header itself:
81   backdrop-filter on the header would make it a containing block for
82   position:fixed descendants, trapping the mobile menu overlay inside
83   the header strip instead of letting it fill the screen. */
84.eds-site-header::before {
85    content: "";
86    position: absolute;
87    inset: 0;
88    z-index: -1;
89    background: rgba(32, 157, 80, 0.09);
90    -webkit-backdrop-filter: blur(12px);
91    backdrop-filter: blur(12px);
92    /* Original: .header-dropshadow renders box-shadow 0 12px 12px black at
93       element opacity .15 — a soft gray band under the header */
94    box-shadow: 0 12px 12px rgba(0, 0, 0, 0.15);
95}
96
97/* Fallback when backdrop-filter is unsupported: keep the old solid tint */
98@supports not ((backdrop-filter: blur(12px)) or (-webkit-backdrop-filter: blur(12px))) {
99    .eds-site-header::before {
100        background: rgba(168, 197, 181, 0.95);
101    }
102}
103
104/* Full-screen mobile menu overlay, matching the original Squarespace one:
105   warm light-gray sheet (sampled #e1dfd9), large left-aligned black links
106   vertically centered, close X top-right (WP renders the X natively). */
107.eds-site-header .wp-block-navigation__responsive-container.is-menu-open {
108    /* !important: WP core sets background-color:inherit on .is-menu-open,
109       which resolves to the theme's white */
110    background-color: #e1dfd9 !important;
111    /* Let note-dragging own the gesture (menu doesn't scroll) */
112    touch-action: none;
113}
114
115/* While the menu is open nothing is selectable — double/triple taps and
116   long-presses were selecting text mid-play. Links and the close X still
117   tap fine (user-select doesn't affect clicks). :has() covers the page
118   behind the sheet too. */
119body:has(.eds-site-header .wp-block-navigation__responsive-container.is-menu-open) {
120    -webkit-user-select: none;
121    user-select: none;
122}
123
124/* Home-only hitch: while the sheet is open, skip painting the page behind
125   it (hero WebP layers + live drop-shadow filters + embeds). Other pages
126   are light enough that this barely matters; on home it frees the main
127   thread for the note canvas. Header stays visible (owns the overlay). */
128html.eds-menu-open body > *:not(.eds-site-header) {
129    visibility: hidden !important;
130    pointer-events: none !important;
131}
132html.eds-menu-open .eds-ed-overlay,
133html.eds-menu-open .eds-ed-highlight,
134html.eds-menu-open .eds-logo-overlay,
135html.eds-menu-open .eds-logo-red-boost {
136    filter: none !important;
137    animation: none !important;
138}
139
140.eds-site-header .wp-block-navigation__responsive-container.is-menu-open,
141.eds-site-header .wp-block-navigation__responsive-container.is-menu-open * {
142    -webkit-user-select: none !important;
143    user-select: none !important;
144    -webkit-touch-callout: none;
145}
146
147.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-close,
148.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-dialog,
149.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
150    height: 100%;
151}
152
153.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
154    display: flex;
155    flex-direction: column;
156    overflow-y: auto;
157    padding-left: 11px; /* original links start 27px from edge (16px WP padding + 11) */
158}
159
160/* Vertical centering via auto margins instead of justify-content:center —
161   centers when there's room, top-aligns and scrolls (no clipping) when the
162   viewport is short (landscape phones). The ul defaults to flex:1, which
163   would swallow the auto margins, so pin it to its content size. */
164.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
165    flex: 0 0 auto;
166    margin-top: auto;
167    margin-bottom: auto;
168}
169
170/* Short viewports (landscape phones): smaller links + tighter gap so all
171   five items fit without scrolling */
172@media (max-height: 500px) {
173    .eds-site-header .wp-block-navigation__responsive-container.is-menu-open a {
174        font-size: 24px;
175    }
176
177    .eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
178        gap: 10px;
179    }
180}
181
182.eds-site-header .wp-block-navigation__responsive-container.is-menu-open a {
183    font-size: 33px;
184    font-weight: 400;
185    color: #000;
186}
187
188/* Full-bleed inner row, tight vertical padding, logo near the corner */
189.eds-site-header > .wp-block-group {
190    max-width: none;
191    padding: 2px 24px 2px 26px;
192}
193
194.eds-site-header .wp-block-site-logo {
195    margin: 0;
196}
197
198/* Nav font — original computed: InterFace 19.18px w400, letter-spacing 0.19px */
199.eds-site-header .wp-block-navigation a {
200    font-family: "Source Sans 3", "Roboto", sans-serif;
201    font-size: 19.2px;
202    font-weight: 400;
203    letter-spacing: 0.01em;
204}
205
206/* Current-page underline — original: 1px black repeat-x gradient sitting
207   ~2px above the link box bottom (Squarespace .header-nav-item--active) */
208.eds-site-header .wp-block-navigation .current-menu-item > a {
209    background-image: linear-gradient(currentColor, currentColor);
210    background-size: 1px 1px;
211    background-repeat: repeat-x;
212    background-position: 0 calc(100% - 2px);
213}
214
215/* Load animation — Squarespace header-element preSlide/slideIn:
216   .preSlide{opacity:0;transform:translate(0,30%)} then slides to rest.
217   Pure CSS (animation plays without JS; `backwards` holds the start state
218   during the delay). */
219@keyframes eds-header-in {
220    from {
221        opacity: 0;
222        transform: translateY(30%);
223    }
224}
225
226.eds-site-header .wp-block-site-logo {
227    animation: eds-header-in 0.65s ease 0.05s backwards;
228}
229
230.eds-site-header .wp-block-navigation {
231    animation: eds-header-in 0.65s ease 0.18s backwards;
232}
233
234@media (prefers-reduced-motion: reduce) {
235    .eds-site-header .wp-block-site-logo,
236    .eds-site-header .wp-block-navigation {
237        animation: none;
238    }
239}
240
241/* Music-note particle layer behind the mobile menu links. The canvas is a
242   direct child of the overlay (z-index 0) and the dialog sits above it. */
243.eds-menu-notes {
244    display: none;
245}
246
247.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .eds-menu-notes {
248    display: block;
249    position: absolute;
250    inset: 0;
251    width: 100%;
252    height: 100%;
253    pointer-events: none;
254    z-index: 0;
255}
256
257.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-close {
258    position: relative;
259    z-index: 1;
260}
261
262/* Guitar-hero mode (easter egg): the menu links vanish, the canvas game
263   takes the sheet. The close X lives outside the content container and
264   stays usable. NOTE: opacity, not visibility:hidden — hiding the focused
265   menu link fires a focusout that WP's navigation script treats as "focus
266   left the modal" and it closes the menu mid-game. */
267.eds-site-header .wp-block-navigation__responsive-container.is-menu-open.eds-gh-mode .wp-block-navigation__responsive-container-content {
268    opacity: 0;
269    pointer-events: none;
270    transition: opacity 0.25s ease;
271}
272</style>
273<script>
274/* Mobile-menu music notes: float up behind the links, drift with device
275   tilt, and "break" on a vigorous shake — falling into a pile at the
276   bottom until the menu is reopened.
277   No bare less-than chars in this script (wptexturize gotcha — see
278   docs/wordpress-operations.md). Comparisons are written flipped. */
279(function () {
280  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
281
282  var GLYPHS = ['\u266A', '\u266B', '\u2669', '\u266C'];
283  var COLORS = [[26, 110, 60], [32, 157, 80], [70, 70, 70]];
284  var canvas, ctx, overlay, raf = 0, running = false, startPending = false;
285  var notes = [], W = 0, H = 0, dpr = 1;
286  var tilt = 0;                 /* smoothed gamma, -1..1 */
287  var broken = false;
288  var shakeHits = [], lastJolt = 0, joltMag = 0;
289  var pile = [], BIN = 22, STEP = 11;
290  var desiredFloats = 0, refillAt = 0;
291
292  function rnd(a, b) { return a + Math.random() * (b - a); }
293
294  function makeNote(atBottom) {
295    var c = COLORS[Math.floor(rnd(0, COLORS.length))];
296    return {
297      x: rnd(10, Math.max(11, W - 10)),
298      y: atBottom ? rnd(H, H + H * 0.6) : rnd(0, H),
299      spd: rnd(0.35, 1.1),
300      ph: rnd(0, 6.28),
301      phv: rnd(0.01, 0.03),
302      size: rnd(26, 54),
303      glyph: GLYPHS[Math.floor(rnd(0, GLYPHS.length))],
304      col: 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + rnd(0.16, 0.38).toFixed(2) + ')',
305      mode: 'float',
306      rot: 0, vx: 0, vy: 0, settled: false, restY: 0
307    };
308  }
309
310  function resetNotes() {
311    broken = false;
312    shakeHits = [];
313    pile = [];
314    notes = [];
315    var count = Math.max(16, Math.min(32, Math.round((W * H) / 22000)));
316    desiredFloats = count;
317    for (var i = 0; count > i; i++) notes.push(makeNote(false));
318  }
319
320  /* Keep the sky stocked: as notes go dead (dropped or shaken), send fresh
321     floaters from the bottom — until over 50 dead ones have piled up, then
322     recycle the pile instead of growing it forever. */
323  function recycleDead() {
324    for (var i = 0; notes.length > i; i++) {
325      var n = notes[i];
326      if (n.mode === 'fall' && n.settled) {
327        var b = Math.max(0, Math.min(Math.floor(n.x / BIN), Math.floor(W / BIN)));
328        if (pile[b]) pile[b]--;
329        notes[i] = makeNote(true);
330        return;
331      }
332    }
333  }
334
335  function refill() {
336    if (broken) return;                          /* shaken-broke stays broke */
337    if (egg && egg.phase !== 'collect') return;  /* not during the game */
338    var floats = 0, dead = 0;
339    for (var i = 0; notes.length > i; i++) {
340      if (notes[i].mode === 'float') floats++;
341      else if (notes[i].mode === 'fall') dead++;
342    }
343    if (floats >= desiredFloats) return;
344    if (dead > 50) recycleDead();
345    else notes.push(makeNote(true));
346  }
347
348  function unsettleAll() {
349    pile = [];
350    for (var i = 0; notes.length > i; i++) {
351      notes[i].settled = false;
352      if (notes[i].mode !== 'drag') notes[i].mode = 'fall';
353    }
354  }
355
356  function floorFor(n) {
357    if (n.settled) return n.restY;
358    var b = Math.max(0, Math.min(Math.floor(n.x / BIN), Math.floor(W / BIN)));
359    return H - 8 - (pile[b] || 0) * STEP;
360  }
361
362  function settle(n) {
363    var b = Math.max(0, Math.min(Math.floor(n.x / BIN), Math.floor(W / BIN)));
364    pile[b] = (pile[b] || 0) + 1;
365    n.restY = H - 8 - (pile[b] - 1) * STEP;
366    n.settled = true;
367    n.vy = 0;
368  }
369
370  function step() {
371    ctx.clearRect(0, 0, W, H);
372    var now = Date.now();
373    var jolting = 400 > now - lastJolt;
374    if (now > refillAt) { refillAt = now + 700; refill(); }
375    for (var i = 0; notes.length > i; i++) {
376      var n = notes[i];
377      if (n.mode === 'drag') {
378        /* position is driven by the pointer handlers */
379      } else if (n.mode === 'float') {
380        n.ph += n.phv;
381        n.y -= n.spd;
382        n.x += Math.sin(n.ph) * 0.5 + tilt * 1.4;
383        n.rot = Math.sin(n.ph) * 0.3 + tilt * 0.5;
384        if (0 > n.y + 60) { notes[i] = makeNote(true); continue; }
385        if (0 > n.x - 10) n.x = W + 8;
386        if (n.x > W + 12) n.x = -8;
387      } else { /* fall (broken by shake, or dropped after a drag) */
388        if (jolting && 0.35 > Math.random()) {
389          n.settled = false;
390          n.vy -= rnd(0, joltMag * 0.9);
391          n.vx += rnd(-1, 1) * joltMag * 0.6;
392        }
393        if (!n.settled) {
394          n.vy += 0.42;
395          n.vx += tilt * 0.06;
396          n.vx *= 0.985;
397          n.x += n.vx;
398          n.y += n.vy;
399          n.rot += n.vx * 0.02;
400          var fy = floorFor(n);
401          if (n.y > fy) {
402            n.y = fy;
403            if (Math.abs(n.vy) > 2.2) {
404              n.vy = -n.vy * 0.32;
405              n.vx *= 0.7;
406            } else {
407              settle(n);
408            }
409          }
410          if (8 > n.x) { n.x = 8; n.vx = Math.abs(n.vx) * 0.5; }
411          if (n.x > W - 8) { n.x = W - 8; n.vx = -Math.abs(n.vx) * 0.5; }
412          if (0 > n.y) { n.y = 0; n.vy = Math.abs(n.vy) * 0.4; }
413        }
414      }
415      ctx.save();
416      ctx.translate(n.x, n.y);
417      ctx.rotate(n.rot);
418      ctx.font = n.size + 'px serif';
419      ctx.fillStyle = n.col;
420      ctx.textAlign = 'center';
421      ctx.fillText(n.glyph, 0, 0);
422      ctx.restore();
423    }
424    updateEgg(now);
425    if (running) raf = requestAnimationFrame(step);
426  }
427
428  function start() {
429    if (running || startPending || !overlay) return;
430    startPending = true;
431    if (!canvas) {
432      canvas = document.createElement('canvas');
433      canvas.className = 'eds-menu-notes';
434      canvas.setAttribute('aria-hidden', 'true');
435      overlay.insertBefore(canvas, overlay.firstChild);
436      ctx = canvas.getContext('2d');
437    }
438    /* Menu layout often settles a frame after is-menu-open — measuring too
439       early yields a tiny bitmap that CSS stretches fullscreen (huge notes). */
440    function tryStart() {
441      if (!overlay || !overlay.classList.contains('is-menu-open')) {
442        startPending = false;
443        return;
444      }
445      var m = readOverlaySize();
446      if (!m) {
447        requestAnimationFrame(tryStart);
448        return;
449      }
450      startPending = false;
451      applyCanvasSize(m.w, m.h);
452      resetNotes();
453      resetEgg();
454      if (overlay.classList.contains('eds-gh-mode')) {
455        overlay.classList.remove('eds-gh-mode');
456      }
457      running = true;
458      raf = requestAnimationFrame(step);
459    }
460    requestAnimationFrame(function () {
461      requestAnimationFrame(tryStart);
462    });
463  }
464
465  function stop() {
466    startPending = false;
467    running = false;
468    drag = null;
469    try { releaseAllHolds(); } catch (err) {}
470    /* Only touch the class if present: classList.remove() writes the
471       attribute even when the token is absent, which re-fires the
472       MutationObserver and would loop stop() forever. */
473    if (overlay && overlay.classList.contains('eds-gh-mode')) {
474      overlay.classList.remove('eds-gh-mode');
475    }
476    if (raf) cancelAnimationFrame(raf);
477    raf = 0;
478  }
479
480  function readOverlaySize() {
481    if (!overlay) return null;
482    var r = overlay.getBoundingClientRect();
483    var w = Math.round(r.width), h = Math.round(r.height);
484    if (80 > w || 80 > h) return null;
485    return { w: w, h: h };
486  }
487
488  function applyCanvasSize(w, h) {
489    dpr = Math.min(window.devicePixelRatio || 1, 2);
490    W = w;
491    H = h;
492    canvas.width = W * dpr;
493    canvas.height = H * dpr;
494    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
495  }
496
497  /* Rotation/resize while the menu is open: re-measure the overlay, resize
498     the bitmap, scale note positions into the new space and let settled
499     notes re-fall to the new floor (their old rest heights are stale). */
500  function resize() {
501    if (!running || !overlay || !canvas) return;
502    var m = readOverlaySize();
503    if (!m) return;
504    var w = m.w, h = m.h;
505    if (w === W && h === H) {
506      /* Still refresh dpr (zoom / move between screens). */
507      var nextDpr = Math.min(window.devicePixelRatio || 1, 2);
508      if (nextDpr === dpr) return;
509      applyCanvasSize(w, h);
510      return;
511    }
512    var sx = w / W, sy = h / H;
513    applyCanvasSize(w, h);
514    pile = [];
515    drag = null;
516    for (var i = 0; notes.length > i; i++) {
517      var n = notes[i];
518      n.x = Math.max(8, Math.min(n.x * sx, W - 8));
519      n.y = Math.min(n.y * sy, H - 8);
520      if (n.mode === 'drag') n.mode = 'fall';
521      n.settled = false;
522    }
523  }
524
525  /* Electric guitar: each touched note plays the next pitch of the intro
526     riff (user-supplied tab, standard tuning, key of D). Karplus-Strong
527     plucked string into tanh distortion + lowpass "cab". MIDI numbers:
528     string base + fret (D=50, G=55, B=59, e=64). */
529  var AC = null, distIn = null, drvIn = null, offIn = null, seqIdx = 0;
530
531  var SEQ = (function () {
532    function rep2(a) { return a.concat(a); }
533    /* half-measure pattern: low note, then B14 G13 G11 e14 G13 e13 G13 */
534    function line(low) { return [low, 73, 68, 66, 78, 68, 77, 68]; }
535    var L1 = rep2(line(61)); /* D11 measures */
536    var L2 = rep2(line(63)); /* D13 measures */
537    var L3 = rep2(line(66)); /* G11 measures */
538    var part1 = [].concat(L1, L2, L3, L1);
539    var part3 = [75, 68, 73, 68, 75, 68, 77, 68, 78, 68, 77, 68, 75, 68, 73];
540    /* part 1 twice, part 2 (= first three lines) once, part 3 once */
541    return [].concat(part1, part1, L1, L2, L3, part3);
542  })();
543
544  function ensureAudio() {
545    if (AC) return true;
546    var Ctor = window.AudioContext || window.webkitAudioContext;
547    if (!Ctor) return false;
548    AC = new Ctor();
549    var master = AC.createGain();
550    master.gain.value = 0.24;
551    /* Heavy drive: tanh(16x) is nearly a square wave at full amplitude —
552       the saturated lead tone. Mid bump before the clip adds growl. */
553    var mid = AC.createBiquadFilter();
554    mid.type = 'peaking';
555    mid.frequency.value = 850;
556    mid.Q.value = 0.9;
557    mid.gain.value = 6;
558    var dist = AC.createWaveShaper();
559    var n = 2048, curve = new Float32Array(n);
560    for (var i = 0; n > i; i++) {
561      var x = (i * 2) / (n - 1) - 1;
562      curve[i] = Math.tanh(16 * x);
563    }
564    dist.curve = curve;
565    dist.oversample = '4x';
566    var tone = AC.createBiquadFilter();
567    tone.type = 'lowpass';
568    tone.frequency.value = 3200;
569    tone.Q.value = 0.7;
570    mid.connect(dist);
571    dist.connect(tone);
572    tone.connect(master);
573    master.connect(AC.destination);
574    distIn = mid;
575
576    /* ROCK-mode amp: huge pre-gain slammed into a hard clipper — the
577       waveform is essentially a square by the time it leaves. Separate
578       chain so SOCK-mode plucks keep the cleaner sound above. */
579    var pre = AC.createGain();
580    pre.gain.value = 14;
581    var mid2 = AC.createBiquadFilter();
582    mid2.type = 'peaking';
583    mid2.frequency.value = 750;
584    mid2.Q.value = 0.8;
585    mid2.gain.value = 8;
586    var clip = AC.createWaveShaper();
587    var curve2 = new Float32Array(n);
588    for (i = 0; n > i; i++) {
589      var x2 = (i * 2) / (n - 1) - 1;
590      curve2[i] = Math.tanh(80 * x2); /* near-square hard clip */
591    }
592    clip.curve = curve2;
593    clip.oversample = '4x';
594    var cab = AC.createBiquadFilter();
595    cab.type = 'lowpass';
596    cab.frequency.value = 2600;
597    cab.Q.value = 1.1;
598    var master2 = AC.createGain();
599    master2.gain.value = 0.1; /* clipped signal is loud — pull it back */
600    pre.connect(mid2);
601    mid2.connect(clip);
602    clip.connect(cab);
603    cab.connect(master2);
604    master2.connect(AC.destination);
605    drvIn = pre;
606
607    /* off-note chain: identical amp, slightly quieter output (the clipper
608       compresses everything, so post-clip gain is the only loudness lever) */
609    var pre3 = AC.createGain();
610    pre3.gain.value = 14;
611    var mid3 = AC.createBiquadFilter();
612    mid3.type = 'peaking';
613    mid3.frequency.value = 750;
614    mid3.Q.value = 0.8;
615    mid3.gain.value = 8;
616    var clip3 = AC.createWaveShaper();
617    clip3.curve = curve2;
618    clip3.oversample = '4x';
619    var cab3 = AC.createBiquadFilter();
620    cab3.type = 'lowpass';
621    cab3.frequency.value = 2600;
622    cab3.Q.value = 1.1;
623    var master3 = AC.createGain();
624    master3.gain.value = 0.075;
625    pre3.connect(mid3);
626    mid3.connect(clip3);
627    clip3.connect(cab3);
628    cab3.connect(master3);
629    master3.connect(AC.destination);
630    offIn = pre3;
631    return true;
632  }
633
634  var pluckBufs = {}; /* per-pitch cache, keyed midi + clean/dirty */
635
636  function pluck(midi, dirty) {
637    if (!ensureAudio()) return;
638    if (AC.state === 'suspended') AC.resume();
639    /* dirty (ROCK mode): longer, less damped string — the clipper turns
640       the sustained tail into a singing square wave */
641    var dur = dirty ? 1.6 : 1.1;
642    var key = midi + (dirty ? 'd' : 'c');
643    var buf = pluckBufs[key];
644    if (!buf) {
645      var f = 440 * Math.pow(2, (midi - 69) / 12);
646      var sr = AC.sampleRate, N = Math.round(sr * dur);
647      var damp = dirty ? 0.4995 : 0.498;
648      buf = AC.createBuffer(1, N, sr);
649      var d = buf.getChannelData(0);
650      var period = Math.max(2, Math.round(sr / f));
651      var i;
652      for (i = 0; period >
652 i; i++) d[i] = Math.random() * 2 - 1;
653      for (i = period; N > i; i++) d[i] = (d[i - period] + d[i - period + 1]) * damp;
654      pluckBufs[key] = buf;
655    }
656    var src = AC.createBufferSource();
657    src.buffer = buf;
658    var g = AC.createGain();
659    var t = AC.currentTime;
660    g.gain.setValueAtTime(1, t);
661    g.gain.exponentialRampToValueAtTime(0.001, t + dur);
662    src.connect(g);
663    g.connect(dirty ? drvIn : distIn);
664    src.start(t);
665    src.stop(t + dur);
666  }
667
668  function playNext() {
669    try {
670      pluck(SEQ[seqIdx]);
671      seqIdx = (seqIdx + 1) % SEQ.length;
672    } catch (err) { /* audio is a bonus — never break the menu */ }
673  }
674
675  /* off-note for presses that don't count: same square-wave amp and the
676     same attack as a real note, but randomly sour (0.6-1.7 semitones flat
677     or sharp), slightly quieter, and with much less sustain — just "off" */
678  function offNote(midi) {
679    if (!ensureAudio()) return;
680    if (AC.state === 'suspended') AC.resume();
681    var sour = (0.5 > Math.random() ? -1 : 1) * rnd(0.6, 1.7);
682    var f = 440 * Math.pow(2, (midi + sour - 69) / 12);
683    var sr = AC.sampleRate, dur = 0.5, N = Math.round(sr * dur);
684    var buf = AC.createBuffer(1, N, sr);
685    var d = buf.getChannelData(0);
686    var period = Math.max(2, Math.round(sr / f));
687    var i;
688    for (i = 0; period > i; i++) d[i] = Math.random() * 2 - 1;
689    for (i = period; N > i; i++) d[i] = (d[i - period] + d[i - period + 1]) * 0.4995;
690    var src = AC.createBufferSource();
691    src.buffer = buf;
692    var g = AC.createGain();
693    var t = AC.currentTime;
694    g.gain.setValueAtTime(1, t);
695    g.gain.exponentialRampToValueAtTime(0.001, t + dur);
696    src.connect(g);
697    g.connect(offIn);
698    src.start(t);
699    src.stop(t + dur);
700  }
701
702  /* representative pitch per lane, for empty-lane flubs */
703  var LANE_PITCH = [61, 66, 68, 73, 78];
704
705  /* Held game notes: the string barely decays and the clipper compresses
706     whatever is left, so it sings until the finger lifts (the Slash way).
707     Keyed by pointerId so multi-finger play works. */
708  var held = {};
709  var holdBufs = {}; /* per-pitch cache: synthesizing 6s of string on every
710                        hit hitched the frame and bunched the lane notes */
711
712  function pluckHold(midi, pid) {
713    if (!ensureAudio()) return;
714    if (AC.state === 'suspended') AC.resume();
715    releaseHold(pid);
716    var dur = 6;
717    var buf = holdBufs[midi];
718    if (!buf) {
719      var f = 440 * Math.pow(2, (midi - 69) / 12);
720      var sr = AC.sampleRate, N = Math.round(sr * dur);
721      buf = AC.createBuffer(1, N, sr);
722      var d = buf.getChannelData(0);
723      var period = Math.max(2, Math.round(sr / f));
724      var i;
725      for (i = 0; period > i; i++) d[i] = Math.random() * 2 - 1;
726      for (i = period; N > i; i++) d[i] = (d[i - period] + d[i - period + 1]) * 0.49985;
727      holdBufs[midi] = buf;
728    }
729    var src = AC.createBufferSource();
730    src.buffer = buf;
731    var g = AC.createGain();
732    var t = AC.currentTime;
733    g.gain.setValueAtTime(1, t);
734    src.connect(g);
735    g.connect(drvIn);
736    src.start(t);
737    src.stop(t + dur);
738    held[pid] = { g: g, src: src };
739    src.onended = function () {
740      if (held[pid] && held[pid].src === src) delete held[pid];
741    };
742  }
743
744  function releaseHold(pid) {
745    var v = held[pid];
746    if (!v) return;
747    delete held[pid];
748    /* short tap = short note: the clipper rebuilds even a whisper back to
749       full volume, so a pre-clip fade alone can't end the note — the
750       source must actually stop. Disconnect as a belt-and-braces so the
751       voice dies even if stop() misbehaves. */
752    var t = AC.currentTime;
753    try {
754      v.g.gain.cancelScheduledValues(t);
755      v.g.gain.setValueAtTime(Math.max(0.001, v.g.gain.value), t);
756      v.g.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
757    } catch (err) {}
758    try { v.src.stop(t + 0.12); } catch (err2) {}
759    setTimeout(function () {
760      try { v.src.disconnect(); } catch (e2) {}
761      try { v.g.disconnect(); } catch (e3) {}
762    }, 200);
763  }
764
765  function releaseAllHolds() {
766    for (var k in held) releaseHold(k);
767  }
768
769  /* ---- SOCK/ROCK easter egg ----------------------------------------
770     Faint "SOCK" letters sit above the menu. Dragging or throwing a note
771     across a letter lights it. All four lit: the word flashes ("guitar
772     hero mode"), the ambient notes vanish, the S explodes into shards and
773     becomes an R (ROCK slams), the menu links hide, and a 5-button
774     rhythm game starts — lane notes rise from the bottom carrying the
775     riff's own pitches; pressing the matching button in the hit window
776     plucks that pitch. Reset on menu reopen. */
777  var egg = null;
778  var WORD = ['S', 'O', 'C', 'K'];
779  var GH_COLORS = ['#2ecc40', '#ff4136', '#ffdc00', '#0074d9', '#ff851b'];
780  /* difficulty knobs */
781  var HIT_WINDOW = 110;    /* px around the button row that still counts */
782  var APPROACH = 300;      /* a lane re-arms when its next note is this close */
783  var MISS_DEBOUNCE = 700; /* misses this close together count as one */
784  var STRIKE_MISSES = 6;   /* counted misses inside STRIKE_WINDOW = one strike */
785  var STRIKE_WINDOW = 8000;
786  var MAX_STRIKES = 3;
787  var edImg = null, edReady = false;
788
789  function loadEd() {
790    if (edImg) return;
791    edImg = new Image();
792    edImg.onload = function () { edReady = true; };
793    edImg.src = '/assets/images/ed-side-eyes.webp';
794  }
795
796  function resetEgg() {
797    egg = {
798      phase: 'collect', lit: [false, false, false, false], t0: 0,
799      litAt: [0, 0, 0, 0],
800      shards: [], rings: [], gnotes: [], gseq: 0, spawnAt: 0,
801      press: [0, 0, 0, 0, 0],
802      score: 0, gcount: 0, nextSeq: 0, badPresses: 0,
803      strikes: 0, strikeAt: 0, recentMisses: [],
804      laneLock: [false, false, false, false, false]
805    };
806  }
807
808  function laneFor(m) {
809    if (m === 68) return 2;      /* the pedal tone, middle lane */
810    if (64 > m) return 0;        /* low riff notes */
811    if (67 > m) return 1;
812    if (76 > m) return 3;
813    return 4;                    /* highest notes */
814  }
815
816  function letterRects() {
817    var lw = Math.min(60, W / 7.5), gap = Math.min(16, W * 0.03);
818    var total = 4 * lw + 3 * gap;
819    var x0 = (W - total) / 2, top = 46, h = Math.round(lw * 1.1);
820    var out = [];
821    for (var i = 0; 4 > i; i++) out.push({ x: x0 + i * (lw + gap), y: top, w: lw, h: h });
822    return out;
823  }
824
825  function btnGeom() {
826    /* landscape: bigger buttons, higher row — more runway to watch the
827       notes climb (the score lives in the top-left corner there) */
828    var landscape = W > H;
829    var spacing = Math.min(landscape ? 88 : 72, (W - 60) / 4);
830    var xs = [];
831    for (var i = 0; 5 > i; i++) xs.push(W / 2 + (i - 2) * spacing);
832    return {
833      xs: xs,
834      y: landscape ? 112 : letterRects()[0].y + 130,
835      r: landscape ? Math.min(34, spacing * 0.42) : Math.min(26, spacing * 0.4)
836    };
837  }
838
839  function drawWord(allLit, asRock, scale, jx, jy) {
840    var rects = letterRects();
841    for (var i = 0; 4 > i; i++) {
842      var r = rects[i];
843      var ch = (asRock && i === 0) ? 'R' : WORD[i];
844      var cx = r.x + r.w / 2 + (jx || 0), cy = r.y + r.h - 6 + (jy || 0);
845      /* pop: freshly lit letters swell to ~1.6x and settle back over 420ms */
846      var pop = 1;
847      if (egg.litAt && egg.litAt[i]) {
848        var pt = (Date.now() - egg.litAt[i]) / 420;
849        if (1 > pt) pop = 1 + 0.6 * Math.sin(pt * 3.1416);
850      }
851      ctx.save();
852      ctx.translate(cx, cy);
853      ctx.scale((scale || 1) * pop, (scale || 1) * pop);
854      ctx.font = 'bold ' + r.h + 'px sans-serif';
855      ctx.textAlign = 'center';
856      if (allLit || egg.lit[i]) {
857        ctx.shadowColor = '#209d50';
858        ctx.shadowBlur = 16;
859        ctx.fillStyle = '#1a7a40';
860        ctx.fillText(ch, 0, 0);
861      } else {
862        ctx.globalAlpha = 0.2;
863        ctx.lineWidth = 2;
864        ctx.strokeStyle = '#333';
865        ctx.strokeText(ch, 0, 0);
866      }
867      ctx.restore();
868    }
869  }
870
871  function updateEgg(now) {
872    if (!egg) return;
873    var i, j, r;
874    if (egg.phase === 'collect') {
875      var rects = letterRects();
876      for (i = 0; notes.length > i; i++) {
877        var n = notes[i];
878        /* one letter per interaction: a note lights at most one letter
879           per grab/throw (re-armed on the next grab) */
880        if ((n.mode === 'drag' || (n.mode === 'fall' && !n.settled)) && !n.letterUsed) {
881          for (j = 0; 4 > j; j++) {
882            if (!egg.lit[j]) {
883              r = rects[j];
884              if (n.x > r.x && r.x + r.w > n.x && n.y > r.y && r.y + r.h + 12 > n.y) {
885                egg.lit[j] = true;
886                egg.litAt[j] = now;
887                n.letterUsed = true;
888                egg.rings.push({ x: r.x + r.w / 2, y: r.y + r.h / 2, r: 16, life: 1 });
889                break;
890              }
891            }
892          }
893        }
894      }
895      drawWord(false, false, 1);
896      if (egg.lit[0] && egg.lit[1] && egg.lit[2] && egg.lit[3]) {
897        egg.phase = 'flash';
898        egg.t0 = now;
899      }
900    } else if (egg.phase === 'flash') {
901      var blink = Math.floor((now - egg.t0) / 130);
902      drawWord(blink % 2 === 0, false, 1);
903      if (blink > 7) {
904        egg.phase = 'explode';
905        egg.t0 = now;
906        notes = [];
907        drag = null;
908        var s = letterRects()[0];
909        for (i = 0; 26 > i; i++) {
910          egg.shards.push({
911            x: s.x + s.w / 2, y: s.y + s.h / 2,
912            vx: rnd(-7, 7), vy: rnd(-9, 3), life: 1,
913            col: 0.5 > Math.random() ? '#1a7a40' : '#555'
914          });
915        }
916      }
917    } else if (egg.phase === 'explode') {
918      var dur = now - egg.t0;
919      var asRock = dur > 240;
920      var scale = 1, jx = 0, jy = 0;
921      if (asRock) {
922        var p = Math.min(1, (dur - 240) / 280);
923        scale = 1.9 - 0.9 * p;
924        if (500 > dur) { jx = rnd(-4, 4); jy = rnd(-3, 3); } /* the slam */
925      }
926      drawWord(true, asRock, scale, jx, jy);
927      for (i = egg.shards.length - 1; i >= 0; i--) {
928        var sh = egg.shards[i];
929        sh.vy += 0.45;
930        sh.x += sh.vx;
931        sh.y += sh.vy;
932        sh.life -= 0.025;
933        if (0 > sh.life) { egg.shards.splice(i, 1); continue; }
934        ctx.globalAlpha = Math.max(0, sh.life);
935        ctx.fillStyle = sh.col;
936        ctx.fillRect(sh.x - 3, sh.y - 3, 6, 6);
937        ctx.globalAlpha = 1;
938      }
939      if (dur > 1100) {
940        egg.phase = 'game';
941        egg.shards.length = 0;
942        overlay.classList.add('eds-gh-mode');
943        egg.spawnAt = now + 600;
944      }
945    } else if (egg.phase === 'over') {
946      /* more than 25 missed notes: GAME OVER, Ed fading into view */
947      var oa = Math.min(1, (now - egg.t0) / 1800);
948      if (edReady) {
949        var iw = edImg.width, ih = edImg.height;
950        var sc = Math.min((W * 0.7) / iw, (H * 0.5) / ih);
951        /* slow breathing pulse around Ed's center */
952        var pulse = 1 + 0.05 * Math.sin((now - egg.t0) / 280);
953        ctx.save();
954        ctx.globalAlpha = oa;
955        ctx.translate(W / 2, H * 0.38 + ih * sc * 0.5);
956        ctx.scale(pulse, pulse);
957        ctx.drawImage(edImg, -iw * sc / 2, -ih * sc / 2, iw * sc, ih * sc);
958        ctx.restore();
959      }
960      ctx.save();
961      ctx.globalAlpha = oa;
962      var fs = Math.min(76, W / 5.2);
963      ctx.font = 'bold ' + fs + 'px sans-serif';
964      ctx.textAlign = 'center';
965      ctx.shadowColor = '#209d50';
966      ctx.shadowBlur = 18;
967      ctx.fillStyle = '#1a7a40';
968      ctx.fillText('GAME', W / 2, H * 0.15);
969      ctx.fillText('OVER', W / 2, H * 0.15 + fs * 1.08);
970      var ostr = '' + egg.score;
971      while (5 > ostr.length) ostr = '0' + ostr;
972      ctx.shadowBlur = 8;
973      ctx.font = 'bold ' + Math.round(fs * 0.45) + 'px monospace';
974      ctx.fillText(ostr, W / 2, H * 0.15 + fs * 1.08 + fs * 0.75);
975      ctx.restore();
976    } else { /* game */
977      /* ROCK gives way to the score counter, zero-padded to 5 digits.
978         Landscape: tuck it in the top-left so the lanes stay clear. */
979      var landscape = W > H;
980      var str = '' + egg.score;
981      while (5 > str.length) str = '0' + str;
982      var lr = letterRects()[0];
983      ctx.save();
984      ctx.shadowColor = '#209d50';
985      ctx.shadowBlur = 16;
986      ctx.fillStyle = '#1a7a40';
987      if (landscape) {
988        ctx.font = 'bold 30px monospace';
989        ctx.textAlign = 'left';
990        ctx.fillText(str, 16, 44);
991      } else {
992        ctx.font = 'bold ' + Math.round(lr.h * 0.85) + 'px monospace';
993        ctx.textAlign = 'center';
994        ctx.fillText(str, W / 2, lr.y + lr.h - 6);
995      }
996      ctx.restore();
997      var b = btnGeom();
998      /* spawn on a fixed 300ms grid (steady 1/8th notes). born is the grid
999         time, so spacing stays exact even when a frame arrives late */
1000      while (now > egg.spawnAt) {
1001        if (now - egg.spawnAt > 1200) egg.spawnAt = now; /* resync after a stall */
1002        var midi = SEQ[egg.gseq];
1003        egg.gnotes.push({
1004          lane: laneFor(midi), midi: midi, y: H + 26,
1005          born: egg.spawnAt, id: egg.gcount++
1006        });
1007        egg.gseq = (egg.gseq + 1) % SEQ.length;
1008        egg.spawnAt += 300;
1009      }
1010      /* analytic position from the wall clock: frame hitches (audio
1011         synthesis, GC, refresh-rate changes) can never bunch notes up.
1012         ~1.4s bottom-to-buttons, capped for portrait's long lane. */
1013      var vel = Math.min((H - b.y) / 1400, 0.204); /* px per ms */
1014      for (i = egg.gnotes.length - 1; i >= 0; i--) {
1015        var gn = egg.gnotes[i];
1016        gn.y = H + 26 - (now - gn.born) * vel;
1017        if (b.y - HIT_WINDOW > gn.y) { /* missed: sequence moves on regardless */
1018          if (gn.id >= egg.nextSeq) egg.nextSeq = gn.id + 1;
1019          egg.gnotes.splice(i, 1);
1020          /* strikes: a burst of misses in a short window is a strike;
1021             spaced-out misses age out and cost nothing. Back-to-back
1022             misses (a "double miss") are debounced into one. */
1023          var lastMiss = egg.recentMisses.length
1024            ? egg.recentMisses[egg.recentMisses.length - 1] : 0;
1025          if (now - lastMiss > MISS_DEBOUNCE) egg.recentMisses.push(now);
1026          while (egg.recentMisses.length && now - egg.recentMisses[0] > STRIKE_WINDOW) {
1027            egg.recentMisses.shift();
1028          }
1029          if (egg.recentMisses.length >= STRIKE_MISSES) {
1030            egg.strikes++;
1031            egg.strikeAt = now;
1032            egg.recentMisses = [];
1033            if (egg.strikes >= MAX_STRIKES) {
1034              egg.phase = 'over';
1035              egg.t0 = now;
1036              egg.gnotes = [];
1037              loadEd();
1038              try { releaseAllHolds(); } catch (err) {}
1039              break;
1040            }
1041          }
1042          continue;
1043        }
1044        ctx.beginPath();
1045        ctx.arc(b.xs[gn.lane], gn.y, 18, 0, 6.29);
1046        ctx.fillStyle = GH_COLORS[gn.lane];
1047        ctx.fill();
1048        ctx.fillStyle = '#fff';
1049        ctx.font = 'bold 19px serif';
1050        ctx.textAlign = 'center';
1051        ctx.fillText('\u266A', b.xs[gn.lane], gn.y + 7);
1052      }
1053      /* re-arm locked lanes once their next note is approaching the keys */
1054      for (i = 0; egg.gnotes.length > i; i++) {
1055        var an = egg.gnotes[i];
1056        if (egg.laneLock[an.lane] && APPROACH > an.y - b.y) {
1057          egg.laneLock[an.lane] = false;
1058        }
1059      }
1060      /* strikes: three X slots under the score (beside it in landscape) */
1061      for (i = 0; MAX_STRIKES > i; i++) {
1062        var got = egg.strikes > i;
1063        var xpop = (got && i === egg.strikes - 1 && 320 > now - egg.strikeAt)
1064          ? 1 + 0.9 * (1 - (now - egg.strikeAt) / 320) : 1;
1065        ctx.save();
1066        if (landscape) ctx.translate(30 + i * 28, 70);
1067        else ctx.translate(W / 2 + (i - 1) * 30, letterRects()[0].y + 92);
1068        ctx.scale(xpop, xpop);
1069        ctx.font = 'bold 20px sans-serif';
1070        ctx.textAlign = 'center';
1071        ctx.globalAlpha = got ? 0.95 : 0.25;
1072        ctx.fillStyle = got ? '#c0392b' : '#555';
1073        ctx.fillText('\u2715', 0, 7);
1074        ctx.restore();
1075      }
1076      /* buttons (1–5 labels on desktop only — keyboard map) */
1077      for (i = 0; 5 > i; i++) {
1078        var pressed = 180 > now - egg.press[i];
1079        ctx.beginPath();
1080        ctx.arc(b.xs[i], b.y, pressed ? b.r + 4 : b.r, 0, 6.29);
1081        ctx.fillStyle = GH_COLORS[i];
1082        ctx.globalAlpha = pressed ? 1 : 0.85;
1083        ctx.fill();
1084        ctx.globalAlpha = 1;
1085        ctx.lineWidth = pressed ? 5 : 3;
1086        ctx.strokeStyle = 'rgba(255,255,255,0.9)';
1087        ctx.stroke();
1088        /* 1–5 labels are desktop keyboard hints — hide on touch */
1089        if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
1090          ctx.fillStyle = 'rgba(0,0,0,0.55)';
1091          ctx.font = 'bold ' + Math.round(b.r * 0.95) + 'px sans-serif';
1092          ctx.textAlign = 'center';
1093          ctx.textBaseline = 'middle';
1094          ctx.fillText(String(i + 1), b.xs[i], b.y + 1);
1095        }
1096      }
1097    }
1098    /* hit / letter-light rings (all phases) */
1099    for (i = egg.rings.length - 1; i >= 0; i--) {
1100      var rg = egg.rings[i];
1101      rg.r += 3;
1102      rg.life -= 0.06;
1103      if (0 > rg.life) { egg.rings.splice(i, 1); continue; }
1104      ctx.beginPath();
1105      ctx.arc(rg.x, rg.y, rg.r, 0, 6.29);
1106      ctx.globalAlpha = Math.max(0, rg.life);
1107      ctx.lineWidth = 4;
1108      ctx.strokeStyle = rg.col || '#209d50';
1109      ctx.stroke();
1110      ctx.globalAlpha = 1;
1111    }
1112  }
1113
1114  /* Finger drag: grab the nearest note (unless tapping a link), carry it,
1115     and on release it falls with the toss velocity. */
1116  var drag = null;
1117
1118  function grabAt(x, y) {
1119    var best = null, bd = 3600; /* 60px reach, squared */
1120    for (var i = 0; notes.length > i; i++) {
1121      var n = notes[i];
1122      var dx = n.x - x;
1123      var dy = n.y - n.size * 0.35 - y; /* glyph is drawn above the baseline */
1124      var d = dx * dx + dy * dy;
1125      if (bd > d) { bd = d; best = n; }
1126    }
1127    return best;
1128  }
1129
1130  function bestNoteInLane(lane, by) {
1131    var bn = null, bd = HIT_WINDOW;
1132    for (var k = 0; egg.gnotes.length > k; k++) {
1133      var gn = egg.gnotes[k];
1134      if (gn.lane === lane) {
1135        var dd = Math.abs(gn.y - by);
1136        if (bd > dd) { bd = dd; bn = gn; }
1137      }
1138    }
1139    return bn;
1140  }
1141
1142  /* Shared hit path for touch/click and desktop keys 1–5. */
1143  function hitLane(lane, holdId) {
1144    if (!egg || egg.phase !== 'game' || 0 > lane || lane > 4) return false;
1145    var b = btnGeom();
1146    var best = bestNoteInLane(lane, b.y);
1147    var scoring = !!(best && (best.id === egg.nextSeq || egg.badPresses >= 2));
1148    egg.press[lane] = Date.now();
1149    if (egg.laneLock[lane]) return true;
1150    if (scoring) {
1151      egg.badPresses = 0;
1152      egg.nextSeq = best.id + 1;
1153      egg.score += 100;
1154      egg.laneLock[lane] = true;
1155      try { pluckHold(best.midi, holdId); } catch (err) {}
1156      egg.rings.push({ x: b.xs[lane], y: b.y, r: 14, life: 1 });
1157      egg.gnotes.splice(egg.gnotes.indexOf(best), 1);
1158    } else if (best) {
1159      egg.badPresses++;
1160      try { offNote(best.midi); } catch (err) {}
1161      egg.rings.push({ x: b.xs[lane], y: b.y, r: 10, life: 0.6, col: '#888' });
1162    } else {
1163      try { offNote(LANE_PITCH[lane]); } catch (err) {}
1164    }
1165    return true;
1166  }
1167
1168  function onDown(e) {
1169    if (!running || drag) return;
1170    if (e.target && e.target.closest && e.target.closest('a, button')) return;
1171    var r = canvas.getBoundingClientRect();
1172    var x = e.clientX - r.left, y = e.clientY - r.top;
1173    /* game mode: buttons instead of grabs */
1174    if (egg && egg.phase === 'game') {
1175      var b = btnGeom();
1176      /* full-square hit boxes: the squares tile the button row edge to
1177         edge, so no dead zones between keys */
1178      var half = (b.xs[1] - b.xs[0]) / 2;
1179      var i, lane = -1;
1180      for (i = 0; 5 > i; i++) {
1181        if (half >= Math.abs(x - b.xs[i]) && half >= Math.abs(y - b.y)) {
1182          lane = i;
1183          break;
1184        }
1185      }
1186      var best = 0 > lane ? null : bestNoteInLane(lane, b.y);
1187      var scoring = !!(best && (best.id === egg.nextSeq || egg.badPresses >= 2));
1188      if (!scoring) {
1189        /* would-be miss: double the squares. If a key holding the correct
1190           note sits in the generous zone, that key activates — forgives a
1191           finger that lands just off the note it was aiming for. */
1192        var gd = 1e9;
1193        for (i = 0; 5 > i; i++) {
1194          if (2 * half >= Math.abs(x - b.xs[i]) && 2 * half >= Math.abs(y - b.y)) {
1195            var cand = bestNoteInLane(i, b.y);
1196            if (cand && (cand.id === egg.nextSeq || egg.badPresses >= 2)) {
1197              var dc = Math.abs(x - b.xs[i]);
1198              if (gd > dc) { gd = dc; lane = i; }
1199            }
1200          }
1201        }
1202      }
1203      if (0 > lane) return;
1204      hitLane(lane, e.pointerId);
1205      e.preventDefault();
1206      return;
1207    }
1208    if (egg && egg.phase !== 'collect') return; /* mid-transition */
1209    var n = grabAt(x, y);
1210    if (!n) return;
1211    var wasFloating = n.mode === 'float';
1212    n.mode = 'drag';
1213    n.settled = false;
1214    n.letterUsed = false; /* re-arm: this grab may light one letter */
1215    drag = { n: n, x: x, y: y, vx: 0, vy: 0, id: e.pointerId };
1216    if (wasFloating) playNext(); /* dead (fallen) notes stay dead */
1217    e.preventDefault();
1218  }
1219
1220  var KEY_LANES = {
1221    Digit1: 0, Digit2: 1, Digit3: 2, Digit4: 3, Digit5: 4,
1222    Numpad1: 0, Numpad2: 1, Numpad3: 2, Numpad4: 3, Numpad5: 4
1223  };
1224
1225  function onKeyDown(e) {
1226    if (!running || !egg || egg.phase !== 'game') return;
1227    if (!(e.code in KEY_LANES)) return;
1228    if (e.repeat) return;
1229    if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' ||
1230        e.target.isContentEditable)) return;
1231    e.preventDefault();
1232    hitLane(KEY_LANES[e.code], 'key-' + e.code);
1233  }
1234
1235  function onKeyUp(e) {
1236    if (!(e.code in KEY_LANES)) return;
1237    try { releaseHold('key-' + e.code); } catch (err) {}
1238  }
1239
1240  function onMove(e) {
1241    if (!drag || e.pointerId !== drag.id) return;
1242    var r = canvas.getBoundingClientRect();
1243    var x = e.clientX - r.left, y = e.clientY - r.top;
1244    drag.vx = drag.vx * 0.4 + (x - drag.x) * 0.6;
1245    drag.vy = drag.vy * 0.4 + (y - drag.y) * 0.6;
1246    var n = drag.n;
1247    n.x = x;
1248    n.y = y + n.size * 0.35;
1249    n.rot = Math.max(-0.6, Math.min(0.6, drag.vx * 0.05));
1250    drag.x = x;
1251    drag.y = y;
1252    e.preventDefault();
1253  }
1254
1255  function onUp(e) {
1256    try { releaseHold(e.pointerId); } catch (err) {}
1257    if (!drag || e.pointerId !== drag.id) return;
1258    var n = drag.n;
1259    n.mode = 'fall';
1260    n.vx = Math.max(-14, Math.min(14, drag.vx));
1261    n.vy = Math.max(-16, Math.min(16, drag.vy));
1262    drag = null;
1263  }
1264
1265  function onOrientation(e) {
1266    if (e.gamma == null) return;
1267    var g = Math.max(-45, Math.min(45, e.gamma)) / 45;
1268    tilt = tilt * 0.85 + g * 0.15;
1269  }
1270
1271  function onMotion(e) {
1272    var a = e.acceleration;
1273    var grav = false;
1274    if (!a || (a.x == null && a.y == null && a.z == null)) {
1275      a = e.accelerationIncludingGravity;
1276      grav = true;
1277    }
1278    if (!a) return;
1279    var m = Math.sqrt((a.x || 0) * (a.x || 0) + (a.y || 0) * (a.y || 0) + (a.z || 0) * (a.z || 0));
1280    if (grav) m = Math.abs(m - 9.81);
1281    if (m > 16) {
1282      var now = Date.now();
1283      shakeHits.push(now);
1284      shakeHits = shakeHits.filter(function (t) { return 1200 > now - t; });
1285      lastJolt = now;
1286      joltMag = Math.min(m / 8, 5);
1287      if (shakeHits.length > 3 && !broken) {
1288        broken = true;
1289        unsettleAll();
1290      } else if (broken) {
1291        unsettleAll();
1292      }
1293    }
1294  }
1295
1296  /* iOS 13+ requires a user-gesture permission request for motion/gyro */
1297  function askPermission() {
1298    ['DeviceOrientationEvent', 'DeviceMotionEvent'].forEach(function (k) {
1299      var C = window[k];
1300      if (C && typeof C.requestPermission === 'function') {
1301        C.requestPermission().catch(function () {});
1302      }
1303    });
1304  }
1305
1306  function boot() {
1307    var nav = document.querySelector('.eds-site-header .wp-block-navigation');
1308    if (!nav) return;
1309    overlay = nav.querySelector('.wp-block-navigation__responsive-container');
1310    if (!overlay) return;
1311
1312    /* Act only on actual open/close transitions — the callback fires for
1313       every class-attribute write on the overlay, including our own. */
1314    new MutationObserver(function () {
1315      var open = overlay.classList.contains('is-menu-open');
1316      if (open && !running) start();
1317      else if (!open && running) stop();
1318    }).observe(overlay, { attributes: true, attributeFilter: ['class'] });
1319
1320    overlay.addEventListener('pointerdown', onDown);
1321    overlay.addEventListener('pointermove', onMove);
1322    overlay.addEventListener('pointerup', onUp);
1323    overlay.addEventListener('pointercancel', onUp);
1324
1325    /* WP closes the modal when a tap lands on its backdrop elements. In
1326       guitar-hero mode the link content is visibility:hidden, so EVERY tap
1327       is a "backdrop" tap and the menu would slam shut mid-game. Swallow
1328       clicks in the capture phase unless they're on a real control (menu
1329       link or the close X) — matches the original sheet behavior, where
1330       empty-space taps did nothing. */
1331    overlay.addEventListener('click', function (e) {
1332      if (!running) return;
1333      if (e.target && e.target.closest && e.target.closest('a, button')) return;
1334      e.stopImmediatePropagation();
1335      e.preventDefault();
1336    }, true);
1337
1338    /* WP's navigation script also closes the menu on focusout when focus
1339       appears to leave the modal — which happens when the game hides the
1340       link content. Keep those events away from it once the egg is past
1341       normal play. */
1342    overlay.addEventListener('focusout', function (e) {
1343      if (running && egg && egg.phase !== 'collect') e.stopImmediatePropagation();
1344    }, true);
1345
1346    document.addEventListener('click', function (e) {
1347      if (e.target && e.target.closest &&
1348          e.target.closest('.wp-block-navigation__responsive-container-open')) {
1349        askPermission();
1350      }
1351    }, true);
1352
1353    window.addEventListener('deviceorientation', onOrientation);
1354    window.addEventListener('devicemotion', onMotion);
1355    window.addEventListener('resize', resize);
1356    window.addEventListener('keydown', onKeyDown);
1357    window.addEventListener('keyup', onKeyUp);
1358    /* orientationchange can fire before layout settles — measure late */
1359    window.addEventListener('orientationchange', function () {
1360      setTimeout(resize, 300);
1361    });
1362    if (typeof ResizeObserver !== 'undefined') {
1363      new ResizeObserver(function () {
1364        if (running) resize();
1365      }).observe(overlay);
1366    }
1367  }
1368
1369  if (document.readyState === 'loading') {
1370    document.addEventListener('DOMContentLoaded', boot);
1371  } else {
1372    boot();
1373  }
1374})();
1375</script>
1375
1376<div class="nfd-container eds-site-header wp-block-group alignfull nfd-bg-surface nfd-theme-white nfd-wb-header__header-1 has-background" style="background-color:#a8c5b5"><div class="wp-block-group"><div class="wp-block-site-logo"><a href="/" rel="home"><img src="/assets/images/banner-top-white.png" alt="Ed The Sock" width="307" decoding="async"/></a></div><div class="nfd-gap-2xl wp-block-group"><nav class="wp-block-navigation nfd-gap-lg" aria-label="Site">
1377  <button type="button" class="wp-block-navigation__responsive-container-open" aria-label="Open menu">
1378    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path d="M4 7h16v2H4V7zm0 5h16v2H4v-2zm0 5h16v2H4v-2z" fill="currentColor"/></svg>
1379  </button>
1380  <div class="wp-block-navigation__responsive-container" tabindex="-1">
1381    <button type="button" class="wp-block-navigation__responsive-container-close" aria-label="Close menu">&times;</button>
1382    <div class="wp-block-navigation__responsive-dialog">
1383      <div class="wp-block-navigation__responsive-container-content">
1384        <ul class="wp-block-navigation__container">
1385<li class="wp-block-navigation-item"><a href="/">Home</a></li>
1386<li class="wp-block-navigation-item"><a href="/podcasts/">Podcasts</a></li>
1387<li class="wp-block-navigation-item current-menu-item"><a href="/sock-drawer/">The Sock Drawer</a></li>
1388<li class="wp-block-navigation-item"><a href="/faqs/">FAQs</a></li>
1389<li class="wp-block-navigation-item"><a href="/about/">About</a></li>
1390        </ul>
1391      </div>
1392    </div>
1393  </div>
1394</nav></div></div></div>
1395<main id="content">
1396<div class="wp-block-cover alignfull is-light has-custom-content-position is-position-top-center" style="min-height:100vh"><span aria-hidden="true" class="wp-block-cover__background has-background-dim-20 has-background-dim" style="background-color:#20a052"></span><img class="wp-block-cover__image-background wp-image-94" alt="" src="/assets/images/site-background.webp" data-object-fit="cover"/><div class="wp-block-cover__inner-container">
1397<style>
1398/* Original: flap top = header bottom + 27px (page cover is top-aligned) */
1399:root .eds-sd {
1400    margin-block-start: calc(var(--eds-header-h, 119px) + 27px);
1401}
1402</style>
1403<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Saira+Stencil+One&display=swap">
1404<!-- COMPONENT: sock-drawer-under-construction — ED THE SOCK · "Sock Drawer" title -->
1405<!-- Fonts: site-wide via config/site-fonts.json (docs/site-fonts.md) -->
1406<!-- Ed the Sock · "Sock Drawer" page title — paste into a Squarespace Code Block -->
1407
1408<div class="eds-sd">
1409  <div class="eds-sd-flap">
1410    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1040 300" width="1040" height="300">
1411<defs>
1412  <linearGradient id="kraft" x1="0" y1="0" x2="0" y2="1">
1413    <stop offset="0" stop-color="#d8b888"></stop>
1414    <stop offset="0.55" stop-color="#c69c63"></stop>
1415    <stop offset="1" stop-color="#b3863f"></stop>
1416  </linearGradient>
1417  <radialGradient id="mott" cx="0.25" cy="0.18" r="1">
1418    <stop offset="0" stop-color="#ffffff" stop-opacity="0.16"></stop>
1419    <stop offset="0.5" stop-color="#ffffff" stop-opacity="0"></stop>
1420    <stop offset="1" stop-color="#3a2410" stop-opacity="0.18"></stop>
1421  </radialGradient>
1422  <clipPath id="flap"><path d="M10.0,18.1 L34.8,28.9 L59.5,7.5 L84.3,16.4 L109.0,10.0 L133.8,8.1 L158.6,12.4 L183.3,19.9 L208.1,11.0 L232.9,13.3 L257.6,15.1 L282.4,12.3 L307.1,16.8 L331.9,23.0 L356.7,12.7 L381.4,7.1 L406.2,11.2 L431.0,18.8 L455.7,21.0 L480.5,16.0 L505.2,19.8 L530.0,20.2 L554.8,14.4 L579.5,13.3 L604.3,8.4 L629.0,16.6 L653.8,22.3 L678.6,13.9 L703.3,22.9 L728.1,14.5 L752.9,20.0 L777.6,11.4 L802.4,20.3 L827.1,13.1 L851.9,14.9 L876.7,17.3 L901.4,8.8 L926.2,16.5 L951.0,14.9 L975.7,7.6 L1000.5,12.9 L1025.2,8.2 L1026.7,15.0 L1021.9,36.4 L1021.8,57.9 L1030.9,79.3 L1020.9,100.7 L1030.4,122.1 L1030.4,143.6 L1026.7,165.0 L1032.0,186.4 L1030.0,207.9 L1028.5,229.3 L1026.2,250.7 L1026.0,272.1 L1030.0,288.9 L1005.2,260.4 L980.5,287.5 L955.7,275.2 L931.0,279.6 L906.2,277.6 L881.4,288.1 L856.7,282.2 L831.9,283.1 L807.1,286.2 L782.4,281.0 L757.6,286.2 L732.9,288.3 L708.1,283.5 L683.3,289.2 L658.6,285.0 L633.8,293.5 L609.0,289.7 L584.3,286.7 L559.5,282.0 L534.8,292.9 L510.0,285.4 L485.2,293.0 L460.5,281.8 L435.7,275.2 L411.
14220,288.0 L386.2,276.5 L361.4,267.0 L336.7,276.3 L311.9,284.3 L287.1,293.7 L262.4,281.5 L237.6,287.1 L212.9,288.4 L188.1,286.7 L163.3,291.1 L138.6,285.8 L113.8,276.6 L89.0,286.4 L64.3,285.1 L39.5,274.1 L14.8,284.6 L16.7,285.0 L14.9,263.6 L13.8,242.1 L15.0,220.7 L7.6,199.3 L9.3,177.9 L12.9,156.4 L11.2,135.0 L15.4,113.6 L12.3,92.1 L14.3,70.7 L16.0,49.3 L19.9,27.9 Z"></path></clipPath>
1423  <filter id="paper"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch" result="n"></feTurbulence>
1424    <feColorMatrix in="n" type="matrix" values="0 0 0 0 0.36  0 0 0 0 0.25  0 0 0 0 0.10  0 0 0 0.5 0"></feColorMatrix></filter>
1425</defs>
1426<g clip-path="url(#flap)">
1427  <rect x="0" y="0" width="1040" height="300" fill="url(#kraft)"></rect>
1428  <rect x="0" y="0" width="1040" height="300" fill="url(#mott)"></rect>
1429  <rect x="0" y="0" width="1040" height="300" filter="url(#paper)" opacity="0.10"></rect>
1430  
1431  <rect x="0" y="268" width="1040" height="32" fill="#a8783f"></rect>
1432  <path d="M0,300 L0,285 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 L1040,300 Z" fill="#e3c794"></path>
1433  <line x1="0" y1="268" x2="1040" y2="268" stroke="#7a5028" stroke-width="2.2" opacity="0.7"></line>
1434  <line x1="16" y1="270" x2="16" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="48" y1="270" x2="48" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="80" y1="270" x2="80" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="112" y1="270" x2="112" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="144" y1="270" x2="144" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="176" y1="270" x2="176" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="208" y1="270" x2="208" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="240" y1="270" x2="240" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="272" y1="270" x2="272" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="304" y1="270" x2="304" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="336" y1="270" x2="336" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="368" y1="270" x2="368" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="400" y1="270" x2="400" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="432" y1="270" x2="432" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="464" y1="270" x2="464" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="496" y1="270" x2="496" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="528" y1="270" x2="528" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="560" y1="270" x2="560" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="592" y1="270" x2="592" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="624" y1="270" x2="624" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="656" y1="270" x2="656" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="688" y1="270" x2="688" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="720" y1="270" x2="720" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="752" y1="270" x2="752" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="784" y1="270" x2="784" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="816" y1="270" x2="816" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="848" y1="270" x2="848" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="880" y1="270" x2="880" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="912" y1="270" x2="912" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="944" y1="270" x2="944" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="976" y1="270" x2="976" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="1008" y1="270" x2="1008" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line>
1435  
1436  <path d="M10.0,18.1 L34.8,28.9 L59.5,7.5 L84.3,16.4 L109.0,10.0 L133.8,8.1 L158.6,12.4 L183.3,19.9 L208.1,11.0 L232.9,13.3 L257.6,15.1 L282.4,12.3 L307.1,16.8 L331.9,23.0 L356.7,12.7 L381.4,7.1 L406.2,11.2 L431.0,18.8 L455.7,21.0 L480.5,16.0 L505.2,19.8 L530.0,20.2 L554.8,14.4 L579.5,13.3 L604.3,8.4 L629.0,16.6 L653.8,22.3 L678.6,13.9 L703.3,22.9 L728.1,14.5 L752.9,20.0 L777.6,11.4 L802.4,20.3 L827.1,13.1 L851.9,14.9 L876.7,17.3 L901.4,8.8 L926.2,16.5 L951.0,14.9 L975.7,7.6 L1000.5,12.9 L1025.2,8.2 L1026.7,15.0 L1021.9,36.4 L1021.8,57.9 L1030.9,79.3 L1020.9,100.7 L1030.4,122.1 L1030.4,143.6 L1026.7,165.0 L1032.0,186.4 L1030.0,207.9 L1028.5,229.3 L1026.2,250.7 L1026.0,272.1 L1030.0,288.9 L1005.2,260.4 L980.5,287.5 L955.7,27
14365.2 L931.0,279.6 L906.2,277.6 L881.4,288.1 L856.7,282.2 L831.9,283.1 L807.1,286.2 L782.4,281.0 L757.6,286.2 L732.9,288.3 L708.1,283.5 L683.3,289.2 L658.6,285.0 L633.8,293.5 L609.0,289.7 L584.3,286.7 L559.5,282.0 L534.8,292.9 L510.0,285.4 L485.2,293.0 L460.5,281.8 L435.7,275.2 L411.0,288.0 L386.2,276.5 L361.4,267.0 L336.7,276.3 L311.9,284.3 L287.1,293.7 L262.4,281.5 L237.6,287.1 L212.9,288.4 L188.1,286.7 L163.3,291.1 L138.6,285.8 L113.8,276.6 L89.0,286.4 L64.3,285.1 L39.5,274.1 L14.8,284.6 L16.7,285.0 L14.9,263.6 L13.8,242.1 L15.0,220.7 L7.6,199.3 L9.3,177.9 L12.9,156.4 L11.2,135.0 L15.4,113.6 L12.3,92.1 L14.3,70.7 L16.0,49.3 L19.9,27.9 Z" fill="none" stroke="#5e3a16" stroke-width="6" opacity="0.28"></path>
1437</g>
1438</svg>
1439    <div class="eds-sd-body">
1440      <span class="eds-sd-mark">▲ THIS SIDE UP ▲</span>
1441      <span class="eds-sd-title">SOCK DRAWER</span>
1442    </div>
1443    <div class="eds-sd-stamp"><span>ED&rsquo;S STASH<br><i>★ ★ ★</i><br>STRAIGHT FROM<br>THE VAULT</span></div>
1444  </div>
1445  <div class="eds-sd-note">
1446    <span class="eds-tape"></span>
1447    <span class="eds-note-big">UNDER<br>CONSTRUCTION</span>
1448    <span class="eds-note-sub">I&rsquo;m busy. Come back later.</span>
1449    <span class="eds-curl"></span>
1450  </div>
1451</div>
1452<style>
1453  .eds-sd{width:100%;max-width:1040px;margin:0 auto;display:flex;flex-direction:column;
1454    align-items:center;justify-content:center;box-sizing:border-box;container-type:inline-size}
1455  .eds-sd *{box-sizing:border-box}
1456  .eds-sd-flap{position:relative;width:100%;max-width:1040px;container-type:inline-size;
1457    transform:rotate(-1.6deg);filter:drop-shadow(0 14px 22px rgba(0,0,0,.42))}
1458  .eds-sd-flap>svg{display:block;width:100%;height:auto}
1459  .eds-sd-body{position:absolute;inset:0 0 6% 0;display:flex;flex-direction:column;
1460    align-items:center;justify-content:center;text-align:center;mix-blend-mode:multiply;
1461    font-family:'Saira Stencil One',sans-serif}
1462  .eds-sd-mark{font-size:2cqw;letter-spacing:.7cqw;color:#322a1f;margin-bottom:1.5cqw}
1463  .eds-sd-title{font-size:9cqw;letter-spacing:.7cqw;line-height:.85;color:#2c2418;
1464    text-shadow:0 0 .8cqw rgba(40,30,16,.18),.2cqw .3cqw 0 rgba(40,30,16,.1)}
1465  .eds-sd-stamp{position:absolute;right:3.2%;bottom:9%;width:15cqw;height:15cqw;border-radius:50%;
1466    border:.3cqw double rgba(150,42,30,.62);color:rgba(150,42,30,.7);display:flex;
1467    align-items:center;justify-content:center;text-align:center;font-family:'Saira Stencil One',sans-serif;
1468    font-size:1.3cqw;line-height:1.3;transform:rotate(-12deg);mix-blend-mode:multiply}
1469  .eds-sd-stamp i{font-style:normal;font-size:1.1cqw;opacity:.8}
1470
1471  /* yellow sticky note with a curled corner, tucked under the flap */
1472  .eds-sd-note{position:relative;width:30.5cqw;max-width:324px;margin:calc(-2cqw - 28px) auto 0;
1473    padding:4.4cqw 3cqw 5cqw;
1474    background:linear-gradient(158deg,#fff39e 0%,#ffe85a 16%,#fcdf4b 100%);
1475    transform:rotate(-3deg);transform-origin:top center;
1476    border-radius:7px 11px 5px 9px / 10px 6px 9px 7px;
1477    box-shadow:0 14px 22px rgba(0,0,0,.30),0 2px 4px rgba(0,0,0,.18);
1478    display:flex;flex-direction:column;align-items:center;text-align:center}
1479  .eds-tape{position:absolute;z-index:3;top:-2.6cqw;left:50%;width:11cqw;height:3.6cqw;
1480    transform:translateX(-50%) rotate(-1.5deg);
1481    background:rgba(116,168,206,.5);box-shadow:0 2px 4px rgba(0,0,0,.15);
1482    clip-path:polygon(4% 0,96% 0,100% 12.5%,96% 25%,100% 37.5%,96% 50%,100% 62.5%,96% 75%,100% 87.5%,96% 100%,4% 100%,0% 87.5%,4% 75%,0% 62.5%,4% 50%,0% 37.5%,4% 25%,0% 12.5%,4% 0)}
1483  .eds-curl{position:absolute;right:0;bottom:0;width:0;height:0;
1484    border-style:solid;border-width:0 0 3.6cqw 3.6cqw;
1485    border-color:transparent transparent #e6d048 transparent;
1486    filter:drop-shadow(-4px -4px 6px rgba(0,0,0,.24))}
1487  .eds-note-big{font-family:'Permanent Marker',cursive;font-size:3cqw;line-height:1.35;
1488    color:#33312e;letter-spacing:.05cqw;transform:rotate(-1.6deg) skewX(-3deg)}
1489  .eds-note-sub{font-family:'Permanent Marker',cursive;font-size:3cqw;line-height:1.35;
1490    color:#33312e;margin-top:2.6cqw;transform:rotate(1deg) skewX(-3deg)}
1491</style>
1492<!-- ╚════ end block ════╝ -->
1493
1494<div class="wp-block-columns alignwide" style="margin-top:3rem">
1495
1496<div class="wp-block-column">
1497<iframe title="Return of Ed the Sock!" src="https://www.youtube.com/embed/lHrphaMnHjc?feature=oembed" style="width:100%;aspect-ratio:16/9;border:0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy"></iframe>
1498</div>
1499
1500
1501<div class="wp-block-column">
1502<iframe title="Ed the Sock — NewMusic" src="https://www.youtube.com/embed/r2Of8-MA3Es?start=4&amp;feature=oembed" style="width:100%;aspect-ratio:16/9;border:0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy"></iframe>
1503</div>
1504
1505</div>
1506
1507</div></div>
1508</main>
1509<style>
1510/* Squarespace-style scroll-in animation (site default): blocks fade in and
1511   slide up as they enter the viewport — images AND text blocks alike.
1512   The hidden state is applied by JS only, so content stays visible without JS.
1513   (.eds-anim-img kept as an alias for anything already carrying it.) */
1514.eds-anim-img,
1515.eds-anim {
1516    opacity: 0;
1517    transform: translateY(26px);
1518}
1519
1520.eds-anim-img.eds-anim-in,
1521.eds-anim.eds-anim-in {
1522    opacity: 1;
1523    transform: none;
1524    transition: opacity 0.9s ease, transform 0.9s ease;
1525}
1526
1527@media (prefers-reduced-motion: reduce) {
1528    .eds-anim-img,
1529    .eds-anim,
1530    .eds-anim-img.eds-anim-in,
1531    .eds-anim.eds-anim-in {
1532        opacity: 1;
1533        transform: none;
1534        transition: none;
1535    }
1536}
1537</style>
1538<script>
1539/* No bare less-than chars in this script (wptexturize gotcha — docs/wordpress-operations.md) */
1540(function () {
1541  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
1542  if (typeof IntersectionObserver === 'undefined') return;
1543
1544  function boot() {
1545    var targets = document.querySelectorAll(
1546      'main figure.wp-block-image, main p, main h1, main h2, main h3, ' +
1547      'main h4, main h5, main h6, main .wp-block-buttons, main cite, ' +
1548      'main .eds-podcast-card, main .eds-footer-social'
1549    );
1550    if (!targets.length) return;
1551
1552    var io = new IntersectionObserver(function (entries) {
1553      entries.forEach(function (entry) {
1554        if (entry.isIntersecting) {
1555          entry.target.classList.add('eds-anim-in');
1556          io.unobserve(entry.target);
1557        }
1558      });
1559    }, { threshold: 0.12, rootMargin: '0px 0px -5% 0px' });
1560
1561    var initial = 0;
1562    targets.forEach(function (el) {
1563      if (el.dataset.edsAnim === '1') return;
1564      /* Skip patterns that animate themselves (hero carousel quotes,
1565         cassette title) — a second transform/opacity would fight them.
1566         Also skip text nested in an already-animated ancestor (e.g. a
1567         cite inside an observed p) and empty paragraphs. */
1568      if (el.closest('.eds-component-container, .eds-pod')) return;
1569      if (el.parentElement && el.parentElement.closest('[data-eds-anim="1"]')) return;
1570      if (el.tagName === 'P' && !el.textContent.trim() && !el.querySelector('img,iframe')) return;
1571      el.dataset.edsAnim = '1';
1572      el.classList.add('eds-anim');
1573      /* Original (Squarespace global animations, "detailed") also animates
1574         above-the-fold blocks on page load, staggered. IO fires immediately
1575         for visible elements; give that initial batch a small stagger. */
1576      var r = el.getBoundingClientRect();
1577      if (window.innerHeight > r.top && r.bottom > 0) {
1578        el.style.transitionDelay = Math.min(initial * 90, 540) + 'ms';
1579        initial += 1;
1580      }
1581      io.observe(el);
1582    });
1583  }
1584
1585  if (document.readyState === 'loading') {
1586    document.addEventListener('DOMContentLoaded', boot);
1587  } else {
1588    boot();
1589  }
1590})();
1591</script>
1591
1592<!-- Footer is intentionally blank like the original Squarespace site (its footer
1593     had zero height — no logo, no social bar, no copyright). The wp:html block
1594     above only carries the site-wide image fade-in helper and renders nothing. -->
1595<script src="/assets/nav.js?v=479a8164" defer></script>
vendor: 1 bytes, line 1595
1595
1596<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"2edaaf8bdf97466dbf51d0efdf012e40","r":1,"spa":2}' crossorigin="anonymous"></script>
1596
1597</body>
1598</html>
1599

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.