MolinoPro

poster-grid-style-spec

Master Codebase Guidebook
Markdown + HTML Dev-Docs Renderer - Frontend Client Module

Default Index
Open README.md
Root: README.mdtrips
Milestones
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.

ModeGrid TypeCard ShapePurpose
PostersMasonry (CSS columns)Portrait, stepped sizesEditorial discovery — dramatic, Pinterest-style browsing
GridCSS Grid (uniform rows)Landscape 4:3, equal heightInformational 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: avoid on .masonry-item prevents 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 Classaspect-ratioVisual
.card-small1 / 1Square
.card-medium2 / 3Portrait
.card-large1 / 2Tall 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
ElementRule
ImageNo text overlay, no gradient, no title, no badges. Just the photo.
TitleLives in the content strip, not on the image.
Status badgeLives in the content strip, not on the image.
OpacityImage uses opacity-90 (slight matte).
HoverOnly image scales (group-hover:scale-105). No dimming, no reveal.
ShadowCard shadow increases on hover (hover:shadow-[0_12px_40px_rgba(0,0,0,0.14)]).
BackgroundContent strip uses bg-[var(--bg-card)] (solid white/off-white).
CTAOchre 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 — cardSize is 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 ViewGrid View
Image aspect1:1, 2:3, 1:2 (stepped)4:3 (uniform)
Text on image❌ None✅ Small label overlay
Card heightVaries (masonry)Uniform (CSS Grid)
Content positionBelow image, solid bgBelow image, within card
Border radiusvar(--radius-lg) (12px)26px
Card bgvar(--bg-card)#fffdfa
Grid typeCSS columnsCSS Grid

H23. Shared Infrastructure
H33.1 Reorder Mode (Hidden Feature)
  • Hold Ctrl / Cmd to activate drag-and-drop reordering
  • @dnd-kit/core + @dnd-kit/sortable handles the interaction
  • Only reorderable when reorderMode === true
H33.2 Scroll Reveal
  • Cards start at opacity: 0; translate-y: 1.5rem
  • IntersectionObserver at threshold: 0.15 triggers reveal
  • Transition: duration-700
H33.3 Search Filter
  • useItemFilter hook filters by name and slug
  • 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
TokenValueUsage
--bg-card#ffffffCard background
--bg-paper#f5f0e8Page background, draft badge
--ink#111111Primary text
--ink-muted#666666Secondary text, labels
--border-light#e8e4dcDividers, borders
--accent-ochre#c4953aCTA buttons, published badges
--radius-lg12pxCard border radius (Poster)

H25. Files to Edit If Changing This Style
FileRole
app/trips/public/PublicTripsPosterGallery.tsxMain gallery component — PosterCard + GalleryCard components
styles/card-grid.css.masonry-grid, .dashboard-grid, .card-image aspect ratios
app/trips/builder/trip-builder.cssDesign tokens, .masonry-grid overrides, .card-image sizing
app/trips/components/sections/TripFeaturedTripsSection.tsxGrid View card design source

H26. Decision Log
DateDecisionRationale
2026-05-11Removed title overlay from Poster View imageUser: "only use the bottom third for details without the image being in the way or behind"
2026-05-11Kept CSS aspect ratios driving image heightPrevents card shrink; maintains masonry effect
2026-05-11Kept stepped sizes (small/medium/large)Maintains Pinterest-style visual rhythm
2026-05-11Poster CTA is ochre pill; Grid CTA is outlined buttonDifferent modes = different visual weight
2026-05-11Reduced masonry max columns from 5 to 4User: "four elements wide for the cards to be slightly bigger"
2026-05-11Landing page masonry uses exact same PosterCard as /trips/publicConsistent editorial experience across all poster views
2026-05-11Grid view QR: 64×64, centred in top ~20%, white badgeUser: "ticket card grid" feel — QR prominent like event ticket
2026-05-11Grid view QR enlarged to 80×80; QR added to PublicTripsPosterGallery GalleryCard for grid view consistencyUser: "could still be another bit larger and still fit in their cards gracefully"
2026-05-11Grid view QR bumped to 120×120User: "another 40px wider without losing its centred position"
2026-05-11Forced 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-11QR only in Grid view, never in Poster viewPoster 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.