wop Claude Sonnet 5 commited on
Commit
231d295
·
1 Parent(s): 59c28c7

Add search/filter/share to index, external link previews, and grazing cow pets

Browse files

- Search bar with basic title/description search and a deep search that fetches
post content for full-text matches; filter popup to sort posts by date.
- Share button (copies ?post= link) on index cards and article pages.
- External post links open in a new tab and show a hover preview card.
- Fix seton-labs/Partnerships link to bench-labs/Members in Collab.html.
- Add cow pets that walk the grass strip, graze with flying grass-bit particles,
and can be patted (open-hand cursor, wiggle + sparkle animation).

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

index.html CHANGED
@@ -21,6 +21,46 @@
21
  </p>
22
  </header>
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  <section class="posts">
25
 
26
  <article class="post-card">
 
21
  </p>
22
  </header>
23
 
24
+ <div class="search-bar">
25
+ <form class="search-pill" id="search-form">
26
+ <input type="search" id="search-input" name="q" placeholder="Search posts…" aria-label="Search post titles and descriptions" autocomplete="off">
27
+ <button type="submit" class="search-circle-btn" id="search-btn" aria-label="Search" title="Search titles &amp; descriptions">
28
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
29
+ <circle cx="11" cy="11" r="6"/>
30
+ <line x1="20" y1="20" x2="15.5" y2="15.5"/>
31
+ </svg>
32
+ </button>
33
+ <button type="button" class="search-circle-btn deep-search-btn" id="deep-search-btn" aria-label="Deep search" title="Deep search — also searches inside post content">
34
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
35
+ <rect x="2.5" y="3" width="9" height="11" rx="1.5"/>
36
+ <line x1="5.3" y1="6.7" x2="9.2" y2="6.7"/>
37
+ <line x1="5.3" y1="9.7" x2="9.2" y2="9.7"/>
38
+ <circle cx="16" cy="15.5" r="5" stroke-width="2"/>
39
+ <line x1="19.8" y1="19.3" x2="22" y2="21.5" stroke-width="2"/>
40
+ </svg>
41
+ </button>
42
+ </form>
43
+ <button type="button" class="search-circle-btn filter-btn" id="filter-btn" aria-label="Filter" aria-expanded="false" title="Filter">
44
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
45
+ <line x1="4" y1="6" x2="20" y2="6"/>
46
+ <circle cx="9" cy="6" r="2"/>
47
+ <line x1="4" y1="12" x2="20" y2="12"/>
48
+ <circle cx="16" cy="12" r="2"/>
49
+ <line x1="4" y1="18" x2="20" y2="18"/>
50
+ <circle cx="7" cy="18" r="2"/>
51
+ </svg>
52
+ </button>
53
+ <div class="filter-popup" id="filter-popup" hidden>
54
+ <div class="filter-popup-title">Sort by Date</div>
55
+ <div class="filter-popup-choices">
56
+ <button type="button" class="filter-choice" data-sort="new">Recent → Old</button>
57
+ <button type="button" class="filter-choice" data-sort="old">Old → Recent</button>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <p class="deep-search-status" id="deep-search-status" hidden></p>
63
+
64
  <section class="posts">
65
 
66
  <article class="post-card">
posts/Collab.html CHANGED
@@ -16,6 +16,7 @@
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
 
19
 
20
  <div class="background"></div>
21
 
@@ -58,7 +59,7 @@ The more people we can gather, more organized everything will be.
58
  You can start a discussion on your server (discord) or the huggingface disscussions.
59
  <br><br>
60
  <h2>Join now</h2>
61
- <a href="https://huggingface.co/spaces/seton-labs/Partnerships">https://huggingface.co/spaces/seton-labs/Partnerships</a>
62
  <br><br>
63
 
64
  With ❤️ for AI capability.
 
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
19
+ <script src="../script.js" defer></script>
20
 
21
  <div class="background"></div>
22
 
 
59
  You can start a discussion on your server (discord) or the huggingface disscussions.
60
  <br><br>
61
  <h2>Join now</h2>
62
+ <a href="https://huggingface.co/spaces/bench-labs/Members">https://huggingface.co/spaces/bench-labs/Members</a>
63
  <br><br>
64
 
65
  With ❤️ for AI capability.
posts/bench-effortless-6-2026.html CHANGED
@@ -16,6 +16,7 @@
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
 
19
 
20
  <div class="background"></div>
21
 
 
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
19
+ <script src="../script.js" defer></script>
20
 
21
  <div class="background"></div>
22
 
posts/benchmarks.html CHANGED
@@ -16,6 +16,7 @@
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
 
19
 
20
  <div class="background"></div>
21
 
 
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
19
+ <script src="../script.js" defer></script>
20
 
21
  <div class="background"></div>
22
 
posts/introducing-Bench-labs.html CHANGED
@@ -9,6 +9,7 @@
9
  </head>
10
  <body>
11
  <script type="module" src="../summarize.js" defer></script>
 
12
 
13
  <div class="background"></div>
14
 
 
9
  </head>
10
  <body>
11
  <script type="module" src="../summarize.js" defer></script>
12
+ <script src="../script.js" defer></script>
13
 
14
  <div class="background"></div>
15
 
posts/join-welcome.html CHANGED
@@ -16,6 +16,7 @@
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
 
19
 
20
  <div class="background"></div>
21
 
 
16
 
17
  <body>
18
  <script type="module" src="../summarize.js" defer></script>
19
+ <script src="../script.js" defer></script>
20
 
21
  <div class="background"></div>
22
 
posts/pixelmodel.html CHANGED
@@ -8,6 +8,7 @@
8
  </head>
