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;