H1Public Trips Gallery Style Specification
Status: LOCKED — Do not deviate without explicit approval
Last updated: 2026-05-11
Applies to: /trips/public (Public Trips Gallery), /trips (Landing — Shared Public Trips section)
Components: PublicTripsPosterGallery.tsx, TripFeaturedTripsSection.tsx
H2Overview
The public trips gallery supports two distinct viewing modes that must remain meaningfully different. This document locks the exact structure, proportions, and behaviour of each mode.
| Mode | Grid Type | Card Shape | Purpose |
|---|---|---|---|
| Posters | Masonry (CSS columns) | Portrait, stepped sizes | Editorial discovery — dramatic, Pinterest-style browsing |
| Grid | CSS Grid (uniform rows) | Landscape 4:3, equal height | Informational scanning — consistent, scannable cards |
H21. Poster View (Masonry)
H31.1 Grid Container
/* styles/card-grid.css + app/trips/builder/trip-builder.css */
.masonry-grid {
column-count: 2;
column-gap: 1rem;
}
@media (min-width: 768px) { .masonry-grid { column-count: 3; } }
@media (min-width: 1024px) { .masonry-grid { column-count: 4; } }
@media (min-width: 1280px) { .masonry-grid { column-count: 4; } }
- Uses CSS
column-count(not CSS Grid) so cards of different heights stack naturally break-inside: avoidon.masonry-itemprevents cards from splitting across columns- Column gap:
1rem(16px) - Max columns: 4 (reduced from 5) — keeps cards slightly larger for better readability
H31.2 Card Height Variants (Stepped Sizes)
Cards cycle through three sizes using a length-7 pattern (coprime with all column counts):
const CARD_SIZES = [
"card-small", // index 0, 4
"card-medium", // index 1, 3, 6
"card-large", // index 2, 5
];
const getCardSize = (index: number) => CARD_SIZES[index % CARD_SIZES.length];
| Size Class | aspect-ratio | Visual |
|---|---|---|
.card-small | 1 / 1 | Square |
.card-medium | 2 / 3 | Portrait |
.card-large | 1 / 2 | Tall portrait |
These aspect ratios apply only to the image area (.card-image), not the entire card. The content strip adds additional height below.
Critical rule: The .card-image element uses position: relative with overflow: hidden, and the <Image> inside uses fill + object-cover.
H31.3 Card Structure — EXACT
The card is a clean split layout with zero text overlaid on the image:
<Link className="group flex flex-col overflow-hidden rounded-[var(--radius-lg)] ...">
{/* Top portion — clean image ONLY */}
<div className="card-image relative">
<Image
src={image}
alt={trip.name}
fill
sizes="(max-width: 768px) 50vw, 25vw"
className="object-cover opacity-90 transition-all duration-700 group-hover:scale-105"
/>
</div>
{/* Bottom portion — content strip, solid background */}
<div className="flex flex-col justify-between p-5">
<div className="space-y-1">
<p className="text-[10px] font-bold uppercase tracking-[0.32em] text-[var(--ink-muted)]">
{trip.published ? "Public Trip" : "Trip Plan"}
</p>
<h3 className="text-lg font-bold leading-tight tracking-[-0.02em] text-[var(--ink)]">
{trip.name}
</h3>
</div>
<div className="mt-4 space-y-2 text-sm text-[var(--ink-muted)]">
{/* Start date row */}
<div className="flex items-center justify-between border-b border-[var(--border-light)] pb-2">
<span>Start date</span>
<span className="font-semibold text-[var(--ink)]">{formatDate(trip.startDate)}</span>
</div>
{/* Group size row */}
<div className="flex items-center justify-between border-b border-[var(--border-light)] pb-2">
<span>Group size</span>
<span className="font-semibold text-[var(--ink)]">{trip.numPax} travellers</span>
</div>
{/* Status row */}
<div className="flex items-center justify-between pb-2">
<span>Status</span>
<span className={published
? "bg-[var(--accent-ochre)] text-[var(--ink)] border-[var(--accent-ochre)]"
: "bg-[var(--bg-paper)] text-[var(--ink-muted)] border-[var(--border-light)]"
}>
{published ? "Published" : "Draft"}
</span>
</div>
</div>
<div className="mt-5">
<span className="inline-flex items-center gap-2 rounded-full bg-[var(--accent-ochre)] px-4 py-2 text-sm font-semibold text-[var(--ink)] shadow-sm transition hover:opacity-90">
View trip →
</span>
</div>
</div>
</Link>
H31.4 What Must Stay Exactly Like This
| Element | Rule |
|---|---|
| Image | No text overlay, no gradient, no title, no badges. Just the photo. |
| Title | Lives in the content strip, not on the image. |
| Status badge | Lives in the content strip, not on the image. |
| Opacity | Image uses opacity-90 (slight matte). |
| Hover | Only image scales (group-hover:scale-105). No dimming, no reveal. |
| Shadow | Card shadow increases on hover (hover:shadow-[0_12px_40px_rgba(0,0,0,0.14)]). |
| Background | Content strip uses bg-[var(--bg-card)] (solid white/off-white). |
| CTA | Ochre pill button at bottom of strip. |
H31.5 What NOT to Do
- ❌ Do not overlay the title on the image with a gradient
- ❌ Do not add a "hover dim" that reveals details
- ❌ Do not shrink card heights — the CSS aspect ratios must drive image size
- ❌ Do not use a uniform height for all cards in Poster View
- ❌ Do not remove the stepped size pattern
H22. Grid View (Dashboard Grid)
H32.1 Grid Container
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
@media (min-width: 640px) { .dashboard-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 768px) { .dashboard-grid { grid-template-columns: repeat(4, 1fr); } }
- CSS Grid, not CSS columns
- All cards are the same height (uniform rows)
- No stepped sizes —
cardSizeis empty string for all items
H32.2 Card Structure
Uses the same GalleryCard component as TripFeaturedTripsSection:
<div className="group overflow-hidden rounded-[26px] border border-neutral-200/90 bg-[#fffdfa] shadow-[0_18px_44px_rgba(17,17,17,0.06)] ...">
<Link href={href} className="block no-underline">
{/* Image area — 4:3 aspect ratio, label overlay */}
<div className="relative aspect-[4/3] overflow-hidden bg-neutral-100">
<div className="absolute inset-0 bg-cover bg-center transition duration-500 group-hover:scale-[1.04]"
style={{ backgroundImage: `url(${image})` }} />
<div className="absolute bottom-4 left-4">
<span className="rounded-full bg-white/90 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.16em] text-neutral-700 backdrop-blur-sm">
{trip.published ? "Live trip" : "Trip plan"}
</span>
</div>
</div>
{/* Content */}
<div className="p-6">
<p className="text-xs uppercase tracking-[0.18em] text-neutral-500">
{trip.published ? "Public trip" : "Draft"}
</p>
<h3 className="mt-2 text-[1.25rem] font-semibold leading-tight text-neutral-950">
{trip.name}
</h3>
{/* ... */}
</div>
</Link>
{/* Action bar */}
<div className="border-t border-neutral-200/80 px-6 pb-6 pt-5">
{/* Updated date + View trip button */}
</div>
</div>
H32.3 Key Differences from Poster View
| Poster View | Grid View | |
|---|---|---|
| Image aspect | 1:1, 2:3, 1:2 (stepped) | 4:3 (uniform) |
| Text on image | ❌ None | ✅ Small label overlay |
| Card height | Varies (masonry) | Uniform (CSS Grid) |
| Content position | Below image, solid bg | Below image, within card |
| Border radius | var(--radius-lg) (12px) | 26px |
| Card bg | var(--bg-card) | #fffdfa |
| Grid type | CSS columns | CSS Grid |
H23. Shared Infrastructure
H33.1 Reorder Mode (Hidden Feature)
- Hold
Ctrl/Cmdto activate drag-and-drop reordering @dnd-kit/core+@dnd-kit/sortablehandles the interaction- Only reorderable when
reorderMode === true
H33.2 Scroll Reveal
- Cards start at
opacity: 0; translate-y: 1.5rem IntersectionObserveratthreshold: 0.15triggers reveal- Transition:
duration-700
H33.3 Search Filter
useItemFilterhook filters bynameandslug- Results count shown when query is non-empty
H33.4 Image Resolution
function resolveTripImage(trip: BuilderTripListItem) {
return (
trip.mainImage?.trim() ||
`https://picsum.photos/seed/trip-index-${trip.id}/800/1200`
);
}
- Primary:
trip.mainImage(DB field, editable via dashboard) - Fallback: Picsum seed URL for deterministic placeholder
H24. Design Tokens Used
| Token | Value | Usage |
|---|---|---|
--bg-card | #ffffff | Card background |
--bg-paper | #f5f0e8 | Page background, draft badge |
--ink | #111111 | Primary text |
--ink-muted | #666666 | Secondary text, labels |
--border-light | #e8e4dc | Dividers, borders |
--accent-ochre | #c4953a | CTA buttons, published badges |
--radius-lg | 12px | Card border radius (Poster) |
H25. Files to Edit If Changing This Style
| File | Role |
|---|---|
app/trips/public/PublicTripsPosterGallery.tsx | Main gallery component — PosterCard + GalleryCard components |
styles/card-grid.css | .masonry-grid, .dashboard-grid, .card-image aspect ratios |
app/trips/builder/trip-builder.css | Design tokens, .masonry-grid overrides, .card-image sizing |
app/trips/components/sections/TripFeaturedTripsSection.tsx | Grid View card design source |
H26. Decision Log
| Date | Decision | Rationale |
|---|---|---|
| 2026-05-11 | Removed title overlay from Poster View image | User: "only use the bottom third for details without the image being in the way or behind" |
| 2026-05-11 | Kept CSS aspect ratios driving image height | Prevents card shrink; maintains masonry effect |
| 2026-05-11 | Kept stepped sizes (small/medium/large) | Maintains Pinterest-style visual rhythm |
| 2026-05-11 | Poster CTA is ochre pill; Grid CTA is outlined button | Different modes = different visual weight |
| 2026-05-11 | Reduced masonry max columns from 5 to 4 | User: "four elements wide for the cards to be slightly bigger" |
| 2026-05-11 | Landing page masonry uses exact same PosterCard as /trips/public | Consistent editorial experience across all poster views |
| 2026-05-11 | Grid view QR: 64×64, centred in top ~20%, white badge | User: "ticket card grid" feel — QR prominent like event ticket |
| 2026-05-11 | Grid view QR enlarged to 80×80; QR added to PublicTripsPosterGallery GalleryCard for grid view consistency | User: "could still be another bit larger and still fit in their cards gracefully" |
| 2026-05-11 | Grid view QR bumped to 120×120 | User: "another 40px wider without losing its centred position" |
| 2026-05-11 | Forced all .masonry-grid at 1280px+ to 4 columns (was 5 in spaces-section.css, editorial-enhancement-layer.css, app/spaces/globals.css) | User: "the poster grids… 4 columns so larger cards result and more visible" |
| 2026-05-11 | QR only in Grid view, never in Poster view | Poster view must stay clean editorial; QR belongs to informational grid |
Locked. Any change to proportions, layout, or visual hierarchy must update this document and get explicit sign-off.