diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md
index 9d37834e..596e3088 100644
--- a/e2e/USER_STORIES.md
+++ b/e2e/USER_STORIES.md
@@ -1577,6 +1577,18 @@ Acceptance:
- The thread list below the md breakpoint is a list of links rather than a
table, and alice's thread carries a dot there; bob's own does not.
+### US-114 — See which listings are unread
+
+As bob, I want the same dot on a board whose items are cards rather than rows,
+so that every content section answers "which one is new" the same way.
+
+Acceptance:
+
+- A listing alice posted raises an unread dot on the Classifieds nav item; one
+ bob posted himself does not.
+- Opening the list keeps the dot on the corner of alice's card, and bob's own
+ card carries none.
+
## Out of scope
Features the e2e suite cannot exercise, and what stops it.
diff --git a/e2e/specs/notifications.spec.ts b/e2e/specs/notifications.spec.ts
index 09e518c6..f326ab70 100644
--- a/e2e/specs/notifications.spec.ts
+++ b/e2e/specs/notifications.spec.ts
@@ -1,7 +1,7 @@
import {neventEncode, npubEncode} from "nostr-tools/nip19"
import {HOUR, MINUTE} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
-import {MessagingRelayList, RelayList, Thread} from "@welshman/domain"
+import {Classified, MessagingRelayList, RelayList, Thread} from "@welshman/domain"
import type {Locator, Page} from "@playwright/test"
import {expect, roomPath, spacePath, test, users} from "../harness"
import type {SeededSpace, TestUser} from "../harness"
@@ -481,6 +481,22 @@ test("US-110 see another space's unread activity from a phone", async ({seed, as
const contentNavItem = (page: Page, name: string) =>
page.locator(".secondary-nav").getByRole("link", {name})
+const seedClassified = (space: SeededSpace, user: TestUser, title: string, createdAt: number) =>
+ space.event(
+ user,
+ () =>
+ space
+ .kind(Classified)
+ .writer()
+ .setRoom(space.url, "general")
+ .setIdentifier()
+ .setTitle(title)
+ .setPrice(100)
+ .setContent("in good condition")
+ .renderTemplate(),
+ createdAt,
+ )
+
const seedThread = (space: SeededSpace, user: TestUser, title: string, createdAt: number) =>
space.event(
user,
@@ -579,3 +595,39 @@ test("US-113 see which threads are unread on a phone", async ({seed, as}) => {
await expect(unreadDot(hers)).toBeVisible()
await expect(unreadDot(his)).toHaveCount(0)
})
+
+// Classifieds stands in for the five boards whose items are cards rather than rows — they all
+// render the same UnreadDot off the same content path, and only the corner it sits in differs.
+test("US-114 see which listings are unread", async ({seed, as}) => {
+ const scenario = await seed(({relay, user, at}) => {
+ const space = relay("space")
+
+ space.room("general", {name: "General"})
+ space.join(user.alice, "general")
+ space.join(user.bob, "general")
+
+ seedClassified(space, user.bob, "vintage sextant", at(3, HOUR))
+ seedClassified(space, user.alice, "brass astrolabe", at(2, HOUR))
+ })
+
+ const space = scenario.space("space")
+ const bob = await as(users.bob, roomPath(space.url, "general"))
+
+ const classifiedsNav = contentNavItem(bob, "Classifieds")
+
+ await expect(classifiedsNav).toBeVisible()
+ await expect(unreadDot(classifiedsNav)).toBeVisible()
+
+ await classifiedsNav.click()
+
+ const hers = bob.getByRole("link").filter({hasText: "brass astrolabe"})
+ const his = bob.getByRole("link").filter({hasText: "vintage sextant"})
+
+ await expect(his).toBeVisible()
+
+ // Same tick and throttle as US-112.
+ await bob.waitForTimeout(1500)
+
+ await expect(unreadDot(hers)).toBeVisible()
+ await expect(unreadDot(his)).toHaveCount(0)
+})
diff --git a/src/app/components/ArticleActions.svelte b/src/app/components/ArticleActions.svelte
index 74fdbb01..3c59e64a 100644
--- a/src/app/components/ArticleActions.svelte
+++ b/src/app/components/ArticleActions.svelte
@@ -1,7 +1,6 @@
-
+
+