Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion THEME_AND_BRANDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,12 @@ gs://deepdivebrewing-web.firebasestorage.app/beers/
* **Purpose**: Full-width hero image on individual beer detail pages (e.g., `/beers/neipa`)
* **Format**: JPG, optimized for web
* **File size target**: Under 400KB
* **Behavior**: Grows with screen width, maintains aspect ratio
* **Behavior**: Rendered as a full-width page banner — `aspect-[4/3]` on
mobile (keeps the can in frame) easing to `aspect-[2/1]` on wider
screens, `object-cover` centered
* **Responsive**: Will be served at different sizes based on viewport
* **Composition**: Cans/subjects should sit near the horizontal center and
survive modest top/bottom crops at the 2:1 banner ratio

### Naming Convention

Expand Down Expand Up @@ -301,6 +305,40 @@ When in doubt, add more space rather than less.
* Page transitions: None (or instant)
* Scroll-triggered reveals: Simple fade-in (600ms ease)

### Shared motion tokens

All motion derives from the tokens in `:root` (`app/globals.css`) so timing
and easing stay consistent site-wide:

* `--motion-press` (150ms) — control press/hover acknowledgement
* `--motion-ui` (200ms) — small UI transitions (menus, chips, borders)
* `--motion-reveal` (600ms) — fade/rise reveals and page-load fades
* `--motion-hero` (800ms) — the homepage hero reveal only
* `--ease-standard` — single shared easing curve for anything that moves

Do not introduce per-component one-off durations or easings; pick the
nearest token instead.

### Motion primitives

* `components/reveal.tsx` (`<Reveal>`) is the single scroll-triggered
pattern: content fades up once it enters the viewport via
IntersectionObserver (one-shot, no re-triggering). Use it for
editorial section reveals — not for content that must be instantly
scannable (forms, dense listings, FAQs) or sits above the fold, since
the hidden state is armed only when the component's effect runs
(fail-visible if JS never hydrates). Reduced-motion sessions get
instant visibility with no movement.
* `animate-hero-fade` + `animate-delay-*` classes handle page-load reveals;
stagger is used sparingly on the homepage hero only.
* Dialogs, menus, and disclosure motion come from `tw-animate-css`
utilities already wired into shadcn/Radix primitives — use the shared
components rather than hand-rolled keyframes.
* All motion must respect `prefers-reduced-motion`: transforms are gated
by `motion-safe:`/`motion-reduce:` Tailwind variants or an explicit
`matchMedia("(prefers-reduced-motion: reduce)")` check, and the global
media query in `globals.css` collapses durations to near-instant.

### Interactive feedback

