Index: src/services/ThumbnailService.js
===================================================================
--- src/services/ThumbnailService.js	(revision 1bc0886e2aa8f340c81090361bc2071ed728a96d)
+++ src/services/ThumbnailService.js	(revision f70e01036da8f19e51f52dcaf4d157675c2c962e)
@@ -165,4 +165,16 @@
  * @returns {Promise<string|null>} cached path, or null.
  */
+// Same animation check as isAnimatedSrc but on an in-memory buffer (remote fetch): ffmpeg-static
+// can't decode an animated WebP, and flattening a GIF/animated WebP to one frame loses its motion.
+function isAnimatedBuf(buf) {
+  try {
+    if (!buf || buf.length < 21) return false;
+    if (buf.toString('ascii', 0, 3) === 'GIF') return true; // any GIF (a flattened one loses its animation)
+    // Animated WebP: RIFF…WEBP with a VP8X chunk whose flags byte (20) has the animation bit (0x02).
+    if (buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP'
+        && buf.toString('ascii', 12, 16) === 'VP8X' && (buf[20] & 0x02) !== 0) return true;
+    return false;
+  } catch { return false; }
+}
 export async function getRemoteThumbnail(url, width) {
   if (!THUMB_SIZES.has(width) || !ffmpegPath || !url) return null;
@@ -185,4 +197,8 @@
   }
   if (buf.length > 12 * 1024 * 1024) return null;
+  // ffmpeg-static can't decode an animated WebP (the doomed downscale just logs an error), and a
+  // flattened GIF/animated WebP loses its motion → skip it and let the route serve the ORIGINAL
+  // (keeps the animation; mirrors the local path's isAnimatedSrc guard).
+  if (isAnimatedBuf(buf)) return null;
 
   await fs.promises.mkdir(path.dirname(cached), { recursive: true });
