☰

Reddit Reels

Mobile-first full-screen reels for Reddit feeds: swipe, one stream with sound, Reddit video and RedGifs, native voting.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Reddit Reels
// @namespace    https://github.com/quantavil/userscript/tree/main/reddit-reels
// @version      3.2.0
// @author       quantavil
// @description  Mobile-first full-screen reels for Reddit feeds: swipe, one stream with sound, Reddit video and RedGifs, native voting.
// @license      MIT
// @homepage     https://github.com/quantavil/userscript/tree/main/reddit-reels
// @supportURL   https://github.com/quantavil/userscript/issues
// @match        https://www.reddit.com/*
// @match        https://reddit.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js
// @connect      api.redgifs.com
// @connect      media.redgifs.com
// @connect      redgifs.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @noframes
// ==/UserScript==

(function(hls_js) {
	"use strict";
	var __create = Object.create;
	var __defProp = Object.defineProperty;
	var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
	var __getOwnPropNames = Object.getOwnPropertyNames;
	var __getProtoOf = Object.getPrototypeOf;
	var __hasOwnProp = Object.prototype.hasOwnProperty;
	var __copyProps = (to, from, except, desc) => {
		if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
			key = keys[i];
			if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
				get: ((k) => from[k]).bind(null, key),
				enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
			});
		}
		return to;
	};
	var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
		value: mod,
		enumerable: true
	}) : target, mod));
	hls_js = __toESM(hls_js);
	var NON_FEED_SEGMENTS = /^\/(?:settings|message|messages|chat|notifications|mod|premium|submit|search|media|login|register|account|coins|prefs|wiki|gallery)(?:\/|$)/i;
	function isReelRoute(pathname) {
		const path = pathname.replace(/\/+$/, "") || "/";
		if (/\/comments\//i.test(path) || /\/s\/[A-Za-z0-9]+$/.test(path)) return false;
		if (NON_FEED_SEGMENTS.test(path)) return false;
		if (path === "/" || /^\/(?:best|hot|new|top|rising|controversial)$/i.test(path)) return true;
		if (/^\/r\/[A-Za-z0-9_]+(?:\/(?:best|hot|new|top|rising|controversial))?$/i.test(path)) return true;
		if (/^\/(?:user|u)\/[A-Za-z0-9_-]+(?:\/submitted)?$/i.test(path)) return true;
		return false;
	}
	function watchRoute(onChange) {
		let lastHref = location.href;
		let timer = null;
		const check = () => {
			if (timer) clearTimeout(timer);
			timer = setTimeout(() => {
				timer = null;
				if (location.href === lastHref) return;
				lastHref = location.href;
				onChange();
			}, 50);
		};
		const origPush = history.pushState;
		const origReplace = history.replaceState;
		history.pushState = function(...args) {
			const ret = origPush.apply(this, args);
			check();
			return ret;
		};
		history.replaceState = function(...args) {
			const ret = origReplace.apply(this, args);
			check();
			return ret;
		};
		const nav = window.navigation;
		nav?.addEventListener?.("navigatesuccess", check);
		window.addEventListener("popstate", check);
		const onPageShow = (e) => {
			if (e.persisted) {
				lastHref = "";
				check();
			}
		};
		window.addEventListener("pageshow", onPageShow);
		return () => {
			history.pushState = origPush;
			history.replaceState = origReplace;
			nav?.removeEventListener?.("navigatesuccess", check);
			window.removeEventListener("popstate", check);
			window.removeEventListener("pageshow", onPageShow);
			if (timer) clearTimeout(timer);
		};
	}
	function formatCount(num) {
		if (!num || Number.isNaN(num)) return "0";
		if (Math.abs(num) >= 1e6) return (num / 1e6).toFixed(1).replace(/\.0$/, "") + "m";
		if (Math.abs(num) >= 1e3) return (num / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
		return num.toString();
	}
	function escapeHtml(str) {
		if (!str) return "";
		return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
	}
	function extractDomain(url) {
		if (!url) return "";
		try {
			return new URL(url.startsWith("http") ? url : `https://${url}`).hostname.replace(/^www\./, "");
		} catch {
			return url.replace(/^https?:\/\//, "").split("/")[0];
		}
	}
	function isSafeUrl(url) {
		if (!url || typeof url !== "string") return false;
		const trimmed = url.trim();
		if (!trimmed) return false;
		try {
			const base = typeof location !== "undefined" ? location.origin : "https://www.reddit.com";
			const parsed = new URL(trimmed, base);
			return parsed.protocol === "http:" || parsed.protocol === "https:";
		} catch {
			return false;
		}
	}
	function formatTime(sec) {
		if (!Number.isFinite(sec) || sec < 0) return "0:00";
		return `${Math.floor(sec / 60)}:${Math.floor(sec % 60).toString().padStart(2, "0")}`;
	}
	var KEEP = new Set([
		"P",
		"BR",
		"STRONG",
		"B",
		"EM",
		"I",
		"DEL",
		"S",
		"SUP",
		"SUB",
		"CODE",
		"PRE",
		"BLOCKQUOTE",
		"UL",
		"OL",
		"LI",
		"HR",
		"H1",
		"H2",
		"H3",
		"H4",
		"H5",
		"H6"
	]);
	var HEADINGS = {
		H1: "H3",
		H2: "H3",
		H3: "H4",
		H4: "H4",
		H5: "H5",
		H6: "H5"
	};
	var MAX_NODES = 4e3;
	function copy(src, out, budget) {
		for (const child of Array.from(src.childNodes)) {
			if (--budget.n < 0) return;
			if (child.nodeType === Node.TEXT_NODE) {
				out.appendChild(document.createTextNode(child.textContent || ""));
				continue;
			}
			if (child.nodeType !== Node.ELEMENT_NODE) continue;
			const el = child;
			const tag = el.tagName;
			if (tag === "SCRIPT" || tag === "STYLE" || tag === "TEMPLATE" || tag === "SVG" || tag === "svg") continue;
			if (tag === "A") {
				const raw = el.getAttribute("href") || "";
				let href = "";
				try {
					href = new URL(raw, "https://www.reddit.com").href;
				} catch {}
				if (href && isSafeUrl(href)) {
					const a = document.createElement("a");
					a.href = href;
					a.target = "_blank";
					a.rel = "noopener noreferrer";
					copy(el, a, budget);
					out.appendChild(a);
				} else copy(el, out, budget);
				continue;
			}
			if (tag === "IMG") {
				const src = el.getAttribute("src") || "";
				if (src && isSafeUrl(src)) {
					const a = document.createElement("a");
					a.href = src;
					a.target = "_blank";
					a.rel = "noopener noreferrer";
					a.textContent = "[image]";
					out.appendChild(a);
				}
				continue;
			}
			if (el.classList.contains("md-spoiler-text") || tag === "SPOILER-TEXT") {
				const s = document.createElement("span");
				s.className = "spoiler";
				copy(el, s, budget);
				out.appendChild(s);
				continue;
			}
			if (KEEP.has(tag)) {
				const node = document.createElement(HEADINGS[tag] || tag);
				copy(el, node, budget);
				out.appendChild(node);
				continue;
			}
			copy(el, out, budget);
		}
	}
	function sanitizeMarkdownHtml(root) {
		const box = document.createElement("div");
		copy(root, box, { n: MAX_NODES });
		return box.innerHTML.trim();
	}
	var REDGIFS_RE = /redgifs\.com\/(?:watch|ifr|i)\/([a-z0-9]+)/i;
	var YOUTUBE_RE = /(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/i;
	var STREAMABLE_RE = /streamable\.com\/(?:e\/)?([a-z0-9]+)/i;
	function redgifsIdFrom(url) {
		const m = url.match(REDGIFS_RE);
		return m ? m[1].toLowerCase() : null;
	}
	function embedUrlFrom(url) {
		const yt = url.match(YOUTUBE_RE);
		if (yt) return `https://www.youtube-nocookie.com/embed/${yt[1]}?autoplay=1&playsinline=1&rel=0`;
		const st = url.match(STREAMABLE_RE);
		if (st && /streamable\.com/i.test(url)) return `https://streamable.com/e/${st[1]}?autoplay=1`;
		return null;
	}
	function num(v) {
		const n = parseInt(v || "", 10);
		return Number.isFinite(n) ? n : 0;
	}
	function readVideo(el) {
		const player = el.querySelector("shreddit-player, shreddit-player-2");
		if (!player) return void 0;
		const mp4 = [];
		const packed = player.getAttribute("packaged-media-json");
		if (packed) try {
			const perms = JSON.parse(packed)?.playbackMp4s?.permutations || [];
			for (const p of perms) {
				const url = p?.source?.url;
				if (typeof url === "string") mp4.push({
					url,
					h: p.source.dimensions?.height || 0,
					w: p.source.dimensions?.width || 0
				});
			}
		} catch {}
		mp4.sort((a, b) => b.h - a.h);
		const src = player.getAttribute("src") || "";
		const isHls = /\.m3u8(\?|$)/i.test(src) || /v\.redd\.it\/.+\/HLSPlaylist/i.test(src);
		let hls = "";
		if (isHls) hls = src;
		else if (src && !mp4.some((m) => m.url === src)) mp4.push({
			url: src,
			h: 0,
			w: 0
		});
		if (!mp4.length && !hls) return void 0;
		return {
			mp4: mp4.map((m) => m.url),
			hls,
			gif: player.hasAttribute("gif") || /\.gif$/i.test(src.split("?")[0]),
			poster: player.getAttribute("poster") || "",
			captions: player.getAttribute("caption-url") || "",
			width: mp4[0]?.w || 0,
			height: mp4[0]?.h || 0
		};
	}
	function imgSrc(img) {
		if (!img) return "";
		return img.getAttribute("src") || img.getAttribute("data-lazy-src") || "";
	}
	function readGallery(el) {
		const carousel = el.querySelector("gallery-carousel");
		if (!carousel) return [];
		const out = [];
		carousel.querySelectorAll("li").forEach((li) => {
			const src = imgSrc(li.querySelector("img"));
			if (src && !out.includes(src)) out.push(src);
		});
		return out;
	}
	function readImage(el, contentHref) {
		const full = imgSrc(el.querySelector("img.media-lightbox-img"));
		if (full) return full;
		const preview = imgSrc(el.querySelector("img.preview-img, img.i18n-post-media-img"));
		if (preview) return preview;
		return /\.(jpe?g|png|webp|gif)(\?|$)/i.test(contentHref) ? contentHref : "";
	}
	function extractPost(el) {
		const id = el.id || el.getAttribute("id") || "";
		if (!id) return null;
		const type = (el.getAttribute("post-type") || "").toLowerCase();
		const contentHref = el.getAttribute("content-href") || "";
		const domain = el.getAttribute("domain") || "";
		const iframeSrc = el.querySelector("iframe")?.getAttribute("src") || "";
		const post = {
			id,
			kind: "link",
			title: el.getAttribute("post-title") || "",
			subreddit: el.getAttribute("subreddit-prefixed-name") || "",
			author: el.getAttribute("author") || "",
			score: num(el.getAttribute("score")),
			comments: num(el.getAttribute("comment-count")),
			permalink: el.getAttribute("permalink") || "",
			nsfw: el.hasAttribute("nsfw"),
			el
		};
		let kind = null;
		const redgifsId = redgifsIdFrom(contentHref) || redgifsIdFrom(iframeSrc);
		const video = readVideo(el);
		const embedUrl = embedUrlFrom(contentHref);
		if (redgifsId) {
			kind = "redgifs";
			post.redgifsId = redgifsId;
		} else if (video) {
			kind = "video";
			post.video = video;
		} else if (embedUrl) {
			kind = "embed";
			post.embedUrl = embedUrl;
		} else if (type === "gallery") {
			const images = readGallery(el);
			if (images.length) {
				kind = "gallery";
				post.images = images;
			}
		}
		if (!kind && (type === "image" || /i\.redd\.it/i.test(domain))) {
			const src = readImage(el, contentHref);
			if (src) {
				kind = "image";
				post.images = [src];
			}
		}
		if (!kind && (type === "text" || type === "self")) {
			kind = "text";
			const body = el.querySelector("shreddit-post-text-body .md") || el.querySelector("[slot=\"text-body\"] .md") || el.querySelector("shreddit-post-text-body");
			post.text = (body?.textContent || "").replace(/\s+\n/g, "\n").trim().slice(0, 6e3);
			if (body) post.html = sanitizeMarkdownHtml(body);
		}
		if (!kind) {
			kind = "link";
			post.linkUrl = contentHref;
			post.thumbnail = imgSrc(el.querySelector("[slot=\"thumbnail\"] img"));
		}
		post.kind = kind;
		return post;
	}
	var LOAD_AFTER = "faceplate-partial[slot=\"load-after\"]";
	function findLoadAfter(root) {
		const slotted = root.querySelector(LOAD_AFTER);
		if (slotted) return slotted;
		for (const p of Array.from(root.querySelectorAll("faceplate-partial"))) {
			const src = p.getAttribute("src") || "";
			if (/[?&]after=/.test(src) && /more-posts|\/feeds\//.test(src)) return p;
		}
		return null;
	}
	var FeedSource = class {
		posts = [];
		byId = new Map();
		loading = null;
		exhausted = false;
		observer = null;
		scanTimer = null;
		listeners = new Set();
		onAdded(cb) {
			this.listeners.add(cb);
			return () => this.listeners.delete(cb);
		}
		scan() {
			const added = [];
			document.querySelectorAll("shreddit-post").forEach((el) => {
				if (el.closest("shreddit-ad-post") || el.hasAttribute("promoted")) return;
				const known = this.byId.get(el.id);
				if (known) {
					if (known.el !== el) known.el = el;
					return;
				}
				const post = extractPost(el);
				if (!post) return;
				this.byId.set(post.id, post);
				this.posts.push(post);
				added.push(post);
			});
			if (added.length) for (const cb of this.listeners) cb(added);
			return added;
		}
		get hasMore() {
			return !this.exhausted && !!findLoadAfter(document);
		}
		loadMore() {
			if (this.loading) return this.loading;
			this.loading = this.fetchNext().finally(() => {
				this.loading = null;
			});
			return this.loading;
		}
		async fetchNext() {
			const partial = findLoadAfter(document);
			const src = partial?.getAttribute("src");
			if (!partial || !src || this.exhausted) return 0;
			let html = "";
			try {
				const res = await fetch(new URL(src, location.origin).toString(), { credentials: "include" });
				if (!res.ok) return 0;
				html = await res.text();
			} catch {
				return 0;
			}
			if (!partial.isConnected) return this.scan().length;
			const doc = new DOMParser().parseFromString(html, "text/html");
			const blocks = [];
			doc.querySelectorAll("shreddit-post").forEach((p) => {
				if (p.closest("shreddit-ad-post")) return;
				const block = p.closest("article") || p;
				if (!blocks.includes(block)) blocks.push(block);
			});
			const next = findLoadAfter(doc);
			const parent = partial.parentElement;
			if (parent) {
				for (const block of blocks) parent.insertBefore(document.importNode(block, true), partial);
				if (next) parent.insertBefore(document.importNode(next, true), partial);
			}
			partial.remove();
			if (!next) this.exhausted = true;
			return this.scan().length;
		}
		observe() {
			if (this.observer) return;
			this.observer = new MutationObserver(() => {
				if (this.scanTimer) return;
				this.scanTimer = setTimeout(() => {
					this.scanTimer = null;
					this.scan();
				}, 200);
			});
			const feed = document.querySelector("shreddit-feed") || document.body;
			this.observer.observe(feed, {
				childList: true,
				subtree: true
			});
		}
		disconnect() {
			this.observer?.disconnect();
			this.observer = null;
			if (this.scanTimer) clearTimeout(this.scanTimer);
			this.scanTimer = null;
		}
		reset() {
			this.disconnect();
			this.posts.length = 0;
			this.byId.clear();
			this.exhausted = false;
		}
	};
	var TRACKING_PARAMS = [
		"utm_source",
		"utm_medium",
		"utm_name",
		"utm_term",
		"utm_content",
		"mweb_loid",
		"ext-referrer"
	];
	var CSS$1 = `
#xpromo-small-header, #open-app-header-cta,
a[name="app-link"][href*="onelink.me"] { display: none !important; }
`;
	function webUrlFor(href) {
		let url;
		try {
			url = new URL(href, location.href);
		} catch {
			return null;
		}
		if (url.hostname !== "applink.reddit.com") return null;
		url.protocol = "https:";
		url.hostname = "www.reddit.com";
		for (const p of TRACKING_PARAMS) url.searchParams.delete(p);
		return url.href;
	}
	function fixAnchor(a) {
		const href = a.getAttribute("href");
		if (!href?.includes("applink.")) return;
		const web = webUrlFor(href);
		if (web) a.setAttribute("href", web);
	}
	function fixTree(root) {
		root.querySelectorAll("a[href*=\"applink.reddit.com\"]").forEach(fixAnchor);
	}
	var started = false;
	function startDeclutter() {
		if (started) return;
		started = true;
		const style = document.createElement("style");
		style.textContent = CSS$1;
		(document.head || document.documentElement).appendChild(style);
		const onActivate = (e) => {
			for (const node of e.composedPath()) if (node instanceof Element && node.tagName === "A") {
				fixAnchor(node);
				break;
			}
		};
		window.addEventListener("click", onActivate, true);
		window.addEventListener("auxclick", onActivate, true);
		window.addEventListener("contextmenu", onActivate, true);
		fixTree(document);
		let pending = false;
		new MutationObserver((records) => {
			if (pending) return;
			if (!records.some((r) => r.addedNodes.length)) return;
			pending = true;
			requestAnimationFrame(() => {
				pending = false;
				fixTree(document);
			});
		}).observe(document.documentElement, {
			childList: true,
			subtree: true
		});
	}
	function voteButton(el, dir) {
		const label = dir === "up" ? "upvote" : "downvote";
		const roots = [];
		if (el.shadowRoot) roots.push(el.shadowRoot);
		roots.push(el);
		for (const root of roots) {
			const btn = root.querySelector(`button[${label}]`) || Array.from(root.querySelectorAll("button")).find((b) => (b.getAttribute("aria-label") || "").trim().toLowerCase() === label);
			if (btn) return btn;
		}
		return null;
	}
	function readVote(post) {
		const el = post.el;
		if (!el) return 0;
		const isOn = (b) => !!b && (b.getAttribute("aria-pressed") === "true" || b.hasAttribute("data-active"));
		if (isOn(voteButton(el, "up"))) return 1;
		if (isOn(voteButton(el, "down"))) return -1;
		const attr = el.getAttribute("vote-type") || el.getAttribute("user-vote") || "";
		if (/up/i.test(attr)) return 1;
		if (/down/i.test(attr)) return -1;
		return 0;
	}
	function vote(post, dir) {
		const el = post.el;
		if (!el?.isConnected) return false;
		const btn = voteButton(el, dir);
		if (!btn) return false;
		btn.click();
		return true;
	}
	function hasGm() {
		return typeof GM_xmlhttpRequest === "function";
	}
	function request(url, opts) {
		if (hasGm()) return new Promise((resolve, reject) => {
			GM_xmlhttpRequest({
				method: "GET",
				url,
				headers: opts.headers,
				responseType: opts.as,
				anonymous: true,
				timeout: 6e4,
				onload: (r) => resolve({
					status: r.status,
					json: async () => typeof r.response === "object" && r.response ? r.response : JSON.parse(r.responseText || "null"),
					blob: async () => r.response
				}),
				onerror: () => reject(new Error("network")),
				ontimeout: () => reject(new Error("timeout"))
			});
		});
		return fetch(url, {
			headers: opts.headers,
			credentials: "omit",
			referrerPolicy: "no-referrer",
			mode: "cors"
		}).then((r) => ({
			status: r.status,
			json: () => r.json(),
			blob: () => r.blob()
		}));
	}
	var API = "https://api.redgifs.com/v2";
	var TOKEN_KEY = "@reddit-reels/redgifs-token";
	var BLOB_CACHE_SIZE = 4;
	var token = null;
	var tokenRequest = null;
	var infoCache = new Map();
	var blobCache = new Map();
	function readStoredToken() {
		if (token) return;
		try {
			const raw = sessionStorage.getItem(TOKEN_KEY);
			if (raw) {
				const t = JSON.parse(raw);
				if (t && typeof t.value === "string" && t.exp > Date.now()) token = t;
			}
		} catch {}
	}
	function tokenExpiry(jwt) {
		try {
			const payload = JSON.parse(atob(jwt.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
			if (typeof payload.exp === "number") return payload.exp * 1e3 - 6e4;
		} catch {}
		return Date.now() + 36e5;
	}
	async function getToken(force = false) {
		if (!force) {
			readStoredToken();
			if (token && token.exp > Date.now()) return token.value;
		}
		if (tokenRequest) return tokenRequest;
		tokenRequest = (async () => {
			const res = await request(`${API}/auth/temporary`, { as: "json" });
			if (res.status !== 200) throw new Error(`redgifs token ${res.status}`);
			const value = (await res.json())?.token;
			if (typeof value !== "string") throw new Error("redgifs token missing");
			token = {
				value,
				exp: tokenExpiry(value)
			};
			try {
				sessionStorage.setItem(TOKEN_KEY, JSON.stringify(token));
			} catch {}
			return value;
		})().finally(() => {
			tokenRequest = null;
		});
		return tokenRequest;
	}
	async function requestGif(id, attempt = 0) {
		const auth = await getToken(attempt > 0);
		const res = await request(`${API}/gifs/${encodeURIComponent(id)}`, {
			as: "json",
			headers: { Authorization: `Bearer ${auth}` }
		});
		if ((res.status === 401 || res.status === 403) && attempt < 2) return requestGif(id, attempt + 1);
		if (res.status !== 200) throw new Error(`redgifs gif ${res.status}`);
		const gif = (await res.json())?.gif;
		if (!gif?.urls) throw new Error("redgifs gif missing");
		return {
			hd: gif.urls.hd || gif.urls.sd || "",
			sd: gif.urls.sd || gif.urls.hd || "",
			poster: gif.urls.poster || gif.urls.thumbnail || "",
			hasAudio: !!gif.hasAudio,
			width: gif.width || 0,
			height: gif.height || 0
		};
	}
	function getRedgifs(id) {
		let p = infoCache.get(id);
		if (!p) {
			p = requestGif(id);
			p.catch(() => infoCache.delete(id));
			infoCache.set(id, p);
		}
		return p;
	}
	function redgifsBlobUrl(url) {
		const cached = blobCache.get(url);
		if (cached) {
			blobCache.delete(url);
			blobCache.set(url, cached);
			return cached;
		}
		const p = (async () => {
			const res = await request(url, { as: "blob" });
			if (res.status !== 200) throw new Error(`redgifs media ${res.status}`);
			const blob = await res.blob();
			const typed = blob.type ? blob : new Blob([blob], { type: "video/mp4" });
			return URL.createObjectURL(typed);
		})();
		p.catch(() => blobCache.delete(url));
		blobCache.set(url, p);
		while (blobCache.size > BLOB_CACHE_SIZE) {
			const [oldUrl, old] = blobCache.entries().next().value;
			blobCache.delete(oldUrl);
			old.then((b) => URL.revokeObjectURL(b)).catch(() => {});
		}
		return p;
	}
	function pickRedgifsUrl(info, mobile) {
		return mobile ? info.sd || info.hd : info.hd || info.sd;
	}
	var MUTED_KEY = "@reddit-reels/muted";
	function isMobile() {
		try {
			return matchMedia("(pointer: coarse)").matches || window.innerWidth < 760;
		} catch {
			return false;
		}
	}
	function pickMp4(source, mobile) {
		if (!source.mp4.length) return "";
		if (!mobile) return source.mp4[0];
		const limit = 1280;
		return source.mp4.find((u) => {
			const m = u.match(/res_(\d+)p/);
			return m ? parseInt(m[1], 10) <= limit : false;
		}) || source.mp4[0];
	}
	function readMuted() {
		try {
			if (typeof GM_getValue === "function") {
				const v = GM_getValue(MUTED_KEY, null);
				if (v !== null) return v === "1";
			}
		} catch {}
		try {
			return localStorage.getItem(MUTED_KEY) === "1";
		} catch {
			return false;
		}
	}
	function writeMuted(muted) {
		try {
			if (typeof GM_setValue === "function") GM_setValue(MUTED_KEY, muted ? "1" : "0");
		} catch {}
		try {
			localStorage.setItem(MUTED_KEY, muted ? "1" : "0");
		} catch {}
	}
	var Player = class {
		video;
		preloader;
		hls = null;
		generation = 0;
		captionsUrl = "";
		captionsBlob = "";
		_muted = readMuted();
		_captions = false;
		listeners = new Set();
		current = null;
		constructor() {
			this.video = document.createElement("video");
			this.video.className = "rr-video";
			this.video.playsInline = true;
			this.video.setAttribute("playsinline", "");
			this.video.setAttribute("webkit-playsinline", "");
			this.video.loop = true;
			this.video.preload = "auto";
			this.video.disableRemotePlayback = true;
			this.video.addEventListener("waiting", () => this.emit("loading"));
			this.video.addEventListener("playing", () => this.emit("ready"));
			this.video.addEventListener("error", () => {
				if (this.current && !this.hls && this.video.getAttribute("src")) this.nextSource(this.generation);
			});
			this.video.addEventListener("volumechange", () => {
				if (this.video.muted !== this._muted && !this.autoplayMuted) {
					this._muted = this.video.muted;
					writeMuted(this._muted);
					this.emit("muted");
				}
			});
			this.preloader = document.createElement("video");
			this.preloader.muted = true;
			this.preloader.preload = "auto";
			this.preloader.playsInline = true;
		}
		autoplayMuted = false;
		get muted() {
			return this._muted;
		}
		get captions() {
			return this._captions;
		}
		on(cb) {
			this.listeners.add(cb);
			return () => this.listeners.delete(cb);
		}
		emit(e) {
			this.listeners.forEach((cb) => {
				try {
					cb(e);
				} catch {}
			});
		}
		resetSource() {
			if (this.hls) {
				this.hls.destroy();
				this.hls = null;
			}
			for (const t of this.video.querySelectorAll("track")) t.remove();
			if (this.captionsBlob) {
				URL.revokeObjectURL(this.captionsBlob);
				this.captionsBlob = "";
			}
			this.captionsUrl = "";
			this.video.removeAttribute("src");
			this.video.removeAttribute("poster");
			try {
				this.video.load();
			} catch {}
		}
		sourcesFor(v) {
			const list = [];
			const mp4 = pickMp4(v, isMobile());
			if (mp4) list.push({
				type: "mp4",
				url: mp4
			});
			if (v.hls) list.push({
				type: "hls",
				url: v.hls
			});
			return list;
		}
		attachHls(url, gen) {
			if (hls_js.default.isSupported()) {
				const hls = new hls_js.default({
					capLevelToPlayerSize: true,
					maxBufferLength: 15,
					startLevel: -1
				});
				this.hls = hls;
				hls.on(hls_js.default.Events.ERROR, (_e, data) => {
					if (!data.fatal || gen !== this.generation) return;
					if (data.type === hls_js.default.ErrorTypes.MEDIA_ERROR) {
						hls.recoverMediaError();
						return;
					}
					this.nextSource(gen);
				});
				hls.loadSource(url);
				hls.attachMedia(this.video);
			} else if (this.video.canPlayType("application/vnd.apple.mpegurl")) this.video.src = url;
			else this.nextSource(gen);
		}
		queue = [];
		nextSource(gen) {
			if (gen !== this.generation) return;
			const next = this.queue.shift();
			if (!next) {
				this.emit("error");
				return;
			}
			if (this.hls) {
				this.hls.destroy();
				this.hls = null;
			}
			if (next.type === "mp4") this.video.src = next.url;
			else this.attachHls(next.url, gen);
			this.play();
		}
		async load(post) {
			const gen = ++this.generation;
			this.current = post;
			this.autoplayMuted = false;
			this.resetSource();
			this.emit("loading");
			try {
				if (post.kind === "video" && post.video) {
					const v = post.video;
					if (v.poster) this.video.poster = v.poster;
					if (v.captions) this.captionsUrl = v.captions;
					this.queue = this.sourcesFor(v);
					this.nextSource(gen);
					if (this._captions) this.applyCaptions(gen);
				} else if (post.kind === "redgifs" && post.redgifsId) {
					const info = await getRedgifs(post.redgifsId);
					if (gen !== this.generation) return;
					if (info.poster) this.video.poster = info.poster;
					const blob = await redgifsBlobUrl(pickRedgifsUrl(info, isMobile()));
					if (gen !== this.generation) return;
					this.queue = [];
					this.video.src = blob;
					this.play();
				}
			} catch {
				if (gen === this.generation) this.emit("error");
			}
		}
		preload(post) {
			if (!post) return;
			if (post.kind === "video" && post.video) {
				const mp4 = pickMp4(post.video, isMobile());
				if (mp4 && this.preloader.src !== mp4) {
					this.preloader.src = mp4;
					try {
						this.preloader.load();
					} catch {}
				}
			} else if (post.kind === "redgifs" && post.redgifsId) getRedgifs(post.redgifsId).then((info) => redgifsBlobUrl(pickRedgifsUrl(info, isMobile()))).catch(() => {});
		}
		play() {
			const v = this.video;
			v.muted = this._muted;
			v.play()?.catch?.((err) => {
				if (err?.name !== "NotAllowedError") return;
				if (!v.muted) {
					this.autoplayMuted = true;
					v.muted = true;
					this.emit("autoplay-muted");
					v.play().catch(() => this.emit("blocked"));
				} else this.emit("blocked");
			});
		}
		pause() {
			this.video.pause();
		}
		toggle() {
			if (this.autoplayMuted && !this._muted) {
				this.autoplayMuted = false;
				this.video.muted = false;
				if (this.video.paused) this.play();
				this.emit("muted");
				return true;
			}
			if (this.video.paused) {
				this.play();
				return true;
			}
			this.video.pause();
			return false;
		}
		setMuted(muted) {
			this._muted = muted;
			this.autoplayMuted = false;
			writeMuted(muted);
			this.video.muted = muted;
			if (!muted && this.video.paused && this.video.src) this.play();
			this.emit("muted");
		}
		toggleCaptions() {
			this._captions = !this._captions;
			if (this._captions) this.applyCaptions(this.generation);
			else for (const t of this.video.querySelectorAll("track")) t.remove();
			return this._captions;
		}
		async applyCaptions(gen) {
			if (!this.captionsUrl || this.video.querySelector("track")) return;
			try {
				if (!this.captionsBlob) {
					const res = await fetch(this.captionsUrl, { credentials: "omit" });
					if (!res.ok) return;
					const blob = new Blob([await res.text()], { type: "text/vtt" });
					if (gen !== this.generation) return;
					this.captionsBlob = URL.createObjectURL(blob);
				}
				const track = document.createElement("track");
				track.kind = "subtitles";
				track.srclang = "en";
				track.default = true;
				track.src = this.captionsBlob;
				this.video.appendChild(track);
				track.track.mode = "showing";
			} catch {}
		}
		dispose() {
			this.stop();
			this.preloader.removeAttribute("src");
			try {
				this.preloader.load();
			} catch {}
		}
		stop() {
			this.generation++;
			this.current = null;
			this.queue = [];
			this.video.pause();
			this.resetSource();
			this.video.remove();
		}
	};
	var svg = (body, fill = "none") => `<svg viewBox="0 0 24 24" width="26" height="26" fill="${fill}" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
	var ICONS = {
		close: svg("<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>"),
		up: (on) => svg("<path d=\"M12 4l7 8h-4v8H9v-8H5z\"/>", on ? "currentColor" : "none"),
		down: (on) => svg("<path d=\"M12 20l-7-8h4V4h6v8h4z\"/>", on ? "currentColor" : "none"),
		comments: svg("<path d=\"M21 12a8 8 0 0 1-11.6 7.1L4 20l1.1-4.6A8 8 0 1 1 21 12z\"/>"),
		soundOn: svg("<polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\"/><path d=\"M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07\"/>"),
		soundOff: svg("<polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\"/><line x1=\"23\" y1=\"9\" x2=\"17\" y2=\"15\"/><line x1=\"17\" y1=\"9\" x2=\"23\" y2=\"15\"/>"),
		fit: svg("<polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/>"),
		cc: svg("<rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"3\"/><path d=\"M10 10.5a2 2 0 1 0 0 3M17 10.5a2 2 0 1 0 0 3\"/>"),
		external: svg("<path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/><polyline points=\"15 3 21 3 21 9\"/><line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/>"),
		play: svg("<polygon points=\"7 4 20 12 7 20 7 4\"/>", "currentColor"),
		pause: svg("<rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/>", "currentColor"),
		heart: svg("<path d=\"M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z\"/>", "currentColor"),
		reel: svg("<rect x=\"5\" y=\"2\" width=\"14\" height=\"20\" rx=\"3\"/><polygon points=\"10 9 15 12 10 15 10 9\" fill=\"currentColor\"/>")
	};
	var reel_default = "/* Reel overlay. Lives in a shadow root: Reddit's CSS can't reach in, ours can't leak out. */\n:host {\n  all: initial;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n.reel {\n  position: fixed;\n  inset: 0;\n  z-index: 2147483646;\n  background: #000;\n  color: #fff;\n  font:\n    15px / 1.35 -apple-system,\n    BlinkMacSystemFont,\n    \"Segoe UI\",\n    Roboto,\n    Helvetica,\n    Arial,\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -webkit-tap-highlight-color: transparent;\n  user-select: none;\n  -webkit-user-select: none;\n}\n\nbutton {\n  font: inherit;\n  color: inherit;\n  background: none;\n  border: 0;\n  padding: 0;\n  margin: 0;\n  cursor: pointer;\n  -webkit-tap-highlight-color: transparent;\n}\n\nbutton:focus-visible,\na:focus-visible {\n  outline: 2px solid #ff4500;\n  outline-offset: 2px;\n}\n\n/* ---------- Vertical track ---------- */\n.track {\n  position: absolute;\n  inset: 0;\n  overflow-y: auto;\n  overflow-x: hidden;\n  scroll-snap-type: y mandatory;\n  overscroll-behavior: contain;\n  scrollbar-width: none;\n}\n\n.track::-webkit-scrollbar {\n  display: none;\n}\n\n.slide {\n  position: relative;\n  height: 100%;\n  width: 100%;\n  overflow: hidden;\n  scroll-snap-align: start;\n  scroll-snap-stop: always;\n  contain: strict;\n  background: #000;\n}\n\n.slide.end {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: #9aa0a6;\n  font-size: 14px;\n}\n\n/* ---------- Media ---------- */\n.backdrop {\n  position: absolute;\n  inset: -40px;\n  width: calc(100% + 80px);\n  height: calc(100% + 80px);\n  object-fit: cover;\n  filter: blur(28px) brightness(0.45);\n  transform: translateZ(0);\n  pointer-events: none;\n}\n\n.media {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.media > video,\n.media > img.main {\n  width: 100%;\n  height: 100%;\n  object-fit: contain;\n  background: transparent;\n  display: block;\n}\n\n.reel.fill .media > video,\n.reel.fill .media > img.main,\n.slide.portrait .media > video {\n  object-fit: cover;\n}\n\n.media > iframe {\n  width: 100%;\n  height: min(100%, 56.25vw);\n  border: 0;\n  background: #000;\n}\n\n.slide.vertical-embed .media > iframe {\n  height: 100%;\n}\n\n.shade {\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  height: 45%;\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));\n  pointer-events: none;\n}\n\n.shade.top {\n  top: 0;\n  bottom: auto;\n  height: 120px;\n  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));\n}\n\n/* Gallery: native-feeling horizontal swipe */\n.gallery {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  overflow-x: auto;\n  overflow-y: hidden;\n  scroll-snap-type: x mandatory;\n  overscroll-behavior-x: contain;\n  scrollbar-width: none;\n}\n\n.gallery::-webkit-scrollbar {\n  display: none;\n}\n\n.gallery > div {\n  flex: 0 0 100%;\n  height: 100%;\n  scroll-snap-align: center;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.gallery img {\n  width: 100%;\n  height: 100%;\n  object-fit: contain;\n}\n\n.count {\n  position: absolute;\n  top: calc(64px + env(safe-area-inset-top, 0px));\n  right: 14px;\n  padding: 4px 10px;\n  border-radius: 999px;\n  background: rgba(0, 0, 0, 0.55);\n  font-size: 12px;\n  font-weight: 700;\n  pointer-events: none;\n}\n\n/* ---------- Text / link posts: full-height story ----------\n   No inner scrolling (it would swallow the swipe); long bodies fade out and\n   \"Read more\" opens the reader sheet, which lives outside the track. */\n.slide.kind-text,\n.slide.kind-link {\n  background:\n    radial-gradient(120% 70% at 0% 0%, hsl(var(--hue, 16) 60% 24% / 0.9) 0%, transparent 62%),\n    radial-gradient(90% 60% at 100% 100%, hsl(calc(var(--hue, 16) + 40) 50% 18% / 0.8) 0%, transparent 60%), #08090a;\n}\n\n.card {\n  position: absolute;\n  top: calc(64px + env(safe-area-inset-top, 0px));\n  bottom: calc(64px + env(safe-area-inset-bottom, 0px));\n  left: 20px;\n  /* The action rail floats on the right. */\n  right: 72px;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n}\n\n/* A title-only post sits mid-screen, clear of the rail: center it on the screen. */\n.card.solo {\n  right: 20px;\n}\n\n.card-inner {\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n  max-height: 100%;\n  width: 100%;\n  max-width: 620px;\n  margin: 0 auto;\n}\n\n.card-inner.solo {\n  text-align: center;\n}\n\n.card h2 {\n  margin: 0 0 14px;\n  font-weight: 800;\n  letter-spacing: -0.01em;\n  line-height: 1.22;\n  word-break: break-word;\n  text-wrap: balance;\n  flex: 0 0 auto;\n  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35);\n}\n\n.card h2.t-xl {\n  font-size: 30px;\n}\n\n.card h2.t-l {\n  font-size: 24px;\n}\n\n.card h2.t-m {\n  font-size: 20px;\n  display: -webkit-box;\n  -webkit-line-clamp: 6;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n\n.card .body {\n  flex: 0 1 auto;\n  min-height: 0;\n  overflow: hidden;\n  font-size: 16px;\n  line-height: 1.55;\n  color: #e4e6e9;\n}\n\n.card.overflowing .body {\n  -webkit-mask-image: linear-gradient(to bottom, #000 72%, transparent);\n  mask-image: linear-gradient(to bottom, #000 72%, transparent);\n}\n\n.card .more {\n  flex: 0 0 auto;\n  align-self: flex-start;\n  margin-top: 12px;\n  padding: 9px 16px;\n  border-radius: 999px;\n  background: rgba(255, 255, 255, 0.14);\n  border: 1px solid rgba(255, 255, 255, 0.18);\n  font-size: 14px;\n  font-weight: 700;\n  /* Space is always reserved; showing it must not move anything. */\n  visibility: hidden;\n}\n\n.card.overflowing .more {\n  visibility: visible;\n}\n\n.card-inner.solo .more {\n  display: none;\n}\n\n/* Link posts */\n.card .thumb {\n  width: 100%;\n  /* Fixed box: an image finishing its load must not change layout inside the snap track. */\n  aspect-ratio: 16 / 9;\n  max-height: 34vh;\n  border-radius: 14px;\n  overflow: hidden;\n  margin-bottom: 16px;\n  background: rgba(255, 255, 255, 0.06);\n  flex: 0 0 auto;\n}\n\n.card .thumb img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  display: block;\n}\n\n.card .domain {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  color: #b9bec6;\n  font-size: 13px;\n  font-weight: 600;\n  margin-bottom: 10px;\n}\n\n.card .domain svg {\n  width: 14px;\n  height: 14px;\n}\n\n.card a.cta {\n  align-self: flex-start;\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  margin-top: 4px;\n  padding: 11px 18px;\n  border-radius: 999px;\n  background: #ff4500;\n  color: #fff;\n  font-weight: 700;\n  text-decoration: none;\n}\n\n.card a.cta svg {\n  width: 18px;\n  height: 18px;\n}\n\n/* Rendered markdown (story preview and reader) */\n.md p {\n  margin: 0 0 0.75em;\n}\n\n.md > :last-child {\n  margin-bottom: 0;\n}\n\n.md h3,\n.md h4,\n.md h5 {\n  margin: 0.9em 0 0.4em;\n  font-size: 1.1em;\n}\n\n.md ul,\n.md ol {\n  margin: 0 0 0.75em;\n  padding-left: 1.3em;\n}\n\n.md li {\n  margin: 0.2em 0;\n}\n\n.md blockquote {\n  margin: 0 0 0.75em;\n  padding-left: 12px;\n  border-left: 3px solid rgba(255, 255, 255, 0.3);\n  color: #b9bec6;\n}\n\n.md code {\n  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n  font-size: 0.9em;\n  padding: 1px 5px;\n  border-radius: 5px;\n  background: rgba(255, 255, 255, 0.1);\n}\n\n.md pre {\n  margin: 0 0 0.75em;\n  padding: 10px 12px;\n  border-radius: 10px;\n  background: rgba(255, 255, 255, 0.07);\n  overflow-x: auto;\n  white-space: pre;\n}\n\n.md pre code {\n  padding: 0;\n  background: none;\n}\n\n.md hr {\n  border: 0;\n  border-top: 1px solid rgba(255, 255, 255, 0.2);\n  margin: 1em 0;\n}\n\n.md a {\n  color: #ff8b60;\n  text-decoration: underline;\n  text-underline-offset: 2px;\n  word-break: break-word;\n}\n\n.md .spoiler {\n  border-radius: 4px;\n  background: #4a4d55;\n  color: transparent;\n  cursor: pointer;\n}\n\n.md .spoiler.shown {\n  background: rgba(255, 255, 255, 0.1);\n  color: inherit;\n}\n\n/* ---------- Reader sheet (outside the track) ---------- */\n.reader {\n  position: absolute;\n  inset: 0;\n  z-index: 8;\n  display: flex;\n  flex-direction: column;\n  background: radial-gradient(120% 50% at 0% 0%, hsl(var(--hue, 16) 55% 18% / 0.9) 0%, transparent 60%), #0b0c0e;\n  transform: translateY(100%);\n  visibility: hidden;\n  transition:\n    transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1),\n    visibility 0s linear 0.26s;\n  user-select: text;\n  -webkit-user-select: text;\n}\n\n.reader.open {\n  transform: none;\n  visibility: visible;\n  transition: transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1);\n}\n\n.reader-bar {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: calc(8px + env(safe-area-inset-top, 0px)) 8px 8px 16px;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n  font-size: 13px;\n  color: #b9bec6;\n}\n\n.reader-bar .who {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.reader-bar .who b {\n  color: #fff;\n}\n\n.reader-scroll {\n  flex: 1;\n  overflow-y: auto;\n  overscroll-behavior: contain;\n  -webkit-overflow-scrolling: touch;\n  padding: 18px 20px 28px;\n}\n\n.reader-scroll > * {\n  max-width: 680px;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.reader h2 {\n  margin: 0 auto 16px;\n  font-size: 24px;\n  line-height: 1.25;\n  font-weight: 800;\n  word-break: break-word;\n}\n\n.reader .md {\n  font-size: 17px;\n  line-height: 1.6;\n  color: #e4e6e9;\n}\n\n.reader-actions {\n  display: flex;\n  gap: 10px;\n  padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n  border-top: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.reader-actions button {\n  flex: 1;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  height: 44px;\n  border-radius: 999px;\n  background: rgba(255, 255, 255, 0.1);\n  font-weight: 700;\n}\n\n.reader-actions button.primary {\n  background: #ff4500;\n}\n\n.reader-actions svg {\n  width: 20px;\n  height: 20px;\n}\n\n/* ---------- Chrome ---------- */\n.top {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  padding: calc(10px + env(safe-area-inset-top, 0px)) 12px 0;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  z-index: 5;\n  pointer-events: none;\n}\n\n.top button {\n  pointer-events: auto;\n}\n\n.icon-btn {\n  width: 44px;\n  height: 44px;\n  border-radius: 999px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  background: rgba(0, 0, 0, 0.35);\n  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));\n}\n\n.icon-btn svg {\n  width: 24px;\n  height: 24px;\n}\n\n.feed-name {\n  font-weight: 700;\n  font-size: 15px;\n  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n  max-width: 50vw;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.info {\n  position: absolute;\n  left: 14px;\n  right: 78px;\n  bottom: calc(22px + env(safe-area-inset-bottom, 0px));\n  z-index: 3;\n  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);\n}\n\n.meta {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n  font-size: 13px;\n  margin-bottom: 6px;\n  flex-wrap: wrap;\n}\n\n.meta a {\n  color: #fff;\n  text-decoration: none;\n  font-weight: 700;\n}\n\n.meta .author {\n  color: #d7dadc;\n  font-weight: 500;\n}\n\n.nsfw {\n  padding: 1px 6px;\n  border-radius: 4px;\n  background: #d93a00;\n  font-size: 11px;\n  font-weight: 800;\n}\n\n.title {\n  margin: 0;\n  font-size: 15px;\n  font-weight: 600;\n  display: -webkit-box;\n  -webkit-line-clamp: 3;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  word-break: break-word;\n}\n\n.title.open {\n  -webkit-line-clamp: unset;\n  max-height: 40vh;\n  overflow-y: auto;\n}\n\n.rail {\n  position: absolute;\n  right: 8px;\n  bottom: calc(22px + env(safe-area-inset-bottom, 0px));\n  z-index: 4;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 14px;\n}\n\n.rail .item {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 2px;\n  font-size: 12px;\n  font-weight: 700;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n}\n\n.rail button {\n  width: 48px;\n  height: 48px;\n  border-radius: 999px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.7));\n}\n\n.rail button:active {\n  transform: scale(0.9);\n}\n\n.rail .up.on {\n  color: #ff4500;\n}\n\n.rail .down.on {\n  color: #7193ff;\n}\n\n.rail .cc.on {\n  color: #ff4500;\n}\n\n.slide.has-video .info,\n.slide.has-video .rail {\n  bottom: calc(34px + env(safe-area-inset-bottom, 0px));\n}\n\n/* ---------- HUD: fixed layer above the track ----------\n   Everything that changes while you watch lives here, never inside a slide:\n   layout changes inside a scroll-snap track during a swipe make the browser\n   snap back to the old slide. */\n.hud {\n  pointer-events: none;\n  z-index: 4;\n}\n\n.reel:not(.has-video) .seek,\n.reel:not(.has-video) .time {\n  display: none;\n}\n\n.seek {\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: env(safe-area-inset-bottom, 0px);\n  height: 24px;\n  display: flex;\n  align-items: flex-end;\n  touch-action: none;\n  cursor: pointer;\n  pointer-events: auto;\n}\n\n.seek .rail-line {\n  position: relative;\n  width: 100%;\n  height: 3px;\n  background: rgba(255, 255, 255, 0.25);\n  overflow: hidden;\n}\n\n.seek.dragging .rail-line,\n.seek:hover .rail-line {\n  height: 6px;\n}\n\n.seek .fill,\n.seek .buffer {\n  position: absolute;\n  inset: 0;\n  transform-origin: left center;\n  transform: scaleX(0);\n  will-change: transform;\n}\n\n.seek .fill {\n  background: #fff;\n}\n\n.seek .buffer {\n  background: rgba(255, 255, 255, 0.35);\n}\n\n.time {\n  position: absolute;\n  bottom: calc(30px + env(safe-area-inset-bottom, 0px));\n  left: 50%;\n  transform: translateX(-50%);\n  padding: 4px 10px;\n  border-radius: 999px;\n  background: rgba(0, 0, 0, 0.6);\n  font-size: 13px;\n  font-weight: 700;\n  font-variant-numeric: tabular-nums;\n  display: none;\n}\n\n.seek.dragging + .time {\n  display: block;\n}\n\n.spinner {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 44px;\n  height: 44px;\n  margin: -22px 0 0 -22px;\n  border-radius: 50%;\n  border: 3px solid rgba(255, 255, 255, 0.25);\n  border-top-color: #fff;\n  animation: spin 0.8s linear infinite;\n  display: none;\n}\n\n.reel.loading .spinner {\n  display: block;\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.tap-play {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 76px;\n  height: 76px;\n  margin: -38px 0 0 -38px;\n  border-radius: 50%;\n  background: rgba(0, 0, 0, 0.5);\n  display: none;\n  align-items: center;\n  justify-content: center;\n}\n\n.tap-play svg {\n  width: 34px;\n  height: 34px;\n  margin-left: 4px;\n}\n\n.reel.blocked .tap-play {\n  display: flex;\n}\n\n.pulse {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 84px;\n  height: 84px;\n  margin: -42px 0 0 -42px;\n  border-radius: 50%;\n  background: rgba(0, 0, 0, 0.45);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  animation: pulse 0.6s ease-out forwards;\n}\n\n.pulse.heart {\n  background: none;\n  color: #ff4500;\n}\n\n.pulse svg {\n  width: 40px;\n  height: 40px;\n}\n\n.pulse.heart svg {\n  width: 90px;\n  height: 90px;\n  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));\n}\n\n@keyframes pulse {\n  0% {\n    opacity: 0;\n    transform: scale(0.6);\n  }\n  30% {\n    opacity: 1;\n    transform: scale(1.05);\n  }\n  100% {\n    opacity: 0;\n    transform: scale(1.2);\n  }\n}\n\n.unmute-hint {\n  position: absolute;\n  top: calc(64px + env(safe-area-inset-top, 0px));\n  left: 50%;\n  transform: translateX(-50%);\n  padding: 8px 14px;\n  border-radius: 999px;\n  background: rgba(0, 0, 0, 0.7);\n  font-size: 13px;\n  font-weight: 700;\n  display: none;\n  align-items: center;\n  gap: 6px;\n}\n\n.unmute-hint svg {\n  width: 18px;\n  height: 18px;\n}\n\n.reel.autoplay-muted.has-video:not(.gif) .unmute-hint {\n  display: inline-flex;\n}\n\n.error {\n  position: absolute;\n  left: 16px;\n  right: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  text-align: center;\n  color: #d7dadc;\n  font-size: 14px;\n  display: none;\n  /* Only the link takes taps; around it, a tap still reaches the slide. */\n  pointer-events: none;\n}\n\n.reel.errored .error {\n  display: block;\n}\n\n.error a {\n  color: #ff4500;\n  pointer-events: auto;\n}\n\n.toast {\n  position: absolute;\n  left: 50%;\n  bottom: calc(110px + env(safe-area-inset-bottom, 0px));\n  transform: translateX(-50%);\n  padding: 10px 16px;\n  border-radius: 12px;\n  background: rgba(30, 31, 36, 0.95);\n  font-size: 14px;\n  z-index: 9;\n  max-width: 86vw;\n  text-align: center;\n  pointer-events: none;\n  opacity: 0;\n  transition: opacity 0.2s ease;\n}\n\n.toast.show {\n  opacity: 1;\n}\n\n/* ---------- Wider screens: keep the reel a phone-shaped column ---------- */\n@media (min-width: 900px) and (pointer: fine) {\n  .slide-inner,\n  .hud {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 50%;\n    width: min(100vw, calc(100vh * 9 / 16));\n    /* biome-ignore lint/suspicious/noDuplicateProperties: vh fallback for browsers without dvh */\n    width: min(100vw, calc(100dvh * 9 / 16));\n    transform: translateX(-50%);\n  }\n\n  .rail {\n    right: -68px;\n  }\n\n  .info {\n    right: 14px;\n  }\n\n  .card {\n    left: 24px;\n    right: 24px;\n  }\n}\n\n@media (max-width: 899px), (pointer: coarse) {\n  .slide-inner,\n  .hud {\n    position: absolute;\n    inset: 0;\n  }\n}\n\n/* Text / link cards already show the title */\n.slide.kind-text .info .title,\n.slide.kind-link .info .title {\n  display: none;\n}\n\n/* ---------- Landscape phone, video slide: immersive ----------\n   Chrome fades out (opacity only: nothing may change layout inside the track);\n   a tap brings it back for a few seconds. */\n.top,\n.info,\n.rail,\n.shade,\n.seek {\n  transition: opacity 0.25s ease;\n}\n\n.reel.immersive:not(.chrome-on) .top,\n.reel.immersive:not(.chrome-on) .info,\n.reel.immersive:not(.chrome-on) .rail,\n.reel.immersive:not(.chrome-on) .shade,\n.reel.immersive:not(.chrome-on) .seek,\n.reel.immersive:not(.chrome-on) .unmute-hint {\n  opacity: 0;\n  pointer-events: none;\n}\n\n.reel.immersive:not(.chrome-on) .top button,\n.reel.immersive:not(.chrome-on) .rail button,\n.reel.immersive:not(.chrome-on) .info a {\n  pointer-events: none;\n}\n\n/* A tall video on a wide screen: show all of it unless \"fill\" was chosen. */\n.reel.immersive:not(.fill) .slide.portrait .media > video {\n  object-fit: contain;\n}\n";
	var REDDIT = "https://www.reddit.com";
	function postUrl(post) {
		if (post.permalink.startsWith("http")) return post.permalink;
		return `${typeof location !== "undefined" ? location.origin : REDDIT}${post.permalink}`;
	}
	function isVideoKind(post) {
		return post.kind === "video" || post.kind === "redgifs";
	}
	function posterOf(post) {
		if (post.kind === "video") return post.video?.poster || "";
		if (post.kind === "gallery" || post.kind === "image") return post.images?.[0] || "";
		return "";
	}
	function buildSlide(post, index) {
		const root = document.createElement("section");
		root.className = `slide kind-${post.kind}${isVideoKind(post) ? " has-video" : ""}`;
		root.dataset.index = String(index);
		root.dataset.id = post.id;
		if (post.video && post.video.height / Math.max(1, post.video.width) >= 1.5) root.classList.add("portrait");
		if (post.kind === "text" || post.kind === "link") root.style.setProperty("--hue", String(hueOf(post.subreddit)));
		const sub = escapeHtml(post.subreddit);
		const subHref = post.subreddit ? `${REDDIT}/${encodeURI(post.subreddit)}/` : "";
		const author = escapeHtml(post.author);
		root.innerHTML = `
    <div class="slide-inner">
      <div class="media"></div>
      <div class="shade top"></div>
      <div class="shade"></div>
      <div class="info">
        <div class="meta">
          ${sub ? `<a class="sub" href="${escapeHtml(subHref)}">${sub}</a>` : ""}
          ${author ? `<span class="author">u/${author}</span>` : ""}
          ${post.nsfw ? "<span class=\"nsfw\">NSFW</span>" : ""}
        </div>
        <p class="title">${escapeHtml(post.title)}</p>
      </div>
      <div class="rail">
        <div class="item">
          <button type="button" class="up" data-action="up" aria-label="Upvote" aria-pressed="false">${ICONS.up(false)}</button>
          <span class="score">${formatCount(post.score)}</span>
          <button type="button" class="down" data-action="down" aria-label="Downvote" aria-pressed="false">${ICONS.down(false)}</button>
        </div>
        <div class="item">
          <button type="button" data-action="comments" aria-label="Open comments">${ICONS.comments}</button>
          <span>${formatCount(post.comments)}</span>
        </div>
        ${post.kind === "video" && post.video?.captions ? `<div class="item"><button type="button" class="cc" data-action="captions" aria-label="Captions" aria-pressed="false">${ICONS.cc}</button></div>` : ""}
        ${isVideoKind(post) || post.kind === "image" || post.kind === "gallery" ? `<div class="item"><button type="button" data-action="fit" aria-label="Fit or fill screen">${ICONS.fit}</button></div>` : ""}
      </div>
    </div>
  `;
		return {
			root,
			inner: root.querySelector(".slide-inner"),
			media: root.querySelector(".media"),
			backdrop: null,
			mounted: false
		};
	}
	function img(src, cls = "main") {
		const el = document.createElement("img");
		el.className = cls;
		el.decoding = "async";
		el.referrerPolicy = "no-referrer";
		el.alt = "";
		el.draggable = false;
		el.src = src;
		return el;
	}
	function mountSlide(refs, post, active) {
		if (refs.mounted) {
			if (post.kind === "embed") syncEmbed(refs, post, active);
			return;
		}
		refs.mounted = true;
		const poster = posterOf(post);
		if (poster && post.kind !== "gallery") {
			refs.backdrop = img(poster, "backdrop");
			refs.root.insertBefore(refs.backdrop, refs.inner);
		}
		if (post.kind === "image" && post.images?.[0]) refs.media.appendChild(img(post.images[0]));
		else if (post.kind === "gallery" && post.images?.length) {
			const strip = document.createElement("div");
			strip.className = "gallery";
			for (const src of post.images) {
				const cell = document.createElement("div");
				const im = img(src, "");
				im.loading = "lazy";
				cell.appendChild(im);
				strip.appendChild(cell);
			}
			const count = document.createElement("div");
			count.className = "count";
			count.textContent = `1/${post.images.length}`;
			strip.addEventListener("scroll", () => {
				const i = Math.round(strip.scrollLeft / Math.max(1, strip.clientWidth));
				count.textContent = `${i + 1}/${post.images.length}`;
			}, { passive: true });
			refs.media.append(strip, count);
		} else if (post.kind === "video" && post.video?.poster) refs.media.appendChild(img(post.video.poster, "main poster"));
		else if (post.kind === "text" || post.kind === "link") {
			const card = buildCard(post);
			refs.media.appendChild(card);
			if (post.kind === "text") markOverflow(card);
		} else if (post.kind === "embed") syncEmbed(refs, post, active);
	}
	function syncEmbed(refs, post, active) {
		const existing = refs.media.querySelector("iframe");
		if (active && !existing && post.embedUrl && isSafeUrl(post.embedUrl)) {
			const frame = document.createElement("iframe");
			frame.src = post.embedUrl;
			frame.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
			frame.setAttribute("allowfullscreen", "");
			frame.referrerPolicy = "strict-origin-when-cross-origin";
			refs.media.appendChild(frame);
			if (/shorts\/|streamable/i.test(post.linkUrl || post.embedUrl)) refs.root.classList.add("vertical-embed");
		} else if (!active && existing) existing.remove();
	}
	function hueOf(name) {
		let h = 0;
		for (let i = 0; i < name.length; i++) h = h * 31 + name.charCodeAt(i) >>> 0;
		return h % 360;
	}
	function titleSize(title, hasBody) {
		const n = title.length;
		if (!hasBody && n <= 90) return "xl";
		if (n <= 70) return "l";
		return "m";
	}
	function buildCard(post) {
		const card = document.createElement("div");
		card.className = "card";
		const inner = document.createElement("div");
		inner.className = "card-inner";
		if (post.kind === "link") {
			const href = post.linkUrl && isSafeUrl(post.linkUrl) ? post.linkUrl : "";
			inner.innerHTML = `
      ${post.thumbnail ? "<div class=\"thumb\"><img alt=\"\"></div>" : ""}
      <div class="domain">${ICONS.external}<span>${escapeHtml(extractDomain(href))}</span></div>
      <h2 class="t-${titleSize(post.title, true)}">${escapeHtml(post.title)}</h2>
      ${href ? `<a class="cta" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">Open link ${ICONS.external}</a>` : ""}
    `;
			const thumb = inner.querySelector(".thumb img");
			if (thumb && post.thumbnail) {
				thumb.referrerPolicy = "no-referrer";
				thumb.src = post.thumbnail;
			}
		} else {
			const hasBody = !!(post.html || post.text);
			inner.innerHTML = `
      <h2 class="t-${titleSize(post.title, hasBody)}">${escapeHtml(post.title)}</h2>
      ${hasBody ? "<div class=\"body md\"></div>" : ""}
      <button type="button" class="more" data-action="read" tabindex="-1">Read more</button>
    `;
			if (!hasBody) {
				inner.classList.add("solo");
				card.classList.add("solo");
			}
			const body = inner.querySelector(".body");
			if (body) {
				if (post.html) body.innerHTML = post.html;
				else body.textContent = post.text || "";
			}
		}
		card.appendChild(inner);
		return card;
	}
	function markOverflow(card) {
		requestAnimationFrame(() => {
			const body = card.querySelector(".body");
			const cut = !!body && body.scrollHeight > body.clientHeight + 4;
			card.classList.toggle("overflowing", cut);
		});
	}
	function unmountSlide(refs) {
		if (!refs.mounted) return;
		refs.mounted = false;
		refs.backdrop?.remove();
		refs.backdrop = null;
		Array.from(refs.media.children).forEach((c) => {
			if (!(c instanceof HTMLVideoElement)) c.remove();
		});
		refs.root.classList.remove("vertical-embed");
	}
	function setVoteUi(refs, state, score) {
		const up = refs.root.querySelector("button.up");
		const down = refs.root.querySelector("button.down");
		if (up) {
			up.classList.toggle("on", state === 1);
			up.setAttribute("aria-pressed", String(state === 1));
			up.innerHTML = ICONS.up(state === 1);
		}
		if (down) {
			down.classList.toggle("on", state === -1);
			down.setAttribute("aria-pressed", String(state === -1));
			down.innerHTML = ICONS.down(state === -1);
		}
		const label = refs.root.querySelector(".score");
		if (label) label.textContent = formatCount(score);
	}
	var MOUNT_RADIUS = 1;
	var LOAD_AHEAD = 5;
	var TAP_MS = 260;
	var SETTLE_MS = 120;
	var CHROME_MS = 3e3;
	var LANDSCAPE_MQ = "(orientation: landscape) and (pointer: coarse) and (max-height: 600px)";
	var Reel = class {
		opts;
		host;
		shadow;
		el;
		track;
		hud;
		seek;
		seekFill;
		seekBuffer;
		timeEl;
		errorEl;
		toastEl;
		soundBtn;
		endEl;
		slides = [];
		slideIds = new Set();
		active = -1;
		player = new Player();
		cleanup = [];
		tapTimer = null;
		lastTap = 0;
		settleTimer = null;
		toastTimer = null;
		chromeTimer = null;
		landscape = typeof matchMedia === "function" ? matchMedia(LANDSCAPE_MQ) : null;
		votes = new Map();
		readerEl;
		readerPost = null;
		constructor(opts) {
			this.opts = opts;
			this.host = document.createElement("div");
			this.host.id = "rr-reel-host";
			this.shadow = this.host.attachShadow({ mode: "open" });
		}
		get readerInHistory() {
			return !!this.readerPost && !!history.state?.rrReader;
		}
		get activePost() {
			return this.opts.source.posts[this.active] || null;
		}
		open(startIndex) {
			const style = document.createElement("style");
			style.textContent = reel_default;
			this.el = document.createElement("div");
			this.el.className = "reel";
			this.el.setAttribute("role", "dialog");
			this.el.setAttribute("aria-label", "Reddit reels");
			this.el.innerHTML = `
      <div class="track" tabindex="-1"><section class="slide end"></section></div>
      <div class="hud">
        <div class="spinner"></div>
        <div class="tap-play" aria-hidden="true">${ICONS.play}</div>
        <div class="unmute-hint">${ICONS.soundOff}<span>Tap for sound</span></div>
        <div class="error"></div>
        <div class="seek" role="slider" aria-label="Seek"><div class="rail-line"><div class="buffer"></div><div class="fill"></div></div></div>
        <div class="time"></div>
      </div>
      <div class="top">
        <button type="button" class="icon-btn" data-action="close" aria-label="Close reels">${ICONS.close}</button>
        <span class="feed-name"></span>
        <button type="button" class="icon-btn" data-action="sound" aria-label="Mute"></button>
      </div>
      <div class="reader" role="dialog" aria-label="Post" aria-hidden="true">
        <div class="reader-bar">
          <span class="who"></span>
          <button type="button" class="icon-btn" data-action="reader-close" aria-label="Close post">${ICONS.close}</button>
        </div>
        <div class="reader-scroll"><h2></h2><div class="md"></div></div>
        <div class="reader-actions">
          <button type="button" data-action="reader-comments" class="primary">${ICONS.comments}<span>Comments</span></button>
        </div>
      </div>
      <div class="toast" role="status" aria-live="polite"></div>
    `;
			this.shadow.append(style, this.el);
			const q = (sel) => this.el.querySelector(sel);
			this.track = q(".track");
			this.endEl = q(".slide.end");
			this.hud = q(".hud");
			this.seek = q(".seek");
			this.seekFill = q(".seek .fill");
			this.seekBuffer = q(".seek .buffer");
			this.timeEl = q(".time");
			this.errorEl = q(".error");
			this.toastEl = q(".toast");
			this.soundBtn = q("[data-action=\"sound\"]");
			this.readerEl = q(".reader");
			q(".feed-name").textContent = this.opts.feedName;
			document.documentElement.appendChild(this.host);
			this.appendSlides(this.opts.source.posts);
			this.cleanup.push(this.opts.source.onAdded((added) => this.appendSlides(added)));
			this.wireEvents();
			this.syncSound();
			const start = Math.max(0, Math.min(startIndex, this.slides.length - 1));
			this.track.scrollTop = start * this.track.clientHeight;
			this.activate(start);
			this.track.focus({ preventScroll: true });
		}
		close() {
			this.player.dispose();
			for (const fn of this.cleanup) fn();
			this.cleanup = [];
			if (this.settleTimer) clearTimeout(this.settleTimer);
			if (this.tapTimer) clearTimeout(this.tapTimer);
			if (this.toastTimer) clearTimeout(this.toastTimer);
			if (this.chromeTimer) clearTimeout(this.chromeTimer);
			this.host.remove();
		}
		appendSlides(posts) {
			const wasOnEnd = this.active >= 0 && this.currentIndex() >= this.slides.length;
			const firstNew = this.slides.length;
			for (const post of posts) {
				if (this.slideIds.has(post.id)) continue;
				this.slideIds.add(post.id);
				const refs = buildSlide(post, this.slides.length);
				this.slides.push(refs);
				this.track.insertBefore(refs.root, this.endEl);
				this.votes.set(post.id, readVote(post));
				setVoteUi(refs, this.votes.get(post.id) || 0, post.score);
			}
			this.syncEnd();
			if (wasOnEnd && this.slides.length > firstNew) {
				this.track.scrollTop = firstNew * this.track.clientHeight;
				this.activate(firstNew);
			} else if (this.active >= 0) this.mountAround(this.active);
		}
		syncEnd() {
			this.endEl.textContent = this.opts.source.hasMore ? "Loading more…" : "You're all caught up";
		}
		currentIndex() {
			return Math.round(this.track.scrollTop / Math.max(1, this.track.clientHeight));
		}
		onScroll() {
			if (this.settleTimer) clearTimeout(this.settleTimer);
			this.settleTimer = setTimeout(() => this.settle(), SETTLE_MS);
		}
		settle() {
			if (this.settleTimer) clearTimeout(this.settleTimer);
			this.settleTimer = null;
			const i = this.currentIndex();
			if (i >= this.slides.length) {
				this.loadMore();
				return;
			}
			if (i !== this.active) this.activate(i);
		}
		mountAround(center) {
			this.slides.forEach((refs, i) => {
				const post = this.opts.source.posts[i];
				if (!post) return;
				if (Math.abs(i - center) <= MOUNT_RADIUS) mountSlide(refs, post, i === center);
				else unmountSlide(refs);
			});
		}
		activate(i) {
			const posts = this.opts.source.posts;
			const post = posts[i];
			if (!post) return;
			this.slides[this.active]?.root.classList.remove("active");
			this.active = i;
			const refs = this.slides[i];
			refs.root.classList.add("active");
			this.setState({
				loading: false,
				blocked: false,
				error: ""
			});
			this.mountAround(i);
			this.track.querySelectorAll("iframe.rg-fallback").forEach((f) => {
				if (!refs.root.contains(f)) f.remove();
			});
			const video = isVideoKind(post);
			this.el.classList.toggle("has-video", video);
			this.el.classList.toggle("gif", !!post.video?.gif);
			this.updateSeek(true);
			if (post.kind === "video" && post.el?.isConnected) {
				const fresh = extractPost(post.el);
				if (fresh?.video) post.video = fresh.video;
				this.el.classList.toggle("gif", !!post.video?.gif);
			}
			if (video) {
				refs.media.querySelector("img.poster")?.remove();
				refs.media.prepend(this.player.video);
				this.setState({ loading: true });
				this.player.load(post);
			} else this.player.stop();
			this.syncImmersive();
			this.player.preload(posts[i + 1]);
			if (i >= posts.length - LOAD_AHEAD) this.loadMore();
		}
		loadMore() {
			if (!this.opts.source.hasMore) {
				this.syncEnd();
				return;
			}
			this.opts.source.loadMore().then(() => this.syncEnd());
		}
		go(delta) {
			const next = Math.max(0, Math.min(this.slides.length - 1, this.currentIndex() + delta));
			this.track.scrollTo({
				top: next * this.track.clientHeight,
				behavior: "smooth"
			});
		}
		setState(s) {
			if (s.loading !== void 0) this.el.classList.toggle("loading", s.loading);
			if (s.blocked !== void 0) this.el.classList.toggle("blocked", s.blocked);
			if (s.error !== void 0) {
				this.errorEl.innerHTML = s.error;
				this.el.classList.toggle("errored", !!s.error);
			}
		}
		wireEvents() {
			this.el.addEventListener("click", (e) => this.onClick(e));
			this.track.addEventListener("scroll", () => this.onScroll(), { passive: true });
			this.track.addEventListener("scrollend", () => this.settle());
			const onPop = () => {
				if (this.readerPost && !history.state?.rrReader) this.closeReader(true);
			};
			window.addEventListener("popstate", onPop);
			this.cleanup.push(() => window.removeEventListener("popstate", onPop));
			const onKey = (e) => this.onKey(e);
			window.addEventListener("keydown", onKey, true);
			this.cleanup.push(() => window.removeEventListener("keydown", onKey, true));
			this.cleanup.push(this.player.on((ev) => {
				if (ev === "ready") this.setState({
					loading: false,
					blocked: false
				});
				if (ev === "loading" && this.player.video.readyState < 3) this.setState({ loading: true });
				if (ev === "blocked") this.setState({
					loading: false,
					blocked: true
				});
				if (ev === "error") this.onPlayerError();
				if (ev === "muted" || ev === "autoplay-muted") this.syncSound();
			}));
			const v = this.player.video;
			v.addEventListener("timeupdate", () => this.updateSeek());
			v.addEventListener("progress", () => this.updateSeek());
			v.addEventListener("loadedmetadata", () => {
				const refs = this.slides[this.active];
				if (refs && v.videoWidth && v.videoHeight) refs.root.classList.toggle("portrait", v.videoHeight / v.videoWidth >= 1.5);
			});
			this.wireSeek();
			const onOrientation = () => this.syncImmersive();
			this.landscape?.addEventListener("change", onOrientation);
			this.cleanup.push(() => this.landscape?.removeEventListener("change", onOrientation));
			let resizeFrame = 0;
			const onResize = () => {
				cancelAnimationFrame(resizeFrame);
				resizeFrame = requestAnimationFrame(() => {
					if (this.active >= 0) this.track.scrollTop = this.active * this.track.clientHeight;
				});
			};
			window.addEventListener("resize", onResize);
			this.cleanup.push(() => {
				cancelAnimationFrame(resizeFrame);
				window.removeEventListener("resize", onResize);
			});
		}
		onClick(e) {
			const target = e.composedPath()[0];
			const btn = target.closest?.("[data-action]");
			if (btn) {
				if (this.el.classList.contains("immersive")) this.showChrome();
				e.preventDefault();
				this.runAction(btn.dataset.action || "", btn);
				return;
			}
			const spoiler = target.closest?.(".spoiler");
			if (spoiler) {
				spoiler.classList.add("shown");
				return;
			}
			if (target.closest?.(".reader")) return;
			const card = target.closest?.(".card");
			if (card && !target.closest("a")) {
				if (card.classList.contains("overflowing") && this.activePost) this.openReader(this.activePost);
				return;
			}
			if (target.closest?.("a, .seek, .gallery .count, .top")) return;
			if (target.closest?.(".title")) {
				target.closest(".title")?.classList.toggle("open");
				return;
			}
			if (!target.closest?.(".slide")) return;
			const now = Date.now();
			if (now - this.lastTap < TAP_MS) {
				this.lastTap = 0;
				if (this.tapTimer) clearTimeout(this.tapTimer);
				this.tapTimer = null;
				this.doubleTap();
				return;
			}
			this.lastTap = now;
			this.tapTimer = setTimeout(() => {
				this.tapTimer = null;
				this.singleTap();
			}, TAP_MS);
		}
		singleTap() {
			const post = this.activePost;
			if (!post || !isVideoKind(post)) return;
			if (this.el.classList.contains("immersive")) {
				const hidden = !this.el.classList.contains("chrome-on");
				this.showChrome();
				if (hidden) return;
			}
			if (this.el.classList.contains("errored")) return;
			this.setState({ blocked: false });
			const playing = this.player.toggle();
			this.pulse(playing ? ICONS.play : ICONS.pause);
		}
		doubleTap() {
			const post = this.activePost;
			if (!post) return;
			if (this.votes.get(post.id) !== 1) this.doVote(post, "up");
			this.pulse(ICONS.heart, "heart");
		}
		runAction(action, btn) {
			const post = this.activePost;
			switch (action) {
				case "close":
					this.opts.onClose(post);
					break;
				case "sound":
					this.player.setMuted(this.player.autoplayMuted ? false : !this.player.muted);
					this.syncSound();
					break;
				case "up":
				case "down":
					if (post) this.doVote(post, action);
					break;
				case "comments":
					if (post) this.opts.onOpenComments(post);
					break;
				case "read":
					if (post) this.openReader(post);
					break;
				case "reader-close":
					this.closeReader(false);
					break;
				case "reader-comments":
					if (this.readerPost) this.opts.onOpenComments(this.readerPost);
					break;
				case "captions": {
					const on = this.player.toggleCaptions();
					btn.classList.toggle("on", on);
					btn.setAttribute("aria-pressed", String(on));
					break;
				}
				case "fit":
					this.el.classList.toggle("fill");
					this.toast(this.el.classList.contains("fill") ? "Fill screen" : "Fit to screen");
			}
		}
		onKey(e) {
			const t = e.composedPath()[0];
			if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
			const k = e.key;
			if (this.readerPost) {
				if (k === "Escape") {
					e.preventDefault();
					e.stopPropagation();
					this.closeReader(false);
				}
				return;
			}
			let handled = true;
			if (k === "ArrowDown" || k === "j" || k === "J" || k === "PageDown") this.go(1);
			else if (k === "ArrowUp" || k === "k" || k === "K" || k === "PageUp") this.go(-1);
			else if (k === " " || k === "Spacebar") this.singleTap();
			else if (k === "m" || k === "M") this.runAction("sound", this.soundBtn);
			else if (k === "f" || k === "F") this.runAction("fit", this.soundBtn);
			else if (k === "c" || k === "C") {
				const cc = this.slides[this.active]?.root.querySelector("[data-action=\"captions\"]");
				if (cc) this.runAction("captions", cc);
			} else if (k === "Escape") this.opts.onClose(this.activePost);
			else if (k === "ArrowRight" || k === "ArrowLeft") {
				const strip = this.slides[this.active]?.media.querySelector(".gallery");
				if (strip) strip.scrollBy({
					left: (k === "ArrowRight" ? 1 : -1) * strip.clientWidth,
					behavior: "smooth"
				});
				else if (this.activePost && isVideoKind(this.activePost)) this.player.video.currentTime += k === "ArrowRight" ? 5 : -5;
			} else handled = false;
			if (handled) {
				e.preventDefault();
				e.stopPropagation();
			}
		}
		syncImmersive() {
			const post = this.activePost;
			const on = !!this.landscape?.matches && !!post && isVideoKind(post);
			this.el.classList.toggle("immersive", on);
			if (!on) this.hideChrome();
		}
		showChrome() {
			this.el.classList.add("chrome-on");
			if (this.chromeTimer) clearTimeout(this.chromeTimer);
			this.chromeTimer = setTimeout(() => {
				this.chromeTimer = null;
				if (this.player.video.paused || this.seek.classList.contains("dragging")) this.showChrome();
				else this.el.classList.remove("chrome-on");
			}, CHROME_MS);
		}
		hideChrome() {
			if (this.chromeTimer) clearTimeout(this.chromeTimer);
			this.chromeTimer = null;
			this.el.classList.remove("chrome-on");
		}
		openReader(post) {
			if (post.kind !== "text") return;
			this.readerPost = post;
			const r = this.readerEl;
			r.style.setProperty("--hue", this.slides[this.active]?.root.style.getPropertyValue("--hue") || "16");
			const who = r.querySelector(".who");
			who.innerHTML = `<b>${escapeHtml(post.subreddit)}</b>${post.author ? ` · u/${escapeHtml(post.author)}` : ""}`;
			r.querySelector(".reader-scroll h2").textContent = post.title;
			const md = r.querySelector(".reader-scroll .md");
			if (post.html) md.innerHTML = post.html;
			else md.textContent = post.text || "";
			r.querySelector(".reader-scroll").scrollTop = 0;
			r.classList.add("open");
			r.setAttribute("aria-hidden", "false");
			if (!history.state?.rrReader) history.pushState({
				...history.state || {},
				rrReader: true
			}, "");
			r.querySelector("[data-action=\"reader-close\"]").focus({ preventScroll: true });
		}
		closeReader(fromHistory) {
			if (!this.readerPost) return;
			this.readerPost = null;
			this.readerEl.classList.remove("open");
			this.readerEl.setAttribute("aria-hidden", "true");
			if (!fromHistory && history.state?.rrReader) history.back();
			this.track.focus({ preventScroll: true });
		}
		doVote(post, dir) {
			if (!post.el?.isConnected) {
				this.toast("Open the post on Reddit to vote");
				return;
			}
			if (document.querySelector("#login-button")) {
				this.toast("Log in to Reddit to vote");
				return;
			}
			const before = this.votes.get(post.id) || 0;
			if (!vote(post, dir)) {
				this.toast("Voting is not available for this post");
				return;
			}
			const wanted = dir === "up" ? 1 : -1;
			const after = before === wanted ? 0 : wanted;
			this.votes.set(post.id, after);
			const refs = this.slides[this.opts.source.posts.indexOf(post)];
			if (refs) setVoteUi(refs, after, post.score + after - before);
			setTimeout(() => {
				const real = readVote(post);
				if (real !== after && post.el?.isConnected && refs) {
					this.votes.set(post.id, real);
					setVoteUi(refs, real, post.score + real - before);
				}
			}, 900);
		}
		wireSeek() {
			const seek = this.seek;
			const v = this.player.video;
			const seekTo = (clientX) => {
				const rect = seek.getBoundingClientRect();
				const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / Math.max(1, rect.width)));
				if (Number.isFinite(v.duration)) {
					v.currentTime = ratio * v.duration;
					this.timeEl.textContent = `${formatTime(v.currentTime)} / ${formatTime(v.duration)}`;
				}
				this.updateSeek();
			};
			seek.addEventListener("pointerdown", (e) => {
				e.stopPropagation();
				seek.setPointerCapture(e.pointerId);
				seek.classList.add("dragging");
				seekTo(e.clientX);
			});
			seek.addEventListener("pointermove", (e) => {
				if (seek.classList.contains("dragging")) seekTo(e.clientX);
			});
			const end = (e) => {
				seek.classList.remove("dragging");
				try {
					seek.releasePointerCapture(e.pointerId);
				} catch {}
			};
			seek.addEventListener("pointerup", end);
			seek.addEventListener("pointercancel", end);
		}
		updateSeek(reset = false) {
			const v = this.player.video;
			if (reset || !Number.isFinite(v.duration) || v.duration <= 0) {
				this.seekFill.style.transform = "scaleX(0)";
				this.seekBuffer.style.transform = "scaleX(0)";
				return;
			}
			this.seekFill.style.transform = `scaleX(${v.currentTime / v.duration})`;
			try {
				if (v.buffered.length) this.seekBuffer.style.transform = `scaleX(${v.buffered.end(v.buffered.length - 1) / v.duration})`;
			} catch {}
		}
		syncSound() {
			const muted = this.player.muted || this.player.autoplayMuted;
			this.soundBtn.innerHTML = muted ? ICONS.soundOff : ICONS.soundOn;
			this.soundBtn.setAttribute("aria-label", muted ? "Unmute" : "Mute");
			this.el.classList.toggle("autoplay-muted", this.player.autoplayMuted && !this.player.muted);
		}
		pulse(icon, cls = "") {
			const p = document.createElement("div");
			p.className = `pulse ${cls}`;
			p.innerHTML = icon;
			this.hud.appendChild(p);
			setTimeout(() => p.remove(), 650);
		}
		toast(message) {
			this.toastEl.textContent = message;
			this.toastEl.classList.add("show");
			if (this.toastTimer) clearTimeout(this.toastTimer);
			this.toastTimer = setTimeout(() => this.toastEl.classList.remove("show"), 1800);
		}
		onPlayerError() {
			const post = this.activePost;
			const refs = this.slides[this.active];
			this.setState({ loading: false });
			if (!post || !refs) return;
			if (post.kind === "redgifs" && post.redgifsId) {
				this.redgifsFallback(refs, post.redgifsId);
				return;
			}
			this.setState({ error: `Couldn't play this one. <a href="${escapeHtml(postUrl(post))}" target="_blank" rel="noopener">Open on Reddit</a>` });
			if (isMobile()) this.toast("Swipe for the next one");
		}
		redgifsFallback(refs, id) {
			this.player.stop();
			this.el.classList.remove("has-video");
			if (refs.media.querySelector("iframe.rg-fallback")) return;
			const frame = document.createElement("iframe");
			frame.className = "rg-fallback";
			frame.src = `https://www.redgifs.com/ifr/${encodeURIComponent(id)}`;
			frame.allow = "autoplay; fullscreen";
			refs.root.classList.add("vertical-embed");
			refs.media.appendChild(frame);
		}
	};
	var CSS = `
:host { all: initial; }
button {
  position: fixed;
  right: calc(16px + env(safe-area-inset-right, 0px));
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  top: auto;
  left: auto;
  z-index: 2147483000;
  width: 56px;
  height: 56px;
  border-radius: 999px;
  border: 0;
  background: #ff4500;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease;
}
button:active { transform: scale(0.92); }
button:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
button svg { width: 28px; height: 28px; }
@media (pointer: fine) and (min-width: 900px) {
  button { bottom: 28px; right: 28px; }
}
`;
	function createFab(onClick) {
		const host = document.createElement("div");
		host.id = "rr-fab-host";
		const shadow = host.attachShadow({ mode: "open" });
		const style = document.createElement("style");
		style.textContent = CSS;
		const btn = document.createElement("button");
		btn.type = "button";
		btn.setAttribute("aria-label", "Open reels");
		btn.title = "Reels";
		btn.innerHTML = ICONS.reel;
		btn.addEventListener("click", (e) => {
			e.preventDefault();
			e.stopPropagation();
			onClick();
		});
		shadow.append(style, btn);
		return host;
	}
	var PAGE_CSS = `
html.rr-open { overflow: hidden !important; }
html.rr-open body { display: none !important; }
`;
	var source = new FeedSource();
	var reel = null;
	var fab = null;
	var sourcePath = location.pathname;
	var savedRestoration = "auto";
	function feedName(path) {
		const p = path.replace(/\/+$/, "");
		const sub = p.match(/^\/r\/([^/]+)/);
		if (sub) return `r/${sub[1]}`;
		const user = p.match(/^\/(?:user|u)\/([^/]+)/);
		if (user) return `u/${user[1]}`;
		return "Home";
	}
	function nearestPostIndex(posts) {
		const mid = window.innerHeight / 2;
		let best = 0;
		let bestDist = Infinity;
		posts.forEach((p, i) => {
			if (!p.el?.isConnected) return;
			const r = p.el.getBoundingClientRect();
			if (r.height === 0) return;
			const d = Math.abs(r.top + r.height / 2 - mid);
			if (d < bestDist) {
				bestDist = d;
				best = i;
			}
		});
		return best;
	}
	function pausePageMedia() {
		const walk = (root) => {
			root.querySelectorAll("video").forEach((v) => {
				try {
					v.pause();
				} catch {}
			});
			root.querySelectorAll("*").forEach((el) => {
				if (el.shadowRoot) walk(el.shadowRoot);
			});
		};
		walk(document);
	}
	function openReel() {
		if (reel) return;
		if (location.pathname !== sourcePath) {
			source.reset();
			source = new FeedSource();
			sourcePath = location.pathname;
		}
		source.scan();
		source.observe();
		if (!source.posts.length) return;
		const start = nearestPostIndex(source.posts);
		pausePageMedia();
		savedRestoration = history.scrollRestoration;
		history.scrollRestoration = "manual";
		document.documentElement.classList.add("rr-open");
		if (!history.state?.rrReel) history.pushState({
			...history.state || {},
			rrReel: true
		}, "");
		reel = new Reel({
			source,
			feedName: feedName(location.pathname),
			onClose: () => closeReel(false),
			onOpenComments: openComments
		});
		reel.open(start);
	}
	function closeReel(fromHistory) {
		if (!reel) return;
		const last = reel.activePost;
		const entries = reel.readerInHistory ? 2 : 1;
		reel.close();
		reel = null;
		source.disconnect();
		document.documentElement.classList.remove("rr-open");
		if (!fromHistory && history.state?.rrReel) history.go(-entries);
		const land = () => {
			if (last?.el?.isConnected) last.el.scrollIntoView({ block: "center" });
		};
		land();
		requestAnimationFrame(land);
		setTimeout(() => {
			land();
			history.scrollRestoration = savedRestoration;
		}, 120);
	}
	function openComments(post) {
		window.open(postUrl(post), "_blank", "noopener");
	}
	function syncFab() {
		const show = isReelRoute(location.pathname);
		if (show && !fab) {
			fab = createFab(() => openReel());
			document.documentElement.appendChild(fab);
		} else if (!show && fab) {
			fab.remove();
			fab = null;
		}
	}
	function init() {
		if (window.top !== window.self) return;
		const style = document.createElement("style");
		style.textContent = PAGE_CSS;
		(document.head || document.documentElement).appendChild(style);
		startDeclutter();
		syncFab();
		watchRoute(() => {
			if (reel) closeReel(true);
			syncFab();
		});
		window.addEventListener("popstate", () => {
			if (reel && !history.state?.rrReel) closeReel(true);
		});
	}
	if (typeof window !== "undefined" && typeof document !== "undefined") {
		if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true });
		else init();
	}
})(Hls);