Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Partners & Members — Bench-labs</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Newsreader:ital,opsz,wght@0,6..72,400;1,6..72,300;1,6..72,400&display=swap" rel="stylesheet" /> | |
| <script src="https://code.iconify.design/3/3.1.0/iconify.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'], | |
| serif: ['Newsreader', 'serif'], | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| :root { | |
| --bg-0: #05060a; | |
| --bg-1: #0a0c14; | |
| --ink: #e8ecf5; | |
| --muted: #8b93a8; | |
| --muted-2: #5c6478; | |
| --line: rgba(255,255,255,0.08); | |
| --line-strong: rgba(255,255,255,0.16); | |
| --accent: #7c9cff; | |
| --accent-soft: rgba(124,156,255,0.5); | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { | |
| background: var(--bg-0); | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| color: var(--ink); | |
| overflow-x: hidden; | |
| } | |
| /* ============ Space background: gradient + layered star canvas ============ */ | |
| .space-bg { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 0; | |
| background: | |
| radial-gradient(ellipse 80% 50% at 50% -10%, rgba(90,100,180,0.18) 0%, transparent 60%), | |
| radial-gradient(ellipse 60% 40% at 85% 100%, rgba(120,80,180,0.12) 0%, transparent 60%), | |
| linear-gradient(180deg, var(--bg-0) 0%, var(--bg-1) 100%); | |
| pointer-events: none; | |
| } | |
| /* Stars are plain divs with box-shadow-free simple dots, GPU-cheap: | |
| opacity keyframe only, no filter/blur/backdrop. */ | |
| .stars { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 0; | |
| pointer-events: none; | |
| overflow: hidden; | |
| } | |
| .star { | |
| position: absolute; | |
| width: 2px; | |
| height: 2px; | |
| border-radius: 50%; | |
| background: #fff; | |
| opacity: 0.5; | |
| animation: twinkle var(--dur, 4s) ease-in-out infinite; | |
| animation-delay: var(--delay, 0s); | |
| } | |
| .star.big { width: 3px; height: 3px; } | |
| @keyframes twinkle { | |
| 0%, 100% { opacity: var(--min-op, 0.15); } | |
| 50% { opacity: var(--max-op, 0.9); } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .star { animation: none; opacity: 0.5; } | |
| } | |
| /* A few slow shooting stars, pure transform, cheap. */ | |
| .shooting-star { | |
| position: absolute; | |
| top: var(--top, 10%); | |
| left: -10%; | |
| width: 2px; | |
| height: 2px; | |
| border-radius: 50%; | |
| background: #fff; | |
| box-shadow: 0 0 0 1px rgba(255,255,255,0.3); | |
| animation: shoot 6s linear infinite; | |
| animation-delay: var(--delay, 0s); | |
| opacity: 0; | |
| } | |
| .shooting-star::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| right: 0; | |
| width: 90px; | |
| height: 1px; | |
| transform: translateY(-50%); | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.7)); | |
| } | |
| @keyframes shoot { | |
| 0% { transform: translate(0, 0); opacity: 0; } | |
| 4% { opacity: 1; } | |
| 18% { opacity: 0; } | |
| 100% { transform: translate(130vw, 40px); opacity: 0; } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .shooting-star { display: none; } | |
| } | |
| .fade { animation: fadeIn 0.6s ease both; } | |
| .d1 { animation-delay: .05s; } .d2 { animation-delay: .12s; } .d3 { animation-delay: .18s; } | |
| .d4 { animation-delay: .24s; } .d5 { animation-delay: .3s; } .d6 { animation-delay: .36s; } .d7 { animation-delay: .42s; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .rotating-headline { | |
| display: inline-block; | |
| transition: opacity 0.4s ease, transform 0.4s ease; | |
| will-change: opacity, transform; | |
| } | |
| .rotating-headline.rolling-out { opacity: 0; transform: translateY(-0.5em); } | |
| .rotating-headline.rolling-in { opacity: 0; transform: translateY(0.5em); } | |
| @media (prefers-reduced-motion: reduce) { | |
| .rotating-headline { transition: opacity 0.3s ease; } | |
| .rotating-headline.rolling-out, .rotating-headline.rolling-in { transform: none; } | |
| } | |
| /* ============ Partners "accumulation" stage ============ */ | |
| .orbit-stage { | |
| position: relative; | |
| width: 100%; | |
| max-width: 980px; | |
| margin: 0 auto; | |
| } | |
| /* Each logo starts flung off-stage in a random direction, drifts/tumbles | |
| inward, then settles permanently into its final grid slot and stops. | |
| Pure transform + opacity keyframes (translate/rotate/scale only) so the | |
| whole sequence is GPU-composited with no per-frame layout/paint cost, | |
| and once the animation ends it simply holds its final state — no | |
| ongoing free-flight, no JS animation loop. */ | |
| .orbit-item { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: var(--size, 160px); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| margin: 0 0 0 calc(var(--size, 160px) / -2); | |
| opacity: 0; | |
| animation: accumulate var(--dur, 1.6s) cubic-bezier(0.16, 0.9, 0.2, 1) forwards; | |
| animation-delay: var(--delay, 0s); | |
| will-change: transform, opacity; | |
| /* final resting position for this item, set inline per-item */ | |
| --end-x: 0px; | |
| --end-y: 0px; | |
| } | |
| @media (max-width: 640px) { | |
| .orbit-item { --size: 118px ; gap: 6px; } | |
| } | |
| .orbit-card { | |
| width: var(--size, 160px); | |
| height: var(--size, 160px); | |
| border-radius: 22px; | |
| border: 1px solid var(--line); | |
| background: rgba(255,255,255,0.02); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16%; | |
| cursor: pointer; | |
| transition: border-color 0.25s ease, background 0.25s ease, transform 0.25s ease; | |
| position: relative; | |
| overflow: visible; | |
| } | |
| .orbit-card:hover { | |
| border-color: var(--accent-soft); | |
| background: rgba(124,156,255,0.06); | |
| transform: scale(1.06); | |
| z-index: 20; | |
| } | |
| .orbit-card img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| display: block; | |
| } | |
| /* Caption sits permanently under the card — no hover/tap required, so it | |
| works identically on desktop and mobile/touch. */ | |
| .orbit-caption { | |
| text-align: center; | |
| max-width: calc(var(--size, 160px) + 40px); | |
| } | |
| .orbit-caption .name { | |
| display: block; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--ink); | |
| margin-bottom: 2px; | |
| } | |
| .orbit-caption .blurb { | |
| display: block; | |
| font-size: 10.5px; | |
| line-height: 1.4; | |
| color: var(--muted); | |
| } | |
| .orbit-caption .since { | |
| display: block; | |
| margin-top: 4px; | |
| font-size: 9px; | |
| letter-spacing: 0.04em; | |
| color: var(--muted-2); | |
| } | |
| @media (max-width: 640px) { | |
| .orbit-caption .name { font-size: 11.5px; } | |
| .orbit-caption .blurb { font-size: 9px; } | |
| } | |
| @keyframes accumulate { | |
| 0% { | |
| opacity: 0; | |
| transform: | |
| translate(var(--start-x, 0px), calc(var(--start-y, 0px) - var(--size, 160px) / 2)) | |
| rotate(var(--start-rot, 0deg)) | |
| scale(0.4); | |
| } | |
| 12% { | |
| opacity: 1; | |
| } | |
| 70% { | |
| opacity: 1; | |
| transform: | |
| translate(calc(var(--end-x, 0px) + var(--overshoot-x, 0px)), calc(var(--end-y, 0px) + var(--overshoot-y, 0px) - var(--size, 160px) / 2)) | |
| rotate(calc(var(--start-rot, 0deg) * 0.08)) | |
| scale(1.06); | |
| } | |
| 100% { | |
| opacity: 1; | |
| transform: translate(var(--end-x, 0px), calc(var(--end-y, 0px) - var(--size, 160px) / 2)) rotate(0deg) scale(1); | |
| } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .orbit-item { | |
| animation: none; | |
| opacity: 1; | |
| transform: translate(var(--end-x, 0px), calc(var(--end-y, 0px) - var(--size, 160px) / 2)); | |
| } | |
| } | |
| /* A soft little settle-flash on arrival, purely decorative and cheap | |
| (opacity + scale only), so the sequential landings read clearly. */ | |
| .orbit-card::after { | |
| content: ''; | |
| position: absolute; | |
| inset: -10px; | |
| border-radius: 24px; | |
| background: radial-gradient(circle, rgba(124,156,255,0.35) 0%, transparent 70%); | |
| opacity: 0; | |
| animation: settleFlash 0.7s ease-out forwards; | |
| animation-delay: inherit; | |
| pointer-events: none; | |
| } | |
| @keyframes settleFlash { | |
| 0%, 60% { opacity: 0; } | |
| 72% { opacity: 1; } | |
| 100% { opacity: 0; } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .orbit-card::after { display: none; } | |
| } | |
| /* "Partners" label sits in normal flow above the stage, so it can never | |
| overlap the accumulating logos regardless of stage height or how many | |
| rows the layout wraps to. */ | |
| .orbit-label { | |
| font-size: 10px; | |
| letter-spacing: 0.25em; | |
| text-transform: uppercase; | |
| color: var(--muted-2); | |
| text-align: center; | |
| margin-bottom: 18px; | |
| } | |
| /* ============ Members section ============ */ | |
| .section-divider { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| width: 100%; | |
| max-width: 42rem; | |
| } | |
| .section-divider .line { | |
| flex: 1; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, var(--line-strong), transparent); | |
| } | |
| .member-card { | |
| border: 1px solid var(--line); | |
| border-radius: 14px; | |
| background: rgba(255,255,255,0.02); | |
| transition: border-color 0.25s ease, background 0.25s ease, transform 0.25s ease; | |
| } | |
| .member-card:hover { | |
| border-color: var(--accent-soft); | |
| background: rgba(124,156,255,0.05); | |
| transform: translateY(-2px); | |
| } | |
| .member-avatar { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| border: 1px solid var(--line-strong); | |
| background: rgba(255,255,255,0.04); | |
| object-fit: cover; | |
| flex-shrink: 0; | |
| } | |
| .member-org-badge { | |
| font-size: 9px; | |
| font-weight: 500; | |
| letter-spacing: 0.03em; | |
| color: var(--muted); | |
| background: rgba(255,255,255,0.04); | |
| border: 1px solid var(--line); | |
| border-radius: 999px; | |
| padding: 2px 8px; | |
| white-space: nowrap; | |
| } | |
| .member-hf-link { | |
| font-size: 10px; | |
| color: var(--accent); | |
| text-decoration: none; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 3px; | |
| transition: color 0.15s ease; | |
| } | |
| .member-hf-link:hover { color: #a9beff; } | |
| ::selection { background: rgba(124,156,255,0.35); } | |
| </style> | |
| </head> | |
| <body class="antialiased min-h-screen overflow-y-auto"> | |
| <div class="space-bg"></div> | |
| <div class="stars" id="stars"></div> | |
| <div class="relative z-10 min-h-screen flex flex-col items-center px-6 py-16"> | |
| <!-- Logo --> | |
| <img src="benchlabs.svg" alt="Bench-labs" class="h-16 md:h-20 w-auto mb-8 fade d1" | |
| style="filter: drop-shadow(0 0 12px rgba(124,156,255,0.35));" /> | |
| <!-- Label --> | |
| <div class="flex items-center gap-2.5 mb-3 fade d2"> | |
| <span class="text-[11px] font-medium tracking-widest uppercase text-[var(--muted)]">Partners & members</span> | |
| </div> | |
| <!-- Heading --> | |
| <h1 class="font-serif italic text-3xl md:text-4xl tracking-tight text-center mb-2 fade d2"> | |
| <span id="rotating-headline" class="rotating-headline">Do you want to try something?</span> | |
| </h1> | |
| <p class="text-[var(--muted)] text-sm font-light text-center max-w-sm mb-6 fade d3"> | |
| Enter our joyful community and let's test together | |
| </p> | |
| <!-- Partners orbit --> | |
| <div class="orbit-label fade d4">Partners</div> | |
| <div class="orbit-stage fade d4" id="orbit-stage"> | |
| <!-- orbit items injected by JS below --> | |
| </div> | |
| <!-- Divider --> | |
| <div class="section-divider mt-14 mb-10 fade d5"> | |
| <div class="line"></div> | |
| <span class="text-[11px] font-medium tracking-widest uppercase text-[var(--muted)] whitespace-nowrap">Members</span> | |
| <div class="line"></div> | |
| </div> | |
| <!-- Members grid - populated at runtime from members.json (same repo) --> | |
| <div id="members-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 w-full max-w-3xl fade d6"> | |
| <!-- cards injected by loadMembers() below --> | |
| </div> | |
| <p id="members-count" class="text-[11px] text-[var(--muted-2)] mt-6 fade d7"> | |
| Loading members… | |
| </p> | |
| <!-- Footer --> | |
| <a class="mt-10 text-xs text-[var(--muted-2)] hover:text-[var(--muted)] transition-colors duration-300 fade d7"> | |
| Experimentation is for everyone | |
| </a> | |
| </div> | |
| <script> | |
| // =================================================================== | |
| // Partner data — same content/links as before, restyled for the orbit. | |
| // Edit this array to add/remove/update partner cards. | |
| // =================================================================== | |
| const PARTNERS = [ | |
| { | |
| name: "FWKV", | |
| url: "https://huggingface.co/FWKV", | |
| img: "FWKV.png", | |
| blurb: "Feed-forward Weighted Key Value", | |
| since: "25/7/2026", | |
| }, | |
| { | |
| name: "FromZero", | |
| url: "https://huggingface.co/fromziro", | |
| img: "qITdWZiMpLE8Kop68m9OZ.webp", | |
| blurb: "We build stuff and share it with the community.", | |
| since: "15/7/2026", | |
| }, | |
| { | |
| name: "İvmeLabs", | |
| url: "https://huggingface.co/IvmeLabs", | |
| img: "orglogobare.png", | |
| blurb: "Stupidly Small Models", | |
| since: "12/7/2026", | |
| }, | |
| { | |
| name: "Simplicity", | |
| url: "https://simplicity-india.com/", | |
| img: "Simplicity.png", | |
| blurb: "Simplicity without complexity", | |
| since: "12/7/2026", | |
| }, | |
| { | |
| name: "Complexity ML", | |
| url: "https://www.complexity-ai.fr/", | |
| img: "Screenshot-2026-06-20-171937.png", | |
| blurb: "Building efficient transformer architectures with Mu-Guided Dynamics and Token-Routed MLP", | |
| since: null, // planned, not yet officially partnered | |
| }, | |
| ]; | |
| (() => { | |
| const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; | |
| const isMobile = | |
| window.matchMedia("(max-width: 767px)").matches || | |
| navigator.userAgentData?.mobile === true || | |
| /Android|iPhone|iPad|iPod|webOS|BlackBerry|Opera Mini|IEMobile/i.test(navigator.userAgent); | |
| // ---------------- Star field ---------------- | |
| // Cheap: absolutely-positioned dots with an opacity-only twinkle. No | |
| // blur/filter/backdrop anywhere. Count is trimmed on mobile. | |
| const starsEl = document.getElementById("stars"); | |
| const starCount = isMobile ? 60 : 140; | |
| const frag = document.createDocumentFragment(); | |
| for (let i = 0; i < starCount; i++) { | |
| const s = document.createElement("div"); | |
| s.className = "star" + (Math.random() < 0.15 ? " big" : ""); | |
| s.style.left = Math.random() * 100 + "%"; | |
| s.style.top = Math.random() * 100 + "%"; | |
| s.style.setProperty("--dur", (3 + Math.random() * 5).toFixed(2) + "s"); | |
| s.style.setProperty("--delay", (-Math.random() * 6).toFixed(2) + "s"); | |
| s.style.setProperty("--min-op", (0.08 + Math.random() * 0.15).toFixed(2)); | |
| s.style.setProperty("--max-op", (0.6 + Math.random() * 0.4).toFixed(2)); | |
| frag.appendChild(s); | |
| } | |
| starsEl.appendChild(frag); | |
| // A couple of slow shooting stars, skipped on mobile / reduced motion. | |
| if (!isMobile && !prefersReducedMotion) { | |
| const shootCount = 3; | |
| for (let i = 0; i < shootCount; i++) { | |
| const sh = document.createElement("div"); | |
| sh.className = "shooting-star"; | |
| sh.style.setProperty("--top", (5 + Math.random() * 40) + "%"); | |
| sh.style.setProperty("--delay", (i * 7 + Math.random() * 5).toFixed(2) + "s"); | |
| starsEl.appendChild(sh); | |
| } | |
| } | |
| // ---------------- Partner accumulation sequence ---------------- | |
| // Logos start flung off in random directions, drift/tumble inward one | |
| // after another, then settle permanently into a fixed grid layout and | |
| // stop — no ongoing free-flight once the sequence finishes. | |
| const stage = document.getElementById("orbit-stage"); | |
| const stageW = isMobile ? 340 : 940; | |
| const stageH = isMobile ? 460 : 560; | |
| const size = isMobile ? 118 : (PARTNERS.length <= 4 ? 190 : 160); | |
| // Final resting layout: a centered row (wraps to two rows if needed), | |
| // expressed as offsets from the stage center (where top/left:50% sits). | |
| // Extra vertical room (captionH) is reserved per row so the always-on | |
| // name/blurb caption under each logo never overlaps the row below it. | |
| const perRow = isMobile ? Math.min(2, PARTNERS.length) : Math.min(4, PARTNERS.length); | |
| const gap = isMobile ? 26 : 46; | |
| const captionH = isMobile ? 58 : 66; | |
| const rows = Math.ceil(PARTNERS.length / perRow); | |
| const rowHeight = size + captionH + gap; | |
| const stageContentHeight = rows * (size + captionH) + (rows - 1) * gap; | |
| // extra headroom above/below so the fly-in overshoot never gets clipped | |
| stage.style.height = (stageContentHeight + (isMobile ? 40 : 60)) + "px"; | |
| function slotPosition(index) { | |
| const row = Math.floor(index / perRow); | |
| const itemsInRow = Math.min(perRow, PARTNERS.length - row * perRow); | |
| const rowWidth = itemsInRow * size + (itemsInRow - 1) * gap; | |
| const col = index % perRow; | |
| const x = -rowWidth / 2 + col * (size + gap) + size / 2; | |
| const rowsTotalHeight = rows * (size + captionH) + (rows - 1) * gap; | |
| const y = -rowsTotalHeight / 2 + row * rowHeight + size / 2; | |
| return { x, y }; | |
| } | |
| PARTNERS.forEach((p, i) => { | |
| const { x: endX, y: endY } = slotPosition(i); | |
| // Random off-stage starting point: pick an angle around the stage and | |
| // push it out well past the visible edge so the entry reads as a | |
| // clear "fly in from a random direction." | |
| const angle = Math.random() * Math.PI * 2; | |
| const throwDist = Math.max(stageW, stageH) * (0.75 + Math.random() * 0.5); | |
| const startX = Math.cos(angle) * throwDist; | |
| const startY = Math.sin(angle) * throwDist; | |
| const startRot = (Math.random() < 0.5 ? -1 : 1) * (140 + Math.random() * 220); | |
| // Small overshoot past the final slot before settling, for a soft | |
| // "drift into place" landing rather than a hard stop. | |
| const overshootAngle = Math.atan2(endY, endX) + (Math.random() - 0.5) * 0.6; | |
| const overshootX = Math.cos(overshootAngle) * (10 + Math.random() * 14); | |
| const overshootY = Math.sin(overshootAngle) * (10 + Math.random() * 14); | |
| const duration = 1.3 + Math.random() * 0.5; | |
| const delay = i * 0.45 + Math.random() * 0.15; // sequential accumulation, one after another | |
| const item = document.createElement("div"); | |
| item.className = "orbit-item"; | |
| item.style.setProperty("--size", size + "px"); | |
| item.style.setProperty("--start-x", startX.toFixed(1) + "px"); | |
| item.style.setProperty("--start-y", startY.toFixed(1) + "px"); | |
| item.style.setProperty("--start-rot", startRot.toFixed(1) + "deg"); | |
| item.style.setProperty("--end-x", endX.toFixed(1) + "px"); | |
| item.style.setProperty("--end-y", endY.toFixed(1) + "px"); | |
| item.style.setProperty("--overshoot-x", overshootX.toFixed(1) + "px"); | |
| item.style.setProperty("--overshoot-y", overshootY.toFixed(1) + "px"); | |
| item.style.setProperty("--dur", duration.toFixed(2) + "s"); | |
| item.style.setProperty("--delay", delay.toFixed(2) + "s"); | |
| const card = document.createElement("div"); | |
| card.className = "orbit-card"; | |
| card.dataset.url = p.url; | |
| const img = document.createElement("img"); | |
| img.src = p.img; | |
| img.alt = p.name; | |
| img.loading = "lazy"; | |
| card.appendChild(img); | |
| card.addEventListener("click", () => { | |
| window.open(p.url, "_blank", "noopener,noreferrer"); | |
| }); | |
| const caption = document.createElement("div"); | |
| caption.className = "orbit-caption"; | |
| caption.innerHTML = | |
| '<span class="name">' + p.name + '</span>' + | |
| '<span class="blurb">' + p.blurb + '</span>' + | |
| '<span class="since">' + (p.since ? "Partnered since " + p.since : "Planned partner") + '</span>'; | |
| item.appendChild(card); | |
| item.appendChild(caption); | |
| stage.appendChild(item); | |
| }); | |
| // ---------------- Adaptive performance ladder ---------------- | |
| // The accumulation animation is one-shot and short-lived, so there's | |
| // little to cut there once it's finished; this ladder mainly protects | |
| // the ambient star field on very low-power devices. | |
| if (!prefersReducedMotion) { | |
| let cuts = 0; | |
| const steps = [ | |
| () => document.querySelectorAll(".shooting-star").forEach((s) => s.remove()), | |
| () => { | |
| const all = document.querySelectorAll(".star"); | |
| all.forEach((s, i) => { if (i % 2 === 0) s.remove(); }); | |
| }, | |
| ]; | |
| let frames = 0, windowStart = 0, lowStreak = 0; | |
| const THRESHOLD = 30, SAMPLE_MS = 1000, PATIENCE = 2; | |
| function tick(now) { | |
| frames++; | |
| const elapsed = now - windowStart; | |
| if (elapsed >= SAMPLE_MS) { | |
| const fps = (frames * 1000) / elapsed; | |
| frames = 0; | |
| windowStart = now; | |
| if (fps < THRESHOLD) { | |
| if (++lowStreak >= PATIENCE && cuts < steps.length) { | |
| lowStreak = 0; | |
| steps[cuts](); | |
| cuts++; | |
| } | |
| } else { | |
| lowStreak = 0; | |
| } | |
| } | |
| if (cuts < steps.length) requestAnimationFrame(tick); | |
| } | |
| setTimeout(() => { | |
| windowStart = performance.now(); | |
| requestAnimationFrame(tick); | |
| }, 1500); | |
| } | |
| })(); | |
| </script> | |
| <script> | |
| // ===== Members grid: fetched from members.json in this same Space repo ===== | |
| // members.json is written by the Discord bot on every /link and | |
| // /hf-visibility change. It only ever contains members who set themselves | |
| // to public - no Discord identity, no hidden users. See storage.py's | |
| // sync_public_members() for how it's produced. | |
| (async () => { | |
| const grid = document.getElementById("members-grid"); | |
| const countEl = document.getElementById("members-count"); | |
| function escapeHtml(str) { | |
| const div = document.createElement("div"); | |
| div.textContent = str ?? ""; | |
| return div.innerHTML; | |
| } | |
| function initials(username) { | |
| if (!username) return "?"; | |
| return username.slice(0, 2).toUpperCase(); | |
| } | |
| function absoluteAvatarUrl(url) { | |
| // Some existing records have a HF-relative path like | |
| // "/avatars/abc.svg" instead of a full URL (from before this was | |
| // normalized server-side in web.py) - fix it up here too so old | |
| // data self-heals without requiring everyone to re-/link. | |
| if (url && url.startsWith("/")) { | |
| return `https://huggingface.co${url}`; | |
| } | |
| return url || ""; | |
| } | |
| function memberCardHtml(member) { | |
| const username = member.hf_username || ""; | |
| const profileUrl = `https://huggingface.co/${encodeURIComponent(username)}`; | |
| const avatarUrl = absoluteAvatarUrl(member.avatar_url); | |
| const avatar = avatarUrl | |
| ? `<img src="${escapeHtml(avatarUrl)}" alt="${escapeHtml(username)}" class="member-avatar" loading="lazy" />` | |
| : `<div class="member-avatar flex items-center justify-center text-sm font-medium text-[var(--muted)]">${escapeHtml(initials(username))}</div>`; | |
| const orgs = Array.isArray(member.orgs) ? member.orgs : []; | |
| const orgBadges = orgs | |
| .map((o) => { | |
| const slug = o?.slug || o?.name || ""; | |
| return `<span class="member-org-badge">${escapeHtml(o?.name || slug)}</span>`; | |
| }) | |
| .join(""); | |
| return ` | |
| <div class="member-card rounded-xl p-4 flex items-center gap-3"> | |
| ${avatar} | |
| <div class="flex flex-col gap-1 min-w-0"> | |
| <span class="text-sm font-medium text-[var(--ink)] truncate">${escapeHtml(username)}</span> | |
| <a href="${profileUrl}" target="_blank" rel="noopener noreferrer" class="member-hf-link"> | |
| huggingface.co/${escapeHtml(username)} | |
| <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5" width="8" height="8"> | |
| <path d="M1 9L9 1M9 1H4M9 1V6"/> | |
| </svg> | |
| </a> | |
| ${orgBadges ? `<div class="flex flex-wrap gap-1 mt-0.5">${orgBadges}</div>` : ""} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| try { | |
| const res = await fetch("members.json", { cache: "no-store" }); | |
| if (!res.ok) throw new Error(`members.json returned ${res.status}`); | |
| const members = await res.json(); | |
| if (!Array.isArray(members) || members.length === 0) { | |
| grid.innerHTML = ""; | |
| countEl.textContent = "No members yet - be the first to /link!"; | |
| return; | |
| } | |
| grid.innerHTML = members.map(memberCardHtml).join(""); | |
| countEl.textContent = | |
| `${members.length} member${members.length === 1 ? "" : "s"} shown \u00b7 members who set their link to private are not listed here`; | |
| } catch (err) { | |
| console.error("Failed to load members.json", err); | |
| grid.innerHTML = ""; | |
| countEl.textContent = "Couldn't load members right now - try refreshing."; | |
| } | |
| })(); | |
| </script> | |
| <script> | |
| // Rotating headline — cycles the hero question through a few variations. | |
| (() => { | |
| const el = document.getElementById("rotating-headline"); | |
| if (!el) return; | |
| const phrases = [ | |
| "Do you want to try something new?", | |
| "Do you want to build something?", | |
| "Do you want to break something?", | |
| "Do you want to benchmark something?", | |
| "Got a model to upgrade?", | |
| "Ready to experiment together?", | |
| "Want a place to share?", | |
| "Need help to start?", | |
| "Made a revolutionary achivement?", | |
| "Want to see more?", | |
| ]; | |
| const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; | |
| let i = 0; | |
| setInterval(() => { | |
| i = (i + 1) % phrases.length; | |
| if (reduce) { el.textContent = phrases[i]; return; } | |
| el.classList.add("rolling-out"); | |
| el.addEventListener("transitionend", function onOut() { | |
| el.textContent = phrases[i]; | |
| el.classList.remove("rolling-out"); | |
| el.classList.add("rolling-in"); | |
| requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove("rolling-in"))); | |
| }, { once: true }); | |
| }, 3400); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |