diff --git a/web/sections/GalleriesPanel.tsx b/web/sections/GalleriesPanel.tsx
index 9c18566..9a3caab 100644
--- a/web/sections/GalleriesPanel.tsx
+++ b/web/sections/GalleriesPanel.tsx
@@ -4,6 +4,7 @@ import { ArrowDown, ArrowUp, FolderOpen, Images, Loader2, Plus, Trash2 } from 'l
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
+import { getHeaders } from 'hooks/useClient';
import { readableError } from '../errors';
import { MediaPicker } from '../MediaPicker';
@@ -27,6 +28,19 @@ import { MediaPicker } from '../MediaPicker';
type Item = { id: number; url: string; alt: string | null; sort_order: number };
type Gallery = { id: number; name: string; slug: string; items: Item[] };
+/**
+ * A thumbnail URL, authenticated by `?token=`.
+ *
+ * An `
` cannot send an Authorization header, which is why `user-middleware` and the permission
+ * gate both accept the token in the query string — the same thing the file browser's own thumbnails do.
+ * Built without it first, and every picture 401'd while the layout looked perfectly correct.
+ */
+const thumbUrl = (path: string): string => {
+ const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
+ const encoded = path.split('/').map(encodeURIComponent).join('/');
+ return `/api/fofos-admin/derived/${encoded}/320.webp?token=${encodeURIComponent(token)}`;
+};
+
export const GalleriesPanel = () => {
const client = useClient();
const clientRef = useRef(client);
@@ -203,7 +217,7 @@ export const GalleriesPanel = () => {
and still sharp at this size on a retina screen.
*/}
.map(encodeURIComponent).join('/')}/320.webp`})