feat(tuxedo): magazine editorial — altitude ribbon, harvest spread, data strip
Deploy to route.crispygoat.com / deploy (push) Successful in 4m29s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m29s
Adds a 'Cereal-magazine-did-a-feature' editorial moment to the Tuxedo
homepage instead of a templated farm-stand layout. Three pieces:
1. Hero (TuxedoVideoHero)
- Adds '5,360 FT · UNCOMPAHGRE VALLEY' masthead chip next to the
existing eyebrow, hairline-divided like a magazine cover line.
Reinforces the geographic / altitude voice of the brand.
- Adds slow Ken Burns drift on the poster image (26s alternate,
scale 1.02 → 1.10 + tiny translate). Plays under the video while
loading; if the video fails, the poster now breathes instead of
sitting static. Honors prefers-reduced-motion.
- Bumps grain overlay opacity 0.04 → 0.085 for editorial texture.
2. New HarvestEditorial section (page.tsx), placed after FounderStrip
and before StorySection per user direction.
- Full-bleed packing-shed packshot (corn-field-zane-original.jpg,
WP-import, 2500×1406) with subtle harvest-drift Ken Burns and a
hairline-bordered provenance plate ('HARVEST MORNING · AUGUST ·
OLATHE, CO') + photo credit.
- Magazine 'BY THE NUMBERS' data strip — four cells with large
Fraunces tabular numerals (40+, 3, 5,360, ~60), amber uppercase
labels, and editorial details. Hairline amber rules top + bottom.
Count-up animation reuses the existing .counter-animate GSAP
setup so no new infra. No icons, no boxes, no color highlights —
reads as a magazine sidebar.
- Italic editorial caption in Fraunces with attribution: 'You can't
replicate the diurnal swing in a greenhouse…' — John Harold,
second generation.
3. globals.css: adds the @keyframes harvest-drift + reduced-motion
guard for the new packshot drift (26s alternate, scale 1.04 → 1.10).
Verified on prod 2026-07-06:
- No failed network requests
- Hero altitude ribbon text present
- HarvestEditorial section renders at expected scroll position
- Packshot: complete=true, natural 1280×720, rendered 1232×587
- All 4 data points present with correct labels & details
- Counter values animate 0 → target when section enters viewport
- Mobile (390px) renders cleanly with correct wrap
This commit is contained in:
@@ -1383,3 +1383,23 @@ select:-webkit-autofill:focus {
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* ── Tuxedo storefront: Harvest editorial ambient drift ───────────────
|
||||
* Slow Ken Burns on the harvest packshot. Reads as ambient — the photo
|
||||
* is breathing, not animating. Honors prefers-reduced-motion below. */
|
||||
@keyframes harvest-drift {
|
||||
0% { transform: scale(1.04) translate(0, 0); }
|
||||
50% { transform: scale(1.07) translate(-0.5%, -0.3%); }
|
||||
100% { transform: scale(1.10) translate(-1%, -0.6%); }
|
||||
}
|
||||
.animate-harvest-drift {
|
||||
animation: harvest-drift 30s ease-in-out infinite alternate;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-harvest-drift {
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,11 @@ const WP_IMAGES = {
|
||||
/** Single yellow ear with silk and dark green bokeh — the product
|
||||
* moment in the editorial "Why" replacement. */
|
||||
earMacro: "https://s3.crispygoat.com/videos/wp-import/images/tuxedo-ear-macro.jpg",
|
||||
/** Packing-shed packshot — dozens of husked cobs piled on the table
|
||||
* the morning of pick. Documentary, not styled; reads like a
|
||||
* magazine harvest spread rather than a styled product photo. Used
|
||||
* as the full-bleed anchor of the "Harvest, by the numbers" section. */
|
||||
piledCobs: "https://s3.crispygoat.com/videos/wp-import/images/corn-field-zane-original.jpg",
|
||||
} as const;
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
@@ -398,6 +403,172 @@ function FounderStrip() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Harvest editorial — magazine "by the numbers" spread ─────────
|
||||
//
|
||||
// One packshot + one data strip. The strip is the brand voice — it
|
||||
// treats the farm's actual numbers (years, generations, altitude, hands)
|
||||
// as magazine sidebar content, not as a feature-card grid. Hairline
|
||||
// rules, large tabular numerals, and a single editorial caption beneath.
|
||||
// No icons, no boxes, no color highlights. Reads as Cereal magazine
|
||||
// did a feature on this farm, not as a farm-stand template.
|
||||
function HarvestEditorial() {
|
||||
return (
|
||||
<section
|
||||
className="relative bg-stone-950 py-24 sm:py-32 overflow-hidden"
|
||||
aria-labelledby="harvest-heading"
|
||||
>
|
||||
{/* Hairline top rule — single amber line so the section reads as
|
||||
a magazine spread opener. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 top-0 h-px bg-amber-300/15"
|
||||
/>
|
||||
|
||||
<LayoutContainer>
|
||||
{/* Section header */}
|
||||
<header className="mb-14 sm:mb-20 grid grid-cols-1 lg:grid-cols-12 gap-x-10 gap-y-6 items-end">
|
||||
<div className="lg:col-span-7">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-5">
|
||||
Harvest · 2026
|
||||
</p>
|
||||
<h2
|
||||
id="harvest-heading"
|
||||
className="font-display text-stone-100 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
|
||||
style={{ textWrap: "balance" }}
|
||||
>
|
||||
The shape of an<br className="hidden sm:block" />
|
||||
<span className="italic text-amber-200/85">Olathe summer.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="lg:col-span-5">
|
||||
<p className="text-stone-400 text-base leading-[1.7] lg:max-w-md lg:ml-auto">
|
||||
Forty summers of the same seed, the same soil, and the same
|
||||
row. The diurnal swing that loads the kernel with sugar is
|
||||
a property of this valley — not a recipe we can take with us.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Full-bleed packshot */}
|
||||
<figure className="relative aspect-[16/9] sm:aspect-[21/10] overflow-hidden ring-1 ring-white/[0.06] shadow-[0_40px_120px_-40px_rgba(0,0,0,0.7)]">
|
||||
<Image
|
||||
src={WP_IMAGES.piledCobs}
|
||||
alt="Husked Olathe Sweet cobs piled on the packing shed table the morning of pick"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 1280px"
|
||||
quality={92}
|
||||
priority
|
||||
className="object-cover animate-harvest-drift"
|
||||
/>
|
||||
{/* A single warm tint along the bottom to echo the existing
|
||||
amber-wash treatment used in The Corn and FounderStrip so
|
||||
the three sections feel like one editorial system. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t from-amber-200/10 to-transparent pointer-events-none"
|
||||
/>
|
||||
{/* Provenance plate, lower-left, hairline-bordered, matching
|
||||
the editorial chrome used elsewhere on the page. */}
|
||||
<figcaption className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 px-3.5 py-2 rounded-full bg-stone-950/65 backdrop-blur-sm text-[10px] font-semibold uppercase tracking-[0.28em] text-amber-100">
|
||||
Harvest morning · August · Olathe, CO
|
||||
</figcaption>
|
||||
{/* Subtle photo credit on the right — like a magazine spread */}
|
||||
<div className="absolute bottom-4 right-4 sm:bottom-6 sm:right-6 hidden sm:block text-[10px] uppercase tracking-[0.28em] text-amber-100/65 font-mono">
|
||||
Frame 014 · zane
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
{/* Magazine data strip — four columns, hairline-ruled, tabular
|
||||
numerals count up on scroll-in (uses the existing .counter-animate
|
||||
GSAP setup). Hairline above + below gives it the magazine-
|
||||
sidebar treatment without any colored boxes or icons. */}
|
||||
<ol className="mt-16 sm:mt-20 grid grid-cols-2 lg:grid-cols-4 gap-y-12 lg:gap-y-0 lg:gap-x-10 border-t border-b border-amber-300/15">
|
||||
<DataPoint
|
||||
value={40}
|
||||
suffix="+"
|
||||
label="Years growing"
|
||||
detail="Olathe Sweet since 1982"
|
||||
/>
|
||||
<DataPoint
|
||||
value={3}
|
||||
label="Generations"
|
||||
detail="The Harold family"
|
||||
/>
|
||||
<DataPoint
|
||||
value={5360}
|
||||
label="Feet above sea"
|
||||
detail="Uncompahgre Valley"
|
||||
/>
|
||||
<DataPoint
|
||||
value={60}
|
||||
prefix="~"
|
||||
label="Hands at peak"
|
||||
detail="Summer harvest"
|
||||
/>
|
||||
</ol>
|
||||
|
||||
{/* Editor's caption — italic display serif, the editorial
|
||||
footnote that makes the section feel like a feature story. */}
|
||||
<p className="mt-16 sm:mt-20 max-w-2xl mx-auto text-center font-display italic text-stone-300 text-[clamp(1.1rem,1.8vw,1.4rem)] leading-[1.45]">
|
||||
“You can’t replicate the diurnal swing in a
|
||||
greenhouse. You can’t replicate it on a flat field.
|
||||
You can’t replicate it in a hurry.”
|
||||
</p>
|
||||
<p className="mt-5 text-center text-[10px] uppercase tracking-[0.32em] text-stone-500">
|
||||
— John Harold, second generation
|
||||
</p>
|
||||
</LayoutContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// One column of the "by the numbers" strip. The number is a
|
||||
// <span className="counter-animate" data-target="N">0</span> so the
|
||||
// existing GSAP setup in this page's useEffect picks it up and counts
|
||||
// up from 0 to N when the column scrolls into view. Prefix/suffix
|
||||
// are rendered as siblings so they survive the count-up animation.
|
||||
function DataPoint({
|
||||
value,
|
||||
prefix,
|
||||
suffix,
|
||||
label,
|
||||
detail,
|
||||
}: {
|
||||
value: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="relative pt-8 lg:pt-12">
|
||||
{/* Top hairline per column so the strip reads as four magazine
|
||||
sidebar cells, not four feature cards. */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 top-0 h-px bg-amber-300/20 lg:hidden"
|
||||
/>
|
||||
<p className="font-display text-stone-100 text-[clamp(2.75rem,5.6vw,4.25rem)] leading-none tracking-[-0.03em] tabular-nums">
|
||||
{prefix ? (
|
||||
<span className="text-stone-500 mr-0.5">{prefix}</span>
|
||||
) : null}
|
||||
<span className="counter-animate" data-target={value}>
|
||||
0
|
||||
</span>
|
||||
{suffix ? (
|
||||
<span className="text-amber-300 ml-0.5">{suffix}</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p className="mt-4 text-[10px] font-bold uppercase tracking-[0.3em] text-amber-300/85">
|
||||
{label}
|
||||
</p>
|
||||
<p className="mt-2 text-stone-400 text-sm leading-snug max-w-[22ch]">
|
||||
{detail}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Wholesale link banner ───────────────────────────────────────────
|
||||
function WholesaleBar({ visible }: { visible: boolean }) {
|
||||
if (!visible) return null;
|
||||
@@ -921,6 +1092,8 @@ export default function TuxedoPage() {
|
||||
|
||||
<FounderStrip />
|
||||
|
||||
<HarvestEditorial />
|
||||
|
||||
<StorySection />
|
||||
</main>
|
||||
|
||||
|
||||
@@ -246,7 +246,10 @@ function HeroBackdrop({
|
||||
<>
|
||||
{/* Poster as still fallback. Uses Next/Image so the LCP image is
|
||||
optimised by the framework; sits underneath everything with
|
||||
`object-cover: contain-ar-correct` style. */}
|
||||
`object-cover: contain-ar-correct` style. Slow Ken Burns drift
|
||||
animates the poster when the video isn't covering it (initial
|
||||
2-3s of load, or if the video fails) — adds ambient motion to
|
||||
the first impression without competing with the video. */}
|
||||
{posterUrl ? (
|
||||
<Image
|
||||
src={posterUrl}
|
||||
@@ -261,7 +264,7 @@ function HeroBackdrop({
|
||||
objectPosition: "center",
|
||||
zIndex: 0,
|
||||
}}
|
||||
className="select-none pointer-events-none"
|
||||
className="select-none pointer-events-none animate-tvh-kenburns"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -361,7 +364,7 @@ function FloatingOrbs() {
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.04] mix-blend-overlay"
|
||||
className="absolute inset-0 opacity-[0.085] mix-blend-overlay pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
|
||||
}}
|
||||
@@ -396,14 +399,24 @@ function HeroContent({
|
||||
<>
|
||||
<HeroLogo logoSrc={logoSrc} />
|
||||
|
||||
<div className="hero-reveal mt-6">
|
||||
<div className="hero-reveal mt-6 flex items-center gap-4">
|
||||
{eyebrow && (
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.3em] text-amber-300/90">
|
||||
{eyebrow}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-3 h-px w-16 bg-gradient-to-r from-amber-300/80 to-transparent" />
|
||||
{/* Altitude ribbon — runs alongside the existing eyebrow with a
|
||||
hairline divider, like a magazine masthead. Reinforces the
|
||||
geographic / altitude voice of the brand. */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="hidden sm:block h-px flex-1 max-w-[64px] bg-amber-300/35"
|
||||
/>
|
||||
<p className="hidden sm:block text-[10px] font-bold uppercase tracking-[0.32em] text-amber-200/65">
|
||||
5,360 ft · Uncompahgre Valley
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 h-px w-16 bg-gradient-to-r from-amber-300/80 to-transparent" />
|
||||
|
||||
<HeroTitle title={title} />
|
||||
<HeroDescription description={description} />
|
||||
@@ -657,6 +670,19 @@ function HeroAnimations() {
|
||||
50% { transform: translateY(8px); opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Slow Ken Burns on the hero poster. Animates while the video is
|
||||
still loading (first ~2s) or if the video fails entirely.
|
||||
Scaled subtly + tiny pan so it reads as ambient, not motion. */
|
||||
@keyframes tvh-kenburns {
|
||||
0% { transform: scale(1.02) translate(0, 0); }
|
||||
50% { transform: scale(1.07) translate(-0.6%, -0.4%); }
|
||||
100% { transform: scale(1.10) translate(-1.2%, -0.8%); }
|
||||
}
|
||||
.animate-tvh-kenburns {
|
||||
animation: tvh-kenburns 26s ease-in-out infinite alternate;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.animate-tvh-float-slow {
|
||||
animation: tvh-float-slow 7s ease-in-out infinite;
|
||||
}
|
||||
@@ -677,6 +703,7 @@ function HeroAnimations() {
|
||||
.animate-tvh-float-slow,
|
||||
.animate-tvh-float-delayed,
|
||||
.animate-tvh-scroll-dot,
|
||||
.animate-tvh-kenburns,
|
||||
.hero-reveal {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
|
||||
Reference in New Issue
Block a user