diff --git a/apps/web/src/components/reminder-wizard/run-eta-pill.test.tsx b/apps/web/src/components/reminder-wizard/run-eta-pill.test.tsx
new file mode 100644
index 0000000..5c9ab79
--- /dev/null
+++ b/apps/web/src/components/reminder-wizard/run-eta-pill.test.tsx
@@ -0,0 +1,46 @@
+import { describe, it, expect } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { RunEtaPill } from "./run-eta-pill";
+
+describe("RunEtaPill", () => {
+ it("renders nothing for zero targets", () => {
+ const html = renderToStaticMarkup(
+
,
+ );
+ expect(html).toBe("");
+ });
+
+ it("shows green 'Fits before deadline' when ETA finishes within the window", () => {
+ const html = renderToStaticMarkup(
+
,
+ );
+ expect(html).toMatch(/Fits before deadline/);
+ expect(html).toMatch(/min/);
+ expect(html).not.toMatch(/Likely to pause/);
+ expect(html).toContain("emerald");
+ });
+
+ it("shows amber 'Likely to pause' when ETA exceeds the window", () => {
+ const html = renderToStaticMarkup(
+
,
+ );
+ expect(html).toMatch(/Likely to pause/);
+ expect(html).toMatch(/Push the deadline later/);
+ expect(html).toContain("amber");
+ });
+});
diff --git a/apps/web/src/components/reminder-wizard/run-eta-pill.tsx b/apps/web/src/components/reminder-wizard/run-eta-pill.tsx
new file mode 100644
index 0000000..11e486a
--- /dev/null
+++ b/apps/web/src/components/reminder-wizard/run-eta-pill.tsx
@@ -0,0 +1,62 @@
+import { ClockIcon, AlertTriangleIcon } from "lucide-react";
+import { estimateRunDuration } from "@/lib/run-eta";
+
+interface RunEtaPillProps {
+ targetCount: number;
+ fireAt: Date;
+ windowEndAt: Date;
+ timezone: string;
+}
+
+/**
+ * Visible at the wizard's review step and on the per-section edit
+ * pages that change ETA inputs (groups, when). Advisory only — does
+ * NOT block submission. The operator can still schedule a run that's
+ * likely to pause; pause-and-resume covers that case. The pill just
+ * removes the surprise.
+ */
+export function RunEtaPill({
+ targetCount,
+ fireAt,
+ windowEndAt,
+ timezone,
+}: RunEtaPillProps) {
+ if (targetCount <= 0) return null;
+
+ const { durationMinutes, estimatedFinishAt } = estimateRunDuration({
+ targetCount,
+ fireAt,
+ });
+ const fits = estimatedFinishAt.getTime() <= windowEndAt.getTime();
+
+ const finishLocal = new Intl.DateTimeFormat("en-GB", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ timeZone: timezone,
+ }).format(estimatedFinishAt);
+
+ if (fits) {
+ return (
+
+
+
+ ~{durationMinutes} min · finishes ~{finishLocal} · Fits before deadline
+
+
+ );
+ }
+ return (
+
+
+
+
+ ~{durationMinutes} min · finishes ~{finishLocal} · Likely to pause
+
+
+ Push the deadline later or split into smaller runs.
+
+
+
+ );
+}