Index: src/assets/css/style.css
===================================================================
--- src/assets/css/style.css	(revision d8f8f079ce7979088f94c7dca8819cb4eb384777)
+++ src/assets/css/style.css	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -2587,4 +2587,14 @@
 .feed-reader .read-head { padding-bottom: .5rem; }
 .feed-reader .read-title { font-size: clamp(1.6rem, 4vw, 2.4rem); line-height: 1.15; margin: 0 0 .3rem; }
+/* Het pin-icoon voor de titel: meeschalend met de kop, maar duidelijk kleiner --
+   het is een markering en geen deel van de titel. */
+.feed-reader .read-pin {
+    display: inline-block;
+    vertical-align: baseline;
+    width: .62em; height: .62em;
+    margin-right: .35em;
+    color: var(--ink-muted);
+}
+.feed-reader .read-pin svg { width: 100%; height: 100%; display: block; }
 /* De titel is een link, maar mag er niet als een link uitzien: dit is een
    leesscherm, geen index. */
@@ -2635,4 +2645,14 @@
 @media (min-width: 768px) {
     .read-top { bottom: 1.5rem; right: 1.5rem; }
+}
+/* Staat de mini-speler onderin, dan wijkt de knop daarvoor -- anders komt hij er
+   bovenop te liggen (Robins schermafbeelding, 20-8). body.has-audio-player wordt
+   door audio-player.js gezet zodra de speler zichtbaar is, en weer weggehaald
+   als hij verdwijnt, dus dit volgt vanzelf.
+   Op mobiel staat de speler BOVEN de tabbalk gestapeld, dus daar komt zijn
+   hoogte bovenop de ruimte die er al gereserveerd was. */
+body.has-audio-player .read-top { bottom: calc(env(safe-area-inset-bottom, 0) + 4.75rem + 4.25rem); }
+@media (min-width: 768px) {
+    body.has-audio-player .read-top { bottom: calc(1.5rem + 4.25rem); }
 }
 @media (prefers-reduced-motion: reduce) {
Index: src/assets/js/mod/read.js
===================================================================
--- src/assets/js/mod/read.js	(revision d8f8f079ce7979088f94c7dca8819cb4eb384777)
+++ src/assets/js/mod/read.js	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -117,5 +117,86 @@
 let rustTimer = null;
 
+/**
+ * LENIS, en alleen op desktop.
+ *
+ * Op touch doet Lenis van zichzelf niets (syncTouch staat standaard uit) en is
+ * het systeem-scrollen al soepel -- daar blijft de native CSS-snap staan die
+ * hierboven beschreven is. Robins keuze (20-8): "enkel voor desktop, dat is
+ * prima, logisch dat het niet op mobiel kan".
+ *
+ * WAAROM LENIS HIER STAAT, en dat is niet het vloeiende scrollen: de DUUR van
+ * een snap is met native scroll-snap niet in te stellen -- die zit in de browser.
+ * Lenis' snap-pakket wel: duration, easing, distanceThreshold en debounce zijn
+ * allemaal van ons. Dat was de aanleiding.
+ *
+ * Het vloeiende scrollen (smoothWheel) kwam er daarna bij, en dat is de kant die
+ * OPPASSEN vraagt. Een Mac-trackpad heeft zijn EIGEN momentum, en Lenis'
+ * demping komt daar bovenop -- dubbel gedempt voelt drijverig. Vandaar lerp 0.2
+ * in plaats van de standaard 0.1. Robin vond dat op 20-8 nog steeds te zweverig,
+ * dus dat getal is nog niet uit; zie de opmerking bij de instellingen.
+ *
+ * De stand met `smoothWheel: false` werkte ook, en dan doet Lenis alleen de
+ * snap. Dat is de terugvalpositie als het vloeiende scrollen niet bevalt.
+ *
+ * lenis/snap haakt alleen in op lenis.on('scroll') en roept lenis.scrollTo aan
+ * (nagekeken in de dist), dus die opzet werkt.
+ */
+const OP_DESKTOP = window.matchMedia('(hover: hover) and (pointer: fine)');
+const VENDOR_V = 1;   // ophogen als de bestanden in /assets/js/vendor wijzigen
+
+let lenis = null;
+let snap = null;
+
+async function startLenis() {
+  if (lenis || !OP_DESKTOP.matches) return;
+  const [L, S] = await Promise.all([
+    import(`/assets/js/vendor/lenis.mjs?v=${VENDOR_V}`),
+    import(`/assets/js/vendor/lenis-snap.mjs?v=${VENDOR_V}`),
+  ]);
+  lenis = new L.default({
+    // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in
+    // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en
+    // heeft die demping nodig; een Mac-trackpad heeft zijn EIGEN momentum en
+    // krijgt er dan een tweede overheen -- dat is precies het drijverige gevoel
+    // waar Robin voor waarschuwde. Hoger betekent korter naijlen, dus dit is de
+    // middenweg: de schokjes weg, de nasleep kort.
+    // Staat het toch te zweven, dan is lerp omhoog (richting 1) of terug naar
+    // smoothWheel:false de knop -- die stand werkte ook, met alleen de snap.
+    smoothWheel: true,
+    lerp: 0.2,
+    syncTouch: false,   // op touch blijft alles van het systeem
+    autoRaf: true,
+  });
+  snap = new S.default(lenis, {
+    type: 'proximity',
+    distanceThreshold: '12%',   // de vangzone, hier WEL instelbaar
+    debounce: 60,               // niet de standaard 500: dat voelt als te laat
+    duration: 0.4,              // in totaal onder de halve seconde
+    // Vlot weg, dan steeds langzamer aankomen (Robin, 20-8). easeOutQuart: op de
+    // helft van de tijd is 94% van de weg af, en de rest dempt zacht uit.
+    // Bewust NIET Lenis' standaard easeOutExpo -- die schiet weg en kruipt dan
+    // zo lang na dat het lijkt of hij niet afmaakt.
+    easing: (t) => 1 - Math.pow(1 - t, 4),
+  });
+  // Het snappunt is de bovenkant van elk bericht. Het laatste doet niet mee:
+  // zijn bovenkant is niet te bereiken, er zit te weinig pagina onder.
+  const berichten = [...document.querySelectorAll('.feed-reader .read-post')];
+  berichten.slice(0, -1).forEach((a) => snap.addElement(a, { align: 'start' }));
+  // Native snappen uit: twee mechanismen op dezelfde scroller vechten.
+  document.documentElement.style.scrollSnapType = 'none';
+}
+
+function stopLenis() {
+  if (snap) { snap.destroy(); snap = null; }
+  if (lenis) { lenis.destroy(); lenis = null; }
+  document.documentElement.style.scrollSnapType = '';
+}
+
+/**
+ * Omhoog niet snappen. Met Lenis is dat snap.stop()/start(); zonder Lenis (dus
+ * op touch) zetten we de CSS-eigenschap om, precies zoals hiervoor.
+ */
 function zetSnappen(aan) {
+  if (snap) { if (aan) snap.start(); else snap.stop(); return; }
   const el = document.documentElement;
   const wil = aan ? '' : 'none';
@@ -197,4 +278,10 @@
   window.addEventListener('touchmove', opRaakBeweeg, { passive: true });
   window.addEventListener('keydown', opToets);
+
+  // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw
+  // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en
+  // wegnavigeren hoort hem op te ruimen.
+  stopLenis();
+  if (document.body.dataset.feedView === 'reader') startLenis();
 }
 
Index: src/assets/js/vendor/lenis-LICENSE.txt
===================================================================
--- src/assets/js/vendor/lenis-LICENSE.txt	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
+++ src/assets/js/vendor/lenis-LICENSE.txt	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -0,0 +1,9 @@
+The MIT License
+
+Copyright (c) 2024 darkroom.engineering
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Index: src/assets/js/vendor/lenis-snap.mjs
===================================================================
--- src/assets/js/vendor/lenis-snap.mjs	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
+++ src/assets/js/vendor/lenis-snap.mjs	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -0,0 +1,334 @@
+//#region packages/snap/src/debounce.ts
+function debounce(callback, delay) {
+	let timer;
+	return function(...args) {
+		clearTimeout(timer);
+		timer = setTimeout(() => {
+			timer = void 0;
+			callback.apply(this, args);
+		}, delay);
+	};
+}
+//#endregion
+//#region packages/snap/src/element.ts
+function removeParentSticky(element) {
+	if (getComputedStyle(element).position === "sticky") {
+		element.style.setProperty("position", "static");
+		element.dataset.sticky = "true";
+	}
+	if (element.offsetParent) removeParentSticky(element.offsetParent);
+}
+function addParentSticky(element) {
+	if (element?.dataset?.sticky === "true") {
+		element.style.removeProperty("position");
+		delete element.dataset.sticky;
+	}
+	if (element.offsetParent) addParentSticky(element.offsetParent);
+}
+function offsetTop(element, accumulator = 0) {
+	const top = accumulator + element.offsetTop;
+	if (element.offsetParent) return offsetTop(element.offsetParent, top);
+	return top;
+}
+function offsetLeft(element, accumulator = 0) {
+	const left = accumulator + element.offsetLeft;
+	if (element.offsetParent) return offsetLeft(element.offsetParent, left);
+	return left;
+}
+function scrollTop(element, accumulator = 0) {
+	const top = accumulator + element.scrollTop;
+	if (element.offsetParent) return scrollTop(element.offsetParent, top);
+	return top + window.scrollY;
+}
+function scrollLeft(element, accumulator = 0) {
+	const left = accumulator + element.scrollLeft;
+	if (element.offsetParent) return scrollLeft(element.offsetParent, left);
+	return left + window.scrollX;
+}
+var SnapElement = class {
+	element;
+	options;
+	align;
+	rect = {};
+	wrapperResizeObserver;
+	resizeObserver;
+	debouncedWrapperResize;
+	constructor(element, { align = ["start"], ignoreSticky = true, ignoreTransform = false } = {}) {
+		this.element = element;
+		this.options = {
+			align,
+			ignoreSticky,
+			ignoreTransform
+		};
+		this.align = [align].flat();
+		this.debouncedWrapperResize = debounce(this.onWrapperResize, 500);
+		this.wrapperResizeObserver = new ResizeObserver(this.debouncedWrapperResize);
+		this.wrapperResizeObserver.observe(document.body);
+		this.onWrapperResize();
+		this.resizeObserver = new ResizeObserver(this.onResize);
+		this.resizeObserver.observe(this.element);
+		this.setRect({
+			width: this.element.offsetWidth,
+			height: this.element.offsetHeight
+		});
+	}
+	destroy() {
+		this.wrapperResizeObserver.disconnect();
+		this.resizeObserver.disconnect();
+	}
+	setRect({ top, left, width, height, element } = {}) {
+		top = top ?? this.rect.top;
+		left = left ?? this.rect.left;
+		width = width ?? this.rect.width;
+		height = height ?? this.rect.height;
+		element = element ?? this.rect.element;
+		if (top === this.rect.top && left === this.rect.left && width === this.rect.width && height === this.rect.height && element === this.rect.element) return;
+		this.rect.top = top;
+		this.rect.y = top;
+		this.rect.width = width;
+		this.rect.height = height;
+		this.rect.left = left;
+		this.rect.x = left;
+		this.rect.bottom = top + height;
+		this.rect.right = left + width;
+	}
+	onWrapperResize = () => {
+		let top;
+		let left;
+		if (this.options.ignoreSticky) removeParentSticky(this.element);
+		if (this.options.ignoreTransform) {
+			top = offsetTop(this.element);
+			left = offsetLeft(this.element);
+		} else {
+			const rect = this.element.getBoundingClientRect();
+			top = rect.top + scrollTop(this.element);
+			left = rect.left + scrollLeft(this.element);
+		}
+		if (this.options.ignoreSticky) addParentSticky(this.element);
+		this.setRect({
+			top,
+			left
+		});
+	};
+	onResize = ([entry]) => {
+		if (!entry?.borderBoxSize[0]) return;
+		const width = entry.borderBoxSize[0].inlineSize;
+		const height = entry.borderBoxSize[0].blockSize;
+		this.setRect({
+			width,
+			height
+		});
+	};
+};
+//#endregion
+//#region packages/snap/src/uid.ts
+let index = 0;
+function uid() {
+	return index++;
+}
+//#endregion
+//#region packages/snap/src/snap.ts
+/**
+* Snap class to handle the snap functionality
+*
+* @example
+* const snap = new Snap(lenis, {
+*   type: 'mandatory', // 'mandatory', 'proximity' or 'lock'
+*   onSnapStart: (snap) => {
+*     console.log('onSnapStart', snap)
+*   },
+*   onSnapComplete: (snap) => {
+*     console.log('onSnapComplete', snap)
+*   },
+* })
+*
+* snap.add(500) // snap at 500px
+*
+* const removeSnap = snap.add(500)
+*
+* if (someCondition) {
+*   removeSnap()
+* }
+*/
+var Snap = class {
+	options;
+	elements = /* @__PURE__ */ new Map();
+	snaps = /* @__PURE__ */ new Map();
+	viewport = {
+		width: window.innerWidth,
+		height: window.innerHeight
+	};
+	isStopped = false;
+	onSnapDebounced;
+	currentSnapIndex;
+	constructor(lenis, { type = "proximity", lerp, easing, duration, distanceThreshold = "50%", debounce: debounceDelay = 500, onSnapStart, onSnapComplete } = {}) {
+		this.lenis = lenis;
+		if (!window.lenis) window.lenis = {};
+		window.lenis.snap = true;
+		this.options = {
+			type,
+			lerp,
+			easing,
+			duration,
+			distanceThreshold,
+			debounce: debounceDelay,
+			onSnapStart,
+			onSnapComplete
+		};
+		this.onWindowResize();
+		window.addEventListener("resize", this.onWindowResize);
+		this.onSnapDebounced = debounce(this.onSnap, this.options.debounce);
+		this.lenis.on("virtual-scroll", this.onSnapDebounced);
+	}
+	/**
+	* Destroy the snap instance
+	*/
+	destroy() {
+		this.lenis.off("virtual-scroll", this.onSnapDebounced);
+		window.removeEventListener("resize", this.onWindowResize);
+		this.elements.forEach((element) => {
+			element.destroy();
+		});
+	}
+	/**
+	* Start the snap after it has been stopped
+	*/
+	start() {
+		this.isStopped = false;
+	}
+	/**
+	* Stop the snap
+	*/
+	stop() {
+		this.isStopped = true;
+	}
+	/**
+	* Add a snap to the snap instance
+	*
+	* @param value The value to snap to
+	* @param userData User data that will be forwarded through the snap event
+	* @returns Unsubscribe function
+	*/
+	add(value) {
+		const id = uid();
+		this.snaps.set(id, { value });
+		return () => this.snaps.delete(id);
+	}
+	/**
+	* Add an element to the snap instance
+	*
+	* @param element The element to add
+	* @param options The options for the element
+	* @returns Unsubscribe function
+	*/
+	addElement(element, options = {}) {
+		const id = uid();
+		this.elements.set(id, new SnapElement(element, options));
+		return () => this.elements.delete(id);
+	}
+	addElements(elements, options = {}) {
+		const map = [...elements].map((element) => this.addElement(element, options));
+		return () => {
+			map.forEach((remove) => {
+				remove();
+			});
+		};
+	}
+	onWindowResize = () => {
+		this.viewport.width = window.innerWidth;
+		this.viewport.height = window.innerHeight;
+	};
+	computeSnaps = () => {
+		const { isHorizontal } = this.lenis;
+		let snaps = [...this.snaps.values()];
+		this.elements.forEach(({ rect, align }) => {
+			let value;
+			align.forEach((align) => {
+				if (align === "start") value = rect.top;
+				else if (align === "center") value = isHorizontal ? rect.left + rect.width / 2 - this.viewport.width / 2 : rect.top + rect.height / 2 - this.viewport.height / 2;
+				else if (align === "end") value = isHorizontal ? rect.left + rect.width - this.viewport.width : rect.top + rect.height - this.viewport.height;
+				if (typeof value === "number") snaps.push({ value: Math.ceil(value) });
+			});
+		});
+		snaps = snaps.sort((a, b) => Math.abs(a.value) - Math.abs(b.value));
+		return snaps;
+	};
+	previous() {
+		this.goTo((this.currentSnapIndex ?? 0) - 1);
+	}
+	next() {
+		this.goTo((this.currentSnapIndex ?? 0) + 1);
+	}
+	goTo(index) {
+		const snaps = this.computeSnaps();
+		if (snaps.length === 0) return;
+		this.currentSnapIndex = Math.max(0, Math.min(index, snaps.length - 1));
+		const currentSnap = snaps[this.currentSnapIndex];
+		if (currentSnap === void 0) return;
+		this.lenis.scrollTo(currentSnap.value, {
+			duration: this.options.duration,
+			easing: this.options.easing,
+			lerp: this.options.lerp,
+			lock: this.options.type === "lock",
+			userData: { initiator: "snap" },
+			onStart: () => {
+				this.options.onSnapStart?.({
+					index: this.currentSnapIndex,
+					...currentSnap
+				});
+			},
+			onComplete: () => {
+				this.options.onSnapComplete?.({
+					index: this.currentSnapIndex,
+					...currentSnap
+				});
+			}
+		});
+	}
+	get distanceThreshold() {
+		let distanceThreshold = Number.POSITIVE_INFINITY;
+		if (this.options.type === "mandatory") return Number.POSITIVE_INFINITY;
+		const { isHorizontal } = this.lenis;
+		const axis = isHorizontal ? "width" : "height";
+		if (typeof this.options.distanceThreshold === "string" && this.options.distanceThreshold.endsWith("%")) distanceThreshold = Number(this.options.distanceThreshold.replace("%", "")) / 100 * this.viewport[axis];
+		else if (typeof this.options.distanceThreshold === "number") distanceThreshold = this.options.distanceThreshold;
+		else distanceThreshold = this.viewport[axis];
+		return distanceThreshold;
+	}
+	onSnap = (e) => {
+		if (this.isStopped) return;
+		if (e.event.type === "touchmove") return;
+		if (this.options.type === "lock" && this.lenis.userData?.initiator === "snap") return;
+		let { scroll, isHorizontal } = this.lenis;
+		const delta = isHorizontal ? e.deltaX : e.deltaY;
+		scroll = Math.ceil(this.lenis.scroll + delta);
+		const snaps = this.computeSnaps();
+		if (snaps.length === 0) return;
+		let snapIndex;
+		const prevSnapIndex = snaps.findLastIndex(({ value }) => value < scroll);
+		const nextSnapIndex = snaps.findIndex(({ value }) => value > scroll);
+		if (this.options.type === "lock") {
+			if (delta > 0) snapIndex = nextSnapIndex;
+			else if (delta < 0) snapIndex = prevSnapIndex;
+		} else {
+			const prevSnap = snaps[prevSnapIndex];
+			const distanceToPrevSnap = prevSnap ? Math.abs(scroll - prevSnap.value) : Number.POSITIVE_INFINITY;
+			const nextSnap = snaps[nextSnapIndex];
+			snapIndex = distanceToPrevSnap < (nextSnap ? Math.abs(scroll - nextSnap.value) : Number.POSITIVE_INFINITY) ? prevSnapIndex : nextSnapIndex;
+		}
+		if (snapIndex === void 0) return;
+		if (snapIndex === -1) return;
+		snapIndex = Math.max(0, Math.min(snapIndex, snaps.length - 1));
+		const snap = snaps[snapIndex];
+		if (Math.abs(scroll - snap.value) <= this.distanceThreshold) this.goTo(snapIndex);
+	};
+	resize() {
+		this.elements.forEach((element) => {
+			element.onWrapperResize();
+		});
+	}
+};
+//#endregion
+export { Snap as default };
+
+//# sourceMappingURL=lenis-snap.mjs.map
Index: src/assets/js/vendor/lenis.mjs
===================================================================
--- src/assets/js/vendor/lenis.mjs	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
+++ src/assets/js/vendor/lenis.mjs	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -0,0 +1,1057 @@
+//#region package.json
+var version = "1.3.26";
+//#endregion
+//#region packages/core/src/maths.ts
+/**
+* Clamp a value between a minimum and maximum value
+*
+* @param min Minimum value
+* @param input Value to clamp
+* @param max Maximum value
+* @returns Clamped value
+*/
+function clamp(min, input, max) {
+	return Math.max(min, Math.min(input, max));
+}
+/**
+*  Linearly interpolate between two values using an amount (0 <= t <= 1)
+*
+* @param x First value
+* @param y Second value
+* @param t Amount to interpolate (0 <= t <= 1)
+* @returns Interpolated value
+*/
+function lerp(x, y, t) {
+	return (1 - t) * x + t * y;
+}
+/**
+* Damp a value over time using a damping factor
+* {@link http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/}
+*
+* @param x Initial value
+* @param y Target value
+* @param lambda Damping factor
+* @param dt Time elapsed since the last update
+* @returns Damped value
+*/
+function damp(x, y, lambda, deltaTime) {
+	return lerp(x, y, 1 - Math.exp(-lambda * deltaTime));
+}
+/**
+* Calculate the modulo of the dividend and divisor while keeping the result within the same sign as the divisor
+* {@link https://anguscroll.com/just/just-modulo}
+*
+* @param n Dividend
+* @param d Divisor
+* @returns Modulo
+*/
+function modulo(n, d) {
+	return (n % d + d) % d;
+}
+//#endregion
+//#region packages/core/src/animate.ts
+/**
+* Animate class to handle value animations with lerping or easing
+*
+* @example
+* const animate = new Animate()
+* animate.fromTo(0, 100, { duration: 1, easing: (t) => t })
+* animate.advance(0.5) // 50
+*/
+var Animate = class {
+	isRunning = false;
+	value = 0;
+	from = 0;
+	to = 0;
+	currentTime = 0;
+	lerp;
+	duration;
+	easing;
+	onUpdate;
+	/**
+	* Advance the animation by the given delta time
+	*
+	* @param deltaTime - The time in seconds to advance the animation
+	*/
+	advance(deltaTime) {
+		if (!this.isRunning) return;
+		let completed = false;
+		if (this.duration && this.easing) {
+			this.currentTime += deltaTime;
+			const linearProgress = clamp(0, this.currentTime / this.duration, 1);
+			completed = linearProgress >= 1;
+			const easedProgress = completed ? 1 : this.easing(linearProgress);
+			this.value = this.from + (this.to - this.from) * easedProgress;
+		} else if (this.lerp) {
+			this.value = damp(this.value, this.to, this.lerp * 60, deltaTime);
+			if (Math.round(this.value) === Math.round(this.to)) {
+				this.value = this.to;
+				completed = true;
+			}
+		} else {
+			this.value = this.to;
+			completed = true;
+		}
+		if (completed) this.stop();
+		this.onUpdate?.(this.value, completed);
+	}
+	/** Stop the animation */
+	stop() {
+		this.isRunning = false;
+	}
+	/**
+	* Set up the animation from a starting value to an ending value
+	* with optional parameters for lerping, duration, easing, and onUpdate callback
+	*
+	* @param from - The starting value
+	* @param to - The ending value
+	* @param options - Options for the animation
+	*/
+	fromTo(from, to, { lerp, duration, easing, onStart, onUpdate }) {
+		this.from = this.value = from;
+		this.to = to;
+		this.lerp = lerp;
+		this.duration = duration;
+		this.easing = easing;
+		this.currentTime = 0;
+		this.isRunning = true;
+		onStart?.();
+		this.onUpdate = onUpdate;
+	}
+};
+//#endregion
+//#region packages/core/src/debounce.ts
+function debounce(callback, delay) {
+	let timer;
+	return function(...args) {
+		clearTimeout(timer);
+		timer = setTimeout(() => {
+			timer = void 0;
+			callback.apply(this, args);
+		}, delay);
+	};
+}
+//#endregion
+//#region packages/core/src/dimensions.ts
+/**
+* Dimensions class to handle the size of the content and wrapper
+*
+* @example
+* const dimensions = new Dimensions(wrapper, content)
+* dimensions.on('resize', (e) => {
+*   console.log(e.width, e.height)
+* })
+*/
+var Dimensions = class {
+	width = 0;
+	height = 0;
+	scrollHeight = 0;
+	scrollWidth = 0;
+	debouncedResize;
+	wrapperResizeObserver;
+	contentResizeObserver;
+	constructor(wrapper, content, { autoResize = true, debounce: debounceValue = 250 } = {}) {
+		this.wrapper = wrapper;
+		this.content = content;
+		if (autoResize) {
+			this.debouncedResize = debounce(this.resize, debounceValue);
+			if (this.wrapper instanceof Window) window.addEventListener("resize", this.debouncedResize);
+			else {
+				this.wrapperResizeObserver = new ResizeObserver(this.debouncedResize);
+				this.wrapperResizeObserver.observe(this.wrapper);
+			}
+			this.contentResizeObserver = new ResizeObserver(this.debouncedResize);
+			this.contentResizeObserver.observe(this.content);
+		}
+		this.resize();
+	}
+	destroy() {
+		this.wrapperResizeObserver?.disconnect();
+		this.contentResizeObserver?.disconnect();
+		if (this.wrapper === window && this.debouncedResize) window.removeEventListener("resize", this.debouncedResize);
+	}
+	resize = () => {
+		this.onWrapperResize();
+		this.onContentResize();
+	};
+	onWrapperResize = () => {
+		if (this.wrapper instanceof Window) {
+			this.width = window.innerWidth;
+			this.height = window.innerHeight;
+		} else {
+			this.width = this.wrapper.clientWidth;
+			this.height = this.wrapper.clientHeight;
+		}
+	};
+	onContentResize = () => {
+		if (this.wrapper instanceof Window) {
+			this.scrollHeight = this.content.scrollHeight;
+			this.scrollWidth = this.content.scrollWidth;
+		} else {
+			this.scrollHeight = this.wrapper.scrollHeight;
+			this.scrollWidth = this.wrapper.scrollWidth;
+		}
+	};
+	get limit() {
+		return {
+			x: this.scrollWidth - this.width,
+			y: this.scrollHeight - this.height
+		};
+	}
+};
+//#endregion
+//#region packages/core/src/emitter.ts
+/**
+* Emitter class to handle events
+* @example
+* const emitter = new Emitter()
+* emitter.on('event', (data) => {
+*   console.log(data)
+* })
+* emitter.emit('event', 'data')
+*/
+var Emitter = class {
+	events = {};
+	/**
+	* Emit an event with the given data
+	* @param event Event name
+	* @param args Data to pass to the event handlers
+	*/
+	emit(event, ...args) {
+		const callbacks = this.events[event] || [];
+		for (let i = 0, length = callbacks.length; i < length; i++) callbacks[i]?.(...args);
+	}
+	/**
+	* Add a callback to the event
+	* @param event Event name
+	* @param cb Callback function
+	* @returns Unsubscribe function
+	*/
+	on(event, cb) {
+		if (this.events[event]) this.events[event].push(cb);
+		else this.events[event] = [cb];
+		return () => {
+			this.events[event] = this.events[event]?.filter((i) => cb !== i);
+		};
+	}
+	/**
+	* Remove a callback from the event
+	* @param event Event name
+	* @param callback Callback function
+	*/
+	off(event, callback) {
+		this.events[event] = this.events[event]?.filter((i) => callback !== i);
+	}
+	/**
+	* Remove all event listeners and clean up
+	*/
+	destroy() {
+		this.events = {};
+	}
+};
+//#endregion
+//#region packages/core/src/virtual-scroll.ts
+const LINE_HEIGHT = 100 / 6;
+const listenerOptions = { passive: false };
+function getDeltaMultiplier(deltaMode, size) {
+	if (deltaMode === 1) return LINE_HEIGHT;
+	if (deltaMode === 2) return size;
+	return 1;
+}
+var VirtualScroll = class {
+	touchStart = {
+		x: 0,
+		y: 0
+	};
+	lastDelta = {
+		x: 0,
+		y: 0
+	};
+	window = {
+		width: 0,
+		height: 0
+	};
+	emitter = new Emitter();
+	constructor(element, options = {
+		wheelMultiplier: 1,
+		touchMultiplier: 1
+	}) {
+		this.element = element;
+		this.options = options;
+		window.addEventListener("resize", this.onWindowResize);
+		this.onWindowResize();
+		this.element.addEventListener("wheel", this.onWheel, listenerOptions);
+		this.element.addEventListener("touchstart", this.onTouchStart, listenerOptions);
+		this.element.addEventListener("touchmove", this.onTouchMove, listenerOptions);
+		this.element.addEventListener("touchend", this.onTouchEnd, listenerOptions);
+	}
+	/**
+	* Add an event listener for the given event and callback
+	*
+	* @param event Event name
+	* @param callback Callback function
+	*/
+	on(event, callback) {
+		return this.emitter.on(event, callback);
+	}
+	/** Remove all event listeners and clean up */
+	destroy() {
+		this.emitter.destroy();
+		window.removeEventListener("resize", this.onWindowResize);
+		this.element.removeEventListener("wheel", this.onWheel, listenerOptions);
+		this.element.removeEventListener("touchstart", this.onTouchStart, listenerOptions);
+		this.element.removeEventListener("touchmove", this.onTouchMove, listenerOptions);
+		this.element.removeEventListener("touchend", this.onTouchEnd, listenerOptions);
+	}
+	/**
+	* Event handler for 'touchstart' event
+	*
+	* @param event Touch event
+	*/
+	onTouchStart = (event) => {
+		const { clientX, clientY } = event.targetTouches ? event.targetTouches[0] : event;
+		this.touchStart.x = clientX;
+		this.touchStart.y = clientY;
+		this.lastDelta = {
+			x: 0,
+			y: 0
+		};
+		this.emitter.emit("scroll", {
+			deltaX: 0,
+			deltaY: 0,
+			event
+		});
+	};
+	/** Event handler for 'touchmove' event */
+	onTouchMove = (event) => {
+		const { clientX, clientY } = event.targetTouches ? event.targetTouches[0] : event;
+		const deltaX = -(clientX - this.touchStart.x) * this.options.touchMultiplier;
+		const deltaY = -(clientY - this.touchStart.y) * this.options.touchMultiplier;
+		this.touchStart.x = clientX;
+		this.touchStart.y = clientY;
+		this.lastDelta = {
+			x: deltaX,
+			y: deltaY
+		};
+		this.emitter.emit("scroll", {
+			deltaX,
+			deltaY,
+			event
+		});
+	};
+	onTouchEnd = (event) => {
+		this.emitter.emit("scroll", {
+			deltaX: this.lastDelta.x,
+			deltaY: this.lastDelta.y,
+			event
+		});
+	};
+	/** Event handler for 'wheel' event */
+	onWheel = (event) => {
+		let { deltaX, deltaY, deltaMode } = event;
+		const multiplierX = getDeltaMultiplier(deltaMode, this.window.width);
+		const multiplierY = getDeltaMultiplier(deltaMode, this.window.height);
+		deltaX *= multiplierX;
+		deltaY *= multiplierY;
+		deltaX *= this.options.wheelMultiplier;
+		deltaY *= this.options.wheelMultiplier;
+		this.emitter.emit("scroll", {
+			deltaX,
+			deltaY,
+			event
+		});
+	};
+	onWindowResize = () => {
+		this.window = {
+			width: window.innerWidth,
+			height: window.innerHeight
+		};
+	};
+};
+//#endregion
+//#region packages/core/src/lenis.ts
+const defaultEasing = (t) => Math.min(1, 1.001 - 2 ** (-10 * t));
+var Lenis = class {
+	_isScrolling = false;
+	_isStopped = false;
+	_isLocked = false;
+	_preventNextNativeScrollEvent = false;
+	_resetVelocityTimeout = null;
+	_rafId = null;
+	_isDraggingSelection = false;
+	reducedMotionMediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
+	/**
+	* Whether or not the user is touching the screen
+	*/
+	isTouching;
+	/**
+	* Whether or not the device is running iOS
+	*/
+	isIos;
+	/**
+	* The time in ms since the lenis instance was created
+	*/
+	time = 0;
+	/**
+	* User data that will be forwarded through the scroll event
+	*
+	* @example
+	* lenis.scrollTo(100, {
+	*   userData: {
+	*     foo: 'bar'
+	*   }
+	* })
+	*/
+	userData = {};
+	/**
+	* The last velocity of the scroll
+	*/
+	lastVelocity = 0;
+	/**
+	* The current velocity of the scroll
+	*/
+	velocity = 0;
+	/**
+	* The direction of the scroll
+	*/
+	direction = 0;
+	/**
+	* The options passed to the lenis instance
+	*/
+	options;
+	/**
+	* The target scroll value
+	*/
+	targetScroll;
+	/**
+	* The animated scroll value
+	*/
+	animatedScroll;
+	animate = new Animate();
+	emitter = new Emitter();
+	dimensions;
+	virtualScroll;
+	constructor({ wrapper = window, content = document.documentElement, eventsTarget = wrapper, smoothWheel = true, syncTouch = false, syncTouchLerp = .075, touchInertiaExponent = 1.7, duration, easing, lerp = .1, infinite = false, orientation = "vertical", gestureOrientation = orientation === "horizontal" ? "both" : "vertical", touchMultiplier = 1, wheelMultiplier = 1, autoResize = true, prevent, virtualScroll, overscroll = true, autoRaf = false, anchors = false, autoToggle = false, allowNestedScroll = false, __experimental__naiveDimensions = false, naiveDimensions = __experimental__naiveDimensions, stopInertiaOnNavigate = false, respectReducedMotion = true } = {}) {
+		window.lenisVersion = version;
+		if (!window.lenis) window.lenis = {};
+		window.lenis.version = version;
+		if (orientation === "horizontal") window.lenis.horizontal = true;
+		if (syncTouch === true) window.lenis.touch = true;
+		this.isIos = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
+		if (!wrapper || wrapper === document.documentElement) wrapper = window;
+		if (typeof duration === "number" && typeof easing !== "function") easing = defaultEasing;
+		else if (typeof easing === "function" && typeof duration !== "number") duration = 1;
+		this.options = {
+			wrapper,
+			content,
+			eventsTarget,
+			smoothWheel,
+			syncTouch,
+			syncTouchLerp,
+			touchInertiaExponent,
+			duration,
+			easing,
+			lerp,
+			infinite,
+			gestureOrientation,
+			orientation,
+			touchMultiplier,
+			wheelMultiplier,
+			autoResize,
+			prevent,
+			virtualScroll,
+			overscroll,
+			autoRaf,
+			anchors,
+			autoToggle,
+			allowNestedScroll,
+			naiveDimensions,
+			stopInertiaOnNavigate,
+			respectReducedMotion
+		};
+		this.dimensions = new Dimensions(wrapper, content, { autoResize });
+		this.updateClassName();
+		this.targetScroll = this.animatedScroll = this.actualScroll;
+		this.options.wrapper.addEventListener("scroll", this.onNativeScroll);
+		this.options.wrapper.addEventListener("scrollend", this.onScrollEnd, { capture: true });
+		if (this.options.anchors || this.options.stopInertiaOnNavigate) this.options.wrapper.addEventListener("click", this.onClick);
+		this.options.wrapper.addEventListener("pointerdown", this.onPointerDown);
+		this.virtualScroll = new VirtualScroll(eventsTarget, {
+			touchMultiplier,
+			wheelMultiplier
+		});
+		this.virtualScroll.on("scroll", this.onVirtualScroll);
+		if (this.options.autoToggle) {
+			this.checkOverflow();
+			this.rootElement.addEventListener("transitionend", this.onTransitionEnd);
+		}
+		if (this.options.autoRaf) this._rafId = requestAnimationFrame(this.raf);
+	}
+	/**
+	* Destroy the lenis instance, remove all event listeners and clean up the class name
+	*/
+	destroy() {
+		this.emitter.destroy();
+		this.options.wrapper.removeEventListener("scroll", this.onNativeScroll);
+		this.options.wrapper.removeEventListener("scrollend", this.onScrollEnd, { capture: true });
+		this.options.wrapper.removeEventListener("pointerdown", this.onPointerDown);
+		if (this.options.anchors || this.options.stopInertiaOnNavigate) this.options.wrapper.removeEventListener("click", this.onClick);
+		this.virtualScroll.destroy();
+		this.dimensions.destroy();
+		this.cleanUpClassName();
+		if (this._rafId) cancelAnimationFrame(this._rafId);
+	}
+	on(event, callback) {
+		return this.emitter.on(event, callback);
+	}
+	off(event, callback) {
+		return this.emitter.off(event, callback);
+	}
+	onScrollEnd = (e) => {
+		if (!(e instanceof CustomEvent)) {
+			if (this.isScrolling === "smooth" || this.isScrolling === false) e.stopPropagation();
+		}
+	};
+	dispatchScrollendEvent = () => {
+		this.options.wrapper.dispatchEvent(new CustomEvent("scrollend", {
+			bubbles: this.options.wrapper === window,
+			detail: { lenisScrollEnd: true }
+		}));
+	};
+	get overflow() {
+		const property = this.isHorizontal ? "overflow-x" : "overflow-y";
+		return getComputedStyle(this.rootElement)[property];
+	}
+	checkOverflow() {
+		if (["hidden", "clip"].includes(this.overflow)) this.internalStop();
+		else this.internalStart();
+	}
+	onTransitionEnd = (event) => {
+		if (event.propertyName?.includes("overflow") && event.target === this.rootElement) this.checkOverflow();
+	};
+	setScroll(scroll) {
+		if (this.isHorizontal) this.options.wrapper.scrollTo({
+			left: scroll,
+			behavior: "instant"
+		});
+		else this.options.wrapper.scrollTo({
+			top: scroll,
+			behavior: "instant"
+		});
+	}
+	onClick = (event) => {
+		const linkElementsUrls = event.composedPath().filter((node) => node instanceof HTMLAnchorElement && node.href).map((element) => new URL(element.href));
+		const currentUrl = new URL(window.location.href);
+		if (this.options.anchors) {
+			const anchorElementUrl = linkElementsUrls.find((targetUrl) => currentUrl.host === targetUrl.host && currentUrl.pathname === targetUrl.pathname && targetUrl.hash);
+			if (anchorElementUrl) {
+				const options = typeof this.options.anchors === "object" && this.options.anchors ? this.options.anchors : void 0;
+				const target = decodeURIComponent(anchorElementUrl.hash);
+				this.scrollTo(target, options);
+				return;
+			}
+		}
+		if (this.options.stopInertiaOnNavigate) {
+			if (linkElementsUrls.some((targetUrl) => currentUrl.host === targetUrl.host && currentUrl.pathname !== targetUrl.pathname)) {
+				this.reset();
+				return;
+			}
+		}
+	};
+	onPointerDown = (event) => {
+		if (event.button === 1) this.reset();
+	};
+	isTouchOnSelectionHandle(event) {
+		const selection = window.getSelection();
+		if (!selection || selection.isCollapsed || selection.rangeCount === 0) return false;
+		const touch = event.targetTouches[0] ?? event.changedTouches[0];
+		if (!touch) return false;
+		const rects = selection.getRangeAt(0).getClientRects();
+		if (rects.length === 0) return false;
+		const first = rects[0];
+		const last = rects[rects.length - 1];
+		const HANDLE_RADIUS = 40;
+		const nearStart = Math.hypot(touch.clientX - first.left, touch.clientY - first.top) <= HANDLE_RADIUS;
+		const nearEnd = Math.hypot(touch.clientX - last.right, touch.clientY - last.bottom) <= HANDLE_RADIUS;
+		return nearStart || nearEnd;
+	}
+	onVirtualScroll = (data) => {
+		if (typeof this.options.virtualScroll === "function" && this.options.virtualScroll(data) === false) return;
+		const { deltaX, deltaY, event } = data;
+		this.emitter.emit("virtual-scroll", {
+			deltaX,
+			deltaY,
+			event
+		});
+		if (event.ctrlKey) return;
+		if (event.lenisStopPropagation) return;
+		const isTouch = event.type.includes("touch");
+		const isWheel = event.type.includes("wheel");
+		if (isTouch && this.isIos) {
+			if (event.type === "touchstart") this._isDraggingSelection = this.isTouchOnSelectionHandle(event);
+			if (this._isDraggingSelection) {
+				if (event.type === "touchend") this._isDraggingSelection = false;
+				return;
+			}
+		}
+		this.isTouching = event.type === "touchstart" || event.type === "touchmove";
+		const isClickOrTap = deltaX === 0 && deltaY === 0;
+		if (this.options.syncTouch && isTouch && event.type === "touchstart" && isClickOrTap && !this.isStopped && !this.isLocked) {
+			this.reset();
+			return;
+		}
+		const isUnknownGesture = this.options.gestureOrientation === "vertical" && deltaY === 0 || this.options.gestureOrientation === "horizontal" && deltaX === 0;
+		if (isClickOrTap || isUnknownGesture) return;
+		let composedPath = event.composedPath();
+		composedPath = composedPath.slice(0, composedPath.indexOf(this.rootElement));
+		const prevent = this.options.prevent;
+		const gestureOrientation = Math.abs(deltaX) >= Math.abs(deltaY) ? "horizontal" : "vertical";
+		if (composedPath.find((node) => node instanceof HTMLElement && (typeof prevent === "function" && prevent?.(node) || node.hasAttribute?.("data-lenis-prevent") || gestureOrientation === "vertical" && node.hasAttribute?.("data-lenis-prevent-vertical") || gestureOrientation === "horizontal" && node.hasAttribute?.("data-lenis-prevent-horizontal") || isTouch && node.hasAttribute?.("data-lenis-prevent-touch") || isWheel && node.hasAttribute?.("data-lenis-prevent-wheel") || this.options.allowNestedScroll && this.hasNestedScroll(node, {
+			deltaX,
+			deltaY
+		})))) return;
+		if (this.isStopped || this.isLocked) {
+			if (event.cancelable) event.preventDefault();
+			return;
+		}
+		if (!(this.options.syncTouch && isTouch || this.options.smoothWheel && isWheel)) {
+			this.isScrolling = "native";
+			this.animate.stop();
+			event.lenisStopPropagation = true;
+			return;
+		}
+		let delta = deltaY;
+		if (this.options.gestureOrientation === "both") delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY : deltaX;
+		else if (this.options.gestureOrientation === "horizontal") delta = deltaX;
+		if (!this.options.overscroll || this.options.infinite || this.options.wrapper !== window && this.limit > 0 && (this.animatedScroll > 0 && this.animatedScroll < this.limit || this.animatedScroll === 0 && deltaY > 0 || this.animatedScroll === this.limit && deltaY < 0)) event.lenisStopPropagation = true;
+		if (event.cancelable) event.preventDefault();
+		const isSyncTouch = isTouch && this.options.syncTouch;
+		const hasTouchInertia = isTouch && event.type === "touchend";
+		if (hasTouchInertia) delta = Math.sign(delta) * Math.abs(this.velocity) ** this.options.touchInertiaExponent;
+		this.scrollTo(this.targetScroll + delta, {
+			programmatic: false,
+			...isSyncTouch ? { lerp: hasTouchInertia ? this.options.syncTouchLerp : 1 } : {
+				lerp: this.options.lerp,
+				duration: this.options.duration,
+				easing: this.options.easing
+			}
+		});
+	};
+	/**
+	* Force lenis to recalculate the dimensions
+	*/
+	resize() {
+		this.dimensions.resize();
+		this.animatedScroll = this.targetScroll = this.actualScroll;
+		this.emit();
+	}
+	emit() {
+		this.emitter.emit("scroll", this);
+	}
+	onNativeScroll = () => {
+		if (this._resetVelocityTimeout !== null) {
+			clearTimeout(this._resetVelocityTimeout);
+			this._resetVelocityTimeout = null;
+		}
+		if (this._preventNextNativeScrollEvent) {
+			this._preventNextNativeScrollEvent = false;
+			return;
+		}
+		if (this.isScrolling === false || this.isScrolling === "native") {
+			const lastScroll = this.animatedScroll;
+			this.animatedScroll = this.targetScroll = this.actualScroll;
+			this.lastVelocity = this.velocity;
+			this.velocity = this.animatedScroll - lastScroll;
+			this.direction = Math.sign(this.animatedScroll - lastScroll);
+			if (!this.isStopped) this.isScrolling = "native";
+			this.emit();
+			if (this.velocity !== 0) this._resetVelocityTimeout = setTimeout(() => {
+				this.lastVelocity = this.velocity;
+				this.velocity = 0;
+				this.isScrolling = false;
+				this.emit();
+			}, 400);
+		}
+	};
+	reset() {
+		this.isLocked = false;
+		this.isScrolling = false;
+		this.animatedScroll = this.targetScroll = this.actualScroll;
+		this.lastVelocity = this.velocity = 0;
+		this.animate.stop();
+	}
+	/**
+	* Start lenis scroll after it has been stopped
+	*/
+	start() {
+		if (!this.isStopped) return;
+		if (this.options.autoToggle) {
+			this.rootElement.style.removeProperty("overflow");
+			return;
+		}
+		this.internalStart();
+	}
+	internalStart() {
+		if (!this.isStopped) return;
+		this.reset();
+		this.isStopped = false;
+		this.emit();
+	}
+	/**
+	* Stop lenis scroll
+	*/
+	stop() {
+		if (this.isStopped) return;
+		if (this.options.autoToggle) {
+			this.rootElement.style.setProperty("overflow", "clip");
+			return;
+		}
+		this.internalStop();
+	}
+	internalStop() {
+		if (this.isStopped) return;
+		this.reset();
+		this.isStopped = true;
+		this.emit();
+	}
+	/**
+	* RequestAnimationFrame for lenis
+	*
+	* @param time The time in ms from an external clock like `requestAnimationFrame` or Tempus
+	*/
+	raf = (time) => {
+		const deltaTime = time - (this.time || time);
+		this.time = time;
+		this.animate.advance(deltaTime * .001);
+		if (this.options.autoRaf) this._rafId = requestAnimationFrame(this.raf);
+	};
+	/**
+	* Scroll to a target value
+	*
+	* @param target The target value to scroll to
+	* @param options The options for the scroll
+	*
+	* @example
+	* lenis.scrollTo(100, {
+	*   offset: 100,
+	*   duration: 1,
+	*   easing: (t) => 1 - Math.cos((t * Math.PI) / 2),
+	*   lerp: 0.1,
+	*   onStart: () => {
+	*     console.log('onStart')
+	*   },
+	*   onComplete: () => {
+	*     console.log('onComplete')
+	*   },
+	* })
+	*/
+	scrollTo(_target, { offset = 0, immediate = false, lock = false, programmatic = true, lerp = programmatic ? this.options.lerp : void 0, duration = programmatic ? this.options.duration : void 0, easing = programmatic ? this.options.easing : void 0, onStart, onComplete, force = false, userData } = {}) {
+		if (this.prefersReducedMotion) if (programmatic) immediate = true;
+		else {
+			lerp = 1;
+			duration = void 0;
+			easing = void 0;
+		}
+		if ((this.isStopped || this.isLocked) && !force) return;
+		let target = _target;
+		let adjustedOffset = offset;
+		if (typeof target === "string" && [
+			"top",
+			"left",
+			"start",
+			"#"
+		].includes(target)) target = 0;
+		else if (typeof target === "string" && [
+			"bottom",
+			"right",
+			"end"
+		].includes(target)) target = this.limit;
+		else {
+			let node = null;
+			if (typeof target === "string") {
+				node = target.startsWith("#") ? document.getElementById(target.slice(1)) : document.querySelector(target);
+				if (!node) if (target === "#top") target = 0;
+				else console.warn("Lenis: Target not found", target);
+			} else if (target instanceof HTMLElement && target?.nodeType) node = target;
+			if (node) {
+				if (this.options.wrapper !== window) {
+					const wrapperRect = this.rootElement.getBoundingClientRect();
+					adjustedOffset -= this.isHorizontal ? wrapperRect.left : wrapperRect.top;
+				}
+				const rect = node.getBoundingClientRect();
+				const targetStyle = getComputedStyle(node);
+				const scrollMargin = this.isHorizontal ? Number.parseFloat(targetStyle.scrollMarginLeft) : Number.parseFloat(targetStyle.scrollMarginTop);
+				const containerStyle = getComputedStyle(this.rootElement);
+				const scrollPadding = this.isHorizontal ? Number.parseFloat(containerStyle.scrollPaddingLeft) : Number.parseFloat(containerStyle.scrollPaddingTop);
+				target = (this.isHorizontal ? rect.left : rect.top) + this.animatedScroll - (Number.isNaN(scrollMargin) ? 0 : scrollMargin) - (Number.isNaN(scrollPadding) ? 0 : scrollPadding);
+			}
+		}
+		if (typeof target !== "number") return;
+		target += adjustedOffset;
+		if (this.options.infinite) {
+			if (programmatic) {
+				this.targetScroll = this.animatedScroll = this.scroll;
+				const distance = target - this.animatedScroll;
+				if (distance > this.limit / 2) target -= this.limit;
+				else if (distance < -this.limit / 2) target += this.limit;
+			}
+		} else target = clamp(0, target, this.limit);
+		if (target === this.targetScroll) {
+			onStart?.(this);
+			onComplete?.(this);
+			return;
+		}
+		this.userData = userData ?? {};
+		if (immediate) {
+			this.animatedScroll = this.targetScroll = target;
+			this.setScroll(this.scroll);
+			this.reset();
+			this.preventNextNativeScrollEvent();
+			this.emit();
+			onComplete?.(this);
+			this.userData = {};
+			requestAnimationFrame(() => {
+				this.dispatchScrollendEvent();
+			});
+			return;
+		}
+		if (!programmatic) this.targetScroll = target;
+		if (typeof duration === "number" && typeof easing !== "function") easing = defaultEasing;
+		else if (typeof easing === "function" && typeof duration !== "number") duration = 1;
+		this.animate.fromTo(this.animatedScroll, target, {
+			duration,
+			easing,
+			lerp,
+			onStart: () => {
+				if (lock) this.isLocked = true;
+				this.isScrolling = "smooth";
+				onStart?.(this);
+			},
+			onUpdate: (value, completed) => {
+				this.isScrolling = "smooth";
+				this.lastVelocity = this.velocity;
+				this.velocity = value - this.animatedScroll;
+				this.direction = Math.sign(this.velocity);
+				this.animatedScroll = value;
+				this.setScroll(this.scroll);
+				if (programmatic) this.targetScroll = value;
+				if (!completed) this.emit();
+				if (completed) {
+					this.reset();
+					this.emit();
+					onComplete?.(this);
+					this.userData = {};
+					requestAnimationFrame(() => {
+						this.dispatchScrollendEvent();
+					});
+					this.preventNextNativeScrollEvent();
+				}
+			}
+		});
+	}
+	preventNextNativeScrollEvent() {
+		this._preventNextNativeScrollEvent = true;
+		requestAnimationFrame(() => {
+			this._preventNextNativeScrollEvent = false;
+		});
+	}
+	hasNestedScroll(node, { deltaX, deltaY }) {
+		const time = Date.now();
+		if (!node._lenis) node._lenis = {};
+		const cache = node._lenis;
+		let hasOverflowX;
+		let hasOverflowY;
+		let isScrollableX;
+		let isScrollableY;
+		let hasOverscrollBehaviorX;
+		let hasOverscrollBehaviorY;
+		let scrollWidth;
+		let scrollHeight;
+		let clientWidth;
+		let clientHeight;
+		if (time - (cache.time ?? 0) > 2e3) {
+			cache.time = Date.now();
+			const computedStyle = window.getComputedStyle(node);
+			cache.computedStyle = computedStyle;
+			hasOverflowX = [
+				"auto",
+				"overlay",
+				"scroll"
+			].includes(computedStyle.overflowX);
+			hasOverflowY = [
+				"auto",
+				"overlay",
+				"scroll"
+			].includes(computedStyle.overflowY);
+			hasOverscrollBehaviorX = ["auto"].includes(computedStyle.overscrollBehaviorX);
+			hasOverscrollBehaviorY = ["auto"].includes(computedStyle.overscrollBehaviorY);
+			cache.hasOverflowX = hasOverflowX;
+			cache.hasOverflowY = hasOverflowY;
+			if (!(hasOverflowX || hasOverflowY)) return false;
+			scrollWidth = node.scrollWidth;
+			scrollHeight = node.scrollHeight;
+			clientWidth = node.clientWidth;
+			clientHeight = node.clientHeight;
+			isScrollableX = scrollWidth > clientWidth;
+			isScrollableY = scrollHeight > clientHeight;
+			cache.isScrollableX = isScrollableX;
+			cache.isScrollableY = isScrollableY;
+			cache.scrollWidth = scrollWidth;
+			cache.scrollHeight = scrollHeight;
+			cache.clientWidth = clientWidth;
+			cache.clientHeight = clientHeight;
+			cache.hasOverscrollBehaviorX = hasOverscrollBehaviorX;
+			cache.hasOverscrollBehaviorY = hasOverscrollBehaviorY;
+		} else {
+			isScrollableX = cache.isScrollableX;
+			isScrollableY = cache.isScrollableY;
+			hasOverflowX = cache.hasOverflowX;
+			hasOverflowY = cache.hasOverflowY;
+			scrollWidth = cache.scrollWidth;
+			scrollHeight = cache.scrollHeight;
+			clientWidth = cache.clientWidth;
+			clientHeight = cache.clientHeight;
+			hasOverscrollBehaviorX = cache.hasOverscrollBehaviorX;
+			hasOverscrollBehaviorY = cache.hasOverscrollBehaviorY;
+		}
+		if (!(hasOverflowX && isScrollableX || hasOverflowY && isScrollableY)) return false;
+		const orientation = Math.abs(deltaX) >= Math.abs(deltaY) ? "horizontal" : "vertical";
+		let scroll;
+		let maxScroll;
+		let delta;
+		let hasOverflow;
+		let isScrollable;
+		let hasOverscrollBehavior;
+		if (orientation === "horizontal") {
+			scroll = Math.round(node.scrollLeft);
+			maxScroll = scrollWidth - clientWidth;
+			delta = deltaX;
+			hasOverflow = hasOverflowX;
+			isScrollable = isScrollableX;
+			hasOverscrollBehavior = hasOverscrollBehaviorX;
+		} else if (orientation === "vertical") {
+			scroll = Math.round(node.scrollTop);
+			maxScroll = scrollHeight - clientHeight;
+			delta = deltaY;
+			hasOverflow = hasOverflowY;
+			isScrollable = isScrollableY;
+			hasOverscrollBehavior = hasOverscrollBehaviorY;
+		} else return false;
+		if (!hasOverscrollBehavior && (scroll >= maxScroll || scroll <= 0)) return true;
+		return (delta > 0 ? scroll < maxScroll : scroll > 0) && hasOverflow && isScrollable;
+	}
+	/**
+	* The root element on which lenis is instanced
+	*/
+	get rootElement() {
+		return this.options.wrapper === window ? document.documentElement : this.options.wrapper;
+	}
+	/**
+	* The limit which is the maximum scroll value
+	*/
+	get limit() {
+		if (this.options.naiveDimensions) {
+			if (this.isHorizontal) return this.rootElement.scrollWidth - this.rootElement.clientWidth;
+			return this.rootElement.scrollHeight - this.rootElement.clientHeight;
+		}
+		return this.dimensions.limit[this.isHorizontal ? "x" : "y"];
+	}
+	/**
+	* Whether or not the scroll is horizontal
+	*/
+	get isHorizontal() {
+		return this.options.orientation === "horizontal";
+	}
+	/**
+	* The actual scroll value
+	*/
+	get actualScroll() {
+		const wrapper = this.options.wrapper;
+		return this.isHorizontal ? wrapper.scrollX ?? wrapper.scrollLeft : wrapper.scrollY ?? wrapper.scrollTop;
+	}
+	/**
+	* The current scroll value
+	*/
+	get scroll() {
+		return this.options.infinite ? modulo(this.animatedScroll, this.limit) : this.animatedScroll;
+	}
+	/**
+	* The progress of the scroll relative to the limit
+	*/
+	get progress() {
+		return this.limit === 0 ? 1 : this.scroll / this.limit;
+	}
+	/**
+	* Current scroll state
+	*/
+	get isScrolling() {
+		return this._isScrolling;
+	}
+	set isScrolling(value) {
+		if (this._isScrolling !== value) {
+			this._isScrolling = value;
+			this.updateClassName();
+		}
+	}
+	/**
+	* Check if lenis is stopped
+	*/
+	get isStopped() {
+		return this._isStopped;
+	}
+	set isStopped(value) {
+		if (this._isStopped !== value) {
+			this._isStopped = value;
+			this.updateClassName();
+		}
+	}
+	/**
+	* Check if lenis is locked
+	*/
+	get isLocked() {
+		return this._isLocked;
+	}
+	set isLocked(value) {
+		if (this._isLocked !== value) {
+			this._isLocked = value;
+			this.updateClassName();
+		}
+	}
+	/**
+	* Check if lenis is smooth scrolling
+	*/
+	get isSmooth() {
+		return this.isScrolling === "smooth";
+	}
+	/**
+	* Whether the user prefers reduced motion and lenis is honoring it (see `respectReducedMotion` option)
+	*/
+	get prefersReducedMotion() {
+		return this.options.respectReducedMotion && this.reducedMotionMediaQuery.matches;
+	}
+	/**
+	* The class name applied to the wrapper element
+	*/
+	get className() {
+		let className = "lenis";
+		if (this.options.autoToggle) className += " lenis-autoToggle";
+		if (this.isStopped) className += " lenis-stopped";
+		if (this.isLocked) className += " lenis-locked";
+		if (this.isScrolling) className += " lenis-scrolling";
+		if (this.isScrolling === "smooth") className += " lenis-smooth";
+		return className;
+	}
+	updateClassName() {
+		this.cleanUpClassName();
+		this.className.split(" ").forEach((className) => {
+			this.rootElement.classList.add(className);
+		});
+	}
+	cleanUpClassName() {
+		for (const className of Array.from(this.rootElement.classList)) if (className === "lenis" || className.startsWith("lenis-")) this.rootElement.classList.remove(className);
+	}
+};
+//#endregion
+export { Lenis as default };
+
+//# sourceMappingURL=lenis.mjs.map
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision d8f8f079ce7979088f94c7dca8819cb4eb384777)
+++ src/services/i18n.js	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -33,5 +33,5 @@
     'switch.solo': 'Solo',
     'switch.circle': 'Cirkels',
-    'switch.grid': 'Grid', 'switch.reader': 'Lezen', 'switch.timeline': 'Tijdlijn', 'read.to_top': 'Terug naar boven',
+    'switch.grid': 'Grid', 'switch.reader': 'Lezen', 'switch.timeline': 'Tijdlijn', 'read.to_top': 'Terug naar boven', 'read.pinned': 'Vastgepind',
     'asite.feed_alt': 'Tweede weergave', 'asite.feed_alt_reader': 'Lezen', 'asite.feed_alt_timeline': 'Tijdlijn', 'asite.feed_alt_auto': 'Lezen op mobiel, Tijdlijn op desktop',
     'switch.reader_solo_only': 'Lezen kan alleen in Solo — in de cirkel staan berichten van anderen',
@@ -1049,5 +1049,5 @@
     'switch.solo': 'Solo',
     'switch.circle': 'Circles',
-    'switch.grid': 'Grid', 'switch.reader': 'Reader', 'switch.timeline': 'Timeline', 'read.to_top': 'Back to top',
+    'switch.grid': 'Grid', 'switch.reader': 'Reader', 'switch.timeline': 'Timeline', 'read.to_top': 'Back to top', 'read.pinned': 'Pinned',
     'asite.feed_alt': 'Second view', 'asite.feed_alt_reader': 'Reader', 'asite.feed_alt_timeline': 'Timeline', 'asite.feed_alt_auto': 'Reader on mobile, Timeline on desktop',
     'switch.reader_solo_only': 'Reader is Solo only — a circle shows other people\u2019s posts',
@@ -2056,5 +2056,5 @@
     'switch.solo': 'Solo',
     'switch.circle': 'Zirkel',
-    'switch.grid': 'Raster', 'switch.reader': 'Lesen', 'switch.timeline': 'Zeitleiste', 'read.to_top': 'Nach oben',
+    'switch.grid': 'Raster', 'switch.reader': 'Lesen', 'switch.timeline': 'Zeitleiste', 'read.to_top': 'Nach oben', 'read.pinned': 'Angeheftet',
     'asite.feed_alt': 'Zweite Ansicht', 'asite.feed_alt_reader': 'Lesen', 'asite.feed_alt_timeline': 'Zeitleiste', 'asite.feed_alt_auto': 'Lesen mobil, Zeitleiste am Desktop',
     'switch.reader_solo_only': 'Lesen gibt es nur in Solo — im Zirkel stehen Beitr\u00e4ge anderer',
Index: src/views/partials/read-article.ejs
===================================================================
--- src/views/partials/read-article.ejs	(revision d8f8f079ce7979088f94c7dca8819cb4eb384777)
+++ src/views/partials/read-article.ejs	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -68,5 +68,10 @@
 
   <header class="read-head">
-    <h1 class="read-title"><a href="<%= _b %>/<%= post.slug %>"><%= post.title || '' %></a></h1>
+    <%# Vastgepind hoort zichtbaar te zijn in de titel: in de leesstroom staat een
+        gepind bericht bovenaan zonder dat je ziet WAAROM het daar staat. Zelfde
+        icoon als in de tijdlijn (post-card.ejs), zodat het hetzelfde ding is. %>
+    <h1 class="read-title">
+      <% if (post.pinned > 0) { %><span class="read-pin" title="<%= t('read.pinned') %>" aria-label="<%= t('read.pinned') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg></span><% } %><a href="<%= _b %>/<%= post.slug %>"><%= post.title || '' %></a>
+    </h1>
     <% if (post.published_at) { %><p class="read-when"><%= formatDateTime(post.published_at) %></p><% } %>
   </header>
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision d8f8f079ce7979088f94c7dca8819cb4eb384777)
+++ src/views/shell.ejs	(revision a505f315f3c64729f2366f33bdd83e156dedce84)
@@ -222,5 +222,5 @@
 
 <!-- v9 stylesheet (full palette system) -->
-<link rel="stylesheet" href="/assets/css/style.css?v=81">
+<link rel="stylesheet" href="/assets/css/style.css?v=82">
 <script>
 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in
@@ -505,5 +505,5 @@
   // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig
   // bumpen kost een bugfix die nooit aankomt.
-  var MOD_V = 16;
+  var MOD_V = 19;
 
   // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
