447 lines
18 KiB
TypeScript
447 lines
18 KiB
TypeScript
|
|
import { useEffect, useRef, useState } from "react";
|
||
|
|
import { Link } from "react-router";
|
||
|
|
import { Users, Shuffle, Trophy, ChevronDown } from "lucide-react";
|
||
|
|
import { Button } from "~/components/ui/button";
|
||
|
|
import { Card, CardContent } from "~/components/ui/card";
|
||
|
|
import { SectionCardHeader } from "~/components/ui/SectionCardHeader";
|
||
|
|
|
||
|
|
// ─── Slot Machine ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const SPORTS = [
|
||
|
|
"Football", "Baseball", "Hockey", "Basketball", "Soccer", "Tennis", "Golf",
|
||
|
|
"eSports", "Formula 1", "Aussie Rules", "Lacrosse", "Darts", "College Football", "Snooker", "March Madness",
|
||
|
|
];
|
||
|
|
|
||
|
|
function shuffle<T>(arr: T[]): T[] {
|
||
|
|
const a = [...arr];
|
||
|
|
for (let i = a.length - 1; i > 0; i--) {
|
||
|
|
const j = Math.floor(Math.random() * (i + 1));
|
||
|
|
[a[i], a[j]] = [a[j], a[i]];
|
||
|
|
}
|
||
|
|
return a;
|
||
|
|
}
|
||
|
|
|
||
|
|
const TOTAL_TICKS = 12;
|
||
|
|
|
||
|
|
// WCAG 2.3.1: no more than 3 flashes/second → minimum interval 334 ms.
|
||
|
|
// Curve runs 800 ms → 340 ms over 12 ticks (ramps to full speed at tick 8).
|
||
|
|
function getInterval(tick: number): number {
|
||
|
|
const pct = Math.min((tick / TOTAL_TICKS) * 1.5, 1);
|
||
|
|
return Math.round(800 - (800 - 340) * (pct * pct));
|
||
|
|
}
|
||
|
|
|
||
|
|
function SlotMachineHeadline() {
|
||
|
|
const [text, setText] = useState(SPORTS[0]);
|
||
|
|
const [landed, setLanded] = useState(false);
|
||
|
|
const [animKey, setAnimKey] = useState(0);
|
||
|
|
const tickRef = useRef(0);
|
||
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
|
const sportsSeq = useRef<string[]>([]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
sportsSeq.current = shuffle(SPORTS);
|
||
|
|
let idx = 0;
|
||
|
|
|
||
|
|
function tick() {
|
||
|
|
tickRef.current++;
|
||
|
|
const sport = sportsSeq.current[idx % sportsSeq.current.length];
|
||
|
|
idx++;
|
||
|
|
setText(sport);
|
||
|
|
setAnimKey((k) => k + 1);
|
||
|
|
|
||
|
|
if (tickRef.current >= TOTAL_TICKS) {
|
||
|
|
setLanded(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
timerRef.current = setTimeout(tick, getInterval(tickRef.current));
|
||
|
|
}
|
||
|
|
|
||
|
|
timerRef.current = setTimeout(tick, 800);
|
||
|
|
return () => {
|
||
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (landed) {
|
||
|
|
return (
|
||
|
|
<span
|
||
|
|
className="animate-sport-land block bg-clip-text text-transparent pb-[0.12em]"
|
||
|
|
style={{ backgroundImage: "linear-gradient(to bottom, #adf661, #2ce1c1)" }}
|
||
|
|
>
|
||
|
|
Every Sport.
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<span key={animKey} className="animate-spin-in block text-white">
|
||
|
|
{text}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Bracket Decoration ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function BracketDecor({ flip = false, mobileTall = false }: { flip?: boolean; mobileTall?: boolean }) {
|
||
|
|
const base = flip ? "r" : "l";
|
||
|
|
const id = mobileTall ? `${base}-m` : base;
|
||
|
|
const W = 399;
|
||
|
|
const H = mobileTall ? 2000 : 560;
|
||
|
|
|
||
|
|
const pad = 36;
|
||
|
|
const spacing = (H - 2 * pad) / 7;
|
||
|
|
const r1Ys = Array.from({ length: 8 }, (_, i) => pad + i * spacing);
|
||
|
|
const r2Ys = [0, 1, 2, 3].map((i) => (r1Ys[i * 2] + r1Ys[i * 2 + 1]) / 2);
|
||
|
|
const r3Ys = [0, 1].map((i) => (r2Ys[i * 2] + r2Ys[i * 2 + 1]) / 2);
|
||
|
|
const champY = (r3Ys[0] + r3Ys[1]) / 2;
|
||
|
|
|
||
|
|
const x0 = 4;
|
||
|
|
const x1 = 77;
|
||
|
|
const x2 = 161;
|
||
|
|
const x3 = 252;
|
||
|
|
|
||
|
|
const gradId = `grad-${id}`;
|
||
|
|
const glowId = `glow-${id}`;
|
||
|
|
const dotsGlowId = `dots-glow-${id}`;
|
||
|
|
const finalsGlowId = `finals-glow-${id}`;
|
||
|
|
const finalsHaloId = `finals-halo-${id}`;
|
||
|
|
const finalsFadeMaskGradId = `finals-fade-mask-grad-${id}`;
|
||
|
|
const finalsFadeMaskId = `finals-fade-mask-${id}`;
|
||
|
|
const finalsLineGlowId = `finals-line-glow-${id}`;
|
||
|
|
|
||
|
|
|
||
|
|
return (
|
||
|
|
<svg
|
||
|
|
viewBox={`0 0 ${W} ${H}`}
|
||
|
|
style={{
|
||
|
|
position: "absolute",
|
||
|
|
top: "50%",
|
||
|
|
transform: `translateY(-52%)${flip ? " scaleX(-1)" : ""}`,
|
||
|
|
[flip ? "right" : "left"]: 0,
|
||
|
|
pointerEvents: "none",
|
||
|
|
height: mobileTall ? "auto" : "75vh",
|
||
|
|
width: mobileTall ? "32vw" : "auto",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<defs>
|
||
|
|
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2={H} gradientUnits="userSpaceOnUse">
|
||
|
|
<stop offset="0%" stopColor="#adf661" />
|
||
|
|
<stop offset="100%" stopColor="#2ce1c1" />
|
||
|
|
</linearGradient>
|
||
|
|
<filter id={glowId} x="-50%" y="-50%" width="200%" height="200%">
|
||
|
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
||
|
|
<feMerge>
|
||
|
|
<feMergeNode in="blur" />
|
||
|
|
<feMergeNode in="SourceGraphic" />
|
||
|
|
</feMerge>
|
||
|
|
</filter>
|
||
|
|
<filter id={finalsGlowId} filterUnits="userSpaceOnUse"
|
||
|
|
x={x2 - 15} y={r3Ys[0] - 15} width={x3 - x2 + 30} height={r3Ys[1] - r3Ys[0] + 30}>
|
||
|
|
<feGaussianBlur stdDeviation="7" result="blur" />
|
||
|
|
<feMerge>
|
||
|
|
<feMergeNode in="blur" />
|
||
|
|
<feMergeNode in="SourceGraphic" />
|
||
|
|
</feMerge>
|
||
|
|
</filter>
|
||
|
|
<filter id={finalsHaloId} filterUnits="userSpaceOnUse"
|
||
|
|
x={x2 - 40} y={r3Ys[0] - 40} width={x3 - x2 + 80} height={r3Ys[1] - r3Ys[0] + 80}>
|
||
|
|
<feGaussianBlur stdDeviation="16" />
|
||
|
|
</filter>
|
||
|
|
<linearGradient id={finalsFadeMaskGradId} x1={x3} y1="0" x2={W} y2="0" gradientUnits="userSpaceOnUse">
|
||
|
|
<stop offset="0%" stopColor="white" />
|
||
|
|
<stop offset="50%" stopColor="white" />
|
||
|
|
<stop offset="100%" stopColor="white" stopOpacity="0" />
|
||
|
|
</linearGradient>
|
||
|
|
<mask id={finalsFadeMaskId} maskUnits="userSpaceOnUse" x={x3} y={champY - 20} width={W - x3} height={40}>
|
||
|
|
<rect x="0" y="0" width={W} height={H} fill={`url(#${finalsFadeMaskGradId})`} />
|
||
|
|
</mask>
|
||
|
|
<filter id={finalsLineGlowId} filterUnits="userSpaceOnUse" x={x3 - 10} y={champY - 15} width={W - x3 + 20} height={30}>
|
||
|
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
||
|
|
<feMerge>
|
||
|
|
<feMergeNode in="blur" />
|
||
|
|
<feMergeNode in="SourceGraphic" />
|
||
|
|
</feMerge>
|
||
|
|
</filter>
|
||
|
|
<filter id={dotsGlowId} x="-200%" y="-200%" width="500%" height="500%">
|
||
|
|
<feGaussianBlur stdDeviation="5" result="blur" />
|
||
|
|
<feMerge>
|
||
|
|
<feMergeNode in="blur" />
|
||
|
|
<feMergeNode in="blur" />
|
||
|
|
<feMergeNode in="SourceGraphic" />
|
||
|
|
</feMerge>
|
||
|
|
</filter>
|
||
|
|
</defs>
|
||
|
|
<g filter={`url(#${finalsHaloId})`} stroke={`url(#${gradId})`} strokeWidth={2} fill="none" opacity={0.75}>
|
||
|
|
{r3Ys.map((midY) => (
|
||
|
|
<line key={midY} x1={x2} y1={midY} x2={x3} y2={midY} />
|
||
|
|
))}
|
||
|
|
<line x1={x3} y1={r3Ys[0]} x2={x3} y2={r3Ys[1]} />
|
||
|
|
</g>
|
||
|
|
<g filter={`url(#${finalsGlowId})`} stroke={`url(#${gradId})`} strokeWidth={1.5} fill="none">
|
||
|
|
{r3Ys.map((midY) => (
|
||
|
|
<line key={midY} x1={x2} y1={midY} x2={x3} y2={midY} />
|
||
|
|
))}
|
||
|
|
<line x1={x3} y1={r3Ys[0]} x2={x3} y2={r3Ys[1]} />
|
||
|
|
</g>
|
||
|
|
<g filter={`url(#${glowId})`} stroke={`url(#${gradId})`} strokeWidth={1.5} fill="none">
|
||
|
|
{r1Ys.map((y) => (
|
||
|
|
<g key={y}>
|
||
|
|
<circle cx={x0} cy={y} r={3} fill={`url(#${gradId})`} stroke="none" filter={`url(#${dotsGlowId})`} />
|
||
|
|
<line x1={x0} y1={y} x2={x1} y2={y} />
|
||
|
|
</g>
|
||
|
|
))}
|
||
|
|
{r2Ys.map((midY, i) => (
|
||
|
|
<g key={midY}>
|
||
|
|
<line x1={x1} y1={r1Ys[i * 2]} x2={x1} y2={r1Ys[i * 2 + 1]} />
|
||
|
|
<circle cx={x1} cy={midY} r={3} fill={`url(#${gradId})`} stroke="none" filter={`url(#${dotsGlowId})`} />
|
||
|
|
<line x1={x1} y1={midY} x2={x2} y2={midY} />
|
||
|
|
</g>
|
||
|
|
))}
|
||
|
|
{r3Ys.map((midY, i) => (
|
||
|
|
<g key={midY}>
|
||
|
|
<line x1={x2} y1={r2Ys[i * 2]} x2={x2} y2={r2Ys[i * 2 + 1]} />
|
||
|
|
<circle cx={x2} cy={midY} r={3} fill={`url(#${gradId})`} stroke="none" filter={`url(#${dotsGlowId})`} />
|
||
|
|
<line x1={x2} y1={midY} x2={x3} y2={midY} />
|
||
|
|
</g>
|
||
|
|
))}
|
||
|
|
<line x1={x3} y1={r3Ys[0]} x2={x3} y2={r3Ys[1]} />
|
||
|
|
<circle cx={x3} cy={champY} r={3} fill={`url(#${gradId})`} stroke="none" filter={`url(#${dotsGlowId})`} />
|
||
|
|
</g>
|
||
|
|
<line
|
||
|
|
x1={x3} y1={champY} x2={W} y2={champY}
|
||
|
|
stroke={`url(#${gradId})`}
|
||
|
|
strokeWidth={1.5}
|
||
|
|
fill="none"
|
||
|
|
filter={`url(#${finalsLineGlowId})`}
|
||
|
|
mask={`url(#${finalsFadeMaskId})`}
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scroll Hint ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function ScrollHint() {
|
||
|
|
const [visible, setVisible] = useState(true);
|
||
|
|
useEffect(() => {
|
||
|
|
const onScroll = () => { if (window.scrollY > 50) setVisible(false); };
|
||
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||
|
|
return () => window.removeEventListener("scroll", onScroll);
|
||
|
|
}, []);
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className="pointer-events-none absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce transition-opacity duration-500"
|
||
|
|
style={{ opacity: visible ? 0.35 : 0 }}
|
||
|
|
>
|
||
|
|
<ChevronDown className="h-6 w-6 text-white" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Sport Ticker ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const TICKER_ITEMS = [
|
||
|
|
...SPORTS.map((s) => ({ sport: s.toUpperCase(), key: `${s}-a` })),
|
||
|
|
...SPORTS.map((s) => ({ sport: s.toUpperCase(), key: `${s}-b` })),
|
||
|
|
];
|
||
|
|
|
||
|
|
function SportTicker() {
|
||
|
|
return (
|
||
|
|
<div className="relative overflow-hidden pb-px" style={{ marginTop: "52px" }}>
|
||
|
|
<div
|
||
|
|
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-[60px]"
|
||
|
|
style={{ background: "linear-gradient(to right, #14171e, transparent)" }}
|
||
|
|
/>
|
||
|
|
<div
|
||
|
|
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-[60px]"
|
||
|
|
style={{ background: "linear-gradient(to left, #14171e, transparent)" }}
|
||
|
|
/>
|
||
|
|
<div className="flex animate-ticker" style={{ width: "max-content" }}>
|
||
|
|
{TICKER_ITEMS.map(({ sport, key }) => (
|
||
|
|
<span
|
||
|
|
key={key}
|
||
|
|
className="mx-1.5 shrink-0 rounded-[3px] px-3 py-1 text-[11px] font-bold uppercase tracking-[0.05em] border bg-card text-muted-foreground"
|
||
|
|
>
|
||
|
|
{sport}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Static data ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const HOW_IT_WORKS_STEPS = [
|
||
|
|
{
|
||
|
|
icon: Users,
|
||
|
|
title: "Create a League",
|
||
|
|
body: "Start a league, pick your sports (we can help), and invite your friends. It takes 30 seconds to set the stage.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: Shuffle,
|
||
|
|
title: "Draft Your Teams",
|
||
|
|
body: "Pick teams from all the sports in order to build a superteam to win you bragging rights! Draft your way, slow or fast, with standard timers or high-pressure chess clocks.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: Trophy,
|
||
|
|
title: "Watch & Win",
|
||
|
|
body: "Your teams earn points for how well they perform in the playoffs. Follow your rooting interests along the way, and gloat over your leaguemates when they fall.",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const FEATURES = [
|
||
|
|
{
|
||
|
|
title: "Dozens of Sports and Counting",
|
||
|
|
body: "Draft from a massive selection of sports including major US leagues, college athletics, and global niches like professional darts or snooker. We are constantly adding new sports to ensure your league has competitive action throughout the entire year.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "The Smart Draft Room",
|
||
|
|
body: "Our interface supports any pace. Use a tactical chess clock with Fischer increments to reward fast decisions, or run a multi-day slow draft with customizable overnight pauses. The system ensures no one is forced to make a pick in the middle of the night.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "Your Master Calendar",
|
||
|
|
body: "Stop switching between different apps to track your roster. Every game, match, and tournament for your specific teams is synced into a single view. You will always know exactly when your points are on the line without any manual tracking.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "Zero Maintenance",
|
||
|
|
body: "Brackt is a draft and done experience. Once the draft is finished, your work is over because we handle all scoring and rankings automatically. You can even paste a Discord webhook URL to get live scoring updates sent directly to your group chat.",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// ─── Main Landing Page ────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export function LandingPage() {
|
||
|
|
// Interleave step cards with gradient triangle separators
|
||
|
|
const howItWorksItems: React.ReactNode[] = [];
|
||
|
|
HOW_IT_WORKS_STEPS.forEach((step, i) => {
|
||
|
|
howItWorksItems.push(
|
||
|
|
<Card key={step.title} className="flex-1 gap-2">
|
||
|
|
<SectionCardHeader icon={step.icon} title={step.title} />
|
||
|
|
<CardContent className="px-3 sm:px-6">
|
||
|
|
<p className="text-sm text-muted-foreground">{step.body}</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
if (i < HOW_IT_WORKS_STEPS.length - 1) {
|
||
|
|
// Triangles reference #brackt-primary-gradient defined in <BracktGradients /> (mounted in root.tsx)
|
||
|
|
howItWorksItems.push(
|
||
|
|
<div key={`arrow-${step.title}`} className="flex shrink-0 items-center justify-center py-1 md:px-1 md:py-0">
|
||
|
|
<svg width="20" height="20" viewBox="0 0 20 20" className="rotate-90 md:rotate-0">
|
||
|
|
<polygon
|
||
|
|
points="3,2 18,10 3,18"
|
||
|
|
fill="url(#brackt-primary-gradient)"
|
||
|
|
stroke="url(#brackt-primary-gradient)"
|
||
|
|
strokeWidth={3}
|
||
|
|
strokeLinejoin="round"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="animate-page-fade-in">
|
||
|
|
{/* ── Hero ── */}
|
||
|
|
<section
|
||
|
|
className="relative flex flex-col items-center justify-center overflow-hidden px-8"
|
||
|
|
style={{ minHeight: "calc(100vh - var(--navbar-height))" }}
|
||
|
|
>
|
||
|
|
{/* Dot-grid texture */}
|
||
|
|
<div
|
||
|
|
aria-hidden
|
||
|
|
className="pointer-events-none absolute inset-0"
|
||
|
|
style={{
|
||
|
|
backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.07) 1px, transparent 1px)",
|
||
|
|
backgroundSize: "32px 32px",
|
||
|
|
maskImage: "radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 100%)",
|
||
|
|
WebkitMaskImage: "radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 100%)",
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Bracket decorations */}
|
||
|
|
<div className="lg:hidden">
|
||
|
|
<BracketDecor mobileTall />
|
||
|
|
<BracketDecor flip mobileTall />
|
||
|
|
</div>
|
||
|
|
<div className="hidden lg:block">
|
||
|
|
<BracketDecor />
|
||
|
|
<BracketDecor flip />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ScrollHint />
|
||
|
|
|
||
|
|
{/* Center content */}
|
||
|
|
<div className="relative z-10 w-[80%] lg:w-full max-w-[680px] text-center px-6 py-4 lg:px-0 lg:py-0">
|
||
|
|
<h1
|
||
|
|
className="mb-0 leading-[0.95] tracking-[-0.04em]"
|
||
|
|
style={{ fontSize: "clamp(38px, 8vw, 88px)", fontWeight: 900 }}
|
||
|
|
>
|
||
|
|
<span className="block text-white">Fantasy</span>
|
||
|
|
<SlotMachineHeadline />
|
||
|
|
</h1>
|
||
|
|
|
||
|
|
<p
|
||
|
|
className="mx-auto mt-8 text-muted-foreground"
|
||
|
|
style={{ fontSize: "16px", fontWeight: 400, lineHeight: 1.65, maxWidth: "414px" }}
|
||
|
|
>
|
||
|
|
Draft teams across every league and sport, like the NFL, NHL, EPL, F1, and more.
|
||
|
|
Compete against your friends in a season-long league.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div className="mt-8 flex flex-col items-center justify-center gap-2.5 sm:flex-row">
|
||
|
|
<Button asChild size="lg" className="text-sm font-extrabold">
|
||
|
|
<Link to="/leagues/new">Create a League</Link>
|
||
|
|
</Button>
|
||
|
|
<Button asChild variant="outline" size="lg" className="text-sm font-semibold">
|
||
|
|
<Link to="/how-to-play">How it works</Link>
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<SportTicker />
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* ── How It Works ── */}
|
||
|
|
<section className="mx-auto w-full max-w-[1100px] px-10 py-18">
|
||
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-stretch">
|
||
|
|
{howItWorksItems}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* ── Features Grid ── */}
|
||
|
|
<section className="mx-auto w-full max-w-[1100px] px-10 pb-18">
|
||
|
|
<div className="overflow-hidden rounded-[6px] border">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2" style={{ gap: "1px", background: "rgb(255 255 255 / 10%)" }}>
|
||
|
|
{FEATURES.map(({ title, body }) => (
|
||
|
|
<div key={title} className="bg-card p-7 transition-colors hover:bg-[#22252e]">
|
||
|
|
<h3 className="mb-2 text-base font-extrabold tracking-[-0.01em]">{title}</h3>
|
||
|
|
<p className="text-sm leading-[1.65] text-muted-foreground">{body}</p>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* ── Footer CTA ── */}
|
||
|
|
<section className="mx-auto w-full max-w-[1100px] px-10 pb-20">
|
||
|
|
<div className="flex flex-col items-center gap-6 p-12 text-center">
|
||
|
|
<h2
|
||
|
|
className="font-black tracking-[-0.025em]"
|
||
|
|
style={{ fontSize: "clamp(22px, 3vw, 34px)" }}
|
||
|
|
>
|
||
|
|
Stop managing your league and start watching it.
|
||
|
|
</h2>
|
||
|
|
<p className="text-sm leading-[1.65] text-muted-foreground" style={{ maxWidth: "600px" }}>
|
||
|
|
Brackt is built for the sports fan who wants a stake in everything without the daily chore of a traditional fantasy roster. Setting up a league takes less than five minutes. Choose your sports, invite your friends, and let the draft decide the winner.
|
||
|
|
</p>
|
||
|
|
<Button asChild size="lg" className="text-sm font-extrabold">
|
||
|
|
<Link to="/leagues/new">Create a League</Link>
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|