- trip-header/trips-list specs now assert the cropZoom wide-strip aspect and the all-or-nothing retina rule (1x-only for sub-2w sources), and a new regression on us-canada-mex-2024 covers the reported portrait-blur - remove the orphaned AE4 test (no-photos-demo fixture was deleted) - backlog: full-res pixelfed reimport (luxury quality item) - plan: record the cover-quality follow-up + AE4 fixture removal - bump user pin -> 536ca20 (trip-description-hero cover fix) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
310 lines
31 KiB
Markdown
310 lines
31 KiB
Markdown
---
|
||
title: Trip Description, One-liner & Hero Image - Plan
|
||
type: feat
|
||
date: 2026-07-05
|
||
topic: trip-description-and-hero
|
||
artifact_contract: ce-unified-plan/v1
|
||
artifact_readiness: implementation-ready
|
||
product_contract_source: ce-brainstorm
|
||
execution: code
|
||
---
|
||
|
||
# Trip Description, One-liner & Hero Image - Plan
|
||
|
||
**Status:** ✅ Complete (2026-07-06)
|
||
|
||
## Goal Capsule
|
||
|
||
- **Objective:** Give each trip an optional one-liner and description, surface them on the trip list and trip page, and fix the low-resolution trip cover image — all editable from the admin panel.
|
||
- **Product authority:** Mischa (site owner).
|
||
- **Open blockers:** None. Ready for planning.
|
||
|
||
## Product Contract
|
||
|
||
### Summary
|
||
|
||
Add an optional one-liner and an optional description to trips, and render them where they help: the one-liner on both the trip-list cards and the trip page, the description on the trip page only. On the trip page, extend the **existing in-column header** — `home-trip-header` in the shared `trip-feed-col` partial, which already shows title + dates/counts — with the one-liner (below the title) and the description, plus a thin banner image strip (~180–220px) directly below the header text and above the filter bar. No new header is introduced above the map+journal split, and the split itself is unchanged. Because `trip-feed-col` is shared with the homepage active-trip view, these additions are gated to the trip-page caller so that view is unaffected. Fix the trip cover image so it renders sharp (larger derivative + retina `srcset`) and is chosen via an admin media picker, with the current auto-pick fallback retained.
|
||
|
||
### Problem Frame
|
||
|
||
Trips currently carry no human-readable summary anywhere the reader sees. The trip-list cards (`user/themes/intotheeast/templates/trips.html.twig`) show only title, dates, and counts; the trip page (`user/themes/intotheeast/templates/trip.html.twig`) shows the title, dates, and counts only *inside* the feed column — via the shared `trip-feed-col` partial's `home-trip-header` block — and no one-liner, description, or banner image. A `header.tagline` field already exists in the trip blueprint but is used only on homepage highlight cards, and a markdown `content` field (labeled "Description" in admin) exists but is never rendered. Separately, the trip-list cover image auto-picks the first journal entry's first photo and crops it to 720×240, which looks soft — especially on high-DPI screens — and the author has no easy way to choose a better shot.
|
||
|
||
### Key Decisions
|
||
|
||
- **Reuse `header.tagline` as the single one-liner.** The existing tagline field becomes the one source for the short subtitle across all three surfaces (homepage highlight cards, trip list, trip page). Rejected a separate new field: two fields to keep in sync for one concept.
|
||
- **Reuse the markdown `content` field as the description.** It is already editable in admin and labeled "Description"; it is simply not rendered on the trip page yet. Rejected adding a new short-text field.
|
||
- **Extend the existing in-column header; no new header above the split.** The one-liner and description are added to the existing `home-trip-header` block (in the shared `trip-feed-col` partial, which already renders title + dates/counts), with a thin banner strip (~180–220px) directly below the header text and above the filter bar — not a full-bleed hero, and not a separate header above the map+journal split. A slim banner plus text is forgiving of source-photo quality; the full-bleed story-style hero was rejected for pushing primary content below the fold and making the page hostage to photo quality, and a new above-split header was rejected because it would duplicate the title/dates/counts the feed column already shows. Because `trip-feed-col` is shared with the homepage active-trip view, the additions are gated (via a partial parameter) to the trip-page caller so that view is unchanged.
|
||
- **One-liner on the list, one-liner + description on the page.** The list stays scannable (short subtitle only); the fuller description lives on the trip page.
|
||
|
||
### Requirements
|
||
|
||
**Trip data & admin**
|
||
|
||
- R1. A trip's one-liner is stored in the existing `header.tagline` field and remains editable in the admin trip form.
|
||
- R2. A trip's description is stored in the existing markdown `content` field and remains editable in the admin trip form.
|
||
- R3. The admin `header.cover_image` control is a media picker that lets the author select an uploaded image on the trip page, replacing the current type-in-a-filename text field. The picker selects from images uploaded to the trip page's own media; if a trip has none yet, the author uploads one there first, and the R7 auto-pick remains the fallback until a cover is chosen.
|
||
- R4. The one-liner and description are both optional.
|
||
|
||
**Trip list card**
|
||
|
||
- R5. When a trip's one-liner is set, the trip-list card displays it (between title and the dates/counts meta line); when unset, no one-liner line renders.
|
||
- R6. The trip-list card cover image renders sharply on standard and high-DPI displays via a larger derivative (rendered at 1440×480) plus a retina `srcset` (720w and 1440w candidates). Sharpness depends on adequate source resolution — see Dependencies / Assumptions.
|
||
- R7. The card cover image source is the author-selected `cover_image` when set; when unset, it falls back to the first journal entry's first image (current behavior).
|
||
|
||
**Trip page header**
|
||
|
||
- R8. On the trip page, the existing in-column header (`home-trip-header` in the shared `trip-feed-col` partial) renders — each only when set — the one-liner (directly below the title) and the description (below the dates/counts), in addition to the title, dates, and counts it already shows.
|
||
- R9. Directly below the header text and above the filter bar, the trip page renders a thin banner image strip (~180–220px) using the same cover-image source and fallback as the list card (R7), rendered sharply per R6 as a fixed-height center-crop (no focal-point control); when no image is available, the header renders text-only with no banner strip.
|
||
- R10. No new header is added above the map+journal split, and the map + journal two-column split is unchanged in structure and position.
|
||
- R11. If a set `cover_image` no longer resolves (file deleted or moved), the trip-list card and the trip-page banner fall back to the R7 auto-pick rather than rendering a broken image.
|
||
- R12. The one-liner, description, and banner strip are added for the trip-page caller of `trip-feed-col` only (via a partial parameter); the homepage active-trip view's header is unchanged.
|
||
- R13. The one-liner is plain text (soft cap ~120 characters). The description is markdown; the header shows the first ~2–3 lines with the remainder collapsed behind an expand control, so the map+journal split stays above the fold by default while the full description remains readable on demand.
|
||
- R14. The cover/banner image's alt text is the trip title.
|
||
- R15. On narrow/mobile viewports the banner strip and header text reflow without pushing the map+journal split off-screen (e.g. reduced banner height); exact breakpoints are decided during planning.
|
||
|
||
### Acceptance Examples
|
||
|
||
- AE1. **Covers R4, R5, R8.** Given a trip with neither one-liner nor description set, when a reader views the trip list and the trip page, then no one-liner line and no description block render on either surface, and the in-column header still shows the title (and dates/counts if present).
|
||
- AE2. **Covers R8.** Given a trip with a one-liner but no description, when a reader views the trip page, then the in-column header shows the title, one-liner, and dates/counts, and renders no description block.
|
||
- AE3. **Covers R7, R9.** Given a trip with no `cover_image` set but at least one journal entry with an image, when a reader views the list card and the trip-page banner strip, then both show the first entry's first image (sharp per R6).
|
||
- AE4. **Covers R9.** Given a trip with no `cover_image` and no journal-entry images, when a reader views the trip page, then the header renders text-only with no banner strip.
|
||
- AE5. **Covers R6.** Given a trip cover image, when a reader views the trip-list card or the trip-page banner strip on a high-DPI (retina) display, then the larger derivative and retina `srcset` apply and the image renders sharply.
|
||
- AE6. **Covers R10.** Given any trip, when a reader views the trip page, then the map + journal two-column split renders unchanged in structure and position, with no new header inserted above it.
|
||
- AE7. **Covers R12.** Given the active trip, when a reader views the homepage active-trip view, then its in-column header is unchanged — no description block and no banner strip are added there.
|
||
|
||
### Scope Boundaries
|
||
|
||
- The full-bleed, story-style hero banner treatment for trips.
|
||
- A new header rendered above the map+journal split (the one-liner, description, and banner extend the existing in-column header instead).
|
||
- Any change to the map/journal two-column split (layout, columns, feed order, filter bar).
|
||
- Any change to the homepage active-trip view's header (the trip-page additions are gated to the trip-page caller of the shared `trip-feed-col` partial).
|
||
- A separate one-liner field distinct from `header.tagline`, or a separate description field distinct from the markdown `content`.
|
||
- Showing the full description on the trip-list cards.
|
||
- Author-adjustable crop / focal-point control for the banner (fixed center-crop only).
|
||
|
||
### Dependencies / Assumptions
|
||
|
||
- Confirmed (2026-07-05): `header.tagline` and the markdown `content` field are already present and editable in the admin trip form (`trip.yaml`), so R1/R2 need no new admin fields. `header.cover_image` is currently a plain `text` field.
|
||
- Resolved (2026-07-05): the media-picker for `cover_image` (R3) uses Grav core's `pagemediaselect` field type. Confirmed present in the Admin2 v2.0.11 compiled field-type registry (`app/_app/immutable/chunks/DzO1nmNX.js`), where `pagemediaselect`, `mediapicker`, and `filepicker` all route to the same picker component. It binds to the page's own media and stores the selected filename — the same value shape `header.cover_image` holds today — so the `trip.media[cover_image]` template lookups need no change and no text-field fallback is required.
|
||
- Grav's image derivative + `srcset` helpers are available in Twig for producing the larger and retina cover renditions.
|
||
- Cover source photos are assumed ≥1440px wide. A smaller source cannot be sharpened by a larger derivative (Grav upscales), so the R6 sharpness goal depends on adequate source resolution, not just a bigger render box.
|
||
- Before enabling description rendering, grep existing `user/pages/01.trips/*/trip.md` for non-empty `content` bodies and confirm each reads as a public description or is intentionally cleared. Verified empty across the four current `trip.md` files as of 2026-07-05; the check guards future/other trips.
|
||
- Reusing one `header.tagline` across the homepage highlight card, the trip-list card, and the trip-page header assumes the existing per-trip tagline copy reads acceptably on all three; per-surface opt-out is out of scope. Audit current taglines before shipping.
|
||
- Reusing the markdown `content` body as the description means a future long-form trip article distinct from the short summary would require splitting the field — accepted tradeoff.
|
||
|
||
### Follow-up (post-implementation)
|
||
|
||
- Backfill one-liners and descriptions for the active and past trips so the reader-facing summary goal is actually realized — the four current `trip.md` files have empty `content` bodies, so shipping the plumbing alone leaves existing trips showing title/dates only.
|
||
|
||
### Sources / Research
|
||
|
||
- `user/themes/intotheeast/templates/trips.html.twig` — current trip-list card markup and `cropResize(720, 240)` cover logic with first-entry fallback.
|
||
- `user/themes/intotheeast/templates/trip.html.twig` — current trip page (map+feed via `entry-map` and `trip-feed-col` partials; no dedicated header above the split).
|
||
- `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig` — the shared feed-column header (`home-trip-header`: title, dates, counts, filter bar, panel toggles) that this plan extends with the one-liner, description, and banner; **also included by `home.html.twig`'s active-trip branch**, hence the trip-page gating in R12.
|
||
- `user/themes/intotheeast/blueprints/trip.yaml` — existing `header.tagline` (homepage-card copy) and markdown `content` ("Description") fields; `header.cover_image` as a text field.
|
||
- `user/themes/intotheeast/templates/story.html.twig` — existing hero pattern (the rejected full-bleed reference).
|
||
|
||
---
|
||
|
||
## Planning Contract
|
||
|
||
**Product Contract preservation:** changed — R13 (description is now expandable rather than a fixed clamp) and the `cover_image` picker assumption (resolved: `pagemediaselect` confirmed renderable in Admin2 v2.0.11, text-field fallback dropped), both per owner decision on 2026-07-05. All other Product Contract IDs unchanged.
|
||
|
||
### Key Technical Decisions
|
||
|
||
- KTD1. **`pagemediaselect` for the cover field, no fallback.** Change `header.cover_image` in `trip.yaml` from `type: text` to `type: pagemediaselect`. Confirmed renderable in Admin2 v2.0.11 (see the resolved dependency note above). Because it stores the selected filename — the value shape `cover_image` already holds — the existing `trip.media[trip.header.cover_image]` lookups in the templates are unchanged. Rejected the text-field fallback: unnecessary once the field type was verified to render.
|
||
- KTD2. **One shared cover macro, not duplicated resolution.** The trip-list card and the trip-page banner need the same three-step cover resolution (author-selected → first journal entry's first image → none, per R7/R11) and the same retina rendering (R6/R14). Put both in a new `macros/cover.html.twig` so the two surfaces cannot drift. Rejected copy-pasting the current inline `trips.html.twig` logic into the partial: two copies of R7/R11 to keep in sync.
|
||
- KTD3. **Retina via two explicit `cropResize` derivatives + `srcset`, not Grav's native helper.** Render a 1× and a 2× derivative with `cropResize` and emit an explicit `srcset` (e.g. `720w`, `1440w` for the card). This mirrors the existing working `cropResize(720, 240)` call and gives exact control, with no dependency on Grav's auto-`srcset`/`derivatives` config. The CSS crop (`object-fit: cover`, fixed `aspect-ratio`) is unchanged — only the derivative resolution and the `srcset` attribute change. Rejected `Medium.derivatives()`: adds a config dependency for no gain here.
|
||
- KTD4. **Gate the header extras with a partial parameter that defaults off.** Add a `trip_header_extras` parameter to `trip-feed-col.html.twig`, defaulted to `false`. `trip.html.twig` passes it `true`; `home.html.twig` is left untouched, so its `include ... only` omits the parameter and the active-trip header renders exactly as today (satisfies R12/AE7 with zero edits to the home template). Rejected a positive flag on the home caller: more edits, more regression surface, on the branch the plan must not change.
|
||
- KTD5. **Expandable description as inline progressive enhancement.** Render the 2–3-line preview and the full body in markup, and toggle an expanded class with a small inline `<script>` in the partial — the same pattern the partial already uses for `initTripStats`. Avoids touching `js/src/main.js` and the `make build-assets` step. Rejected a fixed CSS-only clamp: it would make the full description unreadable anywhere (owner decision). Rejected a `<details>`/`<summary>` element: harder to style the collapsed state as a clean N-line preview.
|
||
|
||
### High-Level Technical Design
|
||
|
||
The trip-page in-column header (`.home-trip-header`), when `trip_header_extras` is true, stacks in this order. Everything from the filter bar down is unchanged; the home active-trip caller renders only the unshaded rows.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
T["h1 title (existing)"]
|
||
O["one-liner — header.tagline (R8, new)"]
|
||
D["dates (existing)"]
|
||
C["counts (existing)"]
|
||
DESC["description — content, 2-3 line preview + expand (R8/R13, new)"]
|
||
B["banner strip ~180-220px — cover macro (R9, new)"]
|
||
F["filter bar (existing, unchanged)"]
|
||
P["panel toggles (existing, unchanged)"]
|
||
T --> O --> D --> C --> DESC --> B --> F --> P
|
||
SPLIT["map + journal two-column split — unchanged, stays above the fold (R10/R15)"]
|
||
P -.-> SPLIT
|
||
```
|
||
|
||
### Assumptions & Constraints
|
||
|
||
- Only `css/style.css` and the `.html.twig` templates are hand-edited; both are loaded directly (`base.html.twig` links `css/style.css`), so no build step is needed for this work. `css-compiled/main.css` is esbuild output and is not touched.
|
||
- Banner dimensions (1× render box and mobile height) are tunable during implementation within the R9 ~180–220px envelope; the plan fixes the approach, not the exact pixel values.
|
||
- Source photos are assumed ≥1440px wide (Product Contract dependency); a smaller source cannot be sharpened by a larger derivative.
|
||
|
||
### Sequencing
|
||
|
||
U1 and U2 are independent and can land first in either order. U3 and U4 both consume the U2 macro. U5 (CSS) supports U3 and U4 and should land with them for meaningful visual verification. Order: U1 → U2 → (U3, U4) → U5.
|
||
|
||
---
|
||
|
||
## Implementation Units
|
||
|
||
### U1. Cover field → `pagemediaselect`
|
||
|
||
- **Goal:** Replace the type-in-a-filename cover control with an Admin2 media picker (R3).
|
||
- **Requirements:** R3.
|
||
- **Dependencies:** none.
|
||
- **Files:** `user/themes/intotheeast/blueprints/trip.yaml`
|
||
- **Approach:** Change `header.cover_image` from `type: text` to `type: pagemediaselect`. Keep the label, refresh the help text (pick from images uploaded to this trip page). The stored value stays a filename, so no template change is required here.
|
||
- **Patterns to follow:** existing field definitions in `trip.yaml`; field type verified against the Admin2 v2.0.11 registry.
|
||
- **Test scenarios:** Test expectation: none — admin-only blueprint config with no automated test surface. Verified manually in U-level verification: the picker renders in the Admin2 trip form, lists the page's uploaded images, and saves the chosen filename into `header.cover_image`.
|
||
- **Verification:** In Admin2, the trip form shows a media dropdown (not a text box); selecting an image and saving writes its filename to the page header.
|
||
|
||
### U2. Shared cover macro
|
||
|
||
- **Goal:** Centralize cover resolution + retina rendering for reuse by the list card and the trip-page banner (R6, R7, R11, R14).
|
||
- **Requirements:** R6, R7, R11, R14.
|
||
- **Dependencies:** none (U1 not required — resolution reads the same `header.cover_image` filename regardless of how it was set).
|
||
- **Files:** `user/themes/intotheeast/templates/macros/cover.html.twig` (new)
|
||
- **Approach:** Two macros.
|
||
- `resolve(trip_page)` → returns a Medium or null: if `trip_page.header.cover_image` is set and `trip_page.media[...]` resolves, return it; else look up `grav.pages.find(trip_page.route ~ '/dailies')`, take the first published entry's first image if present; else null. This encodes R7 (fallback) and R11 (a set-but-missing `cover_image` falls through to the auto-pick rather than returning a broken reference).
|
||
- `img(medium, alt, w, h)` → emits `<img src=cropResize(w,h).url srcset="…(w)w, …(2w)w" sizes=… alt=alt loading="lazy">` using `cropResize(w, h)` and `cropResize(w*2, h*2)` (R6, R14).
|
||
- **Patterns to follow:** the existing inline resolution in `trips.html.twig:16-29`; the existing `cropResize(...).url` calls in the theme; other macros under `user/themes/intotheeast/templates/macros/`.
|
||
- **Test scenarios:** the macro has no standalone harness; these are asserted through the rendered DOM in U3/U4 specs — `cover_image` set + resolvable returns that image; `cover_image` set but file missing falls back to the first-entry image (R11); no `cover_image` but an entry image exists returns the first-entry image (R7/AE3); no `cover_image` and no entry images returns null (drives AE4); rendered `<img>` carries both `srcset` candidates (R6/AE5) and `alt` equal to the trip title (R14).
|
||
- **Verification:** both U3 and U4 render covers through this macro with identical fallback behavior; no inline cover-resolution logic remains in either caller.
|
||
|
||
### U3. Trip-list card: one-liner + retina cover
|
||
|
||
- **Goal:** Show the one-liner on list cards and render the cover sharply, via the shared macro (R5, R6, R7).
|
||
- **Requirements:** R5, R6, R7, R11, R14.
|
||
- **Dependencies:** U2.
|
||
- **Files:** `user/themes/intotheeast/templates/trips.html.twig`, `tests/ui/trip/trips-list.spec.js` (new)
|
||
- **Approach:** Import `macros/cover.html.twig`. Replace the inline cover block (`trips.html.twig:16-29`) with `cover.resolve(trip)` + `cover.img(cover, trip.title, 720, 240)` inside the existing `.trip-card-cover` wrapper (keeps the 3:1 aspect + `object-fit: cover`). Add a one-liner line rendering `trip.header.tagline`, between `.trip-card-title` and `.trip-card-meta`, only when the tagline is set (R5).
|
||
- **Patterns to follow:** existing card markup and classes in `trips.html.twig`; `.trip-card-cover` CSS at `css/style.css:1089`.
|
||
- **Test scenarios:**
|
||
- Covers R5. A trip with a tagline renders a one-liner element between the title and the meta line.
|
||
- Covers R5/AE1. A trip with no tagline renders no one-liner element.
|
||
- Covers R6/AE5. The card cover `<img>` exposes a `srcset` with 720w and 1440w candidates.
|
||
- Covers R7/AE3. With no `cover_image` set, the card cover uses the first journal entry's first image.
|
||
- Covers R11. With a `cover_image` pointing at a missing file, the card falls back to the auto-pick and renders no broken image.
|
||
- Covers R14. The cover `alt` equals the trip title.
|
||
- **Verification:** the past-trips list shows one-liners where set and sharp covers on a 2× DPR emulation.
|
||
|
||
### U4. Trip-page header extras (gated)
|
||
|
||
- **Goal:** Extend the in-column header with the one-liner, expandable description, and banner strip — for the trip-page caller only (R8, R9, R10, R12, R13).
|
||
- **Requirements:** R8, R9, R10, R12, R13.
|
||
- **Dependencies:** U2.
|
||
- **Files:** `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig`, `user/themes/intotheeast/templates/trip.html.twig`, `tests/ui/trip/trip-header.spec.js` (new), `tests/ui/home/home.spec.js` (extend for AE7)
|
||
- **Approach:** Add a `trip_header_extras` parameter to the partial, `|default(false)`. In `trip.html.twig`'s `include`, pass `trip_header_extras: true`; leave `home.html.twig` untouched (its `include ... only` omits the parameter → default false → unchanged, per KTD4/R12). Inside `.home-trip-header`, gated on the flag and on each value's presence, render in the HTD order: one-liner (`trip_page.header.tagline`) directly below the title (R8); description (`trip_page.content|raw`) below the counts as a 2–3-line preview plus an expand control (R8/R13); banner strip below the description and above the filter bar using `cover.resolve(trip_page)` + `cover.img(...)` at banner dimensions, omitted entirely when resolve returns null (R9/AE4). Add a small inline `<script>` (alongside the existing `initTripStats` script) that toggles the expanded class on the description. The map+journal split and everything from the filter bar down are not touched (R10).
|
||
- **Patterns to follow:** the existing `.home-trip-header` block and inline `<script>` in `trip-feed-col.html.twig`; the `include ... with {...} only` calls in `trip.html.twig` and `home.html.twig`.
|
||
- **Test scenarios:**
|
||
- Covers R8/AE2. Trip page with a tagline and no description shows the one-liner below the title and no description block.
|
||
- Covers R8/R13. Trip page with a description shows a clamped preview plus an expand control that reveals the full text.
|
||
- Covers R8/R9. Trip page with tagline + description + cover shows one-liner, description, and a banner strip positioned above the filter bar.
|
||
- Covers R9/AE3. Trip with no `cover_image` but an entry image shows the banner using the first-entry image.
|
||
- Covers R9/AE4. Trip with no cover and no entry images renders a text-only header with no banner element.
|
||
- Covers R10/AE6. The map + journal two-column split renders unchanged with no new header inserted above it.
|
||
- Covers R12/AE7. The homepage active-trip view renders no description block and no banner strip (assertion added to `home.spec.js`).
|
||
- **Verification:** trip page shows the extras in HTD order and expands the description; the homepage active-trip header is visually identical to before.
|
||
|
||
### U5. Header + banner CSS
|
||
|
||
- **Goal:** Style the one-liner, expandable description, and banner strip, and keep the split above the fold on narrow viewports (R6 display, R9, R13, R15).
|
||
- **Requirements:** R9, R13, R15.
|
||
- **Dependencies:** U3, U4 (styles the markup they add).
|
||
- **Files:** `user/themes/intotheeast/css/style.css`
|
||
- **Approach:** Add rules for the trip-card one-liner, the header one-liner, the description preview/expanded states, the expand control, and `.trip-header-banner` (full width, fixed height in the ~180–220px envelope, `object-fit: cover`, matching radius/spacing of the header). Collapse the description preview with a fixed `max-height` + `overflow: hidden` (the expanded state lifts the cap), **not** `-webkit-line-clamp`: `content|raw` renders multi-paragraph markdown (multiple `<p>`), and line-clamp reliably clamps only a single block box, so it would not hold the 2–3-line preview across paragraphs. Add a mobile `@media` block that reduces banner height and reflows the header text so the map+journal split is not pushed off-screen (R15). The existing `.trip-card-cover` needs no change — `object-fit: cover` + `aspect-ratio: 3/1` already crop the larger derivative.
|
||
- **Patterns to follow:** existing `.home-trip-header`, `.trip-dates`, `.home-trip-counts` (`css/style.css:926-951`) and `.trip-card-cover` (`css/style.css:1089`); the theme's CSS custom properties (`--space-*`, `--text-*`, `--color-*`).
|
||
- **Test scenarios:** Test expectation: none — presentational CSS; structural correctness (element presence, expand toggle) is asserted by U3/U4 specs, and appearance/reflow is verified visually including a narrow-viewport check.
|
||
- **Verification:** on desktop and a mobile viewport, the banner and header text render cleanly and the map+journal split remains visible without scrolling past a wall of header content.
|
||
|
||
---
|
||
|
||
## Verification Contract
|
||
|
||
Dev server: the worktree's Docker dev server at `http://localhost:8081` (`docker compose ... up`). Playwright specs live in the outer repo under `tests/ui/` and run against that server.
|
||
|
||
| Gate | Command / action | Applies to |
|
||
|---|---|---|
|
||
| New + extended UI specs pass | `npx playwright test tests/ui/trip/trips-list.spec.js tests/ui/trip/trip-header.spec.js tests/ui/home/home.spec.js` | U3, U4 |
|
||
| No regression in related suites | `npx playwright test tests/ui/trip tests/ui/home tests/ui/maps` | U4 (shared partial), U5 |
|
||
| Admin picker renders + saves | Manual: Admin2 → trip form → cover field is a media picker → select → save → confirm filename stored | U1 |
|
||
| Retina sharpness | Manual: DevTools at 2× DPR on `/trips` and a trip page → cover/banner load the 1440w derivative | U2, U3, U4 |
|
||
| Acceptance examples | Manual walkthrough of AE1–AE7 against a trip with/without tagline, description, and cover | all |
|
||
|
||
No lint/build step applies — the edited `css/style.css` and templates are served directly.
|
||
|
||
---
|
||
|
||
## Definition of Done
|
||
|
||
**Global**
|
||
|
||
- AE1–AE7 all verified against real trip content (with and without tagline, description, and cover).
|
||
- New specs (`trips-list.spec.js`, `trip-header.spec.js`) and the `home.spec.js` AE7 assertion pass; existing `tests/ui/trip`, `tests/ui/home`, and `tests/ui/maps` suites still pass.
|
||
- Admin2 renders the `pagemediaselect` cover field and persists the selected filename.
|
||
- The homepage active-trip view is visually unchanged (no description block, no banner).
|
||
- No abandoned/experimental markup, CSS, or scripts left in the diff.
|
||
- Content backfill of one-liners and descriptions for existing trips remains a post-implementation follow-up (per the Product Contract) and is **not** required for done.
|
||
|
||
**Per unit**
|
||
|
||
| Unit | Done when |
|
||
|---|---|
|
||
| U1 | Cover field is a working Admin2 media picker storing a filename. |
|
||
| U2 | Both callers resolve and render covers through the macro; no inline cover logic remains. |
|
||
| U3 | List cards show one-liners where set and sharp retina covers with correct fallback; U3 specs pass. |
|
||
| U4 | Trip-page header shows one-liner, expandable description, and gated banner in HTD order; home view unchanged; U4 specs pass. |
|
||
| U5 | Header/banner styled; description expands; split stays above the fold on mobile. |
|
||
|
||
---
|
||
|
||
## Post-review follow-up (2026-07-07)
|
||
|
||
A structured code review of the finished diff produced fixes and two
|
||
intentionally-deferred findings.
|
||
|
||
**Applied**
|
||
|
||
- Cover picker restricted to images (`accept:` on the `cover_image`
|
||
`pagemediaselect` field) + macro resolves against `media.images`, so a
|
||
non-image selection (e.g. a `.gpx` from the trip page media) can no longer
|
||
route a non-image Medium into `cropResize`. Also hardens R11.
|
||
- Test quality: replaced a vacuous `toContainText` in the description-clamp
|
||
spec with real clamp/un-clamp assertions; corrected an R11 over-claim in the
|
||
trips-list spec header comment.
|
||
|
||
**Follow-up (2026-07-07)**
|
||
|
||
- **Banner/card cover quality fix.** The macro used `cropResize`, which
|
||
*fits-inside* preserving aspect ratio — so a portrait fallback source was
|
||
handed back as a ~165px sliver that the `object-fit:cover` box then upscaled
|
||
into a blur (reported on `us-canada-mex-2024`). Switched to **`cropZoom`**
|
||
(crop-to-fill → a real w×h cover strip). Retina is now **all-or-nothing**: the
|
||
2x `srcset` descriptor is emitted only when the source is genuinely ≥2×w
|
||
(`cover.width >= 2w`), else 1x-only — no upscaling, no intermediate widths.
|
||
Note: imported pixelfed photos cap at ~1440px wide, so auto-picked covers are
|
||
usually 1x-only; see `docs/working/backlog.md` (full-res re-import, luxury).
|
||
- **AE4 fixture removed.** The `no-photos-demo` fixture (and its browser test)
|
||
was deleted at the user's request — it surfaced as stray demo content in the
|
||
trip list. AE4 (no cover + no images → no banner) is a trivial else-branch of
|
||
the shared macro's `{% if cover %}` guard, covered by construction alongside
|
||
the R7/AE3 fallback tests. A regression test for the reported portrait-blur
|
||
bug now lives in `trip-header.spec.js` against `us-canada-mex-2024`.
|
||
|
||
**Intentionally deferred — explicit plan override (do not re-flag)**
|
||
|
||
- **Macro re-queries dailies/first-entry (reviewer: efficiency/maintainability).**
|
||
Deferred by design: **KTD2** puts cover resolution *inside* the shared macro
|
||
precisely so the list card and trip banner cannot drift. Moving resolution
|
||
out to callers reopens that drift; the extra `grav.pages.find()` is cached and
|
||
negligible.
|
||
- **Inline `<script>` for the description toggle should be bundled into
|
||
`js/src/main.js` (reviewer: convention).** Deferred by design: **U4's
|
||
Approach** explicitly specifies "a small inline `<script>` (alongside the
|
||
existing `initTripStats` script)." The inline placement is the plan's chosen
|
||
approach for a self-contained ~15-line toggle, not an oversight.
|