The eyebrow label in the top-right of the guide hero was redundant
with the headline beside it. Cut the eyebrow prop and use the page
name ('Donor Guide' / 'Activist Guide') as the actual headline. The
'Back to Help' chip stays on its own row at the top of the hero.
Replace the plain sticky 'Back to Help' bar on the two guide pages
with a hero section in the same recipe as the Organize and Actions
homepage heros: rotating photo banner (HeroBanner) + atmospheric tint
(HeroAtmosphere) + scrims + overlay copy. Sub-page sized — ~280px
instead of the 460px homepage heros — and embeds a glassy
'Back to Help' chip in the eyebrow row so navigation out stays
prominent without a separate sticky strip.
- Donor Guide reuses the World Liberty Congress photos in /public/hero
with the cool palette: reads as community / supporters.
- Activist Guide reuses the protest cover gallery from
DEFAULT_ACTION_COVERS with the warm hope palette: reads as people in
motion.
Both pages drop into the new shared GuideHero component to keep the
two pages DRY.
FAQ updates in the Bitcoin Donations section:
- 'Why on-chain Bitcoin?' now spells out that on-chain donations
require zero extra setup for activists who already have a Nostr
account and zero extra setup for donors who already hold Bitcoin —
the accessibility argument that makes Agora viable for normal
people.
- 'Why doesn't Agora use Lightning?' now names Strike and Breez
alongside Wallet of Satoshi as examples of the popular custodial
Lightning wallets that can be shut down or pressured.
- New 'Why not Monero or another cryptocurrency?' item: Bitcoin's
adoption is what makes it easiest for donors to send and activists
to receive and spend; niche coins create a barrier neither side
should have to clear.
The 'Bitcoin Donations' and 'About Bitcoin Payments on Agora' FAQ
sections were saying overlapping things in two places. Combine them
into a single 'Bitcoin Donations on Agora' section, placed in the
higher slot (right after 'About Agora').
Agora's donation flow is on-chain only, so Lightning and zap items are
removed from the visible FAQ. The two IDs that other pages reference
via HelpTip (send-bitcoin-lightning in ZapDialog, what-are-zaps in
ProfileSettings) are kept in a new hidden 'Legacy' category so those
call sites don't break. HelpFAQSection skips hidden categories on the
default render but the per-ID lookup used by HelpTip still finds them.
Also adds a 'Back to Help' button at the bottom of both the Donor
Guide and Activist Guide pages so users don't have to scroll back up
to the header to navigate out.
Help page now opens with an amber disclaimer that Agora is recommended
only for above-ground activism, followed by two large buttons routing to
new /help/donors and /help/activists guide pages.
The guides cover how on-chain donations work on Agora, why they're
publicly visible on the Bitcoin blockchain and Nostr, and the main paths
for protecting donor privacy or cashing out privately (non-KYC purchase,
coinjoin, Lightning swaps via Boltz, peer-to-peer exchanges like Bisq
and RoboSats). Each tradeoff section is rendered as Pros/Cons bullets.
Also adds an 'About Agora' FAQ category to the existing accordion
covering the design rationale for not using Lightning, silent payments,
or server-rotated addresses.
The inline-markup renderer used by FAQ answers is extracted to
src/lib/helpMarkup.tsx so it can be reused by the guide pages.
Drop the destructive treatment from the disclaimer on campaign donation
surfaces — donating isn't itself dangerous, just publicly traceable, so
red + alert icon + a hard checkbox gate overstated the risk.
Add a 'soft' tone to BitcoinPublicDisclaimer (amber, no icon, no role=
alert) and an includeCashOutAdvice flag so the popover can omit the 'or
cash out at an exchange' line — relevant for the wallet (donor holds
sats) but not for a campaign donor sending money away.
The wallet's Send dialog keeps the destructive variant with the
acknowledgement checkbox unchanged.
User-visible copy now matches the Organization rebrand. Internal
symbols, file names, query keys, routes, and storage keys are
intentionally left alone for this pass — they're still pinned to
"community" / "communities" until a dedicated rename commit.
Touched strings:
- `MobileBottomNav` and `sidebarItems` labels: "Communities" →
"Organize", matching the existing TopNav copy.
- `CommunityDetailPage` hero fallback ("Unnamed Community" →
"Unnamed Organization") and "About this organization" aria-label.
- `CommunityContent` thumbnail fallback name.
- `ExternalContentHeader.CommunityPreview` row label and fallback name.
- `NoteCard` kind-34550 noun "community" → "organization" (used in
feed-card action lines like "created an organization") and the
article switches from "a" to "an".
- `NoteMoreMenu` overflow-menu labels: "Report post to community" →
"Report post to organization", "Remove from community" → "Remove
from organization".
- `BanConfirmDialog` title, description, and success/failure toasts.
- `CommunityContentWarning` reporter pluralization and the
fallback report-type label ("community guidelines" →
"organization guidelines"); reporters are now scoped to founder /
moderators per the commit 4 cleanup, so the wording reflects that.
- `CommunityReportDialog` description copy.
- `CreateGoalDialog` placeholder example.
- `CreateActionDialog` org-scoped description string.
- `CreateCommunityEventDialog` NIP-31 `alt` tag prefix.
- `CommentContext` kind-34550 entries in the action-noun and
rendered-noun maps ("a community" / "community" → "an
organization" / "organization").
- `extraKinds` kind-34550 entry: label, description, and blurb.
- `kindLabels` kinds 4550, 10004, 34550.
- `DiscoverHero` ticker stat copy.
- `GetFeedTool` error message drops "communities" since the
Following feed no longer includes organization activity (removed
in the badge-runtime commit).
Removes NIP-58 badge-award membership validation from Agora's
organization model. Authorization collapses to two roles:
- Founder = author of the kind 34550 event (only the founder can edit
organization metadata, enforced by replaceable-event semantics).
- Moderators = `p` tags with role "moderator" on that event (can hide
content via kind 1984 content-bans; cannot edit the org).
There is no "member" tier any more. The kind 8 / kind 30009 chain
that previously gated discussion access, the members-only feed
filter, the avatar-stack member count, and member-ban moderation
actions are all gone.
What changes:
- `useCommunityMembers` returns `{ founderPubkey, moderatorPubkeys }`
read directly from the parsed community event. The kind 8 badge
query, `resolveMembership`, `isAuthorizedAward`, and the rank-1
member tier are removed. The hook still queries kind 1984 events
scoped to the org so content-bans and soft reports keep working.
- `applyCommunityModerationToEvents` keeps content-ban filtering for
founder/mods. `resolveCommunityModeration` is simplified to a
single pass — only founder/moderators can publish moderation
actions, and the parsed report classification drops the
`member-ban` action since wholesale member bans no longer exist.
- `BanConfirmDialog` drops `mode="member"`. Only event-level content
bans remain. `NoteMoreMenu` drops the "Ban from community"
affordance accordingly.
- `ParsedCommunity` loses `memberBadgeATag` and
`memberBadgeRelayHint`. `parseCommunityEvent` no longer reads the
`['a', '30009:…', '', 'member']` tag. `CreateCommunityPage` no
longer mints a "Member of <org>" badge or attaches the member-badge
tag, and on edit it strips any pre-existing member-badge tag so
legacy wiring doesn't linger.
- The "Voices from everywhere" cross-organization activity feed is
deleted from the Organize page. It duplicated per-org activity (now
served by the official-activity shelves on each org detail page)
and was the only consumer of `useCommunityActivityFeed`.
`useFollowingFeed` drops its community-feed leg for the same
reason. `useMyCommunities`, `useMembersOnlyFilter`, and
`MembersOnlyToggle` go with it.
- `CommunityDetailPage` loses the MembersOnlyToggle in the hero, the
"Add members" and "Edit badge" overflow-menu items, and the
per-member ban affordance in the members dialog. The avatar stack
now renders founder + moderators with a "Founder + N moderators"
label and the dialog title becomes "Leadership".
- Deleted: `AddMemberDialog`, `CommunityBadgePanel` (which exported
`CommunityBadgeEditorDialog`), `useCommunityActivityFeed`,
`useMyCommunities`, `useMembersOnlyFilter`, `MembersOnlyToggle`,
plus three already-unused legacy components (`CommunityChatPanel`,
`CommunityPulsePanel`, `CreateCommunityDialog`) and
`useCommunityChatMessages`. `PersonSearch` is extracted from the
deleted `AddMemberDialog` into its own file so the create-campaign
and create-community forms keep their member-picker UI.
The standalone NIP-58 badge tooling (`BadgesPage`, `GiveBadgeDialog`,
`useBadgeFeed`, etc.) is untouched — it's a separate feature surface
from organization membership and continues to work.
Out of scope: file/symbol renames (Community* → Organization*).
That's the next commit.
Restructures the org detail page body to match the rhythm already
established on /campaigns/:naddr and /pledges/:naddr:
- Container widens from `max-w-3xl` to `max-w-6xl` and the hero gets
the same `sm:aspect-[21/9]` treatment so it doesn't feel cramped at
the wider viewport width.
- The tab strip is removed entirely. Activity was the only visible tab
and the Pulse / Chat triggers were already hidden. With one section
left there's no reason to keep the `<Tabs>` wrapper or its plumbing
(`activeTab`, `setActiveTab`, `fabAvailable`).
- Below the hero: the existing Donate / Share action row, then the
official-activity shelves, then a new pledge-style engagement card
(stats counters + `<PostActionBar>`), then a NIP-22 comments section.
- No funding progress bar — an organization isn't a fundraising target
itself. Campaigns continue to provide that for fundraising.
- Comments render in the same `rounded-2xl bg-card border` frame the
pledge page uses, with the same "No comments yet" dashed empty
card. The legacy interleaved "initiatives + discussion" feed is
gone — official campaigns / pledges / events now live in the
shelves, and the comments section is purely NIP-22 replies.
- Adds `useEventStats`, `InteractionsModal`, `NoteMoreMenu`, and a
separate `replyOpen` slot for the engagement bar's Reply button
(the FAB's "New post" item keeps its own `composeOpen` slot so the
two entry points don't interleave state).
- Removes the now-unused `<CommunityChatPanel>`, `<CommunityPulsePanel>`,
`<ComposeBox>`, `<NoteCard>`, and `<FeedCard>` imports, along with
the `useCommunityGoals` / `useCommunityActions` / `useCommunityEvents`
hooks and the chain of derived state (`moderatedGoals`, `activeGoals`,
`pastGoals`, `eventItems`, `actionEvents`, `activeInitiatives`,
`pastInitiatives`, `activityItems`) that fed the old feed. Those
components still exist in the codebase for now; commit 4 will prune
the badge-award member runtime separately.
The FAB is now always available (single-column page) and routes
`New campaign` / `New pledge` to their dedicated create pages with
`?org=<naddr>`. `New event` still opens the in-page calendar dialog
since there's no dedicated create page for that yet.
The org detail page now surfaces three horizontally-scrolling shelves
above the activity feed:
- Campaigns (kind 30223)
- Pledges (kind 36639)
- Upcoming events (NIP-52 kinds 31922 / 31923)
All three shelves are powered by the useOrganization* hooks added in
5b5e8fe8, which author-filter to founder + moderators before querying
by the org's uppercase `A` root-scope tag. Anyone can publish an
event with an org's `A` tag, so the author filter is the actual
trust boundary that decides what counts as "official" activity for
that organization.
Each shelf is suppressed entirely when it has nothing to show, so an
org with no campaigns/pledges/events keeps the discussion feed at the
top of the viewport.
The activity-tab FAB now routes campaign and pledge creation to the
dedicated create pages with `?org=<naddr>` in the query string,
closing the loop with the implicit-tagging change from 8bef15a3. The
in-page `CreateGoalDialog` and `CreateActionDialog` are removed —
zap goals are being deprecated in favor of campaigns, and pledges now
go through the dedicated create page. The calendar-event dialog stays
in-page since there's no dedicated create page yet; it already emits
the uppercase `A` tag.
Pulse and Chat tab triggers are hidden from the tab strip while the
organization-first redesign is in progress. The corresponding
`<TabsContent>` panels are left intact so the code paths stay
verified — only the trigger affordances are suppressed.
Pledges are now open-ended by default — drop the optional start
date input and stop publishing the legacy `start` tag. The kind
36639 `created_at` already marks when the pledge becomes active.
Hide the built-in cover image templates so the upload area is the
only path for cover art, encouraging pledgers to supply their own
visuals. The thumbnail strip remains available via the
`templates` prop on `CoverImageField` for other surfaces.
Rename the description heading from "Story" to "Description"
to avoid implying a narrative expectation, and label the optional
tag input as "Recommended" to nudge categorization.
Mirror the same changes in `CreateActionDialog` (the community
scoped variant) and mark `start` as legacy in `NIP.md`.
Reverts the user-controlled OrganizationSelector added in b3997ec5.
Selecting an organization from inside a create form was the wrong
affordance: if a user starts a pledge or campaign from outside any
organization, the publication is implicitly under their own identity,
and if they start it from inside an organization, the org tag is
already implied by where the create flow was launched.
CreateCampaignPage and CreateActionPage now read `?org=<naddr|aTag>`
from the URL instead of rendering a selector field. When the param is
present, useManageableOrganizations checks whether the current user is
the founder or a moderator of that org; if so, the form emits the
`A` / `K` / `P` tags on publish. If not, the param is silently
dropped (so a stale or copy-pasted link can't forge an org-tagged
event), and a small note explains the publication is going under the
user's account.
OrganizationContextChip is a tiny shared component that surfaces the
resolved org as a non-interactive chip under the form title. The
organization detail page CTAs (next commit) will populate `?org=`
when launching the create flow from inside an org.
OrganizationSelector.tsx is removed — no other call sites.
Adds an optional Organization selector to the create-campaign and
create-pledge forms. Only NIP-72 communities where the current user
is the founder or a listed moderator are offered, so the resulting
event's uppercase `A` root-scope tag lines up with the trust filter
applied by useOrganizationCampaigns / useOrganizationPledges.
Technically anyone could publish a kind 30223 or 36639 with another
org's `A` tag outside the Agora client. Restricting the selector
inside the client is the first line of defense; the author-filter in
the org-detail queries (next commits) is the actual security boundary.
Changes:
- useManageableOrganizations queries kind 34550 events the user
founded (`authors`) or is moderator-tagged on (`#p`), then keeps
only the ones where they're founder OR moderator.
- OrganizationSelector renders those orgs in a popover combobox with
Founder / Moderator badges, plus a 'No organization' option.
- CreateCampaignPage and CreateActionPage gain a new optional 'Publish
under organization' field. On publish, the form emits
`A: 34550:<pubkey>:<d>` plus the `K` and `P` companion tags.
- CreateCampaignPage hydrates the selector from an existing campaign's
`A` tag when editing.
Existing campaigns and pledges without an `A` tag continue to work
unchanged.
Extract the Send dialog's raw-Bitcoin-address privacy warning into a
shared BitcoinPublicDisclaimer component and reuse it across every
on-chain payment surface on campaign pages:
- BeneficiaryDonatePanel (single-beneficiary BIP-21 "Open in wallet",
also used inside BeneficiaryDonateDialog for multi-beneficiary rows).
- DonateDialog FormView: replaces the milder "public, irreversible,
takes time" alert. The irreversible / network-fee note stays as a
separate muted line below.
- DonateDialog ExternalPayView: the logged-out external-wallet
fallback for single-recipient campaigns.
Each surface now gates its primary action (Open in wallet / Review /
Copy payment URI) on the donor checking "I understand this transaction
is public." The acknowledgement resets when the dialog reopens.
Featured was a hardcoded array of naddrs in src/lib/featuredCampaigns.ts.
Promote it to a third moderation axis (`featured` / `unfeatured`)
alongside `approved` and `hidden`, managed by Team Soapbox pack members
through the same kebab menu on each campaign card.
The Featured row on the homepage now:
- Reads from `moderation.featuredCoords`, ordered newest-featured-label first.
- Caps at 4 campaigns.
- Adapts its grid to 1/2/3/4 desktop columns based on count (mobile stays
one column), collapses when nothing is featured, and surfaces the hero
`variant="featured"` card only when exactly one campaign is featured.
- Hide still wins: a featured-then-hidden campaign disappears from the row.
Rename the homepage's second section from "All campaigns" to "Community
Campaigns", which more accurately reflects that it's the approved-not-
hidden set with featured campaigns deduplicated out.
Add a new /campaigns/all page that lists every campaign found on relays
(approved, pending, and unmoderated alike), with a "Show hidden" toggle
that adds hidden campaigns back in. The Discover page's "All campaigns"
link now points here instead of /, since the homepage is no longer a
truly-all view post-moderation. Also surface a small "Browse all
campaigns" link beneath the Community Campaigns grid so the new page is
discoverable from home.
Update NIP.md to document the third axis and the home-page surfacing
rules (Featured row, Community Campaigns grid, Discover shelf).
Delete src/lib/featuredCampaigns.ts entirely — there's no longer a build-
time list to maintain.
Move campaign curation from a hardcoded HIDDEN_CAMPAIGN_COORDS set to a
real moderation system. Team Soapbox (kind 39089 follow pack
k4p5w0n22suf) is the moderator roster; each member signs NIP-32 kind
1985 labels in the agora.moderation namespace to approve or hide a
campaign. The home page and Discore shelf render the approved-and-
not-hidden set; moderators additionally see Pending + Hidden sections
and a per-card kebab menu. Non-moderator authors get a Your Campaigns
section explaining their campaign is live on Nostr but awaiting a
homepage approval.
The previous attempt replaced NoteContent with plain text, losing rich
rendering of hashtags, mentions, custom emoji, and nostr identifiers.
Reverting to NoteContent while keeping the overflow fix needed two
changes to make line-clamp-3 work:
- Render NoteContent as a <span> (as="span") so it participates in
the parent's -webkit-box line counting. The default <div> wrapper
with overflow-hidden created a separate block formatting context
that defeated line-clamp entirely.
- Add disableNoteEmbeds to prevent block-level EmbeddedNote/
EmbeddedNaddr cards from appearing inside the compact preview.
The outer container keeps overflow-wrap-anywhere so long URLs break
safely within the clamped area.
Regression-of: fc950865
Two follow-ups to the new /communities/new page:
1. Stop rendering the founder as an 'anonymous' chip at the top of the
Moderators section. The row was synthesized from just a pubkey
(genUserName fallback, no avatar), which looked broken even though
the founder is always implicitly the first moderator on the
published kind 34550. Drop the row entirely; the founder remains
pubkey #0 in the moderator list when we publish, just isn't
visible as a chip.
2. Wire the page to handle ?edit=<naddr> the same way
CreateCampaignPage handles its edit param:
- Decode the naddr, reject anything that isn't a kind 34550 with
an 'Invalid edit link' guard card.
- Fetch the existing community (inline useQuery against
['community', pubkey, dTag] since there's no useCommunity hook
yet). Show a 'Loading community…' card while it resolves.
- Prefill name, description, image, and moderators when the data
lands. Resolve each moderator's kind-0 profile via the same
two-step cache-then-network pattern campaigns use for recipients,
so chips render with proper avatars and names instead of fallback
stubs.
- Show a 'Community cannot be edited' guard if the viewer isn't
the founder, mirroring the campaign edit author check.
- On submit, fetchFreshEvent + publish kind 34550 with prev. Strip
d/name/description/image/alt and any p-with-role-moderator tags
from the previous tag set; rebuild them from form state, then
re-append the preserved tags (badge a-tag, relay hints, …) and a
fresh alt. The implicit member badge is left alone in edit mode
(matches CreateCommunityDialog's edit branch).
CommunityDetailPage's 'Edit community' dropdown item now navigates to
/communities/new?edit=<naddr> instead of opening CreateCommunityDialog.
The dialog mount and its editCommunityOpen state are removed.
The dialog file is left in the tree even though nothing imports it
anymore — keeping it makes a revert cleaner if the user changes their
mind.
Both forms had their own CoverPicker / dropzone implementation, and the
two had diverged: the action page learned to accept drag-and-drop while
the campaign page was still click-only. Extract the entire affordance
(dashed dropzone + sanitized preview + remove button + template strip +
URL input) into <CoverImageField> in src/components/, used by both
pages.
The new component takes a controlled value/onChange pair and an optional
templates array, so the campaign page (no templates) and the action page
(six Blossom-hosted defaults) reuse the same dropzone and the same
drag-and-drop, MIME-checked upload path. Clicking a template fills both
the dropzone preview and the URL input from a single source of truth.
While here, dedup three more copy-pastes between the two pages:
- Lift FormSection (the titled section wrapper with the Required /
Recommended / Optional badge) into src/components/FormSection.tsx.
Both pages now import the same component instead of redeclaring it.
- Move getTodayDateInput() into src/lib/dateInput.ts. Both pages need
the same YYYY-MM-DD-in-local-tz string for the deadline picker's min
attribute; keeping it in one place means future timezone tweaks land
in one file.
- Run the action page's coverImage through sanitizeUrl() at submit
time the same way the campaign page does, so a paste-in cover URL
that isn't well-formed https:// drops out of the published 'image'
tag instead of getting written verbatim.
No user-visible behavior change on the action page; the campaign page
gains drag-and-drop and the 'Click or drag an image here' prompt copy.
Net diff: -318 / +13 in the page files.
User flagged that the /i/iso3166:VE page still looked disjointed:
the cinematic hero bled edge-to-edge, the ExternalActionBar sat as
a bare Twitter-style border row below it, the ComposeBox sat raw,
and only the Pinned + Recent feeds were wrapped in their own
FeedCards underneath. Five separate stripes stacked vertically with
no shared container.
Restructure the country branch so the whole surface lives inside
one rounded FeedCard:
- CountryContentHeader (cinematic hero) becomes the top of the card
with its rounded corners clipped by the FeedCard's overflow-hidden.
- Hero gradient's bottom stop changes from hsl(var(--background)) to
hsl(var(--card)) so the fade meets the card surface, not the page
background, eliminating the dark-mode color seam.
- Drop the section's mb-2 since the action bar now sits flush.
- ExternalActionBar sits flush inside the card; its existing
border-b reads as an internal section separator.
- ComposeBox renders with hideBorder + bg-transparent so it inherits
the card surface. Added an optional className prop to ComposeBox
so callers can override the default bg-background/85.
- Pinned section gets a border-t border-border heading band; its
loading state inlines NoteCard-shaped skeletons rather than the
FeedCard-wrapped CommentsSkeleton (which would have card chrome
inside card chrome).
- Recent section's heading becomes a border-t band between Pinned
and Recent when both are present; its loading state also inlines
skeletons; FlatThreadedReplyList sits flush in the card.
- The infinite-scroll sentinel + empty state move outside the card.
URL / unknown content types keep the previous edge-to-edge action
bar treatment (their content headers already render their own
chrome and don't need the unified card).
The previous sweep wrapped vertical NoteCard feeds in FeedCard but
missed three surfaces that share the same Twitter-style edge-to-edge
problem:
- PostDetailPage's focused post (`/nevent…`, `/note…`): the
ancestor previews + AncestorThread + focused <article> all sat
bare on the page background while the replies below were wrapped
in a FeedCard. The main post read like background noise and the
replies read like the actual content. Wrap the entire focused
group (previews + ancestors + focused article — all seven kind
variants funnel through this) in one FeedCard so the page reads
"thread context → this post → replies" as two cohesive cards.
Also wrap the ProfileBadgesDetailView's bare top NoteCard.
- CommunityDetailPage's Activity tab (`/naddr…` community kind
34550): bare `divide-y divide-border` for the activity stream
AND the past-initiatives stream. Replace with FeedCard. The
community page already supplies its own `max-w-3xl px-4 sm:px-6`
wrapper, so the FeedCard opts out of its own margins via `mx-0
sm:mx-0` to avoid doubling up the side padding. The Past-Initiatives
heading gains a `border-t border-border` so it reads as a section
divider inside the card rather than a floating header.
- CommunityPulsePanel: same divide-y → FeedCard fix.
- ExternalContentPage's country pages (`/i/iso3166:VE`): Pinned
posts list was bare while Recent posts below it sat in a FeedCard
— same Pinned-bare / Recent-card mismatch on the same screen. Wrap
Pinned in FeedCard. Add a "Recent" eyebrow heading above the
recent posts feed when both sections are visible, so the two
sections are clearly delineated. Move the Pinned heading's
horizontal padding from `px-4` to `px-4 sm:px-6` so it lines
up with the card margins on tablet+.
After fixing /discover's 'Voices from everywhere' feed, the same
Twitter-style edge-to-edge divided-list pattern was hiding in 24
other feed sections across the app: notifications, profile wall,
search results (posts + accounts + community posts + the empty-state
FollowsList), bookmarks, badges, trends, list members + their posts,
domain feeds, relay feeds, events feed, communities feed, the
Bluesky page, external-content comments + book reviews, post detail
replies (in three render paths), and the FollowPage members tab. On
the GoFundMe-shaped layout they all read as 'random separators
floating in space and posts with no sides.'
Introduce a shared FeedCard component that bakes in the standard
canvas — mx-4 sm:mx-6 rounded-2xl bg-card border border-border/60
shadow-sm overflow-hidden — and replace the bare divide-y wrappers
at all 25 feed-list sites with it. NoteCard rows already self-apply
px-4 py-3 border-b border-border, so live feeds don't need a
divide-y; pure skeleton lists (rows with no own borders) keep
divide-y on the FeedCard className.
Sites intentionally left alone:
- The 8 popover/autocomplete dropdown lists inside max-h-* scrollers
on BlueskyPage, WikipediaPage, BooksPage, and ArchivePage (already
inside bg-popover rounded-lg chrome — double-wrapping clashes).
- CampaignDetailPage's beneficiaries list (already inside a Card,
the divide-y is a section separator).
- CampaignDetailPage's comments card (uses a custom -mx-2 sm:-mx-4
inside the article column; FeedCard's mx-4 sm:mx-6 is wrong for
that nesting).
Also fix TrendsPage's hashtag skeleton: it used a vertical divide-y
list shape but the loaded UI is a flex-wrap of pill badges. Replace
TrendSkeleton with a small h-7 w-24 rounded-full so the skeleton
matches the loaded shape and doesn't pop on transition.