9
  <body>
10
  <script type="module" src="../summarize.js" defer></script>
 
11
 
12
  <div class="background"></div>
13
 
 
8
  </head>
9
  <body>
10
  <script type="module" src="../summarize.js" defer></script>
11
+ <script src="../script.js" defer></script>
12
 
13
  <div class="background"></div>
14
 
script.js CHANGED
@@ -127,6 +127,138 @@
127
 
128
  document.body.appendChild(svg);
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  // ---- Scroll-driven spring physics (DESKTOP ONLY) ----
131
  if (!isMobile && !prefersReducedMotion) {
132
  let squash = 0;
@@ -200,10 +332,342 @@
200
  pre.appendChild(btn);
201
  });
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  // ---- Article-only features: sidebar + collapsible code ----
204
  const article = document.querySelector(".article");
205
  if (!article) return;
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  const headings = article.querySelectorAll("h1, h2, h3");
208
  const codeBlocks = article.querySelectorAll("pre");
209
 
 
127
 
128
  document.body.appendChild(svg);
129
 
130
+ // ---- Grazing cow pets: walk back and forth along the grass, pausing at each
131
+ // end to graze (head dips down, tiny grass bits fly from the mouth). One shared
132
+ // per-cow timeline (duration + a randomized negative delay) drives three synced
133
+ // animations — body walk/turn, leg-bounce, head bob — so everything lines up;
134
+ // the JS particle bursts are scheduled against that same timeline. ----
135
+ if (!document.querySelector(".cow-pen")) {
136
+ const pen = document.createElement("div");
137
+ pen.className = "cow-pen";
138
+ pen.setAttribute("aria-hidden", "true");
139
+
140
+ const cowCount = isMobile ? 1 : 2;
141
+ const positions = isMobile ? [55] : [14, 68];
142
+ const GRAZE_FRACTIONS = [0.37, 0.87]; // where in the cycle the head is down (matches cow-head-bob below)
143
+
144
+ for (let i = 0; i < cowCount; i++) {
145
+ const anchor = document.createElement("div");
146
+ anchor.className = "cow-anchor";
147
+ anchor.style.left = positions[i] + "%";
148
+ anchor.style.setProperty("--cow-scale", isMobile ? 0.8 : (1 + (i % 2) * 0.12).toFixed(2));
149
+
150
+ const cow = document.createElement("div");
151
+ cow.className = "cow";
152
+ cow.innerHTML =
153
+ '<svg viewBox="0 0 100 64" class="cow-svg">' +
154
+ '<g class="cow-walkbody">' +
155
+ '<ellipse class="cow-shadow" cx="52" cy="59" rx="32" ry="4"/>' +
156
+ '<g class="cow-legs">' +
157
+ '<rect x="26" y="42" width="7" height="17" rx="3"/>' +
158
+ '<rect x="41" y="44" width="7" height="15" rx="3"/>' +
159
+ '<rect x="58" y="44" width="7" height="15" rx="3"/>' +
160
+ '<rect x="72" y="42" width="7" height="17" rx="3"/>' +
161
+ '</g>' +
162
+ '<ellipse class="cow-body" cx="52" cy="34" rx="32" ry="20"/>' +
163
+ '<path class="cow-spot" d="M20 10c6-3 14-3 20 1-2 4-16 5-20-1z"/>' +
164
+ '<path class="cow-spot" d="M46 20c5-2 11 0 12 4-3 3-11 3-14-1z"/>' +
165
+ '<path class="cow-spot" d="M60 42c5-2 12 0 13 4-4 3-12 2-13-4z"/>' +
166
+ '<path class="cow-tail" d="M84 30q10 2 7 15"/>' +
167
+ '</g>' +
168
+ '<g class="cow-head">' +
169
+ '<ellipse cx="18" cy="26" rx="15" ry="13"/>' +
170
+ '<path class="cow-horn" d="M13 12q-2-5 1-7"/>' +
171
+ '<path class="cow-horn" d="M25 12q2-5-1-7"/>' +
172
+ '<path class="cow-ear" d="M9 14q-6-4-9 1q4 3 9 2z"/>' +
173
+ '<circle class="cow-eye" cx="16" cy="20" r="1.6"/>' +
174
+ '<ellipse class="cow-muzzle" cx="8" cy="32" rx="8" ry="6"/>' +
175
+ '<circle class="cow-nostril" cx="5" cy="31" r="1.1"/>' +
176
+ '<circle class="cow-nostril" cx="10" cy="33" r="1.1"/>' +
177
+ '</g>' +
178
+ '</svg>';
179
+
180
+ const duration = 12000 + Math.random() * 4000; // ms — one full walk-graze-walk-graze loop
181
+ const offset = Math.random() * duration; // desyncs cows from each other
182
+ const walkbody = cow.querySelector(".cow-walkbody");
183
+ const head = cow.querySelector(".cow-head");
184
+ [cow, walkbody, head].forEach((el) => {
185
+ el.style.animationDuration = duration + "ms";
186
+ el.style.animationDelay = -offset + "ms";
187
+ });
188
+
189
+ anchor.appendChild(cow);
190
+ pen.appendChild(anchor);
191
+
192
+ if (!prefersReducedMotion) {
193
+ const muzzle = cow.querySelector(".cow-muzzle");
194
+ const burst = () => {
195
+ if (!muzzle.isConnected) return;
196
+ const rect = muzzle.getBoundingClientRect();
197
+ const count = isMobile ? 1 : 2 + Math.floor(Math.random() * 2);
198
+ for (let b = 0; b < count; b++) {
199
+ setTimeout(() => spawnGrassBit(rect), b * 80);
200
+ }
201
+ };
202
+ GRAZE_FRACTIONS.forEach((fraction) => {
203
+ const target = duration * fraction;
204
+ const firstFire = ((target - offset) % duration + duration) % duration;
205
+ setTimeout(() => {
206
+ burst();
207
+ setInterval(burst, duration);
208
+ }, firstFire);
209
+ });
210
+ }
211
+
212
+ // ---- Pat: click a cow for a happy wiggle + a couple of sparkles.
213
+ // The wiggle animates the <svg> itself (untouched by walk/bounce/bob,
214
+ // which all live on elements inside it), so it layers on top for free. ----
215
+ const svgEl = cow.querySelector(".cow-svg");
216
+ cow.addEventListener("click", () => {
217
+ svgEl.classList.remove("is-patted");
218
+ void svgEl.offsetWidth; // force a reflow so re-adding the class restarts the animation
219
+ svgEl.classList.add("is-patted");
220
+ if (!prefersReducedMotion) {
221
+ const rect = cow.getBoundingClientRect();
222
+ for (let s = 0; s < 3; s++) {
223
+ setTimeout(() => spawnPatSparkle(rect), s * 70);
224
+ }
225
+ }
226
+ });
227
+ svgEl.addEventListener("animationend", (e) => {
228
+ if (e.animationName === "cow-pat-wiggle") svgEl.classList.remove("is-patted");
229
+ });
230
+ }
231
+
232
+ document.body.appendChild(pen);
233
+ }
234
+
235
+ // A tiny fixed-position bit of grass, spawned at a live getBoundingClientRect()
236
+ // (so it starts exactly at the mouth regardless of the cow's current walk/turn/bob
237
+ // state) then falls and fades on its own, independent of the cow's animation.
238
+ function spawnGrassBit(rect) {
239
+ const bit = document.createElement("span");
240
+ bit.className = "grass-bit";
241
+ bit.style.left = rect.left + rect.width / 2 + "px";
242
+ bit.style.top = rect.top + rect.height / 2 + "px";
243
+ bit.style.setProperty("--dx", (Math.random() * 16 - 8).toFixed(1) + "px");
244
+ bit.style.setProperty("--rot", (Math.random() * 90 - 45).toFixed(0) + "deg");
245
+ bit.style.animationDuration = (500 + Math.random() * 250).toFixed(0) + "ms";
246
+ document.body.appendChild(bit);
247
+ bit.addEventListener("animationend", () => bit.remove());
248
+ }
249
+
250
+ // A little heart/sparkle pop for the pat feature, same fixed-position-from-rect trick.
251
+ function spawnPatSparkle(rect) {
252
+ const el = document.createElement("span");
253
+ el.className = "pat-sparkle";
254
+ el.textContent = Math.random() < 0.5 ? "❤" : "✦";
255
+ el.style.left = rect.left + rect.width * (0.25 + Math.random() * 0.5) + "px";
256
+ el.style.top = rect.top + "px";
257
+ el.style.setProperty("--dx", (Math.random() * 20 - 10).toFixed(1) + "px");
258
+ document.body.appendChild(el);
259
+ el.addEventListener("animationend", () => el.remove());
260
+ }
261
+
262
  // ---- Scroll-driven spring physics (DESKTOP ONLY) ----