* Every interactive control (buttons, links styled as buttons, clickable
Expand Down
76 changes: 40 additions & 36 deletions app/(pages)/beers/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,47 +107,49 @@ export default async function BeerDetailPage({ params }: BeerDetailPageProps) {
</span>
</nav>

<div className="flex flex-col gap-8 lg:flex-row lg:gap-12">
{/* Beer image */}
<div className="w-full lg:w-2/5">
<div className="relative aspect-video w-full overflow-hidden rounded-lg bg-stone/50">
<Image
src={beerImageUrl(beer.images.heroPath)}
alt={beer.name}
fill
quality={80}
className="object-cover"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 40vw"
/>
</div>
</div>

{/* Beer details */}
<div className="w-full lg:w-3/5">
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">
{beer.name}
</h1>

<p className="mt-2 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{beer.style}
</p>
{/* Title block */}
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">
{beer.name}
</h1>

<p className="mt-2 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{beer.style}
</p>

{/* Specs */}
<div className="mt-6 flex flex-wrap gap-3">
<Badge variant="secondary">{beer.abv}% ABV</Badge>
<Badge variant="outline">{beer.status}</Badge>
{beer.ibu && <Badge variant="outline">{beer.ibu} IBU</Badge>}
{beer.srm && <Badge variant="outline">SRM {beer.srm}</Badge>}
</div>

{/* Specs */}
<div className="mt-6 flex flex-wrap gap-3">
<Badge variant="secondary">{beer.abv}% ABV</Badge>
<Badge variant="outline">{beer.status}</Badge>
{beer.ibu && <Badge variant="outline">{beer.ibu} IBU</Badge>}
{beer.srm && <Badge variant="outline">SRM {beer.srm}</Badge>}
</div>
{/* Full-width hero image — the 16:9 hero assets are composed for
banner crops: a modest 4:3 on mobile keeps the can visible, 2:1
on wider screens gives an editorial page header. */}
<div className="relative mt-10 aspect-[4/3] w-full overflow-hidden rounded-lg bg-stone/50 sm:aspect-[2/1]">
<Image
src={beerImageUrl(beer.images.heroPath)}
alt={beer.name}
fill
priority
quality={80}
className="object-cover"
sizes="(max-width: 1200px) 100vw, 1200px"
/>
</div>

{/* Beer details — editorial two-column body */}
<div className="mt-10 grid gap-10 lg:grid-cols-5">
<div className="lg:col-span-3">
{/* Description */}
<p className="mt-6 text-muted-foreground">
<p className="text-lg leading-relaxed text-muted-foreground">
{beer.descriptionShort}
</p>

{/* Tasting notes */}
{beer.tastingNotes.length > 0 && (
<div className="mt-6">
<div className="mt-8">
<h2 className="text-sm font-semibold uppercase tracking-wider text-ink">
Tasting Notes
</h2>
Expand All @@ -160,9 +162,11 @@ export default async function BeerDetailPage({ params }: BeerDetailPageProps) {
</div>
</div>
)}
</div>

{/* Where to find */}
<div className="mt-8 border-t border-stone pt-6">
{/* Where to find */}
<aside className="lg:col-span-2">
<div className="rounded-lg border border-stone bg-stone/40 p-6">
<p className="text-sm text-muted-foreground">
Want to try this beer?
</p>
Expand All @@ -180,7 +184,7 @@ export default async function BeerDetailPage({ params }: BeerDetailPageProps) {
See where to buy &rarr;
</Link>
</div>
</div>
</aside>
</div>
</main>
);
Expand Down
64 changes: 33 additions & 31 deletions app/(pages)/beers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,38 +74,40 @@ export default async function BeersPage() {

<BeersFilterGrid beers={beers} />

<section className="mt-12 rounded-lg border border-stone bg-paper p-6">
<h2 className="text-xl font-bold tracking-tight">Food Pairing Ideas</h2>
<ul className="mt-3 space-y-2 text-sm text-muted-foreground">
<li>
<span className="font-medium text-ink">NEIPA:</span> spicy dishes,
grilled fish, and rich cheeses.
</li>
<li>
<span className="font-medium text-ink">American Amber:</span>
burgers, barbecue, roasted chicken, and aged gouda.
</li>
<li>
<span className="font-medium text-ink">Pale Lager:</span> shellfish,
ceviche, salads, and light fried foods.
</li>
<li>
<span className="font-medium text-ink">Tropical Wheat:</span>
citrus-forward dishes, seafood, and island-style lunches.
</li>
</ul>
</section>
<div className="mt-12 grid gap-6 lg:grid-cols-2">
<section className="rounded-lg border border-stone bg-paper p-6">
<h2 className="text-xl font-bold tracking-tight">Food Pairing Ideas</h2>
<ul className="mt-3 space-y-2 text-sm text-muted-foreground">
<li>
<span className="font-medium text-ink">NEIPA:</span> spicy dishes,
grilled fish, and rich cheeses.
</li>
<li>
<span className="font-medium text-ink">American Amber:</span>
burgers, barbecue, roasted chicken, and aged gouda.
</li>
<li>
<span className="font-medium text-ink">Pale Lager:</span> shellfish,
ceviche, salads, and light fried foods.
</li>
<li>
<span className="font-medium text-ink">Tropical Wheat:</span>
citrus-forward dishes, seafood, and island-style lunches.
</li>
</ul>
</section>

<section className="mt-8 rounded-lg border border-stone bg-paper p-6">
<h2 className="text-xl font-bold tracking-tight">For Partners</h2>
<p className="mt-2 text-sm text-muted-foreground">
Interested in carrying Deep Dive Brewing Co? Start with our trade
inquiry form and we&rsquo;ll help you find the right mix for your venue.
</p>
<Link href="/trade" className="mt-3 inline-block text-sm font-medium text-ocean hover:opacity-85">
Submit a trade inquiry &rarr;
</Link>
</section>
<section className="rounded-lg border border-stone bg-paper p-6">
<h2 className="text-xl font-bold tracking-tight">For Partners</h2>
<p className="mt-2 text-sm text-muted-foreground">
Interested in carrying Deep Dive Brewing Co? Start with our trade
inquiry form and we&rsquo;ll help you find the right mix for your venue.
</p>
<Link href="/trade" className="mt-3 inline-block text-sm font-medium text-ocean hover:opacity-85">
Submit a trade inquiry &rarr;
</Link>
</section>
</div>
</main>
);
}
Expand Down
44 changes: 36 additions & 8 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@
--sidebar-accent-foreground: #0B0F14;
--sidebar-border: #E6E7E3;
--sidebar-ring: #334E68;

/* Motion tokens — the shared DDB motion language
(THEME_AND_BRANDING.md → Motion & Animation). Durations stay calm:
press feedback is the fastest thing on the site, reveals the slowest.
One easing curve for everything that moves. */
--motion-press: 150ms;
--motion-ui: 200ms;
--motion-reveal: 600ms;
--motion-hero: 800ms;
--ease-standard: cubic-bezier(0.22, 0.61, 0.36, 1);
}

@layer base {
Expand Down Expand Up @@ -145,7 +155,7 @@
}

.animate-fade-in {
animation: fade-in-up 600ms ease both;
animation: fade-in-up var(--motion-reveal) var(--ease-standard) both;
}

.animate-delay-1 {
Expand Down Expand Up @@ -177,17 +187,27 @@
}

.animate-hero-fade {
animation: hero-fade 800ms ease 200ms both;
animation: hero-fade var(--motion-hero) var(--ease-standard) 200ms both;
}

/* Scroll-triggered fade-in animation */
/* Scroll-triggered fade-in — toggled by components/reveal.tsx, the shared
primitive for scroll reveals (Issue #164). Content renders visible in the
SSR'd document; the hidden start state is applied by the Reveal effect
itself via `reveal-armed`, so sessions without JS — or where hydration
never establishes the observer — keep the content visible. */
.scroll-fade-in {
transition:
opacity var(--motion-reveal) var(--ease-standard),
transform var(--motion-reveal) var(--ease-standard);
}

.scroll-fade-in.reveal-armed {
opacity: 0;
transform: translateY(20px);
transition: opacity 600ms ease, transform 600ms ease;
}

.scroll-fade-in.fade-in-visible {
/* Must out-specify `.scroll-fade-in.reveal-armed` (0,2,0). */
.scroll-fade-in.reveal-armed.fade-in-visible {
opacity: 1;
transform: translateY(0);
}
Expand All @@ -205,7 +225,7 @@
animation-delay: 0s;
}

.scroll-fade-in {
.scroll-fade-in.reveal-armed {
opacity: 1;
transform: none;
transition: none;
Expand Down Expand Up @@ -376,8 +396,16 @@
margin-right: 0;
padding: 0.55em 1em;
transition:
background-color 150ms ease,
border-color 150ms ease;
background-color var(--motion-press) ease,
border-color var(--motion-press) ease,
transform var(--motion-press) ease;
}

/* Same press acknowledgement as site controls (scale 0.98, motion-safe). */
@media (prefers-reduced-motion: no-preference) {
#klaro .cm-btn:active {
transform: scale(0.98);
}
}

#klaro .cm-btn:hover {
Expand Down
17 changes: 9 additions & 8 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Metadata } from "next";
import { Button } from "@/components/ui/button";
import { SiteHeader } from "@/components/site-header";
import IntroSection from "@/components/home/IntroSection";
import { Reveal } from "@/components/reveal";
import { BeerCarousel } from "@/components/beer-carousel";
import { HeroVideo } from "@/components/hero-video";
import { TourInquiryCta } from "@/components/tour-inquiry-cta";
Expand Down Expand Up @@ -73,9 +74,9 @@ export default async function Home() {
className="object-cover"
/>
<div className="absolute inset-0 bg-ink/60" />
<div className="relative z-10 flex h-full flex-col items-center justify-center px-6 text-center animate-hero-fade">
<div className="relative z-10 flex h-full flex-col items-center justify-center px-6 text-center">
<h1
className="text-4xl font-festival tracking-tight text-paper sm:text-5xl md:text-6xl"
className="animate-hero-fade text-4xl font-festival tracking-tight text-paper sm:text-5xl md:text-6xl"
style={
{
textShadow: "2px 2px 4px rgba(11, 15, 20, 0.8)",
Expand All @@ -86,10 +87,10 @@ export default async function Home() {
<br />
Brewing Co
</h1>
<p className="mt-4 max-w-lg text-lg text-paper/90">
<p className="animate-hero-fade animate-delay-1 mt-4 max-w-lg text-lg text-paper/90">
Craft beer, brewed on Saba.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-4">
<div className="animate-hero-fade animate-delay-2 mt-8 flex flex-wrap justify-center gap-4">
<Button asChild className="h-11 min-h-[44px] px-6">
<Link href="/beers">Explore Our Beers</Link>
</Button>
Expand Down Expand Up @@ -141,7 +142,7 @@ export default async function Home() {
<HeroVideo />

{/* Featured beers teaser */}
<section className="animate-fade-in animate-delay-3 border-t border-stone">
<Reveal as="section" className="border-t border-stone">
<div className="mx-auto max-w-300 px-6 py-20 md:py-30">
<div className="flex items-end justify-between">
<h2 className="text-3xl font-bold tracking-tight">Our Beers</h2>
Expand All @@ -156,10 +157,10 @@ export default async function Home() {
<BeerCarousel beers={featuredBeers} imageUrls={imageUrls} />
</div>
</div>
</section>
</Reveal>

{/* Where to find us teaser */}
<section className="animate-fade-in animate-delay-4 border-t border-stone">
<Reveal as="section" className="border-t border-stone">
<div className="mx-auto max-w-300 px-6 py-20 md:py-30">
<div className="mx-auto max-w-180 text-center">
<h2 className="text-3xl font-bold tracking-tight">
Expand Down Expand Up @@ -188,7 +189,7 @@ export default async function Home() {
</div>
</div>
</div>
</section>
</Reveal>
</main>
</>
);
Expand Down
Loading
Loading