diff --git a/src/App.css b/src/App.css
index 99f06328..b9b949d5 100644
--- a/src/App.css
+++ b/src/App.css
@@ -5443,17 +5443,63 @@ body.has-player .chat-fab {
so the four columns close on every row with no gap to backfill — and
without `dense` moving a later card up into one, the order cards are read
in stays the order they were ranked in. */
-.journal-grid {
- list-style: none;
+/* The lane and its gutter belong to the feed, not to any one block: the
+ composer cuts the mosaic into several grids and they all have to line up
+ on the same edges. */
+.journal-feed {
margin: 0 auto;
padding: 1.25rem;
+ max-width: 1180px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.journal-grid {
+ list-style: none;
+ margin: 0;
+ padding: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 9rem;
gap: 0.75rem;
- max-width: 1180px;
- width: 100%;
- box-sizing: border-box;
+}
+
+/* ── Sub-block breaks ──────────────────────────────────────────────
+ Three custom properties are the whole contract a theme writes against:
+ the air around the break, the rule through it, and a glyph centred on
+ that rule. `none` on the last two is the real absence of each — no
+ border, and no pseudo-element at all — so a theme that wants no
+ separation zeroes the space and drops the rule, and the break collapses
+ to nothing rather than to an empty band. Themes wanting something a rule
+ can't be (a gradient, a short centred pill) set height and background
+ here directly; the properties are the convenient path, not the only one. */
+.journal-break {
+ --journal-break-space: 1.6rem;
+ --journal-break-rule: 1px solid var(--color-border);
+ --journal-break-ornament: none;
+
+ position: relative;
+ height: 0;
+ margin: var(--journal-break-space) 0;
+ border: none;
+ border-top: var(--journal-break-rule);
+ overflow: visible;
+}
+
+/* Page-coloured, so the glyph opens a gap in the rule exactly as wide as
+ itself instead of sitting on top of it. */
+.journal-break::before {
+ content: var(--journal-break-ornament);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ translate: -50% -50%;
+ padding: 0 0.8em;
+ background: var(--color-bg);
+ color: var(--color-text-muted);
+ font-family: var(--font-display, inherit);
+ font-size: var(--text-sm);
+ line-height: 1;
}
/* ── Footprints ── */
@@ -5745,10 +5791,13 @@ body.has-player .chat-fab {
}
@media (max-width: 460px) {
+ .journal-feed {
+ padding: 0.85rem 1rem;
+ }
+
.journal-grid {
grid-template-columns: 1fr;
grid-auto-rows: auto;
- padding: 0.85rem 1rem;
}
/* Collapse the mosaic into a single readable column. */
diff --git a/src/pages/index/JournalFeed.tsx b/src/pages/index/JournalFeed.tsx
index db648af5..8633669a 100644
--- a/src/pages/index/JournalFeed.tsx
+++ b/src/pages/index/JournalFeed.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react";
+import { Fragment, useMemo } from "react";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ErrorCard } from "../../components/ErrorCard.tsx";
@@ -21,7 +21,7 @@ export function JournalFeed(
removeVote,
}: MainFeedProps,
) {
- const entries = useMemo(() => composeJournal(dumps), [dumps]);
+ const blocks = useMemo(() => composeJournal(dumps), [dumps]);
if (loading) {
return (
@@ -31,7 +31,7 @@ export function JournalFeed(
);
}
if (error) return