263
  if (!isMobile && !prefersReducedMotion) {
264
  let squash = 0;
 
332
  pre.appendChild(btn);
333
  });
334
 
335
+ // ---- Share button: copies the canonical ?post= URL for a post ----
336
+ const SITE_BASE = "https://huggingface.co/spaces/bench-labs/blog";
337
+ const SHARE_ICON =
338
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
339
+ '<circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/>' +
340
+ '<line x1="8.2" y1="10.7" x2="15.8" y2="6.3"/><line x1="8.2" y1="13.3" x2="15.8" y2="17.7"/>' +
341
+ "</svg>";
342
+ const CHECK_ICON =
343
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
344
+ '<polyline points="20 6 9 17 4 12"/></svg>';
345
+
346
+ function makeShareBtn(filename) {
347
+ const btn = document.createElement("button");
348
+ btn.type = "button";
349
+ btn.className = "share-btn";
350
+ btn.setAttribute("aria-label", "Copy share link");
351
+ btn.title = "Copy share link";
352
+ btn.innerHTML = SHARE_ICON;
353
+ btn.addEventListener("click", async (e) => {
354
+ e.preventDefault();
355
+ e.stopPropagation();
356
+ const url = `${SITE_BASE}?post=${filename}`;
357
+ try {
358
+ await navigator.clipboard.writeText(url);
359
+ btn.classList.remove("error");
360
+ btn.classList.add("copied");
361
+ btn.innerHTML = CHECK_ICON;
362
+ setTimeout(() => {
363
+ btn.classList.remove("copied");
364
+ btn.innerHTML = SHARE_ICON;
365
+ }, 1400);
366
+ } catch {
367
+ btn.classList.add("error");
368
+ setTimeout(() => btn.classList.remove("error"), 1400);
369
+ }
370
+ });
371
+ return btn;
372
+ }
373
+
374
+ // One share button per post card on the index page
375
+ document.querySelectorAll(".post-card").forEach((card) => {
376
+ if (card.querySelector(".share-btn")) return;
377
+ const link = card.querySelector('a[href^="posts/"]');
378
+ if (!link) return;
379
+ const filename = link.getAttribute("href").split("/").pop();
380
+ card.appendChild(makeShareBtn(filename));
381
+ });
382
+
383
+ // ---- Search bar (index page only): basic title/description filter,
384
+ // deep search across fetched post content, and a date-sort filter popup ----
385
+ const postsSection = document.querySelector(".posts");
386
+ if (postsSection) {
387
+ const form = document.getElementById("search-form");
388
+ const input = document.getElementById("search-input");
389
+ const deepBtn = document.getElementById("deep-search-btn");
390
+ const filterBtn = document.getElementById("filter-btn");
391
+ const filterPopup = document.getElementById("filter-popup");
392
+ const statusEl = document.getElementById("deep-search-status");
393
+ const cardData = Array.from(postsSection.querySelectorAll(".post-card")).map((card) => {
394
+ const link = card.querySelector('a[href^="posts/"]');
395
+ return {
396
+ card,
397
+ file: link ? link.getAttribute("href").split("/").pop() : null,
398
+ title: (card.querySelector("h2")?.textContent || "").trim(),
399
+ desc: (card.querySelector("p")?.textContent || "").trim(),
400
+ dateText: (card.querySelector(".date")?.textContent || "").trim(),
401
+ };
402
+ });
403
+
404
+ const deepTextCache = new Map(); // filename -> plain text content of that post
405
+
406
+ function clearSnippets() {
407
+ cardData.forEach(({ card }) => card.querySelector(".post-card-snippet")?.remove());
408
+ }
409
+
410
+ function showAll() {
411
+ clearSnippets();
412
+ cardData.forEach(({ card }) => (card.style.display = ""));
413
+ }
414
+
415
+ function runBasicSearch(query) {
416
+ const q = query.trim().toLowerCase();
417
+ clearSnippets();
418
+ if (!q) {
419
+ cardData.forEach(({ card }) => (card.style.display = ""));
420
+ return;
421
+ }
422
+ cardData.forEach(({ card, title, desc }) => {
423
+ const match = title.toLowerCase().includes(q) || desc.toLowerCase().includes(q);
424
+ card.style.display = match ? "" : "none";
425
+ });
426
+ }
427
+
428
+ async function fetchPostText(file) {
429
+ if (deepTextCache.has(file)) return deepTextCache.get(file);
430
+ const res = await fetch("posts/" + file);
431
+ const html = await res.text();
432
+ const doc = new DOMParser().parseFromString(html, "text/html");
433
+ const text = (doc.querySelector(".article") || doc.body).textContent.replace(/\s+/g, " ").trim();
434
+ deepTextCache.set(file, text);
435
+ return text;
436
+ }
437
+
438
+ function escapeHtml(s) {
439
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
440
+ }
441
+
442
+ function makeSnippet(text, q) {
443
+ const idx = text.toLowerCase().indexOf(q);
444
+ if (idx === -1) return null;
445
+ const start = Math.max(0, idx - 50);
446
+ const end = Math.min(text.length, idx + q.length + 70);
447
+ let snippet = text.slice(start, end);
448
+ if (start > 0) snippet = "…" + snippet;
449
+ if (end < text.length) snippet += "…";
450
+ return snippet;
451
+ }
452
+
453
+ function highlightSnippet(snippet, q) {
454
+ const idx = snippet.toLowerCase().indexOf(q.toLowerCase());
455
+ if (idx === -1) return escapeHtml(snippet);
456
+ return (
457
+ escapeHtml(snippet.slice(0, idx)) +
458
+ "<mark>" + escapeHtml(snippet.slice(idx, idx + q.length)) + "</mark>" +
459
+ escapeHtml(snippet.slice(idx + q.length))
460
+ );
461
+ }
462
+
463
+ async function runDeepSearch(query) {
464
+ const q = query.trim().toLowerCase();
465
+ clearSnippets();
466
+ if (!q) {
467
+ showAll();
468
+ return;
469
+ }
470
+
471
+ deepBtn.classList.add("is-busy");
472
+ deepBtn.disabled = true;
473
+ statusEl.hidden = false;
474
+ statusEl.textContent = "Searching " + cardData.length + " posts…";
475
+
476
+ try {
477
+ await Promise.all(
478
+ cardData.map(async ({ card, title, desc, file }) => {
479
+ const titleHit = title.toLowerCase().includes(q);
480
+ const descHit = desc.toLowerCase().includes(q);
481
+ let snippet = null;
482
+
483
+ if (file) {
484
+ try {
485
+ const text = await fetchPostText(file);
486
+ snippet = makeSnippet(text, q);
487
+ } catch {
488
+ // couldn't fetch this post's content — fall back to title/description match only
489
+ }
490
+ }
491
+
492
+ const match = titleHit || descHit || snippet;
493
+ card.style.display = match ? "" : "none";
494
+
495
+ if (match && snippet) {
496
+ const snippetEl = document.createElement("p");
497
+ snippetEl.className = "post-card-snippet";
498
+ snippetEl.innerHTML = "Found in post: " + highlightSnippet(snippet, q);
499
+ const readLink = card.querySelector('a[href^="posts/"]');
500
+ if (readLink) readLink.insertAdjacentElement("beforebegin", snippetEl);
501
+ else card.appendChild(snippetEl);
502
+ }
503
+ })
504
+ );
505
+ } finally {
506
+ deepBtn.classList.remove("is-busy");
507
+ deepBtn.disabled = false;
508
+ statusEl.hidden = true;
509
+ }
510
+ }
511
+
512
+ form?.addEventListener("submit", (e) => {
513
+ e.preventDefault();
514
+ runBasicSearch(input.value);
515
+ });
516
+
517
+ deepBtn?.addEventListener("click", () => {
518
+ runDeepSearch(input.value);
519
+ });
520
+
521
+ // ---- Filter popup: sort by date ----
522
+ function parseCardDate(dateText) {
523
+ const m = dateText.match(/^(\w+)\s+(\d{4})$/);
524
+ if (!m) return null;
525
+ const t = new Date(`${m[1]} 1, ${m[2]}`).getTime();
526
+ return Number.isNaN(t) ? null : t;
527
+ }
528
+
529
+ function applySort(direction) {
530
+ const withDate = cardData.filter((c) => parseCardDate(c.dateText) !== null);
531
+ const withoutDate = cardData.filter((c) => parseCardDate(c.dateText) === null);
532
+ withDate.sort((a, b) => {
533
+ const ta = parseCardDate(a.dateText);
534
+ const tb = parseCardDate(b.dateText);
535
+ return direction === "new" ? tb - ta : ta - tb;
536
+ });
537
+ [...withDate, ...withoutDate].forEach(({ card }) => postsSection.appendChild(card));
538
+ }
539
+
540
+ filterBtn?.addEventListener("click", () => {
541
+ const opening = filterPopup.hidden;
542
+ filterPopup.hidden = !opening;
543
+ filterBtn.setAttribute("aria-expanded", String(opening));
544
+ });
545
+
546
+ filterPopup?.querySelectorAll(".filter-choice").forEach((btn) => {
547
+ btn.addEventListener("click", () => {
548
+ filterPopup.querySelectorAll(".filter-choice").forEach((b) => b.classList.remove("active"));
549
+ btn.classList.add("active");
550
+ applySort(btn.dataset.sort);
551
+ });
552
+ });
553
+
554
+ document.addEventListener("click", (e) => {
555
+ if (filterPopup && !filterPopup.hidden && !filterPopup.contains(e.target) && e.target !== filterBtn && !filterBtn.contains(e.target)) {
556
+ filterPopup.hidden = true;
557
+ filterBtn.setAttribute("aria-expanded", "false");
558
+ }
559
+ });
560
+ }
561
+
562
  // ---- Article-only features: sidebar + collapsible code ----
