diff --git a/desktop/src/app/AppShell.helpers.ts b/desktop/src/app/AppShell.helpers.ts
index 0a10e72db2..8ce0b8ca19 100644
--- a/desktop/src/app/AppShell.helpers.ts
+++ b/desktop/src/app/AppShell.helpers.ts
@@ -5,7 +5,6 @@ export type AppView =
   | "home"
   | "channel"
   | "agents"
-  | "reminders"
   | "workflows"
   | "pulse"
   | "projects";
@@ -86,13 +85,6 @@ export function deriveShellRoute(pathname: string): {
     };
   }
 
-  if (pathname === "/reminders") {
-    return {
-      selectedChannelId: null,
-      selectedView: "reminders",
-    };
-  }
-
   return {
     selectedChannelId: null,
     selectedView: "home",
diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx
index 99eccf4100..cca89f4716 100644
--- a/desktop/src/app/AppShell.tsx
+++ b/desktop/src/app/AppShell.tsx
@@ -66,6 +66,7 @@ import {
 } from "@/features/settings/ui/SettingsPanels";
 import { HuddleBar, HuddleProvider } from "@/features/huddle";
 import { RemindMeLaterProvider } from "@/features/reminders/ui/RemindMeLaterProvider";
+import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
 import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
 import { useChannelMutes } from "@/features/sidebar/lib/useChannelMutes";
 import { useChannelStars } from "@/features/sidebar/lib/useChannelStars";
@@ -115,7 +116,6 @@ export function AppShell() {
     goChannel,
     goHome,
     goProjects,
-    goReminders,
     goPulse,
     goSettings,
     goWorkflows,
@@ -163,6 +163,10 @@ export function AppShell() {
   const setUserStatusMutation = useSetUserStatusMutation(deferredPubkey);
   const { feedProfilesQuery, homeFeedQuery, notificationSettings } =
     useHomeFeedNotifications(identityQuery.data?.pubkey);
+  useReminderNotifications(
+    identityQuery.data?.pubkey,
+    notificationSettings.settings,
+  );
   const refetchHomeFeedOnLiveMention = React.useEffectEvent(() => {
     void homeFeedQuery.refetch();
   });
@@ -714,7 +718,7 @@ export function AppShell() {
           }}
         >
           <HuddleProvider>
-            <RemindMeLaterProvider>
+            <RemindMeLaterProvider pubkey={identityQuery.data?.pubkey}>
               <div
                 className="buzz-huddle-shell relative h-dvh overflow-hidden overscroll-none"
                 data-huddle-open={isHuddleDrawerOpen}
@@ -879,7 +883,6 @@ export function AppShell() {
                           onSelectHome={() => void goHome()}
                           onSelectProjects={() => void goProjects()}
                           onSelectPulse={() => void goPulse()}
-                          onSelectReminders={() => void goReminders()}
                           onSelectSettings={handleOpenSettings}
                           onSelectWorkflows={() => void goWorkflows()}
                           onSetPresenceStatus={(status) =>
diff --git a/desktop/src/app/navigation/useAppNavigation.ts b/desktop/src/app/navigation/useAppNavigation.ts
index 2062b1dd39..0788bea197 100644
--- a/desktop/src/app/navigation/useAppNavigation.ts
+++ b/desktop/src/app/navigation/useAppNavigation.ts
@@ -90,17 +90,6 @@ export function useAppNavigation() {
     [commitNavigation],
   );
 
-  const goReminders = React.useCallback(
-    (behavior?: NavigationBehavior) =>
-      commitNavigation(
-        {
-          to: "/reminders",
-        },
-        behavior,
-      ),
-    [commitNavigation],
-  );
-
   const goProject = React.useCallback(
     (projectId: string, behavior?: NavigationBehavior) =>
       commitNavigation(
@@ -272,7 +261,6 @@ export function useAppNavigation() {
     goProject,
     goProjects,
     goPulse,
-    goReminders,
     goSettings,
     goWorkflow,
     goWorkflows,
diff --git a/desktop/src/app/routes/reminders.tsx b/desktop/src/app/routes/reminders.tsx
index 37599762b4..83f01e45e1 100644
--- a/desktop/src/app/routes/reminders.tsx
+++ b/desktop/src/app/routes/reminders.tsx
@@ -1,19 +1,11 @@
-import * as React from "react";
-import { createFileRoute } from "@tanstack/react-router";
-
-const RemindersScreen = React.lazy(async () => {
-  const module = await import("@/features/reminders/ui/RemindersScreen");
-  return { default: module.RemindersScreen };
-});
+import { createFileRoute, redirect } from "@tanstack/react-router";
 
+// Reminders is now a filter option inside the inbox dropdown, selected via
+// local state rather than the URL. This redirect preserves existing history
+// entries and bookmarks pointing at `/reminders` so they land in the inbox
+// instead of dead-ending; the user re-selects Reminders from the filter.
 export const Route = createFileRoute("/reminders")({
-  component: RemindersRouteComponent,
+  beforeLoad: () => {
+    throw redirect({ to: "/" });
+  },
 });
-
-function RemindersRouteComponent() {
-  return (
-    <React.Suspense fallback={null}>
-      <RemindersScreen />
-    </React.Suspense>
-  );
-}
diff --git a/desktop/src/features/home/lib/inbox.ts b/desktop/src/features/home/lib/inbox.ts
index 3e40b187ae..99d4fe31ba 100644
--- a/desktop/src/features/home/lib/inbox.ts
+++ b/desktop/src/features/home/lib/inbox.ts
@@ -17,7 +17,8 @@ export type InboxFilter =
   | "mention"
   | "needs_action"
   | "activity"
-  | "agent_activity";
+  | "agent_activity"
+  | "reminders";
 
 export type InboxItem = {
   avatarUrl: string | null;
diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx
index f28e15901f..fd5085bb31 100644
--- a/desktop/src/features/home/ui/HomeView.tsx
+++ b/desktop/src/features/home/ui/HomeView.tsx
@@ -37,6 +37,10 @@ import {
 import { splitOutgoingTags } from "@/features/messages/lib/imetaMediaMarkdown";
 import { useUsersBatchQuery } from "@/features/profile/hooks";
 import { resolveUserLabel } from "@/features/profile/lib/identity";
+import {
+  countDueReminders,
+  useRemindersQuery,
+} from "@/features/reminders/hooks";
 import { deleteMessage, sendChannelMessage } from "@/shared/api/tauri";
 import type { HomeFeedResponse } from "@/shared/api/types";
 import { KIND_REACTION } from "@/shared/constants/kinds";
@@ -81,7 +85,14 @@ export function HomeView({
   // background data loads must never trigger navigations.
   const { applyPatch: applyInboxSearchPatch, values: inboxSearchValues } =
     useHistorySearchState(INBOX_SEARCH_KEYS);
-  const urlSelectedItemId = inboxSearchValues.item;
+  const isReminders = filter === "reminders";
+  const isMessagesMode = !isReminders;
+  const remindersQuery = useRemindersQuery(currentPubkey);
+  const dueReminderCount = countDueReminders(remindersQuery.data ?? []);
+  // `?item=` is Messages-mode-only machinery: a reminder never enters the
+  // FeedItem selection model, so reload while in Reminders mode keeps a stale
+  // `?item=` unconsumed and does not snap back to a feed-item detail view.
+  const urlSelectedItemId = isMessagesMode ? inboxSearchValues.item : null;
   const [selectedItemId, setSelectedItemId] = React.useState<string | null>(
     urlSelectedItemId,
   );
@@ -251,6 +262,14 @@ export function HomeView({
     return localReplies.filter((reply) => !contextIds.has(reply.id));
   }, [contextMessages, localRepliesByItemId, selectedItem]);
   React.useEffect(() => {
+    // Auto-selection is Messages-mode-only: in Reminders mode no FeedItem is
+    // ever selected, so default-selecting one behind the reminders list would
+    // be wasted work and could drive narrow-viewport detail off a stale feed
+    // selection.
+    if (!isMessagesMode) {
+      return;
+    }
+
     // While the feed is loading (e.g. a reload restoring `?item=` from the
     // URL) the selected item simply hasn't arrived yet — don't clobber it.
     if (isLoading || !feed) {
@@ -278,6 +297,7 @@ export function HomeView({
     filteredItems,
     homeInboxWidthPx,
     isLoading,
+    isMessagesMode,
     isNarrowHomeViewport,
     selectedItemId,
   ]);
@@ -347,8 +367,12 @@ export function HomeView({
       selectedItem.item.pubkey.trim().toLowerCase();
   const isSinglePanelDetailView =
     isNarrowHomeViewport && selectedItemId !== null;
+  // Reminders mode is single-pane: the reminders list renders inline row
+  // actions and never drives the FeedItem detail pane, so the detail column is
+  // not rendered at all (no empty pane on wide viewports).
   const showListPane = !isSinglePanelDetailView;
-  const showDetailPane = !isNarrowHomeViewport || isSinglePanelDetailView;
+  const showDetailPane =
+    isMessagesMode && (!isNarrowHomeViewport || isSinglePanelDetailView);
   const maxEffectiveInboxListWidthPx =
     homeInboxWidthPx > 0
       ? Math.max(
@@ -384,6 +408,7 @@ export function HomeView({
         {showListPane ? (
           <InboxListPane
             doneSet={effectiveDoneSet}
+            dueReminderCount={dueReminderCount}
             filter={filter}
             items={filteredItems}
             onFilterChange={setFilter}
@@ -391,6 +416,7 @@ export function HomeView({
               handleUserSelectItem(itemId);
               markItemRead(itemId);
             }}
+            reminderPubkey={currentPubkey}
             selectedId={selectedItemId}
             showRightDivider={showListPane && showDetailPane}
           />
diff --git a/desktop/src/features/home/ui/InboxListPane.tsx b/desktop/src/features/home/ui/InboxListPane.tsx
index c4131d8beb..670c95b465 100644
--- a/desktop/src/features/home/ui/InboxListPane.tsx
+++ b/desktop/src/features/home/ui/InboxListPane.tsx
@@ -1,4 +1,4 @@
-import { ChevronDown, Inbox } from "lucide-react";
+import { ChevronDown } from "lucide-react";
 import * as React from "react";
 
 import {
@@ -6,6 +6,7 @@ import {
   type InboxFilter,
   type InboxItem,
 } from "@/features/home/lib/inbox";
+import { RemindersPanel } from "@/features/reminders/ui/RemindersPanel";
 import { topChromeInset } from "@/shared/layout/chromeLayout";
 import { TopChromeInsetHeader } from "@/shared/layout/TopChromeInsetHeader";
 import { cn } from "@/shared/lib/cn";
@@ -27,6 +28,7 @@ const FILTER_OPTIONS: Array<{ label: string; value: InboxFilter }> = [
   { value: "needs_action", label: "Needs Action" },
   { value: "activity", label: "Activity" },
   { value: "agent_activity", label: "Agents" },
+  { value: "reminders", label: "Reminders" },
 ];
 
 type InboxListPaneProps = {
@@ -37,6 +39,8 @@ type InboxListPaneProps = {
   onSelect: (itemId: string) => void;
   selectedId: string | null;
   showRightDivider?: boolean;
+  dueReminderCount: number;
+  reminderPubkey?: string;
 };
 
 export function InboxListPane({
@@ -47,8 +51,11 @@ export function InboxListPane({
   onSelect,
   selectedId,
   showRightDivider = false,
+  dueReminderCount,
+  reminderPubkey,
 }: InboxListPaneProps) {
   const activeFilter = FILTER_OPTIONS.find((option) => option.value === filter);
+  const isReminders = filter === "reminders";
   const scrollRef = React.useRef<HTMLDivElement>(null);
 
   const renderItem = (item: InboxItem) => {
@@ -144,22 +151,27 @@ export function InboxListPane({
     >
       <TopChromeInsetHeader>
         <div className="px-5 py-1">
-          <div className="flex min-w-0 items-center justify-between gap-3">
-            <div className="flex min-w-0 items-center gap-[6px]">
-              <Inbox className="h-4 w-4 shrink-0 text-muted-foreground" />
-              <h2 className="translate-y-px truncate text-sm font-semibold leading-5 tracking-tight">
-                Inbox
-              </h2>
-            </div>
+          {/* Cap to the list-column width so the right-aligned dropdown stays
+              put when the pane goes full-width in reminders mode. */}
+          <div className="flex min-w-0 max-w-[var(--home-inbox-list-width)] items-center justify-end gap-3">
             <DropdownMenu>
               <DropdownMenuTrigger asChild>
                 <Button
                   className="inline-flex shrink-0 items-center gap-1.5 rounded-full border-border/70 bg-background/70 px-2.5 text-2xs font-medium leading-none text-muted-foreground shadow-xs backdrop-blur-sm hover:bg-muted/60 hover:text-foreground"
+                  data-testid="inbox-filter-trigger"
                   size="sm"
                   type="button"
                   variant="outline"
                 >
                   <span>{activeFilter?.label ?? "All"}</span>
+                  {dueReminderCount > 0 ? (
+                    <span
+                      className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-2xs font-semibold leading-none text-primary-foreground"
+                      data-testid="inbox-reminder-badge"
+                    >
+                      {dueReminderCount}
+                    </span>
+                  ) : null}
                   <ChevronDown className="h-4 w-4" />
                 </Button>
               </DropdownMenuTrigger>
@@ -175,7 +187,18 @@ export function InboxListPane({
                       key={option.value}
                       value={option.value}
                     >
-                      {option.label}
+                      <span className="flex flex-1 items-center justify-between gap-2">
+                        {option.label}
+                        {option.value === "reminders" &&
+                        dueReminderCount > 0 ? (
+                          <span
+                            className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-2xs font-semibold leading-none text-primary-foreground"
+                            data-testid="inbox-reminder-badge-option"
+                          >
+                            {dueReminderCount}
+                          </span>
+                        ) : null}
+                      </span>
                     </DropdownMenuRadioItem>
                   ))}
                 </DropdownMenuRadioGroup>
@@ -185,32 +208,43 @@ export function InboxListPane({
         </div>
       </TopChromeInsetHeader>
 
-      <div
-        className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
-        data-testid="home-inbox-list"
-        ref={scrollRef}
-      >
-        {items.length === 0 ? (
-          <div className="flex h-full min-h-64 items-center justify-center px-6 text-center">
-            <div>
-              <p className="text-sm font-medium text-foreground">
-                No messages found
-              </p>
-              <p className="mt-1 text-sm text-muted-foreground">
-                Switch back to all mail to see more messages.
-              </p>
+      {isReminders ? (
+        <div
+          className="flex min-h-0 flex-1 flex-col overflow-hidden"
+          data-testid="home-inbox-reminders"
+        >
+          {reminderPubkey ? (
+            <RemindersPanel includeDone pubkey={reminderPubkey} />
+          ) : null}
+        </div>
+      ) : (
+        <div
+          className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
+          data-testid="home-inbox-list"
+          ref={scrollRef}
+        >
+          {items.length === 0 ? (
+            <div className="flex h-full min-h-64 items-center justify-center px-6 text-center">
+              <div>
+                <p className="text-sm font-medium text-foreground">
+                  No messages found
+                </p>
+                <p className="mt-1 text-sm text-muted-foreground">
+                  Switch back to all mail to see more messages.
+                </p>
+              </div>
             </div>
-          </div>
-        ) : (
-          <VirtualizedList
-            estimateSize={76}
-            getItemKey={(item) => item.id}
-            items={items}
-            renderItem={renderItem}
-            scrollRef={scrollRef}
-          />
-        )}
-      </div>
+          ) : (
+            <VirtualizedList
+              estimateSize={76}
+              getItemKey={(item) => item.id}
+              items={items}
+              renderItem={renderItem}
+              scrollRef={scrollRef}
+            />
+          )}
+        </div>
+      )}
     </section>
   );
 }
diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx
index 83ece19c7f..869ef0cb4b 100644
--- a/desktop/src/features/messages/ui/MessageRow.tsx
+++ b/desktop/src/features/messages/ui/MessageRow.tsx
@@ -90,7 +90,8 @@ export const MessageRow = React.memo(
       errorMessage: reactionErrorMessage,
       select: handleReactionSelect,
     } = useReactionHandler(message, onToggleReaction);
-    const { openReminder } = useRemindLater();
+    const { openReminder, activeReminderEventIds } = useRemindLater();
+    const hasActiveReminder = activeReminderEventIds.has(message.id);
     const mentionNames = React.useMemo(
       () => resolveMentionNames(message.tags, profiles),
       [profiles, message.tags],
@@ -392,6 +393,7 @@ export const MessageRow = React.memo(
               ? "mx-1 px-2 hover:bg-muted/50 focus-within:bg-muted/50"
               : "px-2",
             "flex items-start gap-2.5",
+            hasActiveReminder ? "bg-blue-500/10" : "",
             highlighted
               ? "-mx-4 rounded-none px-6 before:absolute before:-inset-y-1.5 before:inset-x-0 before:animate-[route-target-highlight-fade_2s_ease-out_forwards] before:bg-primary/10 before:content-[''] motion-reduce:before:animate-none sm:-mx-6 sm:px-8"
               : "",
diff --git a/desktop/src/features/reminders/hooks.ts b/desktop/src/features/reminders/hooks.ts
new file mode 100644
index 0000000000..879015e89d
--- /dev/null
+++ b/desktop/src/features/reminders/hooks.ts
@@ -0,0 +1,69 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import {
+  cancelReminder,
+  completeReminder,
+  createReminder,
+  fetchReminders,
+  snoozeReminder,
+} from "@/features/reminders/lib/reminderService";
+import { countDue } from "@/features/reminders/lib/reminderFilters";
+import type {
+  Reminder,
+  ReminderTarget,
+} from "@/features/reminders/lib/reminderTypes";
+
+export const remindersQueryKey = (pubkey: string) =>
+  ["reminders", pubkey] as const;
+
+/** Re-exported so the inbox badge has one import for the due count. */
+export const countDueReminders = countDue;
+
+/**
+ * The single source of truth for a user's reminders. Badge, channel overlay,
+ * panel, and fire-on-due detection all read this one query, so invalidating it
+ * (see {@link useReminderMutations}) keeps every surface consistent.
+ */
+export function useRemindersQuery(pubkey: string | undefined) {
+  return useQuery({
+    enabled: Boolean(pubkey),
+    queryKey: remindersQueryKey(pubkey ?? ""),
+    queryFn: () => fetchReminders(pubkey ?? ""),
+    staleTime: 30_000,
+  });
+}
+
+/**
+ * Wraps every reminder write so the shared query is invalidated on success —
+ * the consistency spine the panel/badge/overlay all depend on. A mutation that
+ * skipped invalidation would leave those surfaces stale until the next refetch.
+ */
+export function useReminderMutations(pubkey: string) {
+  const queryClient = useQueryClient();
+  const invalidate = () =>
+    queryClient.invalidateQueries({ queryKey: remindersQueryKey(pubkey) });
+
+  const create = useMutation({
+    mutationFn: (input: {
+      target: ReminderTarget;
+      notBefore: number;
+      note?: string;
+    }) => createReminder(input.target, input.notBefore, input.note),
+    onSuccess: invalidate,
+  });
+  const complete = useMutation({
+    mutationFn: (reminder: Reminder) => completeReminder(pubkey, reminder),
+    onSuccess: invalidate,
+  });
+  const snooze = useMutation({
+    mutationFn: (input: { reminder: Reminder; notBefore: number }) =>
+      snoozeReminder(pubkey, input.reminder, input.notBefore),
+    onSuccess: invalidate,
+  });
+  const cancel = useMutation({
+    mutationFn: (reminder: Reminder) => cancelReminder(pubkey, reminder),
+    onSuccess: invalidate,
+  });
+
+  return { create, complete, snooze, cancel };
+}
diff --git a/desktop/src/features/reminders/lib/reminderFilters.test.mjs b/desktop/src/features/reminders/lib/reminderFilters.test.mjs
new file mode 100644
index 0000000000..6a1770a8de
--- /dev/null
+++ b/desktop/src/features/reminders/lib/reminderFilters.test.mjs
@@ -0,0 +1,181 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  countDue,
+  dueSince,
+  groupReminders,
+  isDue,
+} from "./reminderFilters.ts";
+
+/**
+ * Build a Reminder fixture. `notBefore` and `status` are the only fields the
+ * filters read besides `createdAt` (for done-group ordering).
+ */
+function reminder({ id = "r", notBefore, status = "pending", createdAt = 0 }) {
+  return {
+    id,
+    eventId: `${id}-evt`,
+    notBefore,
+    createdAt,
+    content: { status },
+  };
+}
+
+const NOW = 1_000;
+
+test("isDue_pending_reminder_at_now_is_due", () => {
+  assert.equal(isDue(reminder({ notBefore: NOW }), NOW), true);
+});
+
+test("isDue_pending_reminder_in_future_is_not_due", () => {
+  assert.equal(isDue(reminder({ notBefore: NOW + 1 }), NOW), false);
+});
+
+test("isDue_done_reminder_in_past_is_not_due", () => {
+  assert.equal(
+    isDue(reminder({ notBefore: NOW - 100, status: "done" }), NOW),
+    false,
+  );
+});
+
+test("isDue_reminder_without_notBefore_is_not_due", () => {
+  assert.equal(isDue(reminder({ notBefore: undefined }), NOW), false);
+});
+
+test("countDue_counts_only_due_pending_reminders", () => {
+  const reminders = [
+    reminder({ id: "a", notBefore: NOW - 1 }),
+    reminder({ id: "b", notBefore: NOW }),
+    reminder({ id: "c", notBefore: NOW + 1 }),
+    reminder({ id: "d", notBefore: NOW - 1, status: "done" }),
+    reminder({ id: "e", notBefore: NOW - 1, status: "cancelled" }),
+  ];
+  assert.equal(countDue(reminders, NOW), 2);
+});
+
+test("countDue_empty_list_returns_zero", () => {
+  assert.equal(countDue([], NOW), 0);
+});
+
+// dueSince — the watermark fire window. Strict lower bound `>`, inclusive `<=`.
+test("dueSince_fires_reminder_crossing_window_since_watermark", () => {
+  const due = dueSince([reminder({ notBefore: 50 })], 0, 100);
+  assert.equal(due.length, 1);
+});
+
+test("dueSince_excludes_reminder_at_exactly_the_watermark", () => {
+  // notBefore === watermark fails the strict `>`: the seed-to-now first-launch
+  // case must not replay an already-due reminder as a toast.
+  const due = dueSince([reminder({ notBefore: 100 })], 100, 200);
+  assert.equal(due.length, 0);
+});
+
+test("dueSince_includes_reminder_at_exactly_now", () => {
+  const due = dueSince([reminder({ notBefore: 100 })], 0, 100);
+  assert.equal(due.length, 1);
+});
+
+test("dueSince_excludes_future_reminder_past_now", () => {
+  const due = dueSince([reminder({ notBefore: 150 })], 0, 100);
+  assert.equal(due.length, 0);
+});
+
+test("dueSince_excludes_non_pending_in_window", () => {
+  const due = dueSince(
+    [
+      reminder({ id: "done", notBefore: 50, status: "done" }),
+      reminder({ id: "cancel", notBefore: 50, status: "cancelled" }),
+    ],
+    0,
+    100,
+  );
+  assert.equal(due.length, 0);
+});
+
+test("dueSince_excludes_reminder_without_notBefore", () => {
+  const due = dueSince([reminder({ notBefore: undefined })], 0, 100);
+  assert.equal(due.length, 0);
+});
+
+test("dueSince_empty_watermark_window_returns_empty", () => {
+  // watermark === now: no new reminder can have crossed since last check.
+  const due = dueSince([reminder({ notBefore: 50 })], 100, 100);
+  assert.equal(due.length, 0);
+});
+
+// groupReminders — bucketing. It reads the real wall clock for "now", so
+// fixtures are anchored to real time. "Today" = up to local end-of-day.
+const realNow = Math.floor(Date.now() / 1_000);
+const realEndOfTodaySecs = (() => {
+  const d = new Date();
+  d.setHours(23, 59, 59, 999);
+  return Math.floor(d.getTime() / 1_000);
+})();
+
+test("groupReminders_buckets_overdue_today_upcoming", () => {
+  // A timestamp strictly between now and end-of-day, robust to running near
+  // midnight: the midpoint can never coincide with either boundary.
+  const todaySecs = Math.floor((realNow + realEndOfTodaySecs) / 2);
+  const groups = groupReminders([
+    reminder({ id: "over", notBefore: realNow - 100 }),
+    reminder({ id: "today", notBefore: todaySecs }),
+    reminder({ id: "soon", notBefore: realEndOfTodaySecs + 86_400 }),
+  ]);
+  const labels = groups.map((g) => g.label);
+  assert.deepEqual(labels, ["Overdue", "Today", "Upcoming"]);
+});
+
+test("groupReminders_omits_empty_buckets", () => {
+  const groups = groupReminders([reminder({ notBefore: realNow - 100 })]);
+  assert.deepEqual(
+    groups.map((g) => g.label),
+    ["Overdue"],
+  );
+});
+
+test("groupReminders_excludes_done_when_includeDone_false", () => {
+  const groups = groupReminders([
+    reminder({ notBefore: realNow - 100, status: "done" }),
+  ]);
+  assert.equal(groups.length, 0);
+});
+
+test("groupReminders_appends_completed_group_when_includeDone_true", () => {
+  const groups = groupReminders(
+    [
+      reminder({ id: "over", notBefore: realNow - 100 }),
+      reminder({ id: "d1", status: "done", createdAt: 1 }),
+      reminder({ id: "d2", status: "done", createdAt: 2 }),
+    ],
+    true,
+  );
+  const completed = groups.find((g) => g.label === "Completed");
+  assert.ok(completed);
+  // Done reminders are sorted newest-first by createdAt.
+  assert.deepEqual(
+    completed.reminders.map((r) => r.id),
+    ["d2", "d1"],
+  );
+});
+
+test("groupReminders_never_surfaces_cancelled_reminders", () => {
+  const groups = groupReminders(
+    [reminder({ notBefore: realNow - 100, status: "cancelled" })],
+    true,
+  );
+  assert.equal(groups.length, 0);
+});
+
+test("groupReminders_empty_list_returns_empty", () => {
+  assert.deepEqual(groupReminders([]), []);
+});
+
+test("groupReminders_buckets_epoch_zero_notBefore_as_overdue", () => {
+  // Guards on `notBefore !== undefined`, matching isDue/dueSince: 0 is kept.
+  const groups = groupReminders([reminder({ notBefore: 0 })]);
+  assert.deepEqual(
+    groups.map((g) => g.label),
+    ["Overdue"],
+  );
+});
diff --git a/desktop/src/features/reminders/lib/reminderFilters.ts b/desktop/src/features/reminders/lib/reminderFilters.ts
new file mode 100644
index 0000000000..57474f244f
--- /dev/null
+++ b/desktop/src/features/reminders/lib/reminderFilters.ts
@@ -0,0 +1,95 @@
+import type { Reminder } from "@/features/reminders/lib/reminderTypes";
+
+const nowSeconds = () => Math.floor(Date.now() / 1_000);
+
+/** A pending reminder whose `notBefore` has arrived (`<= now`). */
+export function isDue(reminder: Reminder, now: number): boolean {
+  return (
+    reminder.content.status === "pending" &&
+    reminder.notBefore !== undefined &&
+    reminder.notBefore <= now
+  );
+}
+
+/**
+ * Count pending reminders that are due or overdue — the single definition
+ * shared by the inbox badge and the fire-on-due hook so the two surfaces can
+ * never disagree on what "due" means.
+ */
+export function countDue(
+  reminders: readonly Reminder[],
+  now: number = nowSeconds(),
+): number {
+  return reminders.filter((r) => isDue(r, now)).length;
+}
+
+/**
+ * Pending reminders that newly crossed `notBefore` since `watermark` — the
+ * fire-on-due window. The strict `>` lower bound is deliberate: a reminder
+ * already past at the seeded watermark (first launch) never fires a toast, so
+ * history is not replayed. The upper bound (`<= now`) excludes future ones.
+ */
+export function dueSince(
+  reminders: readonly Reminder[],
+  watermark: number,
+  now: number,
+): Reminder[] {
+  return reminders.filter(
+    (r) =>
+      r.content.status === "pending" &&
+      r.notBefore !== undefined &&
+      r.notBefore > watermark &&
+      r.notBefore <= now,
+  );
+}
+
+export type ReminderGroup = {
+  label: string;
+  reminders: Reminder[];
+};
+
+/**
+ * Bucket pending reminders into Overdue/Today/Upcoming, and — when
+ * `includeDone` is set — append a Completed group of done reminders. Cancelled
+ * reminders are never surfaced.
+ */
+export function groupReminders(
+  reminders: Reminder[],
+  includeDone = false,
+): ReminderGroup[] {
+  const now = nowSeconds();
+  const endOfToday = new Date();
+  endOfToday.setHours(23, 59, 59, 999);
+  const endOfTodaySecs = Math.floor(endOfToday.getTime() / 1_000);
+
+  const overdue: Reminder[] = [];
+  const today: Reminder[] = [];
+  const upcoming: Reminder[] = [];
+  const done: Reminder[] = [];
+
+  for (const r of reminders) {
+    if (r.content.status === "done") {
+      if (includeDone) done.push(r);
+      continue;
+    }
+    if (r.content.status !== "pending") continue;
+    if (r.notBefore === undefined) continue;
+    if (r.notBefore <= now) {
+      overdue.push(r);
+    } else if (r.notBefore <= endOfTodaySecs) {
+      today.push(r);
+    } else {
+      upcoming.push(r);
+    }
+  }
+
+  done.sort((a, b) => b.createdAt - a.createdAt);
+
+  const groups: ReminderGroup[] = [];
+  if (overdue.length > 0) groups.push({ label: "Overdue", reminders: overdue });
+  if (today.length > 0) groups.push({ label: "Today", reminders: today });
+  if (upcoming.length > 0)
+    groups.push({ label: "Upcoming", reminders: upcoming });
+  if (done.length > 0) groups.push({ label: "Completed", reminders: done });
+  return groups;
+}
diff --git a/desktop/src/features/reminders/lib/timePresets.test.mjs b/desktop/src/features/reminders/lib/timePresets.test.mjs
new file mode 100644
index 0000000000..b11b7ada78
--- /dev/null
+++ b/desktop/src/features/reminders/lib/timePresets.test.mjs
@@ -0,0 +1,78 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  TIME_PRESETS,
+  parseCustomDateTime,
+  todayDateString,
+} from "./timePresets.ts";
+
+const nowSeconds = () => Math.floor(Date.now() / 1_000);
+
+test("TIME_PRESETS_every_preset_returns_strictly_future_timestamp", () => {
+  const now = nowSeconds();
+  for (const preset of TIME_PRESETS) {
+    assert.ok(
+      preset.getTimestamp() > now,
+      `${preset.label} must be strictly in the future`,
+    );
+  }
+});
+
+test("TIME_PRESETS_relative_offsets_match_their_labels", () => {
+  const before = nowSeconds();
+  const byLabel = Object.fromEntries(
+    TIME_PRESETS.map((p) => [p.label, p.getTimestamp()]),
+  );
+  // Allow a 2s window for clock drift across the getTimestamp calls.
+  assert.ok(Math.abs(byLabel["In 30 minutes"] - (before + 30 * 60)) <= 2);
+  assert.ok(Math.abs(byLabel["In 1 hour"] - (before + 60 * 60)) <= 2);
+  assert.ok(Math.abs(byLabel["In 3 hours"] - (before + 3 * 60 * 60)) <= 2);
+});
+
+test("TIME_PRESETS_9am_presets_land_on_a_9am_boundary", () => {
+  for (const label of ["Tomorrow at 9am", "Next Monday at 9am"]) {
+    const preset = TIME_PRESETS.find((p) => p.label === label);
+    const d = new Date(preset.getTimestamp() * 1_000);
+    assert.equal(d.getHours(), 9);
+    assert.equal(d.getMinutes(), 0);
+  }
+});
+
+test("TIME_PRESETS_next_monday_lands_on_a_monday", () => {
+  const preset = TIME_PRESETS.find((p) => p.label === "Next Monday at 9am");
+  const d = new Date(preset.getTimestamp() * 1_000);
+  assert.equal(d.getDay(), 1); // Monday
+});
+
+test("parseCustomDateTime_future_datetime_returns_timestamp", () => {
+  const future = new Date(Date.now() + 24 * 60 * 60 * 1_000);
+  const date = `${future.getFullYear()}-${String(future.getMonth() + 1).padStart(2, "0")}-${String(future.getDate()).padStart(2, "0")}`;
+  const result = parseCustomDateTime(date, "14:30");
+  assert.ok(result !== null);
+  assert.ok(result > nowSeconds());
+});
+
+test("parseCustomDateTime_past_datetime_returns_null", () => {
+  // One year ago — unambiguously past regardless of run time.
+  const past = new Date(Date.now() - 365 * 24 * 60 * 60 * 1_000);
+  const date = `${past.getFullYear()}-${String(past.getMonth() + 1).padStart(2, "0")}-${String(past.getDate()).padStart(2, "0")}`;
+  assert.equal(parseCustomDateTime(date, "09:00"), null);
+});
+
+test("parseCustomDateTime_empty_inputs_return_null", () => {
+  assert.equal(parseCustomDateTime("", "09:00"), null);
+  assert.equal(parseCustomDateTime("2099-01-01", ""), null);
+  assert.equal(parseCustomDateTime("", ""), null);
+});
+
+test("parseCustomDateTime_malformed_inputs_return_null", () => {
+  assert.equal(parseCustomDateTime("not-a-date", "09:00"), null);
+  assert.equal(parseCustomDateTime("2099-01-01", "99:99"), null);
+});
+
+test("todayDateString_returns_today_in_YYYY_MM_DD_local", () => {
+  const now = new Date();
+  const expected = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
+  assert.equal(todayDateString(), expected);
+});
diff --git a/desktop/src/features/reminders/lib/timePresets.ts b/desktop/src/features/reminders/lib/timePresets.ts
new file mode 100644
index 0000000000..90400e2d08
--- /dev/null
+++ b/desktop/src/features/reminders/lib/timePresets.ts
@@ -0,0 +1,66 @@
+/**
+ * Shared reminder time presets — the single source of truth for both the
+ * create dialog and the snooze dropdown. Each preset returns a Unix timestamp
+ * (seconds) strictly in the future.
+ */
+export type TimePreset = {
+  label: string;
+  getTimestamp: () => number;
+};
+
+function nowSeconds(): number {
+  return Math.floor(Date.now() / 1_000);
+}
+
+/**
+ * Next occurrence of `dayOffset` days from now at 9am local time. If that
+ * instant is already past (e.g. it is after 9am and offset is 0), roll to the
+ * following day so the result is always in the future.
+ */
+function nextDayAt9am(dayOffset: number): number {
+  const now = new Date();
+  const target = new Date(now);
+  target.setDate(target.getDate() + dayOffset);
+  target.setHours(9, 0, 0, 0);
+  if (target.getTime() <= now.getTime()) {
+    target.setDate(target.getDate() + 1);
+  }
+  return Math.floor(target.getTime() / 1_000);
+}
+
+export const TIME_PRESETS: TimePreset[] = [
+  { label: "In 30 minutes", getTimestamp: () => nowSeconds() + 30 * 60 },
+  { label: "In 1 hour", getTimestamp: () => nowSeconds() + 60 * 60 },
+  { label: "In 3 hours", getTimestamp: () => nowSeconds() + 3 * 60 * 60 },
+  { label: "Tomorrow at 9am", getTimestamp: () => nextDayAt9am(1) },
+  {
+    label: "Next Monday at 9am",
+    getTimestamp: () => {
+      const daysUntilMonday = (8 - new Date().getDay()) % 7 || 7;
+      return nextDayAt9am(daysUntilMonday);
+    },
+  },
+];
+
+/** Today as `YYYY-MM-DD` in local time, for the custom date input `min`. */
+export function todayDateString(): string {
+  const now = new Date();
+  const year = now.getFullYear();
+  const month = String(now.getMonth() + 1).padStart(2, "0");
+  const day = String(now.getDate()).padStart(2, "0");
+  return `${year}-${month}-${day}`;
+}
+
+/**
+ * Parse a `YYYY-MM-DD` + `HH:MM` pair into a future Unix timestamp (seconds),
+ * or null if the inputs are malformed or not strictly in the future. The shared
+ * guard for both create and snooze custom surfaces: the native time input has
+ * no `min`, so a past time would otherwise fire immediately.
+ */
+export function parseCustomDateTime(date: string, time: string): number | null {
+  if (!date || !time) return null;
+  const timestamp = Math.floor(new Date(`${date}T${time}`).getTime() / 1_000);
+  if (Number.isNaN(timestamp)) return null;
+  if (timestamp <= nowSeconds()) return null;
+  return timestamp;
+}
diff --git a/desktop/src/features/reminders/ui/RemindMeLaterDialog.tsx b/desktop/src/features/reminders/ui/RemindMeLaterDialog.tsx
index f17ea27933..83e4cc7c1a 100644
--- a/desktop/src/features/reminders/ui/RemindMeLaterDialog.tsx
+++ b/desktop/src/features/reminders/ui/RemindMeLaterDialog.tsx
@@ -2,10 +2,15 @@ import { CalendarClock, Clock } from "lucide-react";
 import * as React from "react";
 import { toast } from "sonner";
 
-import { createReminder } from "@/features/reminders/lib/reminderService";
+import { useReminderMutations } from "@/features/reminders/hooks";
+import {
+  parseCustomDateTime,
+  TIME_PRESETS,
+  todayDateString,
+} from "@/features/reminders/lib/timePresets";
 import type { ReminderTarget } from "@/features/reminders/lib/reminderTypes";
+import { useIdentityQuery } from "@/shared/api/hooks";
 import { Button } from "@/shared/ui/button";
-import { Input } from "@/shared/ui/input";
 import {
   Dialog,
   DialogContent,
@@ -14,59 +19,9 @@ import {
   DialogHeader,
   DialogTitle,
 } from "@/shared/ui/dialog";
+import { Input } from "@/shared/ui/input";
 import { Textarea } from "@/shared/ui/textarea";
 
-type TimePreset = {
-  label: string;
-  getTimestamp: () => number;
-};
-
-function getNextWeekday9am(dayOffset: number): number {
-  const now = new Date();
-  const target = new Date(now);
-  target.setDate(target.getDate() + dayOffset);
-  target.setHours(9, 0, 0, 0);
-  if (target.getTime() <= now.getTime()) {
-    target.setDate(target.getDate() + 1);
-  }
-  return Math.floor(target.getTime() / 1_000);
-}
-
-const TIME_PRESETS: TimePreset[] = [
-  {
-    label: "In 30 minutes",
-    getTimestamp: () => Math.floor(Date.now() / 1_000) + 30 * 60,
-  },
-  {
-    label: "In 1 hour",
-    getTimestamp: () => Math.floor(Date.now() / 1_000) + 60 * 60,
-  },
-  {
-    label: "In 3 hours",
-    getTimestamp: () => Math.floor(Date.now() / 1_000) + 3 * 60 * 60,
-  },
-  {
-    label: "Tomorrow at 9am",
-    getTimestamp: () => getNextWeekday9am(1),
-  },
-  {
-    label: "Next Monday at 9am",
-    getTimestamp: () => {
-      const now = new Date();
-      const daysUntilMonday = (8 - now.getDay()) % 7 || 7;
-      return getNextWeekday9am(daysUntilMonday);
-    },
-  },
-];
-
-function todayDateString(): string {
-  const now = new Date();
-  const year = now.getFullYear();
-  const month = String(now.getMonth() + 1).padStart(2, "0");
-  const day = String(now.getDate()).padStart(2, "0");
-  return `${year}-${month}-${day}`;
-}
-
 export function RemindMeLaterDialog({
   open,
   onOpenChange,
@@ -76,48 +31,26 @@ export function RemindMeLaterDialog({
   onOpenChange: (open: boolean) => void;
   target: ReminderTarget | null;
 }) {
+  const pubkey = useIdentityQuery().data?.pubkey ?? "";
+  const { create } = useReminderMutations(pubkey);
   const [note, setNote] = React.useState("");
-  const [isSubmitting, setIsSubmitting] = React.useState(false);
   const [customDate, setCustomDate] = React.useState(todayDateString);
   const [customTime, setCustomTime] = React.useState("09:00");
+  const customTimestamp = parseCustomDateTime(customDate, customTime);
 
-  const handleSelect = async (preset: TimePreset) => {
-    if (!target || isSubmitting) return;
-    setIsSubmitting(true);
-    try {
-      await createReminder(target, preset.getTimestamp(), note || undefined);
-      toast.success("Reminder set");
-      onOpenChange(false);
-      setNote("");
-    } catch (error) {
-      toast.error("Failed to create reminder");
-      console.error("[RemindMeLaterDialog] create failed:", error);
-    } finally {
-      setIsSubmitting(false);
-    }
-  };
-
-  const handleCustomSubmit = async () => {
-    if (!target || isSubmitting || !customDate || !customTime) return;
-    setIsSubmitting(true);
-    try {
-      const timestamp = Math.floor(
-        new Date(`${customDate}T${customTime}`).getTime() / 1_000,
-      );
-      if (Number.isNaN(timestamp)) {
-        toast.error("Invalid date or time");
-        return;
-      }
-      await createReminder(target, timestamp, note || undefined);
-      toast.success("Reminder set");
-      onOpenChange(false);
-      setNote("");
-    } catch (error) {
-      toast.error("Failed to create reminder");
-      console.error("[RemindMeLaterDialog] custom create failed:", error);
-    } finally {
-      setIsSubmitting(false);
-    }
+  const submit = (notBefore: number) => {
+    if (!target || create.isPending) return;
+    create.mutate(
+      { target, notBefore, note: note || undefined },
+      {
+        onSuccess: () => {
+          toast.success("Reminder set");
+          onOpenChange(false);
+          setNote("");
+        },
+        onError: () => toast.error("Failed to create reminder"),
+      },
+    );
   };
 
   return (
@@ -139,8 +72,8 @@ export function RemindMeLaterDialog({
               key={preset.label}
               variant="outline"
               className="justify-start"
-              disabled={isSubmitting}
-              onClick={() => void handleSelect(preset)}
+              disabled={create.isPending}
+              onClick={() => submit(preset.getTimestamp())}
             >
               {preset.label}
             </Button>
@@ -148,32 +81,35 @@ export function RemindMeLaterDialog({
         </div>
 
         <div className="space-y-3 border-t pt-3">
-          <p className="text-sm font-medium flex items-center gap-2">
+          <p className="flex items-center gap-2 text-sm font-medium">
             <CalendarClock className="h-4 w-4" />
             Custom date & time
           </p>
           <div className="flex gap-2">
             <Input
+              aria-label="Reminder date"
+              className="flex-1"
+              min={todayDateString()}
+              onChange={(e) => setCustomDate(e.target.value)}
               type="date"
               value={customDate}
-              onChange={(e) => setCustomDate(e.target.value)}
-              min={todayDateString()}
-              className="flex-1"
-              aria-label="Reminder date"
             />
             <Input
+              aria-label="Reminder time"
+              className="w-[120px]"
+              onChange={(e) => setCustomTime(e.target.value)}
               type="time"
               value={customTime}
-              onChange={(e) => setCustomTime(e.target.value)}
-              className="w-[120px]"
-              aria-label="Reminder time"
             />
           </div>
           <Button
-            variant="default"
             className="w-full"
-            disabled={isSubmitting || !customDate || !customTime}
-            onClick={() => void handleCustomSubmit()}
+            disabled={create.isPending || customTimestamp === null}
+            onClick={() => {
+              if (customTimestamp === null) return;
+              submit(customTimestamp);
+            }}
+            variant="default"
           >
             Set reminder
           </Button>
@@ -200,7 +136,7 @@ export function RemindMeLaterDialog({
           <Button
             variant="ghost"
             onClick={() => onOpenChange(false)}
-            disabled={isSubmitting}
+            disabled={create.isPending}
           >
             Cancel
           </Button>
diff --git a/desktop/src/features/reminders/ui/RemindMeLaterProvider.tsx b/desktop/src/features/reminders/ui/RemindMeLaterProvider.tsx
index 4aa4483f4f..61c884ef1d 100644
--- a/desktop/src/features/reminders/ui/RemindMeLaterProvider.tsx
+++ b/desktop/src/features/reminders/ui/RemindMeLaterProvider.tsx
@@ -1,14 +1,18 @@
 import * as React from "react";
 
+import { useRemindersQuery } from "@/features/reminders/hooks";
 import type { ReminderTarget } from "@/features/reminders/lib/reminderTypes";
 import { RemindMeLaterDialog } from "./RemindMeLaterDialog";
 
 type RemindMeLaterContextValue = {
   openReminder: (target: ReminderTarget) => void;
+  /** Event IDs of messages with a pending reminder, for channel tinting. */
+  activeReminderEventIds: ReadonlySet<string>;
 };
 
 const RemindMeLaterContext = React.createContext<RemindMeLaterContextValue>({
   openReminder: () => {},
+  activeReminderEventIds: new Set(),
 });
 
 export function useRemindLater() {
@@ -16,8 +20,10 @@ export function useRemindLater() {
 }
 
 export function RemindMeLaterProvider({
+  pubkey,
   children,
 }: {
+  pubkey?: string;
   children: React.ReactNode;
 }) {
   const [open, setOpen] = React.useState(false);
@@ -28,7 +34,25 @@ export function RemindMeLaterProvider({
     setOpen(true);
   }, []);
 
-  const contextValue = React.useMemo(() => ({ openReminder }), [openReminder]);
+  const remindersQuery = useRemindersQuery(pubkey);
+  const reminders = remindersQuery.data;
+  const activeReminderEventIds = React.useMemo(() => {
+    const ids = new Set<string>();
+    for (const reminder of reminders ?? []) {
+      if (
+        reminder.content.status === "pending" &&
+        reminder.content.target?.eventId
+      ) {
+        ids.add(reminder.content.target.eventId);
+      }
+    }
+    return ids;
+  }, [reminders]);
+
+  const contextValue = React.useMemo(
+    () => ({ openReminder, activeReminderEventIds }),
+    [openReminder, activeReminderEventIds],
+  );
 
   return (
     <RemindMeLaterContext.Provider value={contextValue}>
diff --git a/desktop/src/features/reminders/ui/RemindersPanel.tsx b/desktop/src/features/reminders/ui/RemindersPanel.tsx
index 57aab41710..aff838cc93 100644
--- a/desktop/src/features/reminders/ui/RemindersPanel.tsx
+++ b/desktop/src/features/reminders/ui/RemindersPanel.tsx
@@ -1,14 +1,14 @@
-import { Bell, Check, Clock, RotateCcw, X } from "lucide-react";
+import { Bell, Check, Clock, X } from "lucide-react";
 import * as React from "react";
 import { toast } from "sonner";
 
 import {
-  cancelReminder,
-  completeReminder,
-  fetchReminders,
-  snoozeReminder,
-} from "@/features/reminders/lib/reminderService";
+  useRemindersQuery,
+  useReminderMutations,
+} from "@/features/reminders/hooks";
+import { groupReminders } from "@/features/reminders/lib/reminderFilters";
 import type { Reminder } from "@/features/reminders/lib/reminderTypes";
+import { SnoozeMenu } from "@/features/reminders/ui/SnoozeMenu";
 import { Button } from "@/shared/ui/button";
 
 function formatRelativeTime(timestamp: number): string {
@@ -29,205 +29,118 @@ function formatRelativeTime(timestamp: number): string {
   return `in ${Math.floor(diff / 86400)}d`;
 }
 
-type ReminderGroup = {
-  label: string;
-  reminders: Reminder[];
-};
-
-function groupReminders(reminders: Reminder[]): ReminderGroup[] {
-  const now = Math.floor(Date.now() / 1_000);
-  const endOfToday = new Date();
-  endOfToday.setHours(23, 59, 59, 999);
-  const endOfTodaySecs = Math.floor(endOfToday.getTime() / 1_000);
-
-  const overdue: Reminder[] = [];
-  const today: Reminder[] = [];
-  const upcoming: Reminder[] = [];
-
-  for (const r of reminders) {
-    if (r.content.status !== "pending") continue;
-    if (!r.notBefore) continue;
-    if (r.notBefore <= now) {
-      overdue.push(r);
-    } else if (r.notBefore <= endOfTodaySecs) {
-      today.push(r);
-    } else {
-      upcoming.push(r);
-    }
-  }
-
-  const groups: ReminderGroup[] = [];
-  if (overdue.length > 0) groups.push({ label: "Overdue", reminders: overdue });
-  if (today.length > 0) groups.push({ label: "Today", reminders: today });
-  if (upcoming.length > 0)
-    groups.push({ label: "Upcoming", reminders: upcoming });
-  return groups;
-}
-
 function ReminderRow({
   reminder,
   pubkey,
-  onUpdate,
 }: {
   reminder: Reminder;
   pubkey: string;
-  onUpdate: () => void;
 }) {
-  const [isActing, setIsActing] = React.useState(false);
-
-  const handleComplete = async () => {
-    setIsActing(true);
-    try {
-      await completeReminder(pubkey, reminder);
-      toast.success("Reminder completed");
-      onUpdate();
-    } catch {
-      toast.error("Failed to complete reminder");
-    } finally {
-      setIsActing(false);
-    }
+  const { complete, snooze, cancel } = useReminderMutations(pubkey);
+  const isDone = reminder.content.status === "done";
+  const isActing = complete.isPending || snooze.isPending || cancel.isPending;
+
+  const handleComplete = () => {
+    complete.mutate(reminder, {
+      onSuccess: () => toast.success("Reminder completed"),
+      onError: () => toast.error("Failed to complete reminder"),
+    });
   };
 
-  const handleSnooze = async () => {
-    setIsActing(true);
-    try {
-      const newNotBefore = Math.floor(Date.now() / 1_000) + 3600;
-      await snoozeReminder(pubkey, reminder, newNotBefore);
-      toast.success("Snoozed for 1 hour");
-      onUpdate();
-    } catch {
-      toast.error("Failed to snooze reminder");
-    } finally {
-      setIsActing(false);
-    }
+  const handleSnooze = (notBefore: number) => {
+    snooze.mutate(
+      { reminder, notBefore },
+      {
+        onSuccess: () => toast.success("Reminder snoozed"),
+        onError: () => toast.error("Failed to snooze reminder"),
+      },
+    );
   };
 
-  const handleCancel = async () => {
-    setIsActing(true);
-    try {
-      await cancelReminder(pubkey, reminder);
-      toast.success("Reminder cancelled");
-      onUpdate();
-    } catch {
-      toast.error("Failed to cancel reminder");
-    } finally {
-      setIsActing(false);
-    }
+  const handleCancel = () => {
+    cancel.mutate(reminder, {
+      onSuccess: () => toast.success("Reminder cancelled"),
+      onError: () => toast.error("Failed to cancel reminder"),
+    });
   };
 
-  const isOverdue = reminder.notBefore
-    ? reminder.notBefore <= Math.floor(Date.now() / 1_000)
-    : false;
+  const isOverdue =
+    !isDone && reminder.notBefore
+      ? reminder.notBefore <= Math.floor(Date.now() / 1_000)
+      : false;
 
   return (
     <div className="flex items-start gap-3 rounded-md border p-3">
-      <div className="flex-1 min-w-0">
-        <p className="text-sm font-medium truncate">
+      <div className="min-w-0 flex-1">
+        <p className="truncate text-sm font-medium">
           {reminder.content.target?.preview ||
             reminder.content.note ||
             "Reminder"}
         </p>
         {reminder.content.target && reminder.content.note ? (
-          <p className="text-xs text-muted-foreground mt-0.5 truncate">
+          <p className="mt-0.5 truncate text-xs text-muted-foreground">
             {reminder.content.note}
           </p>
         ) : null}
         {reminder.notBefore ? (
           <p
-            className={`text-xs mt-1 ${isOverdue ? "text-destructive font-medium" : "text-muted-foreground"}`}
+            className={`mt-1 text-xs ${isOverdue ? "font-medium text-destructive" : "text-muted-foreground"}`}
           >
-            <Clock className="inline h-3 w-3 mr-1" />
+            <Clock className="mr-1 inline h-3 w-3" />
             {formatRelativeTime(reminder.notBefore)}
           </p>
         ) : null}
       </div>
-      <div className="flex items-center gap-1 shrink-0">
-        <Button
-          size="sm"
-          variant="ghost"
-          className="h-7 w-7 p-0"
-          disabled={isActing}
-          onClick={() => void handleComplete()}
-          title="Complete"
-        >
-          <Check className="h-4 w-4" />
-        </Button>
-        <Button
-          size="sm"
-          variant="ghost"
-          className="h-7 w-7 p-0"
-          disabled={isActing}
-          onClick={() => void handleSnooze()}
-          title="Snooze 1 hour"
-        >
-          <RotateCcw className="h-4 w-4" />
-        </Button>
-        <Button
-          size="sm"
-          variant="ghost"
-          className="h-7 w-7 p-0"
-          disabled={isActing}
-          onClick={() => void handleCancel()}
-          title="Cancel"
-        >
-          <X className="h-4 w-4" />
-        </Button>
-      </div>
+      {isDone ? null : (
+        <div className="flex shrink-0 items-center gap-1">
+          <Button
+            className="h-7 w-7 p-0"
+            disabled={isActing}
+            onClick={handleComplete}
+            size="sm"
+            title="Complete"
+            type="button"
+            variant="ghost"
+          >
+            <Check className="h-4 w-4" />
+          </Button>
+          <SnoozeMenu disabled={isActing} onSnooze={handleSnooze} />
+          <Button
+            className="h-7 w-7 p-0"
+            disabled={isActing}
+            onClick={handleCancel}
+            size="sm"
+            title="Cancel"
+            type="button"
+            variant="ghost"
+          >
+            <X className="h-4 w-4" />
+          </Button>
+        </div>
+      )}
     </div>
   );
 }
 
-export function RemindersPanel({ pubkey }: { pubkey: string }) {
-  const [reminders, setReminders] = React.useState<Reminder[]>([]);
-  const [isLoading, setIsLoading] = React.useState(true);
-
-  const loadReminders = React.useCallback(async () => {
-    try {
-      const fetched = await fetchReminders(pubkey);
-      setReminders(fetched);
-    } catch (error) {
-      console.error("[RemindersPanel] fetch failed:", error);
-    } finally {
-      setIsLoading(false);
-    }
-  }, [pubkey]);
-
-  React.useEffect(() => {
-    void loadReminders();
-  }, [loadReminders]);
-
-  // Due-detection: check every 60s and show toast for newly due reminders.
-  const lastDueCheckRef = React.useRef<number>(Math.floor(Date.now() / 1_000));
-  React.useEffect(() => {
-    const interval = window.setInterval(() => {
-      const now = Math.floor(Date.now() / 1_000);
-      const lastCheck = lastDueCheckRef.current;
-      lastDueCheckRef.current = now;
-
-      for (const r of reminders) {
-        if (r.content.status !== "pending") continue;
-        if (!r.notBefore) continue;
-        if (r.notBefore > lastCheck && r.notBefore <= now) {
-          toast("Reminder due", {
-            description:
-              r.content.target?.preview ||
-              r.content.note ||
-              "A reminder is waiting",
-            icon: <Bell className="h-4 w-4" />,
-          });
-        }
-      }
-    }, 60_000);
-
-    return () => window.clearInterval(interval);
-  }, [reminders]);
-
-  const groups = groupReminders(reminders);
-  const pendingCount = reminders.filter(
-    (r) => r.content.status === "pending",
-  ).length;
+/**
+ * Renders a user's reminders as grouped rows. `includeDone` adds a Completed
+ * group (used by the inbox Reminders view); omit it for pending-only surfaces.
+ */
+export function RemindersPanel({
+  pubkey,
+  includeDone = false,
+}: {
+  pubkey: string;
+  includeDone?: boolean;
+}) {
+  const remindersQuery = useRemindersQuery(pubkey);
+  const reminders = remindersQuery.data;
+  const groups = React.useMemo(
+    () => groupReminders(reminders ?? [], includeDone),
+    [reminders, includeDone],
+  );
 
-  if (isLoading) {
+  if (remindersQuery.isLoading) {
     return (
       <div className="flex h-full items-center justify-center">
         <p className="text-sm text-muted-foreground">Loading reminders...</p>
@@ -235,11 +148,11 @@ export function RemindersPanel({ pubkey }: { pubkey: string }) {
     );
   }
 
-  if (pendingCount === 0) {
+  if (groups.length === 0) {
     return (
       <div className="flex h-full flex-col items-center justify-center gap-2 p-8">
         <Bell className="h-8 w-8 text-muted-foreground/50" />
-        <p className="text-sm text-muted-foreground">No pending reminders</p>
+        <p className="text-sm text-muted-foreground">No reminders</p>
         <p className="text-xs text-muted-foreground/70">
           Use "Remind me later" on any message to create one.
         </p>
@@ -248,33 +161,17 @@ export function RemindersPanel({ pubkey }: { pubkey: string }) {
   }
 
   return (
-    <div className="flex h-full flex-col">
-      <div className="border-b px-4 py-3">
-        <h2 className="text-sm font-semibold flex items-center gap-2">
-          <Bell className="h-4 w-4" />
-          Reminders
-          <span className="text-xs font-normal text-muted-foreground">
-            ({pendingCount})
-          </span>
-        </h2>
-      </div>
-      <div className="flex-1 overflow-y-auto p-4 space-y-4">
-        {groups.map((group) => (
-          <div key={group.label} className="space-y-2">
-            <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
-              {group.label}
-            </h3>
-            {group.reminders.map((r) => (
-              <ReminderRow
-                key={r.id}
-                reminder={r}
-                pubkey={pubkey}
-                onUpdate={() => void loadReminders()}
-              />
-            ))}
-          </div>
-        ))}
-      </div>
+    <div className="flex-1 space-y-4 overflow-y-auto p-4">
+      {groups.map((group) => (
+        <div key={group.label} className="space-y-2">
+          <h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
+            {group.label}
+          </h3>
+          {group.reminders.map((r) => (
+            <ReminderRow key={r.id} reminder={r} pubkey={pubkey} />
+          ))}
+        </div>
+      ))}
     </div>
   );
 }
diff --git a/desktop/src/features/reminders/ui/RemindersScreen.tsx b/desktop/src/features/reminders/ui/RemindersScreen.tsx
deleted file mode 100644
index 69d269542e..0000000000
--- a/desktop/src/features/reminders/ui/RemindersScreen.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useIdentityQuery } from "@/shared/api/hooks";
-import { RemindersPanel } from "./RemindersPanel";
-
-export function RemindersScreen() {
-  const identityQuery = useIdentityQuery();
-
-  if (!identityQuery.data?.pubkey) {
-    return null;
-  }
-
-  return <RemindersPanel pubkey={identityQuery.data.pubkey} />;
-}
diff --git a/desktop/src/features/reminders/ui/SnoozeMenu.tsx b/desktop/src/features/reminders/ui/SnoozeMenu.tsx
new file mode 100644
index 0000000000..f6b4b5799c
--- /dev/null
+++ b/desktop/src/features/reminders/ui/SnoozeMenu.tsx
@@ -0,0 +1,111 @@
+import { Clock } from "lucide-react";
+import * as React from "react";
+
+import {
+  parseCustomDateTime,
+  TIME_PRESETS,
+  todayDateString,
+} from "@/features/reminders/lib/timePresets";
+import { Button } from "@/shared/ui/button";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/shared/ui/dropdown-menu";
+import { Input } from "@/shared/ui/input";
+import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
+
+/**
+ * Clock-icon dropdown of snooze presets plus a "Custom…" popover with a native
+ * date/time picker. Calls `onSnooze` with a future Unix timestamp (seconds).
+ * The custom surface uses the shared {@link parseCustomDateTime} guard so a
+ * past time is rejected rather than firing immediately.
+ */
+export function SnoozeMenu({
+  disabled,
+  onSnooze,
+}: {
+  disabled?: boolean;
+  onSnooze: (notBefore: number) => void;
+}) {
+  const [customOpen, setCustomOpen] = React.useState(false);
+  const [customDate, setCustomDate] = React.useState(todayDateString);
+  const [customTime, setCustomTime] = React.useState("09:00");
+
+  const customTimestamp = parseCustomDateTime(customDate, customTime);
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button
+          className="h-7 w-7 p-0"
+          disabled={disabled}
+          size="sm"
+          title="Snooze"
+          type="button"
+          variant="ghost"
+        >
+          <Clock className="h-4 w-4" />
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end" className="min-w-44">
+        {TIME_PRESETS.map((preset) => (
+          <DropdownMenuItem
+            key={preset.label}
+            onSelect={() => onSnooze(preset.getTimestamp())}
+          >
+            {preset.label}
+          </DropdownMenuItem>
+        ))}
+        <DropdownMenuSeparator />
+        <Popover open={customOpen} onOpenChange={setCustomOpen}>
+          <PopoverTrigger asChild>
+            <DropdownMenuItem
+              onSelect={(event) => {
+                // Keep the dropdown logic from closing the popover trigger.
+                event.preventDefault();
+                setCustomOpen(true);
+              }}
+            >
+              Custom…
+            </DropdownMenuItem>
+          </PopoverTrigger>
+          <PopoverContent align="end" className="w-auto space-y-3">
+            <p className="text-sm font-medium">Snooze until</p>
+            <div className="flex gap-2">
+              <Input
+                aria-label="Snooze date"
+                className="flex-1"
+                min={todayDateString()}
+                onChange={(event) => setCustomDate(event.target.value)}
+                type="date"
+                value={customDate}
+              />
+              <Input
+                aria-label="Snooze time"
+                className="w-[120px]"
+                onChange={(event) => setCustomTime(event.target.value)}
+                type="time"
+                value={customTime}
+              />
+            </div>
+            <Button
+              className="w-full"
+              disabled={customTimestamp === null}
+              onClick={() => {
+                if (customTimestamp === null) return;
+                onSnooze(customTimestamp);
+                setCustomOpen(false);
+              }}
+              type="button"
+            >
+              Snooze
+            </Button>
+          </PopoverContent>
+        </Popover>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}
diff --git a/desktop/src/features/reminders/useReminderNotifications.ts b/desktop/src/features/reminders/useReminderNotifications.ts
new file mode 100644
index 0000000000..c5ba04bafa
--- /dev/null
+++ b/desktop/src/features/reminders/useReminderNotifications.ts
@@ -0,0 +1,118 @@
+import * as React from "react";
+
+import { useRemindersQuery } from "@/features/reminders/hooks";
+import { dueSince } from "@/features/reminders/lib/reminderFilters";
+import type { Reminder } from "@/features/reminders/lib/reminderTypes";
+import {
+  requestDockBounce,
+  sendDesktopNotification,
+} from "@/features/notifications/lib/desktop";
+import type { NotificationSettings } from "@/features/notifications/hooks";
+import {
+  playNotificationSound,
+  resolveSlotSound,
+} from "@/features/notifications/lib/sound";
+
+const WATERMARK_STORAGE_PREFIX = "buzz:lastReminderCheck:";
+const POLL_INTERVAL_MS = 30_000;
+
+function watermarkStorageKey(pubkey: string): string {
+  return `${WATERMARK_STORAGE_PREFIX}${pubkey.trim().toLowerCase()}`;
+}
+
+/**
+ * Read the persisted watermark, seeding it to `now` on first-ever launch.
+ * Seeding to `now` (not 0) is deliberate: a 0 seed would replay the user's
+ * entire reminder history as toasts. A reminder already due at first launch
+ * fails the strict `notBefore > watermark` test and surfaces only in the
+ * panel/badge, never as a toast — see the plan's behavioral note.
+ */
+function readWatermark(pubkey: string): number {
+  const key = watermarkStorageKey(pubkey);
+  const stored = window.localStorage.getItem(key);
+  if (stored !== null) {
+    const parsed = Number(stored);
+    if (Number.isFinite(parsed)) return parsed;
+  }
+  const now = Math.floor(Date.now() / 1_000);
+  window.localStorage.setItem(key, String(now));
+  return now;
+}
+
+/**
+ * App-level fire-on-due detection. On launch and every {@link POLL_INTERVAL_MS}
+ * it fires reminders newly crossing their `not_before` since the persisted
+ * watermark, coalescing multiple due reminders into one toast, then advances
+ * the watermark. The toast respects `desktopEnabled` + the `needs_action`
+ * alert slot. This hook is the sole detector — mount it once at app level.
+ */
+export function useReminderNotifications(
+  pubkey: string | undefined,
+  settings: NotificationSettings,
+): void {
+  const reminders = useRemindersQuery(pubkey).data;
+  const remindersRef = React.useRef<Reminder[]>([]);
+  remindersRef.current = reminders ?? [];
+  const settingsRef = React.useRef(settings);
+  settingsRef.current = settings;
+
+  // Track whether the query has resolved at least once. On mount,
+  // useRemindersQuery is still loading (data === undefined), so
+  // remindersRef.current is []. Without this guard, check() would advance
+  // the watermark past any reminders that came due while the app was closed.
+  const queryResolvedRef = React.useRef(false);
+  if (reminders !== undefined) queryResolvedRef.current = true;
+
+  const fire = React.useEffectEvent((due: Reminder[]) => {
+    const current = settingsRef.current;
+    if (
+      !current.desktopEnabled ||
+      !current.slotAlertsEnabled.needs_action ||
+      due.length === 0
+    ) {
+      return;
+    }
+
+    const body =
+      due.length === 1
+        ? (due[0].content.target?.preview ??
+          due[0].content.note ??
+          "A reminder is waiting")
+        : `${due.length} reminders are due`;
+
+    void sendDesktopNotification({
+      title: "Reminder due",
+      body,
+    }).then((didSend) => {
+      if (!didSend) return;
+      playNotificationSound(resolveSlotSound(current, "needs_action"));
+      void requestDockBounce();
+    });
+  });
+
+  React.useEffect(() => {
+    if (!pubkey) return;
+
+    const check = () => {
+      // Defer until the query has resolved at least once — an empty array from
+      // an unresolved query must not advance the watermark past reminders that
+      // came due while the app was closed (the "missed-while-asleep" window).
+      if (remindersRef.current.length === 0 && !queryResolvedRef.current)
+        return;
+
+      const watermark = readWatermark(pubkey);
+      const now = Math.floor(Date.now() / 1_000);
+      const due = dueSince(remindersRef.current, watermark, now);
+      fire(due);
+      // Advance unconditionally, even when fire() suppressed the toast
+      // (notifications off or needs_action slot muted). Re-enabling later must
+      // not backlog-replay reminders that came due while muted — same no-replay
+      // rationale as seed-to-now. Suppressed reminders still show in panel/badge.
+      window.localStorage.setItem(watermarkStorageKey(pubkey), String(now));
+    };
+
+    check();
+    const interval = window.setInterval(check, POLL_INTERVAL_MS);
+    return () => window.clearInterval(interval);
+  }, [pubkey]);
+}
diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx
index d0c51d3106..9262d27e33 100644
--- a/desktop/src/features/sidebar/ui/AppSidebar.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx
@@ -3,7 +3,6 @@ import {
   Activity,
   ArrowDown,
   ArrowUp,
-  Bell,
   Bot,
   FolderGit2,
   Home,
@@ -101,7 +100,6 @@ type AppSidebarProps = {
     | "home"
     | "channel"
     | "agents"
-    | "reminders"
     | "workflows"
     | "pulse"
     | "projects";
@@ -142,7 +140,6 @@ type AppSidebarProps = {
   onSelectAgents: () => void;
   onSelectProjects: () => void;
   onSelectPulse: () => void;
-  onSelectReminders: () => void;
   onSelectWorkflows: () => void;
   onSelectHome: () => void;
   onSelectChannel: (channelId: string) => void;
@@ -204,7 +201,6 @@ export function AppSidebar({
   onSelectAgents,
   onSelectProjects,
   onSelectPulse,
-  onSelectReminders,
   onSelectWorkflows,
   onSelectHome,
   onSelectChannel,
@@ -539,18 +535,6 @@ export function AppSidebar({
               </SidebarMenuBadge>
             ) : null}
           </SidebarMenuItem>
-          <SidebarMenuItem>
-            <SidebarMenuButton
-              data-testid="open-reminders-view"
-              isActive={selectedView === "reminders"}
-              onClick={onSelectReminders}
-              tooltip="Reminders"
-              type="button"
-            >
-              <Bell className="h-4 w-4" />
-              <span>Reminders</span>
-            </SidebarMenuButton>
-          </SidebarMenuItem>
           <FeatureGate feature="workflows">
             <SidebarMenuItem>
               <SidebarMenuButton
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 197196508f..743e40ded4 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -623,6 +623,9 @@ declare global {
       slotId: string;
     }) => unknown;
     __BUZZ_E2E_SEED_MOCK_REMINDERS__?: (reminders: RelayEvent[]) => void;
+    __BUZZ_E2E_QUERY_CLIENT__?: {
+      invalidateQueries: (filters: { queryKey: readonly unknown[] }) => unknown;
+    };
   }
 }
 
diff --git a/desktop/tests/e2e/reminders-screenshots.spec.ts b/desktop/tests/e2e/reminders-screenshots.spec.ts
index a2ace1abcc..90fd224f2a 100644
--- a/desktop/tests/e2e/reminders-screenshots.spec.ts
+++ b/desktop/tests/e2e/reminders-screenshots.spec.ts
@@ -6,6 +6,39 @@ import { installMockBridge } from "../helpers/bridge";
 const SHOTS = "test-results/reminders";
 const MOCK_PUBKEY = "deadbeef".repeat(8);
 
+// The inbox filter dropdown lives in the home pane, not the chat view. Land on
+// home and wait for the inbox before reaching for the filter trigger.
+async function gotoInboxHome(page: import("@playwright/test").Page) {
+  await page.goto("/");
+  await expect(page.getByTestId("home-inbox")).toBeVisible();
+}
+
+// Reminders is reached by opening the inbox filter dropdown and selecting the
+// "Reminders" option — there is no standalone nav entry or view-mode slider.
+async function openRemindersFilter(page: import("@playwright/test").Page) {
+  await page.getByTestId("inbox-filter-trigger").click();
+  await page.getByRole("menuitemradio", { name: "Reminders" }).click();
+}
+
+// The reminders query mounts (for the badge) before tests seed events, so a
+// bare seed lands behind its cached empty result. Invalidate after seeding to
+// force the refetch that picks up the mock events.
+async function seedReminders(
+  page: import("@playwright/test").Page,
+  events: unknown[],
+) {
+  await page.evaluate((seeded) => {
+    window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.(
+      seeded as Parameters<
+        NonNullable<typeof window.__BUZZ_E2E_SEED_MOCK_REMINDERS__>
+      >[0],
+    );
+    window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
+      queryKey: ["reminders"],
+    });
+  }, events);
+}
+
 function mockReminderEvent(opts: {
   id: string;
   dTag: string;
@@ -32,18 +65,21 @@ test.describe("reminders screenshots", () => {
     await installMockBridge(page);
   });
 
-  test("01 — sidebar shows Reminders nav item", async ({ page }) => {
-    await page.goto("/");
-    await page.getByTestId("channel-general").click();
-    await expect(page.getByTestId("chat-title")).toHaveText("general");
+  test("01 — inbox filter dropdown shows Reminders option", async ({
+    page,
+  }) => {
+    await gotoInboxHome(page);
 
-    const remindersNav = page.getByTestId("open-reminders-view");
-    await expect(remindersNav).toBeVisible();
+    await page.getByTestId("inbox-filter-trigger").click();
+    const remindersOption = page.getByRole("menuitemradio", {
+      name: "Reminders",
+    });
+    await expect(remindersOption).toBeVisible();
     await waitForAnimations(page);
 
     await page.screenshot({
-      path: `${SHOTS}/01-sidebar-reminders-nav.png`,
-      clip: { x: 0, y: 0, width: 256, height: 720 },
+      path: `${SHOTS}/01-inbox-filter-reminders-option.png`,
+      clip: { x: 0, y: 0, width: 900, height: 720 },
     });
   });
 
@@ -106,12 +142,10 @@ test.describe("reminders screenshots", () => {
   });
 
   test("04 — Reminders panel empty state", async ({ page }) => {
-    await page.goto("/");
-    await page.getByTestId("channel-general").click();
-    await expect(page.getByTestId("chat-title")).toHaveText("general");
+    await gotoInboxHome(page);
 
-    await page.getByTestId("open-reminders-view").click();
-    await expect(page.getByText("No pending reminders")).toBeVisible();
+    await openRemindersFilter(page);
+    await expect(page.getByText("No reminders")).toBeVisible();
     await waitForAnimations(page);
 
     await page.screenshot({
@@ -123,9 +157,7 @@ test.describe("reminders screenshots", () => {
   test("05 — Reminders panel with active pending reminder", async ({
     page,
   }) => {
-    await page.goto("/");
-    await page.getByTestId("channel-general").click();
-    await expect(page.getByTestId("chat-title")).toHaveText("general");
+    await gotoInboxHome(page);
 
     // Seed a pending reminder due in the future
     const futureTimestamp = Math.floor(Date.now() / 1000) + 3600;
@@ -140,21 +172,16 @@ test.describe("reminders screenshots", () => {
       status: "pending",
     });
 
-    await page.evaluate(
-      ({ event }) => {
-        window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.([event]);
-      },
-      {
-        event: mockReminderEvent({
-          id: "reminder-active-01",
-          dTag: "rem-active-01",
-          content: reminderContent,
-          notBefore: futureTimestamp,
-        }),
-      },
-    );
+    await seedReminders(page, [
+      mockReminderEvent({
+        id: "reminder-active-01",
+        dTag: "rem-active-01",
+        content: reminderContent,
+        notBefore: futureTimestamp,
+      }),
+    ]);
 
-    await page.getByTestId("open-reminders-view").click();
+    await openRemindersFilter(page);
     await expect(page.getByText("Follow up on this message")).toBeVisible();
     await waitForAnimations(page);
 
@@ -165,9 +192,7 @@ test.describe("reminders screenshots", () => {
   });
 
   test("06 — Reminders panel with fired/overdue reminder", async ({ page }) => {
-    await page.goto("/");
-    await page.getByTestId("channel-general").click();
-    await expect(page.getByTestId("chat-title")).toHaveText("general");
+    await gotoInboxHome(page);
 
     // Seed a reminder that has already fired (notBefore in the past)
     const pastTimestamp = Math.floor(Date.now() / 1000) - 7200;
@@ -195,29 +220,22 @@ test.describe("reminders screenshots", () => {
       status: "pending",
     });
 
-    await page.evaluate(
-      ({ events }) => {
-        window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.(events);
-      },
-      {
-        events: [
-          mockReminderEvent({
-            id: "reminder-overdue-01",
-            dTag: "rem-overdue-01",
-            content: overdueContent,
-            notBefore: pastTimestamp,
-          }),
-          mockReminderEvent({
-            id: "reminder-upcoming-01",
-            dTag: "rem-upcoming-01",
-            content: activeContent,
-            notBefore: futureTimestamp,
-          }),
-        ],
-      },
-    );
-
-    await page.getByTestId("open-reminders-view").click();
+    await seedReminders(page, [
+      mockReminderEvent({
+        id: "reminder-overdue-01",
+        dTag: "rem-overdue-01",
+        content: overdueContent,
+        notBefore: pastTimestamp,
+      }),
+      mockReminderEvent({
+        id: "reminder-upcoming-01",
+        dTag: "rem-upcoming-01",
+        content: activeContent,
+        notBefore: futureTimestamp,
+      }),
+    ]);
+
+    await openRemindersFilter(page);
     await expect(page.getByText("Reply to Alice")).toBeVisible();
     await expect(page.getByRole("heading", { name: "Overdue" })).toBeVisible();
     await waitForAnimations(page);
diff --git a/desktop/tests/helpers/animations.ts b/desktop/tests/helpers/animations.ts
index 2572ae1132..72985a69f6 100644
--- a/desktop/tests/helpers/animations.ts
+++ b/desktop/tests/helpers/animations.ts
@@ -1,15 +1,31 @@
 import type { Page } from "@playwright/test";
 
 /**
- * Wait for all in-flight CSS/Web animations on the page to finish.
+ * Wait for finishing CSS/Web animations on the page to complete, bounded by a
+ * short timeout.
  *
  * Radix UI components animate in via CSS transitions. Playwright's
  * `toBeVisible()` resolves mid-animation, producing greyed-out or
  * partially-rendered screenshots. Call this before any `page.screenshot()`
  * or `locator.screenshot()` to guarantee a fully-rendered frame.
+ *
+ * Looping animations (spinners, pulsing presence dots) never settle, and
+ * animations can also start or restart between sampling and awaiting their
+ * `.finished` promise — so an unbounded `Promise.all` can hang until
+ * Playwright aborts the `evaluate`. Race the settle against a short ceiling:
+ * once the in-flight transitions have had time to land, take the frame.
  */
-export async function waitForAnimations(page: Page): Promise<void> {
-  await page.evaluate(() =>
-    Promise.all(document.getAnimations().map((a) => a.finished)),
-  );
+export async function waitForAnimations(
+  page: Page,
+  timeoutMs = 1000,
+): Promise<void> {
+  await page.evaluate((ceiling) => {
+    const settled = Promise.all(
+      document.getAnimations().map((a) => a.finished.catch(() => undefined)),
+    );
+    const ceilingHit = new Promise<void>((resolve) =>
+      setTimeout(resolve, ceiling),
+    );
+    return Promise.race([settled.then(() => undefined), ceilingHit]);
+  }, timeoutMs);
 }
