diff --git a/.changeset/sign-in-marks.md b/.changeset/sign-in-marks.md
new file mode 100644
index 0000000..95d2202
--- /dev/null
+++ b/.changeset/sign-in-marks.md
@@ -0,0 +1,29 @@
+---
+"seamless-templates": patch
+---
+
+feat(kit): mark the generated sign-in screen with Seamless Auth and Seamless Idea
+
+Every generated application is shared with the handful of people it was made
+for, and every one of them signs in. That screen is the largest owned surface the
+company has, it grows with the fleet, it costs nothing, and it said nothing at
+all.
+
+`AuthFrame` now carries two lines under the form:
+
+- **Secured by Seamless Auth**, to `seamlessauth.com`. Most of the people
+ reading it are about to use passwordless sign in for the first time, and
+ naming it is the only distribution the auth product gets from the fleet.
+- **Made with Seamless Idea**, to `seamlessidea.com`.
+
+Restraint is the whole design. They sit under the sign-in screens rather than
+beside them, at footnote size in the muted ink role, centred, with no logo and no
+lockup. A generated application has to feel like its owner's, and a banner across
+the top would undo the thing that makes personal software worth having. Both
+links open in a new tab so that nobody mid-sign-in loses the page, and they carry
+`noopener` without `noreferrer`, because the referrer is the only way either
+product ever sees that a generated application sent someone.
+
+There is no switch to turn them off. The templates are copied into the owner's
+project, so the lines can always be deleted; whether a paid tier removes them is
+a decision for the CLI rather than a prop on the kit.
diff --git a/shared/react-app/components/kit/AuthFrame.test.tsx b/shared/react-app/components/kit/AuthFrame.test.tsx
new file mode 100644
index 0000000..99ad01a
--- /dev/null
+++ b/shared/react-app/components/kit/AuthFrame.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import AuthFrame from "./AuthFrame";
+
+function renderFrame() {
+ return render(
+
+
+ ,
+ );
+}
+
+describe("AuthFrame", () => {
+ it("carries both marks, each linked out", () => {
+ renderFrame();
+
+ expect(
+ screen.getByRole("link", { name: "Secured by Seamless Auth" }),
+ ).toHaveAttribute("href", "https://seamlessauth.com");
+ expect(
+ screen.getByRole("link", { name: "Made with Seamless Idea" }),
+ ).toHaveAttribute("href", "https://seamlessidea.com");
+ });
+
+ it("keeps the marks behind the sign-in screens", () => {
+ renderFrame();
+
+ const form = screen.getByRole("button", { name: "Send me a link" });
+ const mark = screen.getByRole("link", { name: "Secured by Seamless Auth" });
+
+ expect(
+ form.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+});
diff --git a/shared/react-app/components/kit/AuthFrame.tsx b/shared/react-app/components/kit/AuthFrame.tsx
index 3d094d8..e63a94b 100644
--- a/shared/react-app/components/kit/AuthFrame.tsx
+++ b/shared/react-app/components/kit/AuthFrame.tsx
@@ -5,9 +5,10 @@ import type { AuthFrameProps } from "./types";
* The signed-out surface: a pitch on one half, the sign-in screens on the other.
*
* `children` is the auth package's own screens. They are rendered once and
- * untouched; nothing here adds an input, a button or a link of its own, because
- * the form is the point of the page and a second call to action beside it is a
- * way of losing people.
+ * untouched; nothing here adds an input or a button of its own, because the form
+ * is the point of the page and a second call to action beside it is a way of
+ * losing people. The two marks under the form are the one exception, and their
+ * weight is what keeps them from being one.
*
* This page is outside the application shell, so it mounts the backdrop itself.
*/
@@ -46,7 +47,35 @@ export default function AuthFrame({
-
{children}
+
+ {children}
+
+ {/* Under the form and at footnote weight, because the application has
+ to read as the owner's rather than as a demo of ours. It is the
+ mark a printer leaves on an invitation, not a banner.
+
+ `noopener` without `noreferrer`: the referrer is the only way
+ either product sees that a generated application sent someone. */}
+
);
diff --git a/templates/web/react-oauth/src/components/kit/AuthFrame.test.tsx b/templates/web/react-oauth/src/components/kit/AuthFrame.test.tsx
new file mode 100644
index 0000000..99ad01a
--- /dev/null
+++ b/templates/web/react-oauth/src/components/kit/AuthFrame.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import AuthFrame from "./AuthFrame";
+
+function renderFrame() {
+ return render(
+
+
+ ,
+ );
+}
+
+describe("AuthFrame", () => {
+ it("carries both marks, each linked out", () => {
+ renderFrame();
+
+ expect(
+ screen.getByRole("link", { name: "Secured by Seamless Auth" }),
+ ).toHaveAttribute("href", "https://seamlessauth.com");
+ expect(
+ screen.getByRole("link", { name: "Made with Seamless Idea" }),
+ ).toHaveAttribute("href", "https://seamlessidea.com");
+ });
+
+ it("keeps the marks behind the sign-in screens", () => {
+ renderFrame();
+
+ const form = screen.getByRole("button", { name: "Send me a link" });
+ const mark = screen.getByRole("link", { name: "Secured by Seamless Auth" });
+
+ expect(
+ form.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+});
diff --git a/templates/web/react-oauth/src/components/kit/AuthFrame.tsx b/templates/web/react-oauth/src/components/kit/AuthFrame.tsx
index 3d094d8..e63a94b 100644
--- a/templates/web/react-oauth/src/components/kit/AuthFrame.tsx
+++ b/templates/web/react-oauth/src/components/kit/AuthFrame.tsx
@@ -5,9 +5,10 @@ import type { AuthFrameProps } from "./types";
* The signed-out surface: a pitch on one half, the sign-in screens on the other.
*
* `children` is the auth package's own screens. They are rendered once and
- * untouched; nothing here adds an input, a button or a link of its own, because
- * the form is the point of the page and a second call to action beside it is a
- * way of losing people.
+ * untouched; nothing here adds an input or a button of its own, because the form
+ * is the point of the page and a second call to action beside it is a way of
+ * losing people. The two marks under the form are the one exception, and their
+ * weight is what keeps them from being one.
*
* This page is outside the application shell, so it mounts the backdrop itself.
*/
@@ -46,7 +47,35 @@ export default function AuthFrame({
-
{children}
+
+ {children}
+
+ {/* Under the form and at footnote weight, because the application has
+ to read as the owner's rather than as a demo of ours. It is the
+ mark a printer leaves on an invitation, not a banner.
+
+ `noopener` without `noreferrer`: the referrer is the only way
+ either product sees that a generated application sent someone. */}
+
);
diff --git a/templates/web/react-vite/src/components/kit/AuthFrame.test.tsx b/templates/web/react-vite/src/components/kit/AuthFrame.test.tsx
new file mode 100644
index 0000000..99ad01a
--- /dev/null
+++ b/templates/web/react-vite/src/components/kit/AuthFrame.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import AuthFrame from "./AuthFrame";
+
+function renderFrame() {
+ return render(
+
+
+ ,
+ );
+}
+
+describe("AuthFrame", () => {
+ it("carries both marks, each linked out", () => {
+ renderFrame();
+
+ expect(
+ screen.getByRole("link", { name: "Secured by Seamless Auth" }),
+ ).toHaveAttribute("href", "https://seamlessauth.com");
+ expect(
+ screen.getByRole("link", { name: "Made with Seamless Idea" }),
+ ).toHaveAttribute("href", "https://seamlessidea.com");
+ });
+
+ it("keeps the marks behind the sign-in screens", () => {
+ renderFrame();
+
+ const form = screen.getByRole("button", { name: "Send me a link" });
+ const mark = screen.getByRole("link", { name: "Secured by Seamless Auth" });
+
+ expect(
+ form.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+});
diff --git a/templates/web/react-vite/src/components/kit/AuthFrame.tsx b/templates/web/react-vite/src/components/kit/AuthFrame.tsx
index 3d094d8..e63a94b 100644
--- a/templates/web/react-vite/src/components/kit/AuthFrame.tsx
+++ b/templates/web/react-vite/src/components/kit/AuthFrame.tsx
@@ -5,9 +5,10 @@ import type { AuthFrameProps } from "./types";
* The signed-out surface: a pitch on one half, the sign-in screens on the other.
*
* `children` is the auth package's own screens. They are rendered once and
- * untouched; nothing here adds an input, a button or a link of its own, because
- * the form is the point of the page and a second call to action beside it is a
- * way of losing people.
+ * untouched; nothing here adds an input or a button of its own, because the form
+ * is the point of the page and a second call to action beside it is a way of
+ * losing people. The two marks under the form are the one exception, and their
+ * weight is what keeps them from being one.
*
* This page is outside the application shell, so it mounts the backdrop itself.
*/
@@ -46,7 +47,35 @@ export default function AuthFrame({
-
{children}
+
+ {children}
+
+ {/* Under the form and at footnote weight, because the application has
+ to read as the owner's rather than as a demo of ours. It is the
+ mark a printer leaves on an invitation, not a banner.
+
+ `noopener` without `noreferrer`: the referrer is the only way
+ either product sees that a generated application sent someone. */}
+