Refine topography to hero and footer, improve readability and hero motion.
Some checks are pending
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Waiting to run

Limit mainwhite pattern to the landing hero and a bottom footer band; remove it from sections and page headers. Add vertical water-flow animation and stronger hero particles with hover boost. Fix green text on white sections and card descriptions in green bands, including the two-days program cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“kirukib” 2026-05-21 14:29:39 +03:00
parent 3693495dd0
commit efa48f6f6b
29 changed files with 643 additions and 155 deletions

View File

@ -37,11 +37,13 @@ export default function ExhibitPage() {
</PageRiftHeader> </PageRiftHeader>
<Section variant="muted"> <Section variant="muted">
<div className="topo-on-green-bg max-w-2xl">
<h2 className="text-2xl font-bold">Booth packages</h2> <h2 className="text-2xl font-bold">Booth packages</h2>
<p className="mt-2 max-w-2xl text-muted-foreground"> <p className="mt-2 text-white/85">
Choose a footprint that fits how you want to present your brand and products. Final Choose a footprint that fits how you want to present your brand and products. Final
placement and pricing are confirmed by our exhibitions team. placement and pricing are confirmed by our exhibitions team.
</p> </p>
</div>
<div className="mt-10"> <div className="mt-10">
<BoothPackages /> <BoothPackages />
</div> </div>

View File

@ -389,29 +389,233 @@
font-family: var(--font-hero-serif), Georgia, "Times New Roman", serif; font-family: var(--font-hero-serif), Georgia, "Times New Roman", serif;
} }
/* ─── mainwhite.svg on white sections only ─── */ /* ─── mainwhite.svg: hero (green tint) · footer (white on green) ─── */
.topo-tone-light .topo-pattern-asset { .topo-tone-light .topo-pattern-asset {
mix-blend-mode: multiply; mix-blend-mode: multiply;
filter: sepia(0.35) saturate(2.6) hue-rotate(92deg) brightness(0.92) contrast(1.2); filter: sepia(0.35) saturate(2.6) hue-rotate(92deg) brightness(0.92) contrast(1.2);
} }
/* Hero — water flowing through contour channels (vertical, not side shake) */
.rift-hero-topo .topo-hero-pattern-img {
opacity: 0.48;
mix-blend-mode: multiply;
filter: sepia(0.35) saturate(2.6) hue-rotate(92deg) brightness(0.92) contrast(1.2);
transform: scale(1.08);
animation: topo-hero-base-flow 20s linear infinite;
}
.rift-hero-topo .topo-hero-water-flow-img {
opacity: 0.34;
mix-blend-mode: multiply;
filter: sepia(0.35) saturate(3) hue-rotate(92deg) brightness(1.12) contrast(1.18);
transform: scale(1.12);
animation: topo-hero-water-stream 11s linear infinite;
}
.topo-hero-water-shimmer {
background: linear-gradient(
to top,
transparent 0%,
rgba(26, 92, 56, 0.05) 12%,
rgba(45, 122, 82, 0.18) 28%,
rgba(140, 220, 175, 0.38) 42%,
rgba(80, 160, 120, 0.32) 52%,
rgba(45, 122, 82, 0.22) 62%,
rgba(26, 92, 56, 0.08) 78%,
transparent 100%
);
background-size: 120% 320%;
mix-blend-mode: soft-light;
opacity: 0.65;
animation: topo-hero-shimmer-rise 8s ease-in-out infinite;
}
.topo-hero-channel-glow {
background: radial-gradient(
ellipse 28% 18% at 50% var(--flow-y, 60%),
rgba(120, 200, 160, 0.45) 0%,
rgba(45, 122, 82, 0.2) 35%,
transparent 70%
);
mix-blend-mode: soft-light;
opacity: 0.7;
animation: topo-hero-channel-glow 8s ease-in-out infinite;
}
.topo-hero-water-spotlight {
background: radial-gradient(
circle at var(--topo-hover-x, 50%) var(--topo-hover-y, 50%),
rgba(120, 200, 160, 0.28) 0%,
rgba(45, 122, 82, 0.12) 22%,
transparent 52%
);
mix-blend-mode: soft-light;
opacity: 0;
transition: opacity 0.45s ease;
}
.rift-hero-topo--hover .topo-hero-water-flow-img {
opacity: 0.52;
animation-duration: 5.5s;
}
.rift-hero-topo--hover .topo-hero-water-shimmer {
opacity: 1;
animation-duration: 4s;
}
.rift-hero-topo--hover .topo-hero-channel-glow {
opacity: 1;
animation-duration: 4.5s;
}
.rift-hero-topo--hover .topo-hero-water-spotlight {
opacity: 1;
}
.rift-hero-topo--hover .topo-hero-pattern-img {
opacity: 0.56;
animation-duration: 12s;
}
@keyframes topo-hero-base-flow {
0% {
object-position: 50% 100%;
}
100% {
object-position: 50% 0%;
}
}
@keyframes topo-hero-water-stream {
0% {
object-position: 46% 100%;
opacity: 0.22;
}
40% {
opacity: 0.42;
}
100% {
object-position: 54% 0%;
opacity: 0.26;
}
}
@keyframes topo-hero-shimmer-rise {
0% {
background-position: 40% 110%;
}
100% {
background-position: 60% -15%;
}
}
@keyframes topo-hero-channel-glow {
0% {
--flow-y: 95%;
opacity: 0.35;
}
50% {
--flow-y: 35%;
opacity: 0.85;
}
100% {
--flow-y: -5%;
opacity: 0.4;
}
}
/* Footer — white pattern at bottom only */
.topo-footer-pattern .topo-footer-pattern-asset {
opacity: 0.34;
filter: brightness(0) invert(1);
mix-blend-mode: soft-light;
}
.topo-content-layer { .topo-content-layer {
position: relative; position: relative;
z-index: 10; z-index: 10;
} }
/* Plain readable copy on white — no glow, shadow, or blur */ /* Readable brand-green copy on all white sections */
.section-white {
color: #0d3d26;
}
.section-white .topo-content-layer,
.section-white .topo-content-readable, .section-white .topo-content-readable,
.section-white .topo-content-readable :is(h1, h2, h3, h4, p, li, label, summary), .section-white .topo-content-layer :is(h1, h2, h3, h4, h5, h6, p, li, label, summary),
.section-white .topo-content-readable :is(h1, h2, h3, h4, h5, h6, p, li, label, summary),
.section-white .topo-content-layer a:not([data-slot="button"]),
.section-white .topo-content-readable a:not([data-slot="button"]) { .section-white .topo-content-readable a:not([data-slot="button"]) {
color: #0d3d26; color: #0d3d26;
text-shadow: none; text-shadow: none;
} }
.section-white .topo-content-layer .text-foreground,
.section-white .topo-content-readable .text-foreground {
color: #1a5c38;
}
.section-white .topo-content-layer .text-muted-foreground,
.section-white .topo-content-readable .text-muted-foreground { .section-white .topo-content-readable .text-muted-foreground {
color: #3d5248; color: #3d5248;
} }
.section-white .topo-prose-surface-light :is(h1, h2, h3, h4, p, li, span) {
color: #0d3d26;
}
.section-white .topo-prose-surface-light .text-muted-foreground {
color: #3d5248;
}
.section-white .topo-card-surface,
.section-white .topo-card-surface :is(h1, h2, h3, h4, p, span, li, label, a),
.section-white .bg-white.topo-card-surface :is(h1, h2, h3, h4, p, span),
.section-white [data-slot="card"],
.section-white [data-slot="card"] :is(h1, h2, h3, h4, p, span, li, label) {
color: #1a5c38;
}
.section-white .topo-card-surface .text-muted-foreground,
.section-white [data-slot="card"] .text-muted-foreground,
.section-white .bg-white .text-muted-foreground {
color: #3d5248;
}
.section-white [data-slot="accordion-trigger"] {
color: #0d3d26;
}
.section-white [data-slot="button"][data-variant="outline"],
.section-white [data-slot="button"][data-variant="ghost"],
.section-white [data-slot="button"][data-variant="link"] {
color: #1a5c38;
}
.section-white [data-slot="button"][data-variant="outline"] {
border-color: rgba(26, 92, 56, 0.35);
}
.section-white [data-slot="button"][data-variant="ghost"]:hover,
.section-white [data-slot="button"][data-variant="link"]:hover {
color: #0d3d26;
background-color: rgba(26, 92, 56, 0.06);
}
header.section-white {
color: #0d3d26;
}
header.section-white .text-muted-foreground {
color: #3d5248;
}
.section-white:has(.topo-curvy-extend) {
overflow: visible;
}
.topo-curvy-extend { .topo-curvy-extend {
overflow: visible; overflow: visible;
} }
@ -449,43 +653,127 @@
isolation: isolate; isolation: isolate;
} }
.section-green .topo-content-layer, /* Copy sitting directly on green */
.section-green .topo-content-layer :is(h1, h2, h3, h4, p, li, a, label, span) { .section-green .topo-on-green-bg,
.section-green .topo-on-green-bg :is(h1, h2, h3, h4, p, li, label) {
color: #ffffff; color: #ffffff;
text-shadow: none; text-shadow: none;
} }
.section-green .text-muted-foreground { .section-green .topo-on-green-bg .text-muted-foreground {
color: rgba(255, 255, 255, 0.88); color: rgba(255, 255, 255, 0.88);
} }
.section-green [data-slot="button"][data-variant="outline"] { /* Muted copy on green (outside white cards) → white, not grey */
.section-green
.topo-content-layer
:not([data-slot="card"]):not([data-slot="card"] *)
.text-muted-foreground {
color: rgba(255, 255, 255, 0.88);
}
/* White cards / surfaces inside green sections → brand green text */
.section-green .topo-card-surface,
.section-green .topo-card-surface :is(h1, h2, h3, h4, p, span, li, label, a),
.section-green .bg-white,
.section-green .bg-white :is(h1, h2, h3, h4, p, span, li, label),
.section-green [data-slot="card"],
.section-green [data-slot="card"] :is(h1, h2, h3, h4, p, span, li, label, a) {
color: #1a5c38;
text-shadow: none;
}
.section-green .topo-card-surface .text-muted-foreground,
.section-green .bg-white .text-muted-foreground,
.section-green [data-slot="card"] .text-muted-foreground,
.section-green [data-slot="card"] [data-slot="card-description"],
.section-green .topo-card-surface [data-slot="card-description"] {
color: #3d5248;
}
.topo-card-link,
.topo-card-link svg {
color: #1a5c38;
}
.topo-card-link:hover,
.topo-card-link:hover svg {
color: #0d3d26;
}
.section-green .topo-on-green-bg [data-slot="button"][data-variant="outline"] {
border-color: rgba(255, 255, 255, 0.75); border-color: rgba(255, 255, 255, 0.75);
background-color: transparent; background-color: transparent;
color: #ffffff; color: #ffffff;
box-shadow: none; box-shadow: none;
} }
.section-green [data-slot="button"][data-variant="outline"]:hover { .section-green .topo-on-green-bg [data-slot="button"][data-variant="outline"]:hover {
background-color: rgba(255, 255, 255, 0.14); background-color: rgba(255, 255, 255, 0.14);
color: #ffffff; color: #ffffff;
} }
.section-green [data-slot="button"][data-variant="ghost"] { /* Ghost/link buttons on the green band (outside white cards) */
.section-green .topo-on-green-bg [data-slot="button"][data-variant="ghost"],
.section-green .topo-on-green-bg [data-slot="button"][data-variant="link"],
.section-green > .topo-content-layer > div:not(:has([data-slot="card"])) [data-slot="button"][data-variant="outline"] {
color: #ffffff; color: #ffffff;
} }
.section-green [data-slot="button"][data-variant="ghost"]:hover { .section-green .topo-on-green-bg [data-slot="button"][data-variant="ghost"]:hover {
background-color: rgba(255, 255, 255, 0.12); background-color: rgba(255, 255, 255, 0.12);
color: #ffffff; color: #ffffff;
} }
.section-green [data-slot="button"][data-variant="default"]:not([class*="bg-[#ffb300]"]) { /* Ghost/link/outline actions inside white cards on green sections */
.section-green .topo-card-surface .topo-card-link,
.section-green [data-slot="card"] .topo-card-link,
.section-green .topo-card-surface .text-foreground,
.section-green [data-slot="card"] .text-foreground,
.section-green [data-slot="card"] [data-slot="button"],
.section-green [data-slot="card"] [data-slot="button"][data-variant="ghost"],
.section-green [data-slot="card"] [data-slot="button"][data-variant="link"],
.section-green .topo-card-surface [data-slot="button"],
.section-green .topo-card-surface [data-slot="button"][data-variant="ghost"],
.section-green .topo-card-surface [data-slot="button"][data-variant="link"],
.section-green .bg-white [data-slot="button"][data-variant="ghost"],
.section-green .bg-white [data-slot="button"][data-variant="link"] {
color: #1a5c38;
}
.section-green [data-slot="card"] [data-slot="button"][data-variant="ghost"]:hover,
.section-green [data-slot="card"] [data-slot="button"][data-variant="link"]:hover,
.section-green .topo-card-surface [data-slot="button"][data-variant="ghost"]:hover,
.section-green .topo-card-surface [data-slot="button"][data-variant="link"]:hover {
color: #0d3d26;
background-color: rgba(26, 92, 56, 0.06);
}
.section-green [data-slot="card"] [data-slot="button"][data-variant="outline"],
.section-green .topo-card-surface [data-slot="button"][data-variant="outline"],
.section-green .bg-white [data-slot="button"][data-variant="outline"] {
border-color: rgba(26, 92, 56, 0.4);
color: #1a5c38;
background-color: transparent;
}
.section-green [data-slot="card"] [data-slot="button"][data-variant="outline"]:hover,
.section-green .topo-card-surface [data-slot="button"][data-variant="outline"]:hover {
border-color: rgba(26, 92, 56, 0.55);
color: #0d3d26;
background-color: rgba(26, 92, 56, 0.06);
}
.section-green [data-slot="card"] [data-slot="button"] svg,
.section-green .topo-card-surface [data-slot="button"] svg {
color: currentColor;
}
.section-green .topo-on-green-bg [data-slot="button"][data-variant="default"]:not([class*="bg-[#ffb300]"]) {
background-color: #ffb300; background-color: #ffb300;
color: #0f0404; color: #0f0404;
} }
.section-green [data-slot="button"][data-variant="default"]:not([class*="bg-[#ffb300]"]):hover { .section-green .topo-on-green-bg [data-slot="button"][data-variant="default"]:not([class*="bg-[#ffb300]"]):hover {
background-color: #e6a200; background-color: #e6a200;
color: #0f0404; color: #0f0404;
} }
@ -529,17 +817,6 @@
} }
.rift-hero-settled .rift-hero-pattern-site .rift-contour-path,
.rift-hero-settled .rift-hero-pattern-water .rift-contour-path {
animation: rift-contour-pulse 8s ease-in-out infinite;
stroke-dashoffset: 0;
}
.rift-hero-settled .rift-channel-inner {
animation: rift-river-shimmer 6s ease-in-out infinite;
stroke-dashoffset: 0;
}
.rift-crack-line { .rift-crack-line {
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
@ -778,8 +1055,18 @@
.rift-hero-settled .rift-contour-path, .rift-hero-settled .rift-contour-path,
.rift-hero-settled .rift-channel-inner, .rift-hero-settled .rift-channel-inner,
.rift-ambient-pulse .rift-contour-path, .rift-ambient-pulse .rift-contour-path,
.rift-pulse-animate { .rift-pulse-animate,
.rift-hero-topo .topo-hero-pattern-img,
.rift-hero-topo .topo-hero-water-flow-img,
.rift-hero-topo .topo-hero-water-shimmer,
.rift-hero-topo .topo-hero-channel-glow {
animation: none !important; animation: none !important;
} }
.rift-hero-topo .topo-hero-water-shimmer,
.rift-hero-topo .topo-hero-water-spotlight,
.rift-hero-topo .topo-hero-channel-glow {
opacity: 0 !important;
}
} }
} }

