diff --git a/app/(marketing)/forgot-password/page.tsx b/app/(marketing)/forgot-password/page.tsx
new file mode 100644
index 0000000..6696750
--- /dev/null
+++ b/app/(marketing)/forgot-password/page.tsx
@@ -0,0 +1,35 @@
+import type { Metadata } from "next";
+import { Container } from "@/components/ui/Container";
+import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";
+
+export const metadata: Metadata = {
+ title: "Reset your password",
+ description: "Request a link to reset your PetroBrain password.",
+ robots: { index: false, follow: false },
+};
+
+/**
+ * /forgot-password — request a password-reset link. Lives in the marketing zone alongside
+ * /login and /signup so the auth flow stays on-theme. The emailed link returns the user to
+ * /reset-password with a one-time token.
+ */
+export default function ForgotPasswordPage() {
+ return (
+
+
+
+
+
+ Reset your password
+
+
+ Enter the email tied to your workspace and we’ll send you a link to set a new
+ password.
+
+
+
+
+
+
+ );
+}
diff --git a/app/(marketing)/reset-password/page.tsx b/app/(marketing)/reset-password/page.tsx
new file mode 100644
index 0000000..eb8c66a
--- /dev/null
+++ b/app/(marketing)/reset-password/page.tsx
@@ -0,0 +1,36 @@
+import type { Metadata } from "next";
+import { Suspense } from "react";
+import { Container } from "@/components/ui/Container";
+import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm";
+
+export const metadata: Metadata = {
+ title: "Set a new password",
+ description: "Choose a new password for your PetroBrain workspace.",
+ robots: { index: false, follow: false },
+};
+
+/**
+ * /reset-password — the landing page for the emailed reset link. The form reads the
+ * one-time `?token` via useSearchParams, so it's wrapped in Suspense (Next requirement).
+ */
+export default function ResetPasswordPage() {
+ return (
+
+
+
+
+
+ Set a new password
+
+
+ Choose a new password to finish signing back in.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/auth/ForgotPasswordForm.tsx b/components/auth/ForgotPasswordForm.tsx
new file mode 100644
index 0000000..87f16e3
--- /dev/null
+++ b/components/auth/ForgotPasswordForm.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Field } from "@/components/ui/Field";
+import { Input } from "@/components/ui/Input";
+import { Button } from "@/components/ui/Button";
+import { Banner } from "@/components/ui/Banner";
+import { authClient } from "@/lib/auth/client";
+
+/**
+ * ForgotPasswordForm — kicks off a password reset via Neon Auth (Better Auth).
+ * We always show the same "check your inbox" confirmation on success regardless of
+ * whether the email exists, so the form can't be used to probe which accounts exist.
+ * `redirectTo` is where the emailed link lands; Better Auth appends `?token=…` there.
+ */
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+export function ForgotPasswordForm() {
+ const [email, setEmail] = useState("");
+ const [error, setError] = useState(undefined);
+ const [status, setStatus] = useState<"idle" | "submitting" | "sent" | "error">("idle");
+ const [submitError, setSubmitError] = useState(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setSubmitError(null);
+
+ if (!EMAIL_RE.test(email.trim())) {
+ setError("Enter a valid email address.");
+ document.getElementById("email")?.focus();
+ return;
+ }
+ setError(undefined);
+
+ setStatus("submitting");
+ try {
+ const res = await authClient.requestPasswordReset({
+ email: email.trim(),
+ redirectTo: `${window.location.origin}/reset-password`,
+ });
+ if (res?.error) throw new Error(res.error.message || "We couldn’t send the reset email.");
+ setStatus("sent");
+ } catch (err) {
+ setStatus("error");
+ setSubmitError(err instanceof Error ? err.message : "We couldn’t send the reset email.");
+ }
+ }
+
+ if (status === "sent") {
+ return (
+
+
+ If an account exists for {email.trim()}, we’ve
+ sent a link to reset your password. The link expires shortly, so use it soon.
+
+