diff --git a/frontend/src/components/PlaylistLimitsHint.dom.test.tsx b/frontend/src/components/PlaylistLimitsHint.dom.test.tsx
new file mode 100644
index 00000000..092ad38b
--- /dev/null
+++ b/frontend/src/components/PlaylistLimitsHint.dom.test.tsx
@@ -0,0 +1,46 @@
+// @vitest-environment jsdom
+//
+// The playlists module shows fewer lists than youtube.com, and said nothing about why, so it read as
+// a Siftlode bug (debt burn A3). One quiet line at the foot of the rail; "Why?" opens the three facts.
+import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
+import { FloatingTree } from "@floating-ui/react";
+
+vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (k: string) => k }) }));
+
+import PlaylistLimitsHint from "./PlaylistLimitsHint";
+
+// Same floating-ui recipe as ContextMenu.dom.test.tsx: stub the observers, and query with
+// `hidden: true` — floating-ui never positions in jsdom, so the surface stays visibility:hidden.
+beforeAll(() => {
+ class Stub {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ takeRecords() {
+ return [];
+ }
+ }
+ globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
+ globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
+});
+afterEach(cleanup);
+
+describe("PlaylistLimitsHint", () => {
+ it("keeps the facts behind Why?, and opens them as a named dialog", async () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText("playlists.limits.hint")).toBeTruthy();
+ expect(screen.queryByRole("dialog", { hidden: true })).toBeNull();
+
+ fireEvent.click(screen.getByRole("button", { name: "playlists.limits.why" }));
+ const dialog = await screen.findByRole("dialog", { hidden: true });
+ // Read off the attribute: jsdom computes no name for a visibility:hidden element.
+ expect(dialog.getAttribute("aria-label")).toBe("playlists.limits.title");
+ for (const fact of ["closed", "ownOnly", "noChannel"])
+ expect(dialog.textContent).toContain(`playlists.limits.${fact}`);
+ });
+});
diff --git a/frontend/src/components/PlaylistLimitsHint.tsx b/frontend/src/components/PlaylistLimitsHint.tsx
new file mode 100644
index 00000000..e9682ce3
--- /dev/null
+++ b/frontend/src/components/PlaylistLimitsHint.tsx
@@ -0,0 +1,49 @@
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Info } from "lucide-react";
+
+import { PopoverSurface } from "./ui/AnchoredPopover";
+import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
+
+// The playlists module shows fewer lists than youtube.com does, and without a word about it that
+// reads as Siftlode being broken (the owner's ask, 2026-09-03: say it is YouTube's API, up front).
+// Three facts, behind one quiet line at the foot of the rail — the owner's pick from the S7 mockups
+// (2026-09-19): always there to find again, never in the way.
+export default function PlaylistLimitsHint() {
+ const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
+ const pop = useAnchoredPopover({
+ open,
+ onOpenChange: setOpen,
+ mode: "dialog",
+ role: "dialog",
+ placement: "right-end",
+ offset: 8,
+ });
+ return (
+
+
+ {t("playlists.limits.hint")}
+
+
+
{t("playlists.limits.title")}
+
+
{t("playlists.limits.closed")}
+
{t("playlists.limits.ownOnly")}
+
{t("playlists.limits.noChannel")}
+
+
+
+ );
+}
diff --git a/frontend/src/components/PlaylistsRail.tsx b/frontend/src/components/PlaylistsRail.tsx
index ad6ce147..10c32ba5 100644
--- a/frontend/src/components/PlaylistsRail.tsx
+++ b/frontend/src/components/PlaylistsRail.tsx
@@ -22,6 +22,7 @@ import { usePlaylists, usePlaylistsActions } from "./playlistsContext";
import { useMe } from "../lib/useMe";
import { playlistName } from "../lib/playlistName";
import { usePlaylistActions } from "./usePlaylistActions";
+import PlaylistLimitsHint from "./PlaylistLimitsHint";
type PlSort = {
key: "custom" | "name" | "count" | "duration" | "recent";
@@ -382,6 +383,7 @@ export default function PlaylistsRail({
)}
+
>
),
},
diff --git a/frontend/src/i18n/locales/en/playlists.json b/frontend/src/i18n/locales/en/playlists.json
index d4483693..67276bfc 100644
--- a/frontend/src/i18n/locales/en/playlists.json
+++ b/frontend/src/i18n/locales/en/playlists.json
@@ -118,5 +118,13 @@
"viewDiffers": "Temporary view ({{view}}) — the playlist's saved order is unchanged. Save it to keep this order.",
"viewMatches": "This view ({{view}}) already matches the playlist's saved order, so there is nothing to save.",
"viewFiltered": "Filtered view — {{shown}} of {{total}} shown. The order can't be saved while a filter is on.",
- "addFailed": "Couldn't add it to the playlist."
+ "addFailed": "Couldn't add it to the playlist.",
+ "limits": {
+ "hint": "Missing a list?",
+ "why": "Why?",
+ "title": "What YouTube lets Siftlode read",
+ "closed": "YouTube's own Watch later and History have been closed to its API since 2016, so no app can read them. The Watch later list here is Siftlode's own.",
+ "ownOnly": "Only playlists you own sync. Lists you saved from other channels are not returned.",
+ "noChannel": "A Google account with no YouTube channel has no playlists at all, as far as the API is concerned."
+ }
}
diff --git a/frontend/src/i18n/locales/hu/playlists.json b/frontend/src/i18n/locales/hu/playlists.json
index 8b6c104d..45be5d2c 100644
--- a/frontend/src/i18n/locales/hu/playlists.json
+++ b/frontend/src/i18n/locales/hu/playlists.json
@@ -118,5 +118,13 @@
"viewDiffers": "Ideiglenes nézet ({{view}}) — a lista mentett sorrendje változatlan. Mentsd, ha ez maradjon.",
"viewMatches": "Ez a nézet ({{view}}) megegyezik a lista mentett sorrendjével, így nincs mit menteni.",
"viewFiltered": "Szűrt nézet — {{total}} elemből {{shown}} látszik. Szűrés közben a sorrend nem menthető.",
- "addFailed": "Nem sikerült hozzáadni a lejátszási listához."
+ "addFailed": "Nem sikerült hozzáadni a lejátszási listához.",
+ "limits": {
+ "hint": "Hiányzik egy lista?",
+ "why": "Miért?",
+ "title": "Amit a YouTube kiad a Siftlode-nak",
+ "closed": "A YouTube saját „Megnézem később” listája és az Előzmények 2016 óta zárva vannak az API előtt, így egyik alkalmazás sem olvashatja őket. Az itteni „Megnézem később” lista a Siftlode saját listája.",
+ "ownOnly": "Csak a saját listáid szinkronizálódnak. A más csatornáktól elmentett listákat az API nem adja vissza.",
+ "noChannel": "Ha a Google-fiókodhoz nem tartozik YouTube-csatorna, az API szerint egy listád sincs."
+ }
}