View File

@ -35,7 +35,7 @@ export default function PartnersPage() {
</div> </div>
</PageRiftHeader> </PageRiftHeader>
<Section variant="muted"> <Section>
<div className="space-y-16"> <div className="space-y-16">
{sponsorSections.map((section, index) => ( {sponsorSections.map((section, index) => (
<PartnerSectionBlock <PartnerSectionBlock
@ -55,7 +55,7 @@ export default function PartnersPage() {
</div> </div>
</Section> </Section>
<Section variant="muted"> <Section>
<div className="space-y-16"> <div className="space-y-16">
{supporterSections.map((section) => ( {supporterSections.map((section) => (
<PartnerSectionBlock key={section.id} section={section} /> <PartnerSectionBlock key={section.id} section={section} />

View File

@ -37,7 +37,7 @@ export default function SpeakersPage() {
</h1> </h1>
} }
description={ description={
<p className="text-lg"> <p className="text-lg text-[#3d5248]">
{site.dates.label} · {site.venue.name} {site.dates.label} · {site.venue.name}
</p> </p>
} }
@ -48,10 +48,10 @@ export default function SpeakersPage() {
{(Object.entries(grouped) as [SpeakerGroup, typeof speakers][]).map( {(Object.entries(grouped) as [SpeakerGroup, typeof speakers][]).map(
([group, list]) => ( ([group, list]) => (
<div key={group}> <div key={group}>
<div className="mb-8 flex flex-wrap items-end justify-between gap-4 border-b border-border pb-4"> <div className="mb-8 flex flex-wrap items-end justify-between gap-4 border-b border-white/20 pb-4">
<div> <div className="topo-on-green-bg">
<h2 className="text-3xl font-bold">{speakerGroupLabels[group]}</h2> <h2 className="text-3xl font-bold">{speakerGroupLabels[group]}</h2>
<p className="mt-1 text-sm text-muted-foreground">{site.dates.label}</p> <p className="mt-1 text-sm text-white/88">{site.dates.label}</p>
</div> </div>
</div> </div>
<div className="grid gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> <div className="grid gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">

View File

@ -0,0 +1,38 @@
import { MAIN_WHITE_PATTERN_SRC } from "@/content/topo-patterns";
import { cn } from "@/lib/utils";
type Props = {
className?: string;
};
/** White topography pattern anchored to the bottom of the green footer only. */
export function FooterTopoPattern({ className }: Props) {
return (
<div
className={cn(
"topo-footer-pattern pointer-events-none absolute inset-x-0 bottom-0 z-0 h-[min(42vh,320px)] overflow-hidden",
className
)}
aria-hidden
>
<div
className="absolute inset-x-0 bottom-0 h-full"
style={{
maskImage:
"linear-gradient(to top, black 0%, black 38%, rgba(0,0,0,0.55) 62%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to top, black 0%, black 38%, rgba(0,0,0,0.55) 62%, transparent 100%)",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={MAIN_WHITE_PATTERN_SRC}
alt=""
className="topo-footer-pattern-asset block h-full w-full object-cover object-bottom"
draggable={false}
/>
</div>
<div className="absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-[#1a5c38] to-transparent" />
</div>
);
}

View File

@ -5,53 +5,113 @@ type Props = {
}; };
/** /**
* Curvy contour lines extending from white pattern sections into the green band below. * Curvy contours from the white section edge, extending downward into the green section.
*/ */
export function TopoCurvyExtend({ className }: Props) { export function TopoCurvyExtend({ className }: Props) {
return ( return (
<div <div
className={cn( className={cn(
"topo-curvy-extend pointer-events-none absolute right-0 bottom-0 left-0 z-[6] h-16 translate-y-[45%] md:h-24 md:translate-y-[40%]", "topo-curvy-extend pointer-events-none absolute top-full right-0 left-0 z-[6] h-28 w-full translate-y-[52%] md:h-40",
className className
)} )}
aria-hidden aria-hidden
> >
<svg <svg
className="h-full w-full" className="h-full w-full"
viewBox="0 0 1440 96" viewBox="0 0 1440 160"
preserveAspectRatio="none" preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
{/* Upper curves — sit on the white/green boundary */}
<path <path
d="M-40 52 C 180 8, 420 88, 720 44 S 1180 12, 1480 56" d="M-40 24 C 200 2, 480 44, 720 14 S 1180 4, 1480 28"
fill="none" fill="none"
stroke="#1a5c38" stroke="#1a5c38"
strokeWidth="1.25" strokeWidth="1.25"
strokeOpacity="0.42" strokeOpacity="0.45"
strokeLinecap="round" strokeLinecap="round"
/> />
<path <path
d="M-20 68 C 260 92, 520 28, 800 72 S 1220 88, 1500 38" d="M0 8 C 280 32, 560 4, 880 28 S 1240 42, 1480 12"
fill="none" fill="none"
stroke="#1a5c38" stroke="#1a5c38"
strokeWidth="1" strokeWidth="1"
strokeOpacity="0.32" strokeOpacity="0.32"
strokeLinecap="round" strokeLinecap="round"
/> />
{/* Mid curves — dip into green */}
<path <path
d="M0 36 C 320 64, 640 16, 960 48 S 1320 76, 1480 28" d="M120 42 C 320 64, 520 34, 720 78 S 980 46, 1240 88, 1480 52"
fill="none" fill="none"
stroke="#1a5c38" stroke="#1a5c38"
strokeWidth="1.5" strokeWidth="1.2"
strokeOpacity="0.38"
strokeLinecap="round"
/>
<path
d="M-20 54 C 240 86, 460 58, 700 104 S 1020 68, 1320 112, 1500 76"
fill="none"
stroke="#2d7a52"
strokeWidth="1"
strokeOpacity="0.35"
strokeLinecap="round"
/>
{/* Lower curves — deep in green */}
<path
d="M80 68 C 360 96, 600 72, 840 118 S 1100 86, 1380 128"
fill="none"
stroke="#1a5c38"
strokeWidth="0.9"
strokeOpacity="0.28" strokeOpacity="0.28"
strokeLinecap="round" strokeLinecap="round"
/> />
<path <path
d="M80 82 C 400 58, 700 94, 1040 62 S 1280 42, 1460 78" d="M200 92 C 440 118, 640 98, 880 138 S 1080 108, 1320 148"
fill="none" fill="none"
stroke="#2d7a52" stroke="#2d7a52"
strokeWidth="0.85"
strokeOpacity="0.26"
strokeLinecap="round"
/>
{/* Vertical strokes — continue downward into green */}
<path
d="M360 38 L360 132"
fill="none"
stroke="#1a5c38"
strokeWidth="0.75"
strokeOpacity="0.3"
strokeLinecap="round"
/>
<path
d="M720 14 L720 148"
fill="none"
stroke="#1a5c38"
strokeWidth="0.9" strokeWidth="0.9"
strokeOpacity="0.38" strokeOpacity="0.35"
strokeLinecap="round"
/>
<path
d="M1080 46 L1080 138"
fill="none"
stroke="#2d7a52"
strokeWidth="0.7"
strokeOpacity="0.28"
strokeLinecap="round"
/>
<path
d="M520 34 L520 118"
fill="none"
stroke="#1a5c38"
strokeWidth="0.65"
strokeOpacity="0.25"
strokeLinecap="round"
/>
<path
d="M920 28 L920 124"
fill="none"
stroke="#2d7a52"
strokeWidth="0.65"
strokeOpacity="0.22"
strokeLinecap="round" strokeLinecap="round"
/> />
</svg> </svg>

View File

@ -5,13 +5,13 @@ export function BoothPackages() {
return ( return (
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
{boothPackages.map((pkg) => ( {boothPackages.map((pkg) => (
<Card key={pkg.id} className="border-border"> <Card key={pkg.id} className="topo-card-surface border-border bg-white text-[#1a5c38]">
<CardHeader> <CardHeader>
<CardTitle className="text-lg">{pkg.name}</CardTitle> <CardTitle className="text-lg">{pkg.name}</CardTitle>
<CardDescription className="font-medium text-[#1f3d7e]">{pkg.size}</CardDescription> <CardDescription className="font-medium text-[#1a5c38]/90">{pkg.size}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-sm text-muted-foreground leading-relaxed">{pkg.description}</p> <p className="text-sm text-[#3d5248] leading-relaxed">{pkg.description}</p>
<ul className="space-y-2"> <ul className="space-y-2">
{pkg.highlights.map((h) => ( {pkg.highlights.map((h) => (
<li key={h} className="flex gap-2 text-sm"> <li key={h} className="flex gap-2 text-sm">

View File

@ -10,7 +10,7 @@ import { cn } from "@/lib/utils";
export function AttendSummitSection() { export function AttendSummitSection() {
return ( return (
<Section id="attend" variant="muted" className="py-14 md:py-20"> <Section id="attend" variant="muted" className="py-14 md:py-20">
<div className="mx-auto max-w-3xl text-center"> <div className="topo-on-green-bg mx-auto max-w-3xl text-center">
<p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]"> <p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]">
{attendCopy.eyebrow} {attendCopy.eyebrow}
</p> </p>
@ -33,25 +33,25 @@ export function AttendSummitSection() {
delay={i * 75} delay={i * 75}
as="article" as="article"
className={cn( className={cn(
"group flex flex-col rounded-2xl border border-border bg-white p-6 shadow-sm", "topo-card-surface group flex flex-col rounded-2xl border border-border bg-white p-6 text-[#1a5c38] shadow-sm",
"transition-shadow duration-200 hover:border-[#1a5c38]/25 hover:shadow-md" "transition-shadow duration-200 hover:border-[#1a5c38]/25 hover:shadow-md"
)} )}
> >
<span className="inline-flex size-11 items-center justify-center rounded-xl bg-[#1a5c38]/10 text-[#1a5c38]"> <span className="inline-flex size-11 items-center justify-center rounded-xl bg-[#1a5c38]/10 text-[#1a5c38]">
<Icon className="size-5" strokeWidth={1.75} aria-hidden /> <Icon className="size-5" strokeWidth={1.75} aria-hidden />
</span> </span>
<h3 className="mt-4 text-lg font-bold text-foreground">{path.title}</h3> <h3 className="mt-4 text-lg font-bold text-[#0d3d26]">{path.title}</h3>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted-foreground"> <p className="mt-2 flex-1 text-sm leading-relaxed text-[#3d5248]">
{path.description} {path.description}
</p> </p>
<Button <Button
variant="ghost" variant="ghost"
className="mt-5 h-auto justify-start px-0 text-[#1a5c38] hover:bg-transparent hover:text-[#0d3d26]" className="topo-card-link mt-5 h-auto justify-start px-0 hover:bg-transparent"
asChild asChild
> >
<Link href={path.href}> <Link href={path.href}>
{path.cta} {path.cta}
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" /> <ArrowRight className="size-4 text-current transition-transform group-hover:translate-x-0.5" />
</Link> </Link>
</Button> </Button>
</ScrollReveal> </ScrollReveal>

View File

@ -21,12 +21,12 @@ export function BoothAcquisitionBand() {
{exhibitCopy.eyebrow} {exhibitCopy.eyebrow}
</p> </p>
<h2 className="mt-3 text-3xl font-bold md:text-4xl">{exhibitCopy.headline}</h2> <h2 className="mt-3 text-3xl font-bold md:text-4xl">{exhibitCopy.headline}</h2>
<p className="mt-4 text-muted-foreground leading-relaxed">{exhibitCopy.subheadline}</p> <p className="mt-4 text-[#3d5248] leading-relaxed">{exhibitCopy.subheadline}</p>
<div className="mt-8 flex flex-wrap gap-3"> <div className="mt-8 flex flex-wrap gap-3">
<Button className="rounded-full bg-[#ffb300] text-[#0f0404] hover:bg-[#ffb300]/90" asChild> <Button className="rounded-full bg-[#ffb300] text-[#0f0404] hover:bg-[#ffb300]/90" asChild>
<Link href="/exhibit#reserve-booth">Reserve a booth</Link> <Link href="/exhibit#reserve-booth">Reserve a booth</Link>
</Button> </Button>
<Button variant="outline" className="rounded-full" asChild> <Button variant="outline" className="topo-card-link rounded-full" asChild>
<Link href="/exhibit">View booth packages</Link> <Link href="/exhibit">View booth packages</Link>
</Button> </Button>
</div> </div>

View File

@ -11,7 +11,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
export function ExperienceCards() { export function ExperienceCards() {
return ( return (
<Section variant="inverse" id="program"> <Section variant="inverse" id="program">
<TopoProseSurface tone="green" className="max-w-3xl"> <TopoProseSurface tone="green" className="topo-on-green-bg max-w-3xl">
<h2 className="text-3xl font-bold md:text-5xl"> <h2 className="text-3xl font-bold md:text-5xl">
Two days to go deep into what Ethiopia&apos;s innovators need Two days to go deep into what Ethiopia&apos;s innovators need
</h2> </h2>
@ -22,7 +22,7 @@ export function ExperienceCards() {
<div className="mt-12 grid gap-6 md:grid-cols-3"> <div className="mt-12 grid gap-6 md:grid-cols-3">
{experiences.map((exp, i) => ( {experiences.map((exp, i) => (
<ScrollReveal key={exp.id} variant="card" delay={i * 90} as="div"> <ScrollReveal key={exp.id} variant="card" delay={i * 90} as="div">
<Card className="overflow-hidden border-0 bg-white text-foreground"> <Card className="topo-card-surface overflow-hidden border-0 bg-white text-[#1a5c38]">
<div className="relative h-48"> <div className="relative h-48">
<Image <Image
src="/branding/booth-mockup.png" src="/branding/booth-mockup.png"
@ -32,14 +32,18 @@ export function ExperienceCards() {
/> />
</div> </div>
<CardHeader> <CardHeader>
<p className="text-xs font-medium uppercase text-muted-foreground"> <p className="text-xs font-medium uppercase text-[#3d5248]">
Day {i + 1} Day {i + 1}
</p> </p>
<CardTitle>{exp.title}</CardTitle> <CardTitle>{exp.title}</CardTitle>
<CardDescription>{exp.description}</CardDescription> <CardDescription className="text-[#3d5248]">{exp.description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button variant="ghost" className="px-0 text-[#1f3d7e]" asChild> <Button
variant="ghost"
className="topo-card-link px-0 hover:bg-transparent"
asChild
>
<Link href={exp.href}> <Link href={exp.href}>
Learn more <ArrowRight className="size-4" /> Learn more <ArrowRight className="size-4" />
</Link> </Link>

View File

@ -22,7 +22,7 @@ export function Faq() {
<span className="mr-3 text-[#ffb300]">{String(i + 1).padStart(2, "0")}.</span> <span className="mr-3 text-[#ffb300]">{String(i + 1).padStart(2, "0")}.</span>
{faq.question} {faq.question}
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-muted-foreground">{faq.answer}</AccordionContent> <AccordionContent className="text-[#3d5248]">{faq.answer}</AccordionContent>
</AccordionItem> </AccordionItem>
))} ))}
</Accordion> </Accordion>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { site } from "@/content/site"; import { site } from "@/content/site";
@ -19,6 +19,24 @@ const INTRO_MS = 10000;
export function Hero() { export function Hero() {
const [reduceMotion, setReduceMotion] = useState(false); const [reduceMotion, setReduceMotion] = useState(false);
const [introPhase, setIntroPhase] = useState<"intro" | "settled" | "static">("intro"); const [introPhase, setIntroPhase] = useState<"intro" | "settled" | "static">("intro");
const [heroHover, setHeroHover] = useState(false);
const [hoverPoint, setHoverPoint] = useState({ x: 50, y: 50 });
const handleHeroPointerMove = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
setHoverPoint({
x: ((e.clientX - rect.left) / rect.width) * 100,
y: ((e.clientY - rect.top) / rect.height) * 100,
});
setHeroHover(true);
},
[]
);
const handleHeroPointerLeave = useCallback(() => {
setHeroHover(false);
}, []);
useEffect(() => { useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
@ -41,8 +59,18 @@ export function Hero() {
}, []); }, []);
return ( return (
<section className="section-white relative isolate min-h-[min(100svh,900px)] overflow-x-hidden bg-white pb-10"> <section
<HeroTopographyBackground introPhase={introPhase} className="absolute inset-0" /> className="section-white relative isolate min-h-[min(100svh,900px)] overflow-x-hidden overflow-y-visible bg-white"
onMouseMove={handleHeroPointerMove}
onMouseLeave={handleHeroPointerLeave}
>
<HeroTopographyBackground
introPhase={introPhase}
hoverActive={heroHover && !reduceMotion}
hoverX={hoverPoint.x}
hoverY={hoverPoint.y}
className="absolute inset-0"
/>
<TopoCurvyExtend /> <TopoCurvyExtend />
<HeroRiftParticles <HeroRiftParticles
active={!reduceMotion} active={!reduceMotion}

View File

@ -2,7 +2,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
const MAX_PARTICLES = 48; const MAX_PARTICLES = 64;
type Particle = { type Particle = {
x: number; x: number;
@ -11,6 +11,7 @@ type Particle = {
vx: number; vx: number;
size: number; size: number;
alpha: number; alpha: number;
glow: number;
}; };
type Props = { type Props = {
@ -45,15 +46,19 @@ export function HeroRiftParticles({ active, className }: Props) {
canvas.style.height = `${h}px`; canvas.style.height = `${h}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const count = Math.min(MAX_PARTICLES, Math.floor((w * h) / 12000)); const count = Math.min(MAX_PARTICLES, Math.floor((w * h) / 8500));
particles = Array.from({ length: count }, () => ({ particles = Array.from({ length: count }, () => {
x: w * (0.35 + Math.random() * 0.3), const large = Math.random() < 0.18;
y: h * (0.45 + Math.random() * 0.35), return {
vy: -0.15 - Math.random() * 0.35, x: w * (0.28 + Math.random() * 0.44),
vx: (Math.random() - 0.5) * 0.2, y: h * (0.38 + Math.random() * 0.42),
size: 0.6 + Math.random() * 1.4, vy: -0.25 - Math.random() * (large ? 0.55 : 0.35),
alpha: 0.15 + Math.random() * 0.35, vx: (Math.random() - 0.5) * 0.12,
})); size: large ? 2.2 + Math.random() * 3.2 : 1.2 + Math.random() * 2.4,
alpha: 0.35 + Math.random() * 0.45,
glow: large ? 14 + Math.random() * 10 : 8 + Math.random() * 6,
};
});
}; };
const tick = () => { const tick = () => {
@ -64,14 +69,31 @@ export function HeroRiftParticles({ active, className }: Props) {
for (const p of particles) { for (const p of particles) {
p.x += p.vx; p.x += p.vx;
p.y += p.vy; p.y += p.vy;
if (p.y < h * 0.25) {
p.y = h * (0.55 + Math.random() * 0.3); if (p.y < h * 0.18) {
p.x = w * (0.35 + Math.random() * 0.3); p.y = h * (0.52 + Math.random() * 0.35);
p.x = w * (0.28 + Math.random() * 0.44);
} }
if (p.x < w * 0.15) p.vx += 0.02;
if (p.x > w * 0.85) p.vx -= 0.02;
ctx.save();
ctx.shadowBlur = p.glow;
ctx.shadowColor = "rgba(255, 200, 90, 0.85)";
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = `rgba(255, 191, 80, ${p.alpha})`; const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 1.8);
g.addColorStop(0, `rgba(255, 220, 140, ${p.alpha})`);
g.addColorStop(0.45, `rgba(255, 191, 80, ${p.alpha * 0.75})`);
g.addColorStop(1, `rgba(45, 122, 82, 0)`);
ctx.fillStyle = g;
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = `rgba(255, 235, 180, ${p.alpha * 0.55})`;
ctx.lineWidth = 0.6;
ctx.stroke();
ctx.restore();
} }
raf = requestAnimationFrame(tick); raf = requestAnimationFrame(tick);
@ -89,11 +111,5 @@ export function HeroRiftParticles({ active, className }: Props) {
if (!active) return null; if (!active) return null;
return ( return <canvas ref={canvasRef} className={className} aria-hidden />;
<canvas
ref={canvasRef}
className={className}
aria-hidden
/>
);
} }

View File

@ -1,31 +1,65 @@
"use client"; "use client";
import { TopographicPattern } from "@/components/brand/TopographicPattern"; import type { CSSProperties } from "react";
import { SITE_TOPO_PATTERN } from "@/content/topo-patterns"; import { MAIN_WHITE_PATTERN_SRC } from "@/content/topo-patterns";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type Props = { type Props = {
introPhase: "intro" | "settled" | "static"; introPhase: "intro" | "settled" | "static";
className?: string; className?: string;
hoverActive?: boolean;
hoverX?: number;
hoverY?: number;
}; };
export function HeroTopographyBackground({ introPhase, className }: Props) { export function HeroTopographyBackground({
introPhase,
className,
hoverActive = false,
hoverX = 50,
hoverY = 50,
}: Props) {
return ( return (
<div <div
className={cn( className={cn(
"group/topo-section absolute inset-0 isolate overflow-hidden bg-white", "rift-hero-topo group/topo-section absolute inset-0 isolate overflow-hidden bg-white",
introPhase === "intro" && "rift-hero-intro", introPhase === "intro" && "rift-hero-intro",
introPhase === "settled" && "rift-hero-settled", introPhase === "settled" && "rift-hero-settled",
introPhase === "static" && "rift-hero-static", introPhase === "static" && "rift-hero-static",
hoverActive && "rift-hero-topo--hover",
className className
)} )}
style={
{
"--topo-hover-x": `${hoverX}%`,
"--topo-hover-y": `${hoverY}%`,
} as CSSProperties
}
aria-hidden aria-hidden
> >
<TopographicPattern <div className="topo-hero-pattern-site absolute inset-0">
pattern={SITE_TOPO_PATTERN} {/* eslint-disable-next-line @next/next/no-img-element */}
tone="light" <img
className="topo-hero-layer" src={MAIN_WHITE_PATTERN_SRC}
alt=""
className="topo-pattern-asset topo-hero-pattern-img block h-full w-full object-cover"
draggable={false}
/> />
</div> </div>
<div className="topo-hero-pattern-water absolute inset-0" aria-hidden>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={MAIN_WHITE_PATTERN_SRC}
alt=""
className="topo-hero-water-flow-img block h-full w-full object-cover"
draggable={false}
/>
</div>
<div className="topo-hero-channel-glow absolute inset-0" aria-hidden />
<div className="topo-hero-water-shimmer absolute inset-0" aria-hidden />
<div className="topo-hero-water-spotlight absolute inset-0" aria-hidden />
</div>
); );
} }

View File

@ -4,8 +4,8 @@ export function PartnerMarquee() {
const slots = Array.from({ length: 8 }, (_, i) => i); const slots = Array.from({ length: 8 }, (_, i) => i);
return ( return (
<section className="relative overflow-hidden border-y border-border bg-white py-8"> <section className="section-white relative overflow-hidden border-y border-border bg-white py-8 text-[#0d3d26]">
<p className="mb-6 text-center text-xs font-semibold uppercase tracking-widest text-muted-foreground"> <p className="mb-6 text-center text-xs font-semibold uppercase tracking-widest text-[#3d5248]">
With the support of With the support of
</p> </p>
<div className="overflow-hidden"> <div className="overflow-hidden">

View File

@ -13,12 +13,12 @@ export function PurposeBand() {
<h2 className="mt-3 text-3xl font-bold md:text-4xl"> <h2 className="mt-3 text-3xl font-bold md:text-4xl">
A first-of-its-kind gathering for Ethiopia&apos;s innovators A first-of-its-kind gathering for Ethiopia&apos;s innovators
</h2> </h2>
<p className="mt-4 text-muted-foreground leading-relaxed"> <p className="mt-4 text-[#3d5248] leading-relaxed">
The Great Rift Valley Innovation Summit, presented by the Ethiopian Diaspora Trust The Great Rift Valley Innovation Summit, presented by the Ethiopian Diaspora Trust
Fund (EDTF), convenes entrepreneurs, investors, companies, startups, and jobseekers to Fund (EDTF), convenes entrepreneurs, investors, companies, startups, and jobseekers to
advance tech-enabled innovation in agriculture, healthcare, and education. advance tech-enabled innovation in agriculture, healthcare, and education.
</p> </p>
<p className="mt-4 text-muted-foreground leading-relaxed"> <p className="mt-4 text-[#3d5248] leading-relaxed">
Programming includes an exhibitor hall, workshops and panel discussions, and the Programming includes an exhibitor hall, workshops and panel discussions, and the
inaugural Great Rift Valley Pitch Competition<PurposeGrantText /> Ten companies will inaugural Great Rift Valley Pitch Competition<PurposeGrantText /> Ten companies will
be selected from the most impactful ventures. be selected from the most impactful ventures.

View File

@ -30,7 +30,7 @@ export function Speakers() {
<h2 className="mt-2 text-4xl font-bold tracking-tight md:text-5xl"> <h2 className="mt-2 text-4xl font-bold tracking-tight md:text-5xl">
Meet the voices of GRV Summit Meet the voices of GRV Summit
</h2> </h2>
<p className="mt-4 text-muted-foreground leading-relaxed"> <p className="mt-4 text-[#3d5248] leading-relaxed">
Keynotes, panelists, judges, and opening speakers {site.dates.label} at{" "} Keynotes, panelists, judges, and opening speakers {site.dates.label} at{" "}
{site.venue.name}. {site.venue.name}.
</p> </p>
@ -40,12 +40,12 @@ export function Speakers() {
{(Object.entries(grouped) as [SpeakerGroup, typeof speakers][]).map( {(Object.entries(grouped) as [SpeakerGroup, typeof speakers][]).map(
([group, list]) => ( ([group, list]) => (
<div key={group}> <div key={group}>
<div className="mb-2 flex flex-wrap items-end justify-between gap-4 border-b border-border pb-4"> <div className="mb-2 flex flex-wrap items-end justify-between gap-4 border-b border-[#1a5c38]/15 pb-4">
<div> <div>
<h3 className="text-2xl font-bold md:text-3xl"> <h3 className="text-2xl font-bold md:text-3xl">
{speakerGroupLabels[group]} {speakerGroupLabels[group]}
</h3> </h3>
<p className="mt-1 text-sm text-muted-foreground">{site.dates.label}</p> <p className="mt-1 text-sm text-[#3d5248]">{site.dates.label}</p>
</div> </div>
</div> </div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">

View File

@ -2,26 +2,38 @@ import Link from "next/link";
import { partnerTiers } from "@/content/partners"; import { partnerTiers } from "@/content/partners";
import { PartnerLogoPlaceholder } from "@/components/brand/PartnerLogoPlaceholder"; import { PartnerLogoPlaceholder } from "@/components/brand/PartnerLogoPlaceholder";
import { Section } from "@/components/layout/Section"; import { Section } from "@/components/layout/Section";
import { TopoProseSurface } from "@/components/layout/TopoProseSurface";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
export function SponsorTiers() { export function SponsorTiers() {
return ( return (
<Section variant="muted" id="partners"> <Section id="partners">
<h2 className="text-center text-3xl font-bold text-white">Partners & sponsors</h2> <TopoProseSurface className="mx-auto max-w-3xl text-center">
<p className="mx-auto mt-3 max-w-xl text-center text-white/80"> <p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]">
Partners
</p>
<h2 className="mt-2 text-3xl font-bold text-[#0d3d26] md:text-4xl">
Partners &amp; sponsors
</h2>
<p className="mt-3 text-[#3d5248]">
Logo slots below are open partner with GRV Summit and feature your brand here. Logo slots below are open partner with GRV Summit and feature your brand here.
</p> </p>
</TopoProseSurface>
<div className="mt-12 space-y-12"> <div className="mt-12 space-y-12">
{partnerTiers.slice(0, 2).map((tier) => ( {partnerTiers.slice(0, 2).map((tier) => (
<div key={tier.id}> <div key={tier.id}>
<Separator className="mb-6 bg-white/20" /> <Separator className="mb-6" />
<h3 className="text-center text-sm font-semibold uppercase tracking-wider text-[#ffb300]"> <h3 className="text-center text-sm font-semibold uppercase tracking-wider text-[#1a5c38]">
{tier.name} {tier.name}
</h3> </h3>
<div className="mt-6 flex flex-wrap items-center justify-center gap-6"> <div className="mt-6 flex flex-wrap items-center justify-center gap-6">
{tier.partners.map((p, i) => ( {tier.partners.map((p, i) => (
<PartnerLogoPlaceholder key={`${tier.id}-${i}`} size="md" /> <PartnerLogoPlaceholder
key={`${tier.id}-${i}`}
size="md"
className="border-[#1a5c38]/20 bg-[#f0f5f2] text-[#1a5c38]/50"
/>
))} ))}
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ import { CyclingGrantAmount } from "@/components/grants/CyclingGrantAmount";
export function StatsGrid() { export function StatsGrid() {
return ( return (
<Section variant="muted" id="stats"> <Section variant="muted" id="stats">
<TopoProseSurface className="mx-auto max-w-3xl text-center"> <TopoProseSurface className="topo-on-green-bg mx-auto max-w-3xl text-center">
<p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]"> <p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]">
The future starts here The future starts here
</p> </p>
@ -18,18 +18,18 @@ export function StatsGrid() {
{site.stats.map((stat) => ( {site.stats.map((stat) => (
<div <div
key={stat.label} key={stat.label}
className="rounded-2xl border border-border bg-white p-6 text-center shadow-sm" className="topo-card-surface rounded-2xl border border-border bg-white p-6 text-center text-[#1a5c38] shadow-sm"
> >
{stat.type === "cycling" ? ( {stat.type === "cycling" ? (
<CyclingGrantAmount <CyclingGrantAmount
showCaption showCaption
valueClassName="text-3xl font-bold text-[#1f3d7e] md:text-4xl" valueClassName="text-3xl font-bold text-[#1a5c38] md:text-4xl"
captionClassName="text-muted-foreground" captionClassName="text-[#3d5248]"
/> />
) : ( ) : (
<p className="text-3xl font-bold text-[#1f3d7e] md:text-4xl">{stat.value}</p> <p className="text-3xl font-bold text-[#1a5c38] md:text-4xl">{stat.value}</p>
)} )}
<p className="mt-2 text-sm text-muted-foreground">{stat.label}</p> <p className="mt-2 text-sm text-[#3d5248]">{stat.label}</p>
</div> </div>
))} ))}
</div> </div>

View File

@ -1,19 +1,27 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { topicChips } from "@/content/tracks"; import { topicChips } from "@/content/tracks";
import { Section } from "@/components/layout/Section"; import { Section } from "@/components/layout/Section";
import { TopoProseSurface } from "@/components/layout/TopoProseSurface";
export function TopicMarquee() { export function TopicMarquee() {
const items = [...topicChips, ...topicChips]; const items = [...topicChips, ...topicChips];
return ( return (
<Section variant="muted" className="py-12 overflow-hidden"> <Section id="topics" className="overflow-hidden py-12">
<h2 className="mb-8 text-center text-2xl font-bold">Topics shaping the summit</h2> <TopoProseSurface className="mx-auto mb-8 max-w-3xl text-center">
<p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]">
Program themes
</p>
<h2 className="mt-2 text-2xl font-bold text-[#0d3d26] md:text-3xl">
Topics shaping the summit
</h2>
</TopoProseSurface>
<div className="flex overflow-hidden"> <div className="flex overflow-hidden">
<div className="marquee flex shrink-0 gap-3"> <div className="marquee flex shrink-0 gap-3">
{items.map((topic, i) => ( {items.map((topic, i) => (
<Badge <Badge
key={`${topic}-${i}`} key={`${topic}-${i}`}
variant="secondary" variant="secondary"
className="shrink-0 rounded-full border-white/25 bg-white/15 px-4 py-2 text-sm text-white" className="shrink-0 rounded-full border border-[#1a5c38]/15 bg-[#f0f5f2] px-4 py-2 text-sm text-[#1a5c38]"
> >
{topic} {topic}
</Badge> </Badge>

View File

@ -8,7 +8,7 @@ export function Venue() {
return ( return (
<Section id="venue" variant="muted"> <Section id="venue" variant="muted">
<div className="grid gap-8 lg:grid-cols-2"> <div className="grid gap-8 lg:grid-cols-2">
<div> <div className="topo-on-green-bg">
<p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]">The venue</p> <p className="text-xs font-semibold uppercase tracking-widest text-[#ffb300]">The venue</p>
<h2 className="mt-2 text-3xl font-bold text-white">{site.venue.name}</h2> <h2 className="mt-2 text-3xl font-bold text-white">{site.venue.name}</h2>
<p className="mt-4 text-white/85">{site.venue.address}</p> <p className="mt-4 text-white/85">{site.venue.address}</p>

View File

@ -1,9 +1,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend"; import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend";
import { TopographicPattern } from "@/components/brand/TopographicPattern";
import { TopoProseSurface } from "@/components/layout/TopoProseSurface"; import { TopoProseSurface } from "@/components/layout/TopoProseSurface";
import { TopoSectionProvider } from "@/components/layout/TopoSectionContext"; import { TopoSectionProvider } from "@/components/layout/TopoSectionContext";
import { SITE_TOPO_PATTERN } from "@/content/topo-patterns";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export type PageRiftHeaderVariant = export type PageRiftHeaderVariant =
@ -41,11 +39,10 @@ export function PageRiftHeader({
return ( return (
<header <header
className={cn( className={cn(
"section-white group/topo-section relative isolate min-h-[220px] overflow-x-hidden border-b border-[#1a5c38]/10 bg-white pt-24 pb-10 md:min-h-[260px] md:pb-12", "section-white group/topo-section relative isolate min-h-[220px] overflow-x-hidden overflow-y-visible border-b border-[#1a5c38]/10 bg-white pt-24 pb-10 md:min-h-[260px] md:pb-12",
className className
)} )}
> >
<TopographicPattern pattern={SITE_TOPO_PATTERN} tone="light" />
<TopoCurvyExtend /> <TopoCurvyExtend />
<TopoSectionProvider tone="light"> <TopoSectionProvider tone="light">
<div className="topo-content-layer topo-content-readable relative z-10 mx-auto max-w-6xl px-4 pt-4 md:px-6"> <div className="topo-content-layer topo-content-readable relative z-10 mx-auto max-w-6xl px-4 pt-4 md:px-6">
@ -57,7 +54,7 @@ export function PageRiftHeader({
)} )}
<div className="mt-3">{title}</div> <div className="mt-3">{title}</div>
{description && ( {description && (
<div className="mt-4 text-muted-foreground leading-relaxed">{description}</div> <div className="mt-4 text-[#3d5248] leading-relaxed">{description}</div>
)} )}
</TopoProseSurface> </TopoProseSurface>
{children && <div className="relative z-[15] mt-8">{children}</div>} {children && <div className="relative z-[15] mt-8">{children}</div>}

View File

@ -1,13 +1,8 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend"; import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend";
import { TopographicPattern } from "@/components/brand/TopographicPattern";
import { ScrollReveal } from "@/components/motion/ScrollReveal"; import { ScrollReveal } from "@/components/motion/ScrollReveal";
import { TopoSectionProvider } from "@/components/layout/TopoSectionContext"; import { TopoSectionProvider } from "@/components/layout/TopoSectionContext";
import { import { toneFromSectionVariant, type TopoPatternId } from "@/content/topo-patterns";
SITE_TOPO_PATTERN,
toneFromSectionVariant,
type TopoPatternId,
} from "@/content/topo-patterns";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type Props = { type Props = {
@ -15,7 +10,9 @@ type Props = {
className?: string; className?: string;
children: ReactNode; children: ReactNode;
variant?: "default" | "muted" | "inverse"; variant?: "default" | "muted" | "inverse";
/** @deprecated Patterns are hero + footer only; kept for API compatibility */
riftPattern?: TopoPatternId; riftPattern?: TopoPatternId;
/** @deprecated */
riftFlow?: boolean; riftFlow?: boolean;
}; };
@ -24,31 +21,27 @@ export function Section({
className, className,
children, children,
variant = "default", variant = "default",
riftPattern = SITE_TOPO_PATTERN, riftPattern,
riftFlow, riftFlow,
}: Props) { }: Props) {
let pattern: TopoPatternId = riftPattern; void riftPattern;
if (riftFlow && pattern === "none") pattern = SITE_TOPO_PATTERN; void riftFlow;
const tone = toneFromSectionVariant(variant); const tone = toneFromSectionVariant(variant);
const isGreen = tone === "green"; const isGreen = tone === "green";
const showPattern = !isGreen && pattern !== "none";
return ( return (
<section <section
id={id} id={id}
className={cn( className={cn(
"group/topo-section relative isolate py-16 md:py-24", "group/topo-section relative isolate py-16 md:py-24",
isGreen && "section-green bg-[#1a5c38] text-white", isGreen && "section-green bg-[#1a5c38]",
!isGreen && "section-white bg-white text-[#0d3d26]", !isGreen && "section-white bg-white text-[#0d3d26]",
showPattern && "overflow-x-hidden", !isGreen && "overflow-x-hidden overflow-y-visible",
isGreen && "overflow-hidden", isGreen && "overflow-hidden",
className className
)} )}
data-section-tone={tone} data-section-tone={tone}
> >
{showPattern && (
<TopographicPattern pattern={pattern} tone="light" />
)}
{!isGreen && <TopoCurvyExtend />} {!isGreen && <TopoCurvyExtend />}
<TopoSectionProvider tone={tone}> <TopoSectionProvider tone={tone}>
<ScrollReveal <ScrollReveal

View File

@ -1,5 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import { FooterTopographicBand } from "@/components/brand/FooterTopographicBand"; import { FooterTopoPattern } from "@/components/brand/FooterTopoPattern";
import { FooterNewsletter } from "@/components/layout/FooterNewsletter"; import { FooterNewsletter } from "@/components/layout/FooterNewsletter";
import { site } from "@/content/site"; import { site } from "@/content/site";
@ -44,10 +44,10 @@ const footerColumns = [
export function SiteFooter() { export function SiteFooter() {
return ( return (
<footer className="relative mt-24 bg-[#1a5c38] text-white"> <footer className="relative mt-24 overflow-hidden bg-[#1a5c38] text-white">
<FooterTopographicBand /> <FooterTopoPattern />
<div className="relative z-10 -mt-20 px-4 pb-4 md:px-6"> <div className="relative z-10 px-4 pb-4 md:px-6">
<FooterNewsletter /> <FooterNewsletter />
</div> </div>

View File

@ -11,21 +11,28 @@ type Props = {
export function PartnerCard({ partner, tierLabel }: Props) { export function PartnerCard({ partner, tierLabel }: Props) {
return ( return (
<Card className="h-full border-border/80"> <Card className="topo-card-surface h-full border-border/80 bg-white text-[#1a5c38]">
{tierLabel && ( {tierLabel && (
<p className="px-6 pt-6 text-xs font-semibold uppercase tracking-widest text-muted-foreground"> <p className="px-6 pt-6 text-xs font-semibold uppercase tracking-widest text-muted-foreground">
{tierLabel} {tierLabel}
</p> </p>
)} )}
<CardHeader className={tierLabel ? "pt-2" : undefined}> <CardHeader className={tierLabel ? "pt-2" : undefined}>
<PartnerLogoPlaceholder size="lg" className="w-full" /> <PartnerLogoPlaceholder
size="lg"
className="w-full border-[#1a5c38]/20 bg-[#f0f5f2] text-[#1a5c38]/50"
/>
</CardHeader> </CardHeader>
<CardContent className="flex flex-1 flex-col"> <CardContent className="flex flex-1 flex-col">
<CardDescription className="flex-1 text-base leading-relaxed"> <CardDescription className="flex-1 text-base leading-relaxed">
{partner.description} {partner.description}
</CardDescription> </CardDescription>
{partner.url && !partner.isPlaceholder && ( {partner.url && !partner.isPlaceholder && (
<Button variant="link" className="mt-4 h-auto p-0 text-[#1f3d7e]" asChild> <Button
variant="link"
className="topo-card-link mt-4 h-auto p-0"
asChild
>
<Link href={partner.url} target="_blank" rel="noopener noreferrer"> <Link href={partner.url} target="_blank" rel="noopener noreferrer">
More info More info
</Link> </Link>

View File

@ -12,12 +12,14 @@ export function PartnerSectionBlock({ section, showTitle = true }: Props) {
<div className="space-y-8"> <div className="space-y-8">
{showTitle && ( {showTitle && (
<> <>
<h2 className="text-3xl font-bold tracking-tight">{section.title}</h2> <h2 className="text-3xl font-bold tracking-tight text-[#0d3d26]">
{section.title}
</h2>
<Separator /> <Separator />
</> </>
)} )}
{section.tierLabel && ( {section.tierLabel && (
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"> <p className="text-xs font-semibold uppercase tracking-widest text-[#1a5c38]">
{section.tierLabel} {section.tierLabel}
</p> </p>
)} )}

View File

@ -54,7 +54,7 @@ export function PartnershipInquiryForm() {
} }
return ( return (
<div className="rounded-2xl bg-white p-6 shadow-xl md:p-8"> <div className="topo-card-surface rounded-2xl bg-white p-6 text-[#1a5c38] shadow-xl md:p-8">
<h3 className="text-xl font-bold text-foreground">Request Partnership Information</h3> <h3 className="text-xl font-bold text-foreground">Request Partnership Information</h3>
<form onSubmit={onSubmit} className="mt-6 space-y-4"> <form onSubmit={onSubmit} className="mt-6 space-y-4">
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">

View File

@ -13,7 +13,7 @@ export function SpeakerCard({ speaker, className, revealDelay = 0 }: Props) {
<ScrollReveal variant="card" delay={revealDelay} as="div" className={className}> <ScrollReveal variant="card" delay={revealDelay} as="div" className={className}>
<Link <Link
href="/speakers" href="/speakers"
className="topo-card-layer group relative z-20 isolate block overflow-hidden rounded-2xl border border-border bg-white p-4 shadow-sm transition-all hover:border-[#1a5c38]/25 hover:shadow-md" className="topo-card-surface topo-card-layer group relative z-20 isolate block overflow-hidden rounded-2xl border border-border bg-white p-4 text-[#1a5c38] shadow-sm transition-all hover:border-[#1a5c38]/25 hover:shadow-md"
> >
<div className="relative mx-auto aspect-[4/5] w-full max-h-[220px] overflow-hidden rounded-xl bg-muted/30"> <div className="relative mx-auto aspect-[4/5] w-full max-h-[220px] overflow-hidden rounded-xl bg-muted/30">
<Image <Image
@ -24,13 +24,13 @@ export function SpeakerCard({ speaker, className, revealDelay = 0 }: Props) {
sizes="(max-width: 768px) 50vw, 25vw" sizes="(max-width: 768px) 50vw, 25vw"
/> />
</div> </div>
<h3 className="mt-4 text-lg font-bold leading-tight group-hover:text-[#1a5c38]"> <h3 className="mt-4 text-lg font-bold leading-tight text-[#0d3d26] group-hover:text-[#1a5c38]">
{speaker.name} {speaker.name}
</h3> </h3>
<p className="mt-1 text-sm text-muted-foreground">{speaker.title}</p> <p className="mt-1 text-sm text-[#3d5248]">{speaker.title}</p>
<p className="mt-0.5 text-sm font-medium text-[#1a5c38]/90">{speaker.company}</p> <p className="mt-0.5 text-sm font-medium text-[#1a5c38]/90">{speaker.company}</p>
{speaker.panel && ( {speaker.panel && (
<p className="mt-2 line-clamp-2 text-xs text-muted-foreground">{speaker.panel}</p> <p className="mt-2 line-clamp-2 text-xs text-[#3d5248]">{speaker.panel}</p>
)} )}
</Link> </Link>
</ScrollReveal> </ScrollReveal>

View File

@ -26,7 +26,7 @@ export function TicketInclusionsPopover({ tier, serial, className }: Props) {
variant="ghost" variant="ghost"
size="sm" size="sm"
className={cn( className={cn(
"h-8 gap-1.5 rounded-full px-3 text-xs font-medium text-[#1a5c38] hover:bg-[#1a5c38]/8", "topo-card-link h-8 gap-1.5 rounded-full px-3 text-xs font-medium hover:bg-[#1a5c38]/8",
className className
)} )}
aria-label={`What's included in ${tier.name}`} aria-label={`What's included in ${tier.name}`}