# Inline Journal Feed Design Spec *2026-06-20* --- ## Goal Replace click-through journal entry cards with fully inline posts across the trip page, dailies page, and home page. Each journal entry renders its full content in the feed — title, meta, photo strip, and body text — without requiring navigation to the detail page. --- ## Scope **In scope:** - Journal entry display in `trip.html.twig`, `dailies.html.twig`, `home.html.twig` - New `.journal-post` CSS component and photo strip styles - Dot-sync JS for the photo strip (one shared block in `base.html.twig`) - Map flash animation extended to `.journal-post.is-highlighted` - Test updates for T1, T2 **Out of scope:** - Story cards in the feed — remain as click-through ``, unchanged - The journal entry detail page (`entry.html.twig`) — kept as-is; just not linked from the feed - The post form — photos are already uploaded correctly - Lightbox on the feed — only on the detail page --- ## Layout Each journal entry in the feed renders as: ``` Title (DM Serif Display, ~xl) DATE · 📍 City, Country · ☀️ Weather ← meta row; DATE is the permalink to detail page ┌──────────────────────────────────────┐ │ │ │ Photo (full-width, 3:2 ratio) │ ← swipe left/right for 2–4 photos │ │ └──────────────────────────────────────┘ ● ○ ○ ← dots; hidden when only 1 photo Body text paragraph(s) ──────────────────────────────────────── ← border-bottom separator ``` - **Title** sits above the photo, using `var(--font-display)` at `var(--text-xl)` - **Meta row** (date, location, weather) sits between title and photo; the date is a small `` permalink to the detail page, styled in `var(--color-ink-muted)`. Location and weather are plain text spans - **Photo strip**: CSS scroll-snap, no JS library required for swipe - **Dots**: visible only when the entry has 2+ images; update via scroll listener - **Body**: full entry body text — not truncated, not excerpted - **Separator**: `border-bottom: 1px solid var(--color-border)` on the post root, matching the current entry card separator --- ## HTML Structure ```html

{{ entry.title }}

{% set images = entry.media.images %} {% if images|length > 0 %}
{% for img in images %}
{{ entry.title }}
{% endfor %}
{% if images|length > 1 %} {% endif %} {% endif %}
{{ entry.content|raw }}
``` **Key attribute notes:** - `id="entry-{{ entry.slug }}"` — required for map marker scroll targeting (`document.getElementById`) - `data-type="journal"` — required for the trip page filter bar (`querySelectorAll('[data-type]')`) - `data-lat` / `data-lng` — required for map marker rendering - The `
` root replaces the old `` — the entry is no longer a clickable card The `weather_icons` map (currently defined inline in `entry.html.twig`) must also be defined at the top of `trip.html.twig`, `dailies.html.twig`, and `home.html.twig` so the meta row can use it. --- ## Photo Strip: CSS ```css /* ── Journal post ──────────────────────────────────────────── */ .journal-post { border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-12); margin-bottom: var(--space-12); } .journal-post-header { margin-bottom: var(--space-4); } .journal-post-title { font-family: var(--font-display); font-size: var(--text-xl); font-weight: 400; line-height: var(--leading-snug); color: var(--color-ink); margin-bottom: var(--space-2); } .journal-post-meta { font-size: var(--text-xs); color: var(--color-ink-muted); display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); } .journal-post-permalink { color: var(--color-ink-muted); text-decoration: none; font-weight: 700; letter-spacing: 0.07em; } .journal-post-permalink:hover { color: var(--color-accent); } .journal-post-location, .journal-post-weather { color: var(--color-ink-muted); } /* Photo strip */ .journal-photo-strip { display: flex; overflow-x: scroll; scroll-snap-type: x mandatory; scrollbar-width: none; border-radius: var(--radius-md); margin-bottom: var(--space-3); } .journal-photo-strip::-webkit-scrollbar { display: none; } .journal-photo-slide { flex: 0 0 100%; scroll-snap-align: start; aspect-ratio: 3 / 2; overflow: hidden; } .journal-photo-slide img { width: 100%; height: 100%; object-fit: cover; display: block; } /* Dot indicators */ .journal-photo-dots { display: flex; justify-content: center; gap: var(--space-2); margin-bottom: var(--space-4); } .journal-photo-dot { width: 6px; height: 6px; border-radius: 9999px; background: var(--color-border); transition: background 0.2s; } .journal-photo-dot.is-active { background: var(--color-ink-muted); } /* Body */ .journal-post-body { font-size: var(--text-base); line-height: var(--leading-normal); color: var(--color-ink-2); } .journal-post-body p { margin-bottom: var(--space-4); } .journal-post-body p:last-child { margin-bottom: 0; } /* Map flash — extends existing keyframe */ .journal-post.is-highlighted { animation: card-highlight 0.7s ease-out forwards; } ``` --- ## Photo Strip: JS One shared script block added to `base.html.twig`, just before ``. It is a no-op on pages with no strips. ```html ``` --- ## CSS Cleanup The following selectors are used exclusively by the old journal entry card and can be removed from `style.css` once the new `.journal-post` component is in place. Story cards in the feed (`entry-card--story`) do **not** use them: - `.entry-card-textmeta` and children (`.entry-date-plain`, `.entry-location-plain`) - `.entry-card-photo-overlay` and children (`.entry-date-overlay`, `.entry-location-overlay`) - `.entry-excerpt` - `.entry-read-more` - `.entry-card .entry-title` — the title rule scoped to `.entry-card`; replace with `.journal-post-title` - `.entry-card:hover .entry-card-photo img` — photo zoom on hover; journal posts have no hover interaction - `.entry-card:hover .entry-title` — title tint on hover; same reason - `.entry-card.is-highlighted` — replaced by `.journal-post.is-highlighted` **Keep** the following — they are still used by story cards (`entry-card--story`) or elsewhere: - `.entry-card` base styles — story cards still use this class - `.entry-card-photo` and `.entry-card-photo img` — story cards use `.entry-card-photo--story` - `.entry-card:hover` background lift (in the shared three-card selector) — story cards still hover - All single-entry-page styles (`.entry-hero`, `.entry-header`, `.entry-body`, etc.) --- ## Test Updates **T1** (`tests/ui/dailies.spec.js`): ```js // OLD await expect(page.locator('.entry-card').first()).toBeVisible(); // NEW await expect(page.locator('.journal-post').first()).toBeVisible(); ``` **T2** (`tests/ui/dailies.spec.js`): ```js // OLD — used href on the root const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`); const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`); // ... findIndex(c => c === el) // NEW — use id attribute (journal posts are
, not ) const newerCard = page.locator(`#entry-${NEWER_SLUG}`); const olderCard = page.locator(`#entry-${OLDER_SLUG}`); // ... findIndex(c => c.id === el.id) ``` --- ## Out of scope - Swipe velocity / momentum — native browser scroll-snap handles this - Lightbox on the feed photo strip — photos are not tappable in the feed; the detail page retains the lightbox - Lazy-load placeholder shimmer - Image ordering UI — photos appear in filesystem order (same as the detail page gallery)