From 68851cfedea2be2d5073681a029fe77d33ae1ba4 Mon Sep 17 00:00:00 2001 From: Nora Date: Mon, 6 Jul 2026 13:55:48 -0600 Subject: [PATCH] =?UTF-8?q?feat(tuxedo):=20magazine=20editorial=20?= =?UTF-8?q?=E2=80=94=20altitude=20ribbon,=20harvest=20spread,=20data=20str?= =?UTF-8?q?ip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/globals.css | 20 ++ src/app/tuxedo/page.tsx | 173 ++++++++++++++++++ src/components/storefront/TuxedoVideoHero.tsx | 37 +++- 3 files changed, 225 insertions(+), 5 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 2d9bcee..7acdc2f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; + } +} diff --git a/src/app/tuxedo/page.tsx b/src/app/tuxedo/page.tsx index 5c64277..72ec700 100644 --- a/src/app/tuxedo/page.tsx +++ b/src/app/tuxedo/page.tsx @@ -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 ( +
+ {/* Hairline top rule — single amber line so the section reads as + a magazine spread opener. */} +
+ ); +} + +// One column of the "by the numbers" strip. The number is a +// 0 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 ( +
  • + {/* Top hairline per column so the strip reads as four magazine + sidebar cells, not four feature cards. */} +
  • + ); +} + // ── Wholesale link banner ─────────────────────────────────────────── function WholesaleBar({ visible }: { visible: boolean }) { if (!visible) return null; @@ -921,6 +1092,8 @@ export default function TuxedoPage() { + + diff --git a/src/components/storefront/TuxedoVideoHero.tsx b/src/components/storefront/TuxedoVideoHero.tsx index 5d03681..e52623d 100644 --- a/src/components/storefront/TuxedoVideoHero.tsx +++ b/src/components/storefront/TuxedoVideoHero.tsx @@ -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 ? ( ) : null} @@ -361,7 +364,7 @@ function FloatingOrbs() { }} />
    -
    +
    {eyebrow && (

    {eyebrow}

    )} -
    + {/* Altitude ribbon — runs alongside the existing eyebrow with a + hairline divider, like a magazine masthead. Reinforces the + geographic / altitude voice of the brand. */} +
    +
    @@ -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;