From ffdc80e60e3a266c7b75fc162d6b423bd4149a4f Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 7 Jul 2026 09:10:40 -0600 Subject: [PATCH] feat(tuxedo): Field Almanac editorial composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layers on top of origin/main's existing editorial primitives. Replaces the cinematic video hero with a typographic 'cover spread' and rebuilds the page composition around numbered editorial sections. Cover spread (TuxedoVideoHero): - Newspaper-style masthead with location, altitude, weather, date - Massive display headline with oversize 'XL' page numeral in margin - Cover photo plate with crop marks, photo credit, plate number - Floating marginalia (vertical text) drifting in on scroll - Slow horizontal ticker with harvest facts at bottom Page composition (page.tsx) — numbered editorial sections: № 00 Editor's Note two-column preamble, drop cap, pull quote № 01 The Almanac 6 specimen cards with hover-reveal notes № 02 The Cover Story photo + sticky stat marginalia № 03 The 2026 Tour typeset schedule with stat strip № 04 This Week's Lot inventory-style product cards w/ lot #s № 05 Chronicle of the Row 3-gen timeline on dark plate № 06 Letter from the Field letterpress subscribe card w/ deckle edge Colophon back-of-book fine print Adds editorial primitives to globals.css: section numbering, drop caps, specimen tabs, pull quotes, crop marks, page numerals, ticker, deckle-edge subscribe, almanac-paper backgrounds. Removes dark fixed gradient from tuxedo/layout.tsx so per-section .almanac-paper backgrounds win. --- src/app/globals.css | 249 +++ src/app/tuxedo/layout.tsx | 11 +- src/app/tuxedo/page.tsx | 1747 +++++++++-------- src/components/storefront/TuxedoVideoHero.tsx | 930 +++------ 4 files changed, 1467 insertions(+), 1470 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index b96d4bf..bcc9455 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1487,3 +1487,252 @@ select:-webkit-autofill:focus { filter: none !important; } } + +/* ────────────────────────────────────────────────────────────────── + FIELD ALMANAC — Editorial primitives (Tuxedo Corn storefront) + Layered on top of the existing editorial palette from origin/main. + Distinct additions: section numbering, specimen tabs, drop caps, + pull quotes, crop marks, page numerals, ticker, deckle subscribe. + ────────────────────────────────────────────────────────────────── */ + +.almanac-paper { + background: + radial-gradient(ellipse 100% 60% at 50% 0%, rgba(180, 150, 100, 0.08), transparent 60%), + radial-gradient(ellipse 80% 50% at 50% 100%, rgba(120, 90, 50, 0.05), transparent 60%), + #f3ece0; + color: #1a1814; + position: relative; +} +.almanac-paper--bone { background: #ece4d4; } +.almanac-paper--ink { background: #1a1814; color: #f3ece0; } + +.almanac-grain::after { + content: ""; + position: absolute; inset: 0; pointer-events: none; + opacity: 0.12; mix-blend-mode: multiply; + background-image: url("data:image/svg+xml;utf8,"); + z-index: 1; +} +.almanac-grain > * { position: relative; z-index: 2; } + +.almanac-display { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "SOFT" 50, "WONK" 1, "opsz" 144; + letter-spacing: -0.035em; line-height: 0.88; font-weight: 500; +} +.almanac-display-tight { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "SOFT" 30, "opsz" 60; + letter-spacing: -0.018em; line-height: 1.02; font-weight: 450; +} +.almanac-body { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "opsz" 14; font-weight: 400; + line-height: 1.55; letter-spacing: -0.005em; +} +.almanac-italic { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "opsz" 14; font-style: italic; font-weight: 400; +} +.almanac-smallcaps { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variant: all-small-caps; letter-spacing: 0.06em; font-weight: 500; +} + +.almanac-eyebrow { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 10.5px; letter-spacing: 0.22em; + text-transform: uppercase; font-weight: 400; +} +.almanac-dateline { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 10.5px; letter-spacing: 0.18em; + text-transform: uppercase; +} +.almanac-marginalia { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 10.5px; letter-spacing: 0.08em; line-height: 1.5; +} + +.almanac-dropcap::first-letter { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "WONK" 1, "opsz" 144; + float: left; font-size: 5.5em; line-height: 0.82; + padding: 0.06em 0.08em 0 0; font-weight: 500; color: #1a1814; +} + +.almanac-rule { height: 1px; background: #1a1814; border: 0; width: 100%; } +.almanac-rule--hair { height: 1px; background: rgba(26, 24, 20, 0.18); } +.almanac-rule--double { + height: 4px; border-top: 1px solid #1a1814; + border-bottom: 1px solid #1a1814; background: transparent; +} + +.crop-marks { + --c: #1a1814; position: absolute; inset: 0; + pointer-events: none; z-index: 5; +} +.crop-marks::before, +.crop-marks::after, +.crop-marks > span::before, +.crop-marks > span::after { + content: ""; position: absolute; background: var(--c); +} +.crop-marks::before { top: -1px; left: 24px; width: 1px; height: 14px; } +.crop-marks::after { top: -1px; right: 24px; width: 1px; height: 14px; } +.crop-marks > span::before { bottom: -1px; left: 24px; width: 1px; height: 14px; } +.crop-marks > span::after { bottom: -1px; right: 24px; width: 1px; height: 14px; } + +.almanac-section-number { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 11px; letter-spacing: 0.32em; + text-transform: uppercase; + display: inline-flex; align-items: baseline; gap: 0.7em; +} +.almanac-section-number::before { + content: ""; display: inline-block; + width: 32px; height: 1px; background: currentColor; + margin-right: 0.2em; vertical-align: middle; +} + +.almanac-pullquote { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "SOFT" 50, "WONK" 1, "opsz" 144; + font-weight: 400; line-height: 1.02; + letter-spacing: -0.025em; font-style: italic; +} + +.almanac-card { + background: #fbf6ec; border: 1px solid rgba(26, 24, 20, 0.16); + position: relative; + transition: background 200ms cubic-bezier(0.32,0.72,0,1), + transform 280ms cubic-bezier(0.32,0.72,0,1); +} +.almanac-card:hover { background: #fffaf0; transform: translateY(-2px); } + +.almanac-tab { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; + background: #1a1814; color: #f3ece0; + padding: 4px 10px 4px 8px; display: inline-block; position: relative; +} +.almanac-tab::before { + content: ""; position: absolute; top: 0; right: -6px; + border-left: 6px solid #1a1814; + border-top: 13px solid transparent; + border-bottom: 13px solid transparent; +} + +@keyframes almanac-ticker { + from { transform: translate3d(0,0,0); } + to { transform: translate3d(-50%,0,0); } +} +.almanac-ticker { + display: flex; width: max-content; + animation: almanac-ticker 80s linear infinite; + will-change: transform; +} + +.almanac-underline { + background-image: linear-gradient(transparent calc(100% - 3px), #c89517 3px); + background-size: 100% 100%; background-repeat: no-repeat; + transition: background-size 320ms cubic-bezier(0.32,0.72,0,1); +} +.almanac-underline:hover { + background-image: linear-gradient(transparent calc(100% - 6px), #c89517 6px); +} + +.almanac-btn-primary { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 12px; letter-spacing: 0.22em; text-transform: uppercase; + font-weight: 500; padding: 16px 28px; + background: #1a1814; color: #f3ece0; + border: 1px solid #1a1814; + display: inline-flex; align-items: center; gap: 12px; + transition: background 220ms cubic-bezier(0.32,0.72,0,1), + color 220ms cubic-bezier(0.32,0.72,0,1), + transform 220ms cubic-bezier(0.32,0.72,0,1); + cursor: pointer; text-decoration: none; +} +.almanac-btn-primary:hover { + background: #c89517; color: #1a1814; + border-color: #c89517; transform: translateY(-1px); +} + +.almanac-btn-ghost { + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 12px; letter-spacing: 0.22em; text-transform: uppercase; + font-weight: 500; padding: 16px 4px; + color: #1a1814; display: inline-flex; align-items: center; gap: 12px; + border-bottom: 1px solid #1a1814; + transition: gap 220ms cubic-bezier(0.32,0.72,0,1); + background: transparent; cursor: pointer; text-decoration: none; +} +.almanac-btn-ghost:hover { gap: 18px; } + +.almanac-vertical { + writing-mode: vertical-rl; transform: rotate(180deg); + font-family: var(--font-fragment-mono, "JetBrains Mono"), monospace; + font-size: 10.5px; letter-spacing: 0.3em; text-transform: uppercase; +} + +.almanac-pagenum { + font-family: var(--font-fraunces, "Georgia"), serif; + font-variation-settings: "WONK" 1, "opsz" 144; + font-weight: 500; font-size: 9vw; line-height: 0.78; + letter-spacing: -0.04em; + pointer-events: none; user-select: none; + color: rgba(26, 24, 20, 0.08); +} + +.almanac-flourish { + display: inline-block; + transition: transform 380ms cubic-bezier(0.34, 1.56, 0.64, 1); +} +.almanac-flourish:hover { transform: rotate(-3deg) scale(1.05); } + +.almanac-photo { position: relative; overflow: hidden; } +.almanac-photo > img, +.almanac-photo > .img { filter: contrast(1.04) saturate(0.92) sepia(0.08); } +.almanac-photo::after { + content: ""; position: absolute; inset: 0; + background-image: url("data:image/svg+xml;utf8,"); + mix-blend-mode: multiply; opacity: 0.35; pointer-events: none; +} + +/* Inverted (dark) section overrides */ +.almanac-paper--ink .almanac-card { + background: rgba(243, 236, 224, 0.04); + border-color: rgba(243, 236, 224, 0.18); +} +.almanac-paper--ink .almanac-card:hover { background: rgba(243, 236, 224, 0.08); } +.almanac-paper--ink .almanac-rule { background: rgba(243, 236, 224, 0.6); } +.almanac-paper--ink .almanac-rule--hair { background: rgba(243, 236, 224, 0.18); } +.almanac-paper--ink .almanac-tab { background: #c89517; color: #1a1814; } +.almanac-paper--ink .almanac-tab::before { border-left-color: #c89517; } +.almanac-paper--ink .almanac-btn-primary { + background: #f3ece0; color: #1a1814; border-color: #f3ece0; +} +.almanac-paper--ink .almanac-btn-primary:hover { + background: #c89517; color: #1a1814; border-color: #c89517; +} +.almanac-paper--ink .almanac-btn-ghost { + color: #f3ece0; border-color: rgba(243, 236, 224, 0.4); +} +.almanac-paper--ink .almanac-eyebrow, +.almanac-paper--ink .almanac-dateline, +.almanac-paper--ink .almanac-section-number, +.almanac-paper--ink .almanac-marginalia { + color: rgba(243, 236, 224, 0.55); +} +.almanac-paper--ink .almanac-pagenum { color: rgba(243, 236, 224, 0.07); } + +@media (prefers-reduced-motion: reduce) { + .almanac-ticker { animation: none; } + .almanac-grain::after { opacity: 0.05; } +} + +.almanac-paper ::selection, +.almanac-paper--ink ::selection { + background: #c89517; color: #1a1814; +} diff --git a/src/app/tuxedo/layout.tsx b/src/app/tuxedo/layout.tsx index 35d5831..0ca9d84 100644 --- a/src/app/tuxedo/layout.tsx +++ b/src/app/tuxedo/layout.tsx @@ -103,14 +103,9 @@ export const viewport: Viewport = { export default function TuxedoLayout({ children }: { children: React.ReactNode }) { return (
- {/* Background gradient */} -
-
-
-
- {/* The storefront backdrop and ambient orbs are persistent. The - page body itself crossfades between routes via the View - Transitions API. */} + {/* No fixed dark gradient — the Field Almanac composition sets its + own per-section backgrounds (cream / bone / ink) via the + .almanac-paper utility classes on each section. */}
{children}
diff --git a/src/app/tuxedo/page.tsx b/src/app/tuxedo/page.tsx index b8ab9e7..b472261 100644 --- a/src/app/tuxedo/page.tsx +++ b/src/app/tuxedo/page.tsx @@ -5,41 +5,38 @@ import Link from "next/link"; import Image from "next/image"; import { m as motion, useInView } from "framer-motion"; import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero"; -import CinematicShowcase from "@/components/storefront/CinematicShowcase"; -import LayoutContainer from "@/components/layout/LayoutContainer"; import ProductCard from "@/components/storefront/ProductCard"; import PaginatedStops from "@/components/storefront/PaginatedStops"; import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import BrandStylesProvider from "@/components/storefront/BrandStylesProvider"; -import { TUXEDO_IMAGES as WP_IMAGES } from "@/components/storefront/tuxedo-images"; -import { ScrollReveal, ParallaxLayer, FadeOnScroll } from "@/components/ui/ScrollAnimations"; import { supabase } from "@/lib/supabase"; +import { getBrandSettingsPublic } from "@/actions/brand-settings"; + +// ───────────────────────────────────────────────────────────────────────────── +// TUXEDO PAGE — Field Almanac composition +// +// Section order: +// 0. Cover spread → TuxedoVideoHero (typographic masthead) +// 1. Editor's note → two-column preamble with pull quote +// 2. I. The Almanac → specimen cards (altitude, swing, hands, …) +// 3. II. The Cover Story → editorial photo + marginalia +// 4. III. The 2026 Tour → stops as typeset schedule +// 5. IV. This Week's Lot → inventory-style product cards +// 6. V. Chronicle of the Row → three-generation timeline +// 7. Letter from the Field → closing subscribe (letterpress card) +// 8. Colophon → back-of-book fine print +// ───────────────────────────────────────────────────────────────────────────── function scrollToStops() { - document.getElementById("stops")?.scrollIntoView({ behavior: "smooth" }); + document.getElementById("tour")?.scrollIntoView({ behavior: "smooth" }); } function scrollToStory() { - document.getElementById("story")?.scrollIntoView({ behavior: "smooth" }); -} -function scrollToProducts() { - document.getElementById("products")?.scrollIntoView({ behavior: "smooth" }); -} -import { getBrandSettingsPublic } from "@/actions/brand-settings"; -import { gsap } from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; - -// Register GSAP plugins -if (typeof window !== "undefined") { - gsap.registerPlugin(ScrollTrigger); + document.getElementById("chronicle")?.scrollIntoView({ behavior: "smooth" }); } -type Brand = { - id: string; - name: string; - slug: string; -}; - +// ── Types ──────────────────────────────────────────────────────────────────── +type Brand = { id: string; name: string; slug: string }; type Stop = { id: string; city: string; @@ -50,7 +47,6 @@ type Stop = { slug: string; brand_id: string; }; - type Product = { id: string; name: string; @@ -63,6 +59,7 @@ type Product = { pickup_type?: "scheduled_stop" | "shed"; }; +// ── State machine ──────────────────────────────────────────────────────────── type State = { brand: Brand | null; stops: Stop[]; @@ -75,8 +72,6 @@ type State = { showSchedulePdf: boolean; showWholesaleLink: boolean; heroTagline: string | null; - heroImageUrl: string | null; - heroVideoUrl: string | null; isAdmin: boolean; customFooterText: string | null; contactEmail: string | null; @@ -85,7 +80,6 @@ type State = { brandBgColor: string | null; brandTextColor: string | null; }; - type Action = | { type: "SET_BRAND"; brand: Brand | null } | { type: "SET_STOPS"; stops: Stop[] } @@ -100,8 +94,6 @@ type Action = showSchedulePdf: boolean; showWholesaleLink: boolean; heroTagline: string | null; - heroImageUrl: string | null; - heroVideoUrl: string | null; customFooterText: string | null; contactEmail: string | null; contactPhone: string | null; @@ -123,8 +115,6 @@ const initialState: State = { showSchedulePdf: true, showWholesaleLink: true, heroTagline: null, - heroImageUrl: null, - heroVideoUrl: null, isAdmin: false, customFooterText: null, contactEmail: null, @@ -136,718 +126,964 @@ const initialState: State = { function reducer(state: State, action: Action): State { switch (action.type) { - case "SET_BRAND": - return { ...state, brand: action.brand }; - case "SET_STOPS": - return { ...state, stops: action.stops }; - case "SET_PRODUCTS": - return { ...state, products: action.products }; - case "SET_SETTINGS": - return { - ...state, - wholesaleEnabled: action.wholesaleEnabled, - logoUrl: action.logoUrl, - logoUrlDark: action.logoUrlDark, - olatheSweetLogoUrl: action.olatheSweetLogoUrl, - olatheSweetLogoUrlDark: action.olatheSweetLogoUrlDark, - showSchedulePdf: action.showSchedulePdf, - showWholesaleLink: action.showWholesaleLink, - heroTagline: action.heroTagline, - heroImageUrl: action.heroImageUrl, - heroVideoUrl: action.heroVideoUrl, - customFooterText: action.customFooterText, - contactEmail: action.contactEmail, - contactPhone: action.contactPhone, - brandPrimaryColor: action.brandPrimaryColor, - brandBgColor: action.brandBgColor, - brandTextColor: action.brandTextColor, - }; - case "SET_IS_ADMIN": - return { ...state, isAdmin: action.isAdmin }; + case "SET_BRAND": return { ...state, brand: action.brand }; + case "SET_STOPS": return { ...state, stops: action.stops }; + case "SET_PRODUCTS": return { ...state, products: action.products }; + case "SET_SETTINGS": return { + ...state, + wholesaleEnabled: action.wholesaleEnabled, + logoUrl: action.logoUrl, + logoUrlDark: action.logoUrlDark, + olatheSweetLogoUrl: action.olatheSweetLogoUrl, + olatheSweetLogoUrlDark: action.olatheSweetLogoUrlDark, + showSchedulePdf: action.showSchedulePdf, + showWholesaleLink: action.showWholesaleLink, + heroTagline: action.heroTagline, + customFooterText: action.customFooterText, + contactEmail: action.contactEmail, + contactPhone: action.contactPhone, + brandPrimaryColor: action.brandPrimaryColor, + brandBgColor: action.brandBgColor, + brandTextColor: action.brandTextColor, + }; + case "SET_IS_ADMIN": return { ...state, isAdmin: action.isAdmin }; } } -// ── Pull-quote band — full-bleed editorial interlude ──────────────── -// A single Fraunces italic quote floating over the pre-storm field. -// No chrome. The image does the talking. -function PullQuoteBand() { +// ───────────────────────────────────────────────────────────────────────────── +// 1. EDITOR'S NOTE +// ───────────────────────────────────────────────────────────────────────────── +function EditorsNote() { return ( -
- Pre-storm corn rows receding to the mountains at dusk - {/* Layered tints: top darkens the sky, bottom grounds the type. */} -
- {/* One warm amber rim along the lower edge to echo the corn. */} -