563
  const article = document.querySelector(".article");
564
  if (!article) return;
565
 
566
+ // ---- Share button for the current post ----
567
+ {
568
+ const filename = location.pathname.split("/").pop();
569
+ if (filename && /\.html?$/i.test(filename) && !article.querySelector(".share-btn")) {
570
+ const shareBtn = makeShareBtn(filename);
571
+ shareBtn.classList.add("share-btn-article");
572
+ const back = article.querySelector(".back");
573
+ if (back) back.insertAdjacentElement("afterend", shareBtn);
574
+ else article.prepend(shareBtn);
575
+ }
576
+ }
577
+
578
+ // ---- External links: open in a new tab + hover preview card with fetched metadata ----
579
+ {
580
+ const linkMetaCache = new Map();
581
+
582
+ function faviconlessTitle(href) {
583
+ try {
584
+ const u = new URL(href);
585
+ const parts = u.pathname.split("/").filter(Boolean);
586
+ if (u.hostname === "huggingface.co" && parts[0] === "spaces" && parts.length >= 3) {
587
+ return `${parts[1]}/${parts[2]} · Hugging Face Space`;
588
+ }
589
+ if (u.hostname === "huggingface.co" && parts.length >= 2) {
590
+ return `${parts[0]}/${parts[1]} · Hugging Face Hub`;
591
+ }
592
+ return u.hostname;
593
+ } catch {
594
+ return href;
595
+ }
596
+ }
597
+
598
+ async function loadLinkMeta(href) {
599
+ if (linkMetaCache.has(href)) return linkMetaCache.get(href);
600
+ const metaPromise = (async () => {
601
+ try {
602
+ const res = await fetch(href, { mode: "cors" });
603
+ if (!res.ok) throw new Error("bad status");
604
+ const html = await res.text();
605
+ const doc = new DOMParser().parseFromString(html, "text/html");
606
+ const title = doc.querySelector("title")?.textContent?.trim() || null;
607
+ const desc =
608
+ doc.querySelector('meta[name="description"]')?.getAttribute("content")?.trim() ||
609
+ doc.querySelector('meta[property="og:description"]')?.getAttribute("content")?.trim() ||
610
+ null;
611
+ return { title, desc };
612
+ } catch {
613
+ return { title: null, desc: null };
614
+ }
615
+ })();
616
+ linkMetaCache.set(href, metaPromise);
617
+ return metaPromise;
618
+ }
619
+
620
+ article.querySelectorAll('a[href^="http"]').forEach((link) => {
621
+ link.addEventListener("click", (e) => {
622
+ e.preventDefault();
623
+ window.open(link.href, "_blank", "noopener,noreferrer");
624
+ });
625
+
626
+ let card = null;
627
+ let hideTimer = null;
628
+
629
+ link.addEventListener("mouseenter", () => {
630
+ clearTimeout(hideTimer);
631
+ card = document.createElement("div");
632
+ card.className = "link-preview-card";
633
+ const urlEl = document.createElement("div");
634
+ urlEl.className = "link-preview-url";
635
+ urlEl.textContent = link.href;
636
+ const titleEl = document.createElement("div");
637
+ titleEl.className = "link-preview-title";
638
+ titleEl.textContent = faviconlessTitle(link.href);
639
+ card.appendChild(titleEl);
640
+ card.appendChild(urlEl);
641
+ document.body.appendChild(card);
642
+
643
+ const rect = link.getBoundingClientRect();
644
+ card.style.top = rect.bottom + window.scrollY + 8 + "px";
645
+ const maxLeft = window.scrollX + document.documentElement.clientWidth - card.offsetWidth - 12;
646
+ card.style.left = Math.min(rect.left + window.scrollX, Math.max(12, maxLeft)) + "px";
647
+ requestAnimationFrame(() => card?.classList.add("visible"));
648
+
649
+ loadLinkMeta(link.href).then((meta) => {
650
+ if (!card || !card.isConnected) return;
651
+ if (meta.title) titleEl.textContent = meta.title;
652
+ if (meta.desc) {
653
+ const descEl = document.createElement("div");
654
+ descEl.className = "link-preview-desc";
655
+ descEl.textContent = meta.desc;
656
+ card.appendChild(descEl);
657
+ }
658
+ });
659
+ });
660
+
661
+ link.addEventListener("mouseleave", () => {
662
+ const toRemove = card;
663
+ card = null;
664
+ if (!toRemove) return;
665
+ toRemove.classList.remove("visible");
666
+ hideTimer = setTimeout(() => toRemove.remove(), 180);
667
+ });
668
+ });
669
+ }
670
+
671
  const headings = article.querySelectorAll("h1, h2, h3");
672
  const codeBlocks = article.querySelectorAll("pre");
673
 
style.css CHANGED
@@ -910,4 +910,371 @@ pre:hover .copy-btn { opacity: 1; }
910
  @media (prefers-reduced-motion: reduce) {
911
  .ai-sum-btn.is-busy svg { animation: none; }
912
  .ai-sum-ring.is-indeterminate .ai-sum-ring-fill { animation: none; stroke-dashoffset: 40; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  }
 
910
  @media (prefers-reduced-motion: reduce) {
911
  .ai-sum-btn.is-busy svg { animation: none; }
912
  .ai-sum-ring.is-indeterminate .ai-sum-ring-fill { animation: none; stroke-dashoffset: 40; }
913
+ }
914
+
915
+ /* -------------------- SEARCH BAR -------------------- */
916
+
917
+ .search-bar {
918
+ display: flex;
919
+ align-items: center;
920
+ gap: 10px;
921
+ margin-bottom: 22px;
922
+ position: relative;
923
+ }
924
+
925
+ .search-pill {
926
+ display: flex;
927
+ align-items: center;
928
+ flex: 1;
929
+ gap: 6px;
930
+ padding: 6px 6px 6px 20px;
931
+ background: var(--surface);
932
+ border: 1px solid var(--border);
933
+ border-radius: 999px;
934
+ backdrop-filter: blur(20px) saturate(1.2);
935
+ -webkit-backdrop-filter: blur(20px) saturate(1.2);
936
+ box-shadow: var(--shadow-soft);
937
+ transition: border-color var(--transition), box-shadow var(--transition);
938
+ }
939
+
940
+ .search-pill:focus-within {
941
+ border-color: rgba(74, 144, 194, 0.5);
942
+ box-shadow: var(--shadow-hover);
943
+ }
944
+
945
+ .search-pill input {
946
+ flex: 1;
947
+ min-width: 0;
948
+ border: none;
949
+ background: transparent;
950
+ outline: none;
951
+ font-family: inherit;
952
+ font-size: 0.95rem;
953
+ color: var(--text);
954
+ padding: 8px 4px;
955
+ }
956
+
957
+ .search-pill input::placeholder { color: var(--muted); }
958
+
959
+ .search-circle-btn {
960
+ flex-shrink: 0;
961
+ width: 40px;
962
+ height: 40px;
963
+ border-radius: 50%;
964
+ border: 1px solid var(--border);
965
+ background: var(--surface-2);
966
+ color: var(--accent);
967
+ display: flex;
968
+ align-items: center;
969
+ justify-content: center;
970
+ cursor: pointer;
971
+ transition: background var(--transition), border-color var(--transition), transform var(--transition), color var(--transition), opacity var(--transition);
972
+ backdrop-filter: blur(10px);
973
+ -webkit-backdrop-filter: blur(10px);
974
+ font-family: inherit;
975
+ }
976
+
977
+ .search-circle-btn:hover:not(:disabled) {
978
+ background: #fff;
979
+ border-color: var(--accent);
980
+ transform: scale(1.06);
981
+ }
982
+
983
+ .search-circle-btn:disabled { opacity: 0.5; cursor: default; }
984
+ .search-circle-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
985
+
986
+ .deep-search-btn.is-busy svg { animation: ai-sum-icon-pulse 1.4s ease-in-out infinite; }
987
+
988
+ .filter-btn[aria-expanded="true"] {
989
+ background: #fff;
990
+ border-color: var(--accent);
991
+ color: var(--accent);
992
+ }
993
+
994
+ .filter-popup {
995
+ position: absolute;
996
+ top: calc(100% + 10px);
997
+ right: 0;
998
+ z-index: 200;
999
+ min-width: 220px;
1000
+ padding: 14px;
1001
+ background: var(--surface-2);
1002
+ border: 1px solid var(--border);
1003
+ border-radius: 14px;
1004
+ backdrop-filter: blur(20px) saturate(1.2);
1005
+ -webkit-backdrop-filter: blur(20px) saturate(1.2);
1006
+ box-shadow: var(--shadow-hover);
1007
+ }
1008
+
1009
+ .filter-popup-title {
1010
+ font-size: 0.72rem;
1011
+ text-transform: uppercase;
1012
+ letter-spacing: 0.08em;
1013
+ color: var(--muted);
1014
+ margin-bottom: 10px;
1015
+ }
1016
+
1017
+ .filter-popup-choices {
1018
+ display: flex;
1019
+ gap: 8px;
1020
+ }
1021
+
1022
+ .filter-choice {
1023
+ flex: 1;
1024
+ padding: 8px 10px;
1025
+ border-radius: 999px;
1026
+ border: 1px solid var(--border-soft);
1027
+ background: rgba(255,255,255,0.5);
1028
+ color: var(--text);
1029
+ font-size: 0.78rem;
1030
+ cursor: pointer;
1031
+ transition: background var(--transition), border-color var(--transition), color var(--transition);
1032
+ font-family: inherit;
1033
+ }
1034
+
1035
+ .filter-choice:hover { background: rgba(255,255,255,0.85); }
1036
+
1037
+ .filter-choice.active {
1038
+ background: var(--accent);
1039
+ border-color: var(--accent);
1040
+ color: #fff;
1041
+ }
1042
+
1043
+ .deep-search-status {
1044
+ margin: -10px 0 20px;
1045
+ color: var(--muted);
1046
+ font-size: 0.82rem;
1047
+ }
1048
+
1049
+ .post-card-snippet {
1050
+ margin: -4px 0 14px;
1051
+ padding: 10px 12px;
1052
+ background: rgba(255,255,255,0.5);
1053
+ border: 1px solid var(--border-soft);
1054
+ border-radius: 10px;
1055
+ color: var(--text);
1056
+ font-size: 0.85rem;
1057
+ line-height: 1.5;
1058
+ }
1059
+ .post-card-snippet mark { background: rgba(74,144,194,0.25); }
1060
+
1061
+ @media (max-width: 560px) {
1062
+ .search-bar { flex-wrap: wrap; }
1063
+ .search-pill { width: 100%; }
1064
+ }
1065
+
1066
+ /* -------------------- SHARE BUTTON -------------------- */
1067
+
1068
+ .post-card { position: relative; }
1069
+
1070
+ .share-btn {
1071
+ position: absolute;
1072
+ top: 18px;
1073
+ right: 18px;
1074
+ width: 32px;
1075
+ height: 32px;
1076
+ border-radius: 50%;
1077
+ border: 1px solid var(--border-soft);
1078
+ background: rgba(255,255,255,0.5);
1079
+ color: var(--muted);
1080
+ display: flex;
1081
+ align-items: center;
1082
+ justify-content: center;
1083
+ cursor: pointer;
1084
+ transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition);
1085
+ backdrop-filter: blur(10px);
1086
+ -webkit-backdrop-filter: blur(10px);
1087
+ }
1088
+
1089
+ .share-btn:hover { background: #fff; border-color: var(--accent); color: var(--accent); transform: scale(1.06); }
1090
+ .share-btn.copied { color: var(--accent-green); border-color: var(--accent-green); }
1091
+ .share-btn.error { color: #b3503f; border-color: #b3503f; }
1092
+ .share-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1093
+
1094
+ .share-btn-article {
1095
+ display: inline-flex;
1096
+ vertical-align: middle;
1097
+ margin-left: 10px;
1098
+ margin-bottom: 28px;
1099
+ }
1100
+
1101
+ /* -------------------- EXTERNAL LINK PREVIEW -------------------- */
1102
+
1103
+ .link-preview-card {
1104
+ position: absolute;
1105
+ z-index: 500;
1106
+ max-width: 320px;
1107
+ padding: 12px 14px;
1108
+ background: var(--surface-2);
1109
+ border: 1px solid var(--border);
1110
+ border-radius: 12px;
1111
+ backdrop-filter: blur(20px) saturate(1.2);
1112
+ -webkit-backdrop-filter: blur(20px) saturate(1.2);
1113
+ box-shadow: var(--shadow-hover);
1114
+ opacity: 0;
1115
+ transform: translateY(-4px);
1116
+ transition: opacity 0.15s ease, transform 0.15s ease;
1117
+ pointer-events: none;
1118
+ }
1119
+
1120
+ .link-preview-card.visible { opacity: 1; transform: translateY(0); }
1121
+
1122
+ .link-preview-title {
1123
+ font-weight: 600;
1124
+ color: var(--heading);
1125
+ font-size: 0.88rem;
1126
+ margin-bottom: 4px;
1127
+ }
1128
+
1129
+ .link-preview-url {
1130
+ color: var(--accent);
1131
+ font-size: 0.75rem;
1132
+ word-break: break-all;
1133
+ margin-bottom: 4px;
1134
+ }
1135
+
1136
+ .link-preview-desc {
1137
+ color: var(--muted);
1138
+ font-size: 0.8rem;
1139
+ line-height: 1.4;
1140
+ }
1141
+
1142
+ @media (prefers-reduced-motion: reduce) {
1143
+ .link-preview-card { transition: none; }
1144
+ }
1145
+
1146
+ /* -------------------- COW PETS -------------------- */
1147
+
1148
+ .cow-pen {
1149
+ position: fixed;
1150
+ left: 0;
1151
+ bottom: 6px;
1152
+ width: 100%;
1153
+ height: 70px;
1154
+ z-index: 51;
1155
+ pointer-events: none;
1156
+ overflow: visible;
1157
+ }
1158
+
1159
+ .cow-anchor {
1160
+ position: absolute;
1161
+ bottom: 0;
1162
+ width: 92px;
1163
+ transform: translateX(-50%) scale(var(--cow-scale, 1));
1164
+ transform-origin: bottom center;
1165
+ }
1166
+
1167
+ .cow {
1168
+ cursor: grab;
1169
+ pointer-events: auto;
1170
+ animation: cow-walk 14s ease-in-out infinite;
1171
+ will-change: transform;
1172
+ }
1173
+
1174
+ .cow:active { cursor: grabbing; }
1175
+
1176
+ .cow-svg {
1177
+ width: 100%;
1178
+ display: block;
1179
+ overflow: visible;
1180
+ transform-origin: 50% 100%;
1181
+ }
1182
+
1183
+ .cow-svg.is-patted { animation: cow-pat-wiggle 480ms ease-in-out; }
1184
+
1185
+ .cow-walkbody {
1186
+ animation: cow-bounce 14s ease-in-out infinite;
1187
+ }
1188
+
1189
+ .cow-head {
1190
+ transform-origin: 30px 26px;
1191
+ animation: cow-head-bob 14s ease-in-out infinite;
1192
+ }
1193
+
1194
+ .cow-shadow { fill: rgba(40, 60, 40, 0.18); }
1195
+ .cow-body, .cow-head > ellipse:first-child { fill: #fbfaf6; stroke: #d8d2c4; stroke-width: 0.6; }
1196
+ .cow-spot { fill: #3a3128; }
1197
+ .cow-legs rect { fill: #3a3128; }
1198
+ .cow-tail { fill: none; stroke: #3a3128; stroke-width: 2.5; stroke-linecap: round; }
1199
+ .cow-muzzle { fill: #e8c9b0; }
1200
+ .cow-nostril { fill: #6b4a38; }
1201
+ .cow-eye { fill: #241d18; }
1202
+ .cow-ear { fill: #f0c9ad; }
1203
+ .cow-horn { fill: none; stroke: #d8cdb8; stroke-width: 2; stroke-linecap: round; }
1204
+
1205
+ /* Walk right, pause + graze, turn, walk left, pause + graze, turn — loops seamlessly
1206
+ since 100% matches 0% exactly. */
1207
+ @keyframes cow-walk {
1208
+ 0% { transform: translateX(0) scaleX(-1); }
1209
+ 28% { transform: translateX(80px) scaleX(-1); }
1210
+ 46% { transform: translateX(80px) scaleX(-1); }
1211
+ 50% { transform: translateX(80px) scaleX(1); }
1212
+ 78% { transform: translateX(0) scaleX(1); }
1213
+ 96% { transform: translateX(0) scaleX(1); }
1214
+ 100% { transform: translateX(0) scaleX(-1); }
1215
+ }
1216
+
1217
+ /* Gentle gait bounce during the two walking stretches, still while grazing/turning. */
1218
+ @keyframes cow-bounce {
1219
+ 0%, 2% { transform: translateY(0); }
1220
+ 14% { transform: translateY(-2px); }
1221
+ 26%, 50% { transform: translateY(0); }
1222
+ 64% { transform: translateY(-2px); }
1223
+ 76%, 100% { transform: translateY(0); }
1224
+ }
1225
+
1226
+ /* Head dips down to graze during the two pauses in cow-walk above (GRAZE_FRACTIONS
1227
+ in script.js targets the middle of these windows for the grass-bit bursts). */
1228
+ @keyframes cow-head-bob {
1229
+ 0%, 26% { transform: translate(0, 0) rotate(0deg); }
1230
+ 32%, 42% { transform: translate(1px, 7px) rotate(10deg); }
1231
+ 48%, 76% { transform: translate(0, 0) rotate(0deg); }
1232
+ 82%, 92% { transform: translate(1px, 7px) rotate(10deg); }
1233
+ 98%, 100% { transform: translate(0, 0) rotate(0deg); }
1234
+ }
1235
+
1236
+ @keyframes cow-pat-wiggle {
1237
+ 0% { transform: scale(1, 1) rotate(0deg); }
1238
+ 20% { transform: scale(1.05, 0.92) rotate(-4deg); }
1239
+ 45% { transform: scale(0.96, 1.05) rotate(3deg); }
1240
+ 70% { transform: scale(1.03, 0.97) rotate(-2deg); }
1241
+ 100% { transform: scale(1, 1) rotate(0deg); }
1242
+ }
1243
+
1244
+ .grass-bit {
1245
+ position: fixed;
1246
+ z-index: 60;
1247
+ width: 3px;
1248
+ height: 6px;
1249
+ margin: -3px 0 0 -1px;
1250
+ background: linear-gradient(#63ad64, #457f4b);
1251
+ border-radius: 1px;
1252
+ pointer-events: none;
1253
+ animation: grass-bit-fall 650ms ease-in forwards;
1254
+ will-change: transform, opacity;
1255
+ }
1256
+
1257
+ @keyframes grass-bit-fall {
1258
+ 0% { opacity: 1; transform: translate(0, 0) rotate(0deg); }
1259
+ 100% { opacity: 0; transform: translate(var(--dx, 0px), 20px) rotate(var(--rot, 0deg)); }
1260
+ }
1261
+
1262
+ .pat-sparkle {
1263
+ position: fixed;
1264
+ z-index: 61;
1265
+ font-size: 14px;
1266
+ line-height: 1;
1267
+ pointer-events: none;
1268
+ animation: pat-sparkle-pop 700ms ease-out forwards;
1269
+ will-change: transform, opacity;
1270
+ }
1271
+
1272
+ @keyframes pat-sparkle-pop {
1273
+ 0% { opacity: 0; transform: translate(0, 0) scale(0.4) rotate(0deg); }
1274
+ 25% { opacity: 1; transform: translate(0, -6px) scale(1.1) rotate(-8deg); }
1275
+ 100% { opacity: 0; transform: translate(var(--dx, 0px), -34px) scale(0.9) rotate(12deg); }
1276
+ }
1277
+
1278
+ @media (max-width: 640px) {
1279
+ .cow-pen { height: 56px; }
1280
  }