diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..b49d604
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+ Every Tab
+
+
+
+
+ Loading tabs...
+
+
+
+
+
+
diff --git a/frontend/site.js b/frontend/site.js
new file mode 100644
index 0000000..f21ad53
--- /dev/null
+++ b/frontend/site.js
@@ -0,0 +1,230 @@
+// Seeded PRNG (mulberry32)
+function mulberry32(seed) {
+ return function() {
+ seed |= 0;
+ seed = seed + 0x6D2B79F5 | 0;
+ let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
+ };
+}
+
+const rng = mulberry32(Date.now());
+const loadedBundles = new Set();
+const container = document.getElementById("tab-container");
+const loadingEl = document.getElementById("loading");
+
+// How many tabs fit in one row?
+function tabsPerRow() {
+ const tabWidth = 160; // avg tab width in px (min 100, max 200)
+ return Math.ceil(window.innerWidth / tabWidth) + 4; // extra for marquee overflow
+}
+
+// How many rows fill the viewport?
+function rowsPerScreen() {
+ const rowHeight = 40;
+ return Math.ceil(window.innerHeight / rowHeight) + 4; // buffer
+}
+
+// Pick a random bundle index we haven't loaded yet
+function pickBundle() {
+ if (loadedBundles.size >= TOTAL_BUNDLES) return -1;
+ let idx;
+ do {
+ idx = Math.floor(rng() * TOTAL_BUNDLES);
+ } while (loadedBundles.has(idx));
+ loadedBundles.add(idx);
+ return idx;
+}
+
+// Fetch a bundle JSON
+async function fetchBundle(idx) {
+ const padded = String(idx).padStart(4, "0");
+ const resp = await fetch(`tabs/${padded}.json`);
+ if (!resp.ok) throw new Error(`Failed to fetch bundle ${padded}`);
+ const data = await resp.json();
+ return data.entries;
+}
+
+// Create a tab DOM element
+function createTab(entry) {
+ const tab = document.createElement("div");
+ tab.className = "tab";
+ if (!entry.iframe_ok) {
+ tab.classList.add("tab-external");
+ }
+
+ if (entry.icon) {
+ const img = document.createElement("img");
+ img.className = "tab-icon";
+ img.src = `data:image/png;base64,${entry.icon}`;
+ img.alt = "";
+ img.loading = "lazy";
+ tab.appendChild(img);
+ }
+
+ const title = document.createElement("span");
+ title.className = "tab-title";
+ title.textContent = entry.title || entry.host;
+ tab.appendChild(title);
+
+ tab.title = entry.title || entry.host;
+
+ // Click handler
+ tab.addEventListener("click", () => {
+ const url = `${entry.protocol || "https"}://${entry.host}`;
+ if (entry.iframe_ok) {
+ openInlineViewer(tab, entry, url);
+ } else {
+ window.open(url, "_blank", "noopener");
+ }
+ });
+
+ return tab;
+}
+
+// Create a row of tabs with marquee animation
+function createRow(entries, rowIndex) {
+ const row = document.createElement("div");
+ row.className = "tab-row";
+
+ const speed = 60 + (rng() * 40); // 60-100s per cycle
+ row.style.setProperty("--speed", `${speed}s`);
+
+ // Random direction
+ const goLeft = rng() > 0.5;
+ row.classList.add(goLeft ? "scroll-left" : "scroll-right");
+
+ // Stagger start so rows aren't synchronized
+ row.style.animationDelay = `${-rng() * speed}s`;
+
+ // Add tabs twice so the marquee loops seamlessly (translate -50% = one full set)
+ for (let copy = 0; copy < 2; copy++) {
+ for (const entry of entries) {
+ row.appendChild(createTab(entry));
+ }
+ }
+
+ return row;
+}
+
+// Render entries into rows
+function renderEntries(entries) {
+ const perRow = tabsPerRow();
+ let rowIndex = container.children.length;
+
+ for (let i = 0; i < entries.length; i += perRow) {
+ const rowEntries = entries.slice(i, i + perRow);
+ if (rowEntries.length < 3) continue; // skip tiny last rows
+ container.appendChild(createRow(rowEntries, rowIndex));
+ rowIndex++;
+ }
+}
+
+// Load enough bundles to fill the screen
+async function loadMore() {
+ const neededRows = rowsPerScreen();
+ const entriesNeeded = neededRows * tabsPerRow();
+
+ let entries = [];
+ while (entries.length < entriesNeeded) {
+ const idx = pickBundle();
+ if (idx === -1) break;
+ try {
+ const bundleEntries = await fetchBundle(idx);
+ entries = entries.concat(bundleEntries);
+ } catch (e) {
+ console.error("Failed to load bundle:", e);
+ }
+ }
+
+ if (entries.length > 0) {
+ renderEntries(entries);
+ loadingEl.style.display = "none";
+ }
+}
+
+// Infinite scroll
+let loading = false;
+window.addEventListener("scroll", async () => {
+ if (loading) return;
+ const scrollBottom = window.innerHeight + window.scrollY;
+ const docHeight = document.documentElement.scrollHeight;
+
+ if (scrollBottom >= docHeight - 500) {
+ loading = true;
+ await loadMore();
+ loading = false;
+ }
+});
+
+// Inline iframe viewer
+let activeViewer = null;
+
+function openInlineViewer(tabEl, entry, url) {
+ // Close existing viewer
+ closeInlineViewer();
+
+ // Find the row this tab belongs to
+ const row = tabEl.closest(".tab-row");
+
+ // Build the viewer
+ const viewer = document.createElement("div");
+ viewer.className = "iframe-viewer";
+
+ const header = document.createElement("div");
+ header.className = "iframe-header";
+
+ if (entry.icon) {
+ const icon = document.createElement("img");
+ icon.className = "tab-icon";
+ icon.src = `data:image/png;base64,${entry.icon}`;
+ header.appendChild(icon);
+ }
+
+ const title = document.createElement("span");
+ title.className = "tab-title";
+ title.textContent = entry.title || entry.host;
+ header.appendChild(title);
+
+ const link = document.createElement("a");
+ link.href = url;
+ link.target = "_blank";
+ link.rel = "noopener";
+ link.textContent = entry.host + " ↗";
+ header.appendChild(link);
+
+ const close = document.createElement("button");
+ close.className = "iframe-close";
+ close.textContent = "✕";
+ close.addEventListener("click", closeInlineViewer);
+ header.appendChild(close);
+
+ const iframe = document.createElement("iframe");
+ iframe.sandbox = "allow-scripts allow-same-origin allow-forms";
+ iframe.src = url;
+
+ viewer.appendChild(header);
+ viewer.appendChild(iframe);
+
+ // Insert after the row
+ row.after(viewer);
+ activeViewer = viewer;
+
+ // Scroll so the viewer is visible
+ viewer.scrollIntoView({ behavior: "smooth", block: "start" });
+}
+
+function closeInlineViewer() {
+ if (activeViewer) {
+ activeViewer.remove();
+ activeViewer = null;
+ }
+}
+
+document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") closeInlineViewer();
+});
+
+// Initial load
+loadMore();