diff --git a/ROADMAP.md b/ROADMAP.md
index 8021826..8011596 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -98,6 +98,11 @@ Living plan for product quality, auth/email reliability, and automation.
- [x] **Core Web Vitals budget** — LCP + CLS gates on home gallery (`viewer.web-vitals.spec.ts`, `web-vitals-budgets.ts`)
- [x] **Unify `/` and `/search`** — `/search` redirects to `/?…` with full query string (`peopleMode`, `tagsMode`, etc.)
+#### UX polish (2026-08)
+
+- [x] **Favorite sign-in copy** — logged-out heart opens favorites messaging (not report) via `SignInRequiredDialog`
+- [x] **Text contrast** — replace misplaced `text-secondary` (pale wash token) with `text-foreground` / `text-primary` on labels, auth dialogs, menus
+
## Later
- [x] Proper DEV deploy (`next start` + CI image) instead of long-lived `next dev` — `scripts/deploy-viewer.sh`, `ecosystem.config.js.example` uses `next start -p 3001`
diff --git a/e2e/README.md b/e2e/README.md
index 08d2d52..388b736 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -70,6 +70,7 @@ not enabled in this CI job yet.
23. Core Web Vitals budget (`viewer.web-vitals.spec.ts`, `web-vitals-budgets.ts`): LCP + CLS gates on anonymous home gallery load
24. Admin review pages smoke (`admin.review-pages.spec.ts`): FastAPI admin login on `punimtagadmindev` → Identify, Auto-Match, Approve Identified load
25. `/search` legacy URLs redirect to `/` with query string preserved (incl. `peopleMode` / `tagsMode`)
+26. Logged-out favorite (`viewer.favorite-signin.spec.ts`): Sign In Required dialog uses favorites copy, not report copy
See repo root [`ROADMAP.md`](../ROADMAP.md) for gaps and next steps.
diff --git a/e2e/tests/viewer.favorite-signin.spec.ts b/e2e/tests/viewer.favorite-signin.spec.ts
new file mode 100644
index 0000000..dd57e17
--- /dev/null
+++ b/e2e/tests/viewer.favorite-signin.spec.ts
@@ -0,0 +1,27 @@
+import { test, expect } from '../fixtures';
+
+test.describe('favorite sign-in copy @smoke', () => {
+ test('logged-out favorite opens sign-in dialog with favorites messaging', async ({
+ page,
+ playkitConfig,
+ }) => {
+ await page.goto(`${playkitConfig.baseUrl}/`);
+ await expect(
+ page.getByRole('button', { name: /Sign in/i }).or(page.getByLabel('Account menu')).first(),
+ ).toBeVisible({ timeout: 20_000 });
+
+ // Open photo viewer (matches the reported bug path) then click the heart.
+ const photoButton = page.locator('main .aspect-square button').first();
+ await expect(photoButton).toBeVisible({ timeout: 15_000 });
+ await photoButton.click();
+ const viewer = page.getByRole('dialog', { name: /Photo viewer/i });
+ await expect(viewer).toBeVisible({ timeout: 15_000 });
+
+ await viewer.getByRole('button', { name: 'Add to favorites' }).click();
+
+ const signInDialog = page.getByRole('dialog').filter({ hasText: 'Sign In Required' });
+ await expect(signInDialog).toBeVisible({ timeout: 10_000 });
+ await expect(signInDialog).toContainText(/save favorites|favorite photos/i);
+ await expect(signInDialog).not.toContainText(/report photos/i);
+ });
+});
diff --git a/viewer-frontend/app/HomePageContent.tsx b/viewer-frontend/app/HomePageContent.tsx
index 9fb5846..dd2a00f 100644
--- a/viewer-frontend/app/HomePageContent.tsx
+++ b/viewer-frontend/app/HomePageContent.tsx
@@ -1094,7 +1094,7 @@ export function HomePageContent({ initialPhotos, people, tags }: HomePageContent