☰

Reddit Reels

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

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==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);