mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 04:22:51 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35fdd11020 |
@@ -37,6 +37,11 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&family=Instrument+Serif:ital@0;1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- Landing4 (brutalist landing) type system -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Inter:opsz,wght@14..32,100..900&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import CreateVideo from "../pages/create-video";
|
||||
import CreateVFX from "../pages/create-vfx";
|
||||
//import Landing2 from "../pages/landing2";
|
||||
import Landing3 from "../pages/landing3";
|
||||
import Landing4 from "../pages/landing4";
|
||||
import LandingSD2 from "../pages/landing-sd2";
|
||||
import LandingSD25 from "../pages/landing-sd25";
|
||||
import TutorialsPage from "../pages/tutorials";
|
||||
@@ -59,14 +60,19 @@ function appOrWebapp(localElement: ReactNode, webappPath: string): ReactNode {
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// Landing4 renders its own scoped navbar/footer; hide the global chrome.
|
||||
const { pathname } = useLocation();
|
||||
const hideGlobalChrome = pathname === "/landing4";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<ScrollToTop />
|
||||
<Navbar />
|
||||
{!hideGlobalChrome && <Navbar />}
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing3 />} />
|
||||
<Route path="/landing3" element={<Landing3 />} />
|
||||
<Route path="/landing4" element={<Landing4 />} />
|
||||
<Route path="/seedance-2" element={<LandingSD2 />} />
|
||||
<Route path="/seedance2-5" element={<LandingSD25 />} />
|
||||
<Route path="/download" element={<Download />} />
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { SOCIAL_LINKS, SUPPORT_EMAIL } from "../../../config/links";
|
||||
import { MonoLabel } from "./ui";
|
||||
|
||||
const SITEMAP: ReadonlyArray<{ label: string; to: string }> = [
|
||||
{ label: "Home", to: "/" },
|
||||
{ label: "Download", to: "/download" },
|
||||
{ label: "Pricing", to: "/pricing" },
|
||||
{ label: "Tutorials", to: "/tutorials" },
|
||||
{ label: "FAQ", to: "/faq" },
|
||||
{ label: "News", to: "/news" },
|
||||
{ label: "Press Kit", to: "/press-kit" },
|
||||
{ label: "Support", to: "/support" },
|
||||
];
|
||||
|
||||
const SOCIALS: ReadonlyArray<{ label: string; href: string }> = [
|
||||
{ label: "GitHub", href: SOCIAL_LINKS.GITHUB },
|
||||
{ label: "Discord", href: SOCIAL_LINKS.DISCORD },
|
||||
{ label: "YouTube", href: SOCIAL_LINKS.YOUTUBE },
|
||||
{ label: "Instagram", href: SOCIAL_LINKS.INSTAGRAM },
|
||||
{ label: "TikTok", href: SOCIAL_LINKS.TIKTOK },
|
||||
{ label: "Reddit", href: SOCIAL_LINKS.REDDIT },
|
||||
];
|
||||
|
||||
// Landing4-only footer. Dark chapter: mono column grid over a hairline, then
|
||||
// a giant clipped wordmark bleeding off the bottom edge.
|
||||
export const BrutFooter = () => (
|
||||
<footer className="border-t border-paper/15 bg-ink text-paper">
|
||||
<div className="mx-auto w-full max-w-[1600px] px-4 sm:px-8">
|
||||
<div className="grid grid-cols-2 gap-10 border-b border-paper/15 py-14 md:grid-cols-4">
|
||||
<div>
|
||||
<MonoLabel className="text-paper/40">SITEMAP</MonoLabel>
|
||||
<ul className="mt-5 space-y-2">
|
||||
{SITEMAP.map((item) => (
|
||||
<li key={item.to}>
|
||||
<Link
|
||||
to={item.to}
|
||||
className="hover:text-volt font-brut-mono text-[13px] uppercase tracking-[0.1em] transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<MonoLabel className="text-paper/40">SOCIAL</MonoLabel>
|
||||
<ul className="mt-5 space-y-2">
|
||||
{SOCIALS.map((item) => (
|
||||
<li key={item.href}>
|
||||
<a
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hover:text-volt font-brut-mono text-[13px] uppercase tracking-[0.1em] transition-colors"
|
||||
>
|
||||
{item.label} ↗
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<MonoLabel className="text-paper/40">CONTACT</MonoLabel>
|
||||
<ul className="mt-5 space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href={`mailto:${SUPPORT_EMAIL}`}
|
||||
className="hover:text-volt font-brut-mono text-[13px] uppercase tracking-[0.1em] transition-colors"
|
||||
>
|
||||
{SUPPORT_EMAIL}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<MonoLabel className="text-paper/40">COLOPHON</MonoLabel>
|
||||
<MonoLabel className="mt-5 block text-paper/60">
|
||||
SET IN BRICOLAGE GROTESQUE
|
||||
</MonoLabel>
|
||||
<MonoLabel className="block text-paper/60">
|
||||
+ SPACE MONO — {new Date().getFullYear()}
|
||||
</MonoLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<MonoLabel className="text-paper/40">
|
||||
© {new Date().getFullYear()} ArtCraft. All rights reserved.
|
||||
</MonoLabel>
|
||||
<MonoLabel className="text-paper/40">MADE BY ARTISTS</MonoLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Giant clipped wordmark */}
|
||||
<div aria-hidden className="overflow-hidden">
|
||||
<div className="mx-auto w-full max-w-[1600px] px-4 sm:px-8">
|
||||
<div className="font-brut-display -mb-[0.34em] select-none text-[18vw] font-bold uppercase leading-none tracking-[-0.03em] text-paper/90">
|
||||
ArtCraft<span className="text-volt">®</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SOCIAL_LINKS } from "../../../config/links";
|
||||
import { MonoLabel } from "./ui";
|
||||
|
||||
const NAV_LINKS: ReadonlyArray<{
|
||||
label: string;
|
||||
index: string;
|
||||
to?: string;
|
||||
href?: string;
|
||||
}> = [
|
||||
{ label: "PRICING", index: "01", to: "/pricing" },
|
||||
{ label: "TUTORIALS", index: "02", to: "/tutorials" },
|
||||
{ label: "NEWS", index: "03", to: "/news" },
|
||||
{ label: "GITHUB ↗", index: "04", href: SOCIAL_LINKS.GITHUB },
|
||||
];
|
||||
|
||||
// Landing4-only fixed navbar. Chapter-aware: text and hairlines read the
|
||||
// --l4-* vars so the bar recolors as the page scrubs between ink and paper.
|
||||
// A backdrop tinted with the current --l4-bg keeps links legible over content.
|
||||
// Mobile: a full-screen ink overlay with numbered index-style rows.
|
||||
export const BrutNavbar = ({
|
||||
onDownloadClick,
|
||||
}: {
|
||||
onDownloadClick: () => void;
|
||||
}) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
// Lock page scroll behind the full-screen menu.
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const previous = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = previous;
|
||||
};
|
||||
}, [menuOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className="l4-line fixed inset-x-0 top-0 z-50 border-b"
|
||||
style={{ backgroundColor: "var(--l4-bg)", color: "var(--l4-ink)" }}
|
||||
>
|
||||
<div className="mx-auto flex h-14 w-full max-w-[1600px] items-center justify-between px-4 sm:px-8">
|
||||
<Link
|
||||
to="/landing4"
|
||||
className="font-brut-display text-lg font-bold uppercase tracking-tight"
|
||||
>
|
||||
ArtCraft<span className="align-super text-[10px]">®</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-8 lg:flex">
|
||||
{NAV_LINKS.map((link) =>
|
||||
link.to ? (
|
||||
<Link key={link.label} to={link.to} className="group">
|
||||
<MonoLabel className="group-hover:text-flare transition-colors">
|
||||
{link.label}{" "}
|
||||
<span className="l4-muted">/{link.index}</span>
|
||||
</MonoLabel>
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group"
|
||||
>
|
||||
<MonoLabel className="group-hover:text-flare transition-colors">
|
||||
{link.label}{" "}
|
||||
<span className="l4-muted">/{link.index}</span>
|
||||
</MonoLabel>
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onDownloadClick}
|
||||
className="bg-flare text-ink hover:bg-paper flex h-9 items-center px-5 font-brut-mono text-[12px] font-bold uppercase tracking-[0.14em] transition-colors"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
aria-expanded={menuOpen}
|
||||
aria-label="Toggle navigation index"
|
||||
className="l4-line flex h-9 items-center border px-4 font-brut-mono text-[12px] uppercase tracking-[0.14em] lg:hidden"
|
||||
>
|
||||
{menuOpen ? "Close" : "Index"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile full-screen index */}
|
||||
{menuOpen && (
|
||||
<div className="bg-ink text-paper fixed inset-0 z-40 flex flex-col justify-center px-6 pt-14 lg:hidden">
|
||||
<nav className="border-t border-paper/15">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const row = (
|
||||
<span className="flex items-baseline justify-between py-5">
|
||||
<span className="font-brut-display text-4xl font-bold uppercase">
|
||||
{link.label}
|
||||
</span>
|
||||
<MonoLabel className="text-paper/40">/{link.index}</MonoLabel>
|
||||
</span>
|
||||
);
|
||||
return link.to ? (
|
||||
<Link
|
||||
key={link.label}
|
||||
to={link.to}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="block border-b border-paper/15"
|
||||
>
|
||||
{row}
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="block border-b border-paper/15"
|
||||
>
|
||||
{row}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<MonoLabel className="mt-10 text-paper/40">
|
||||
ARTCRAFT — IMG / VID / 3D — OPEN SOURCE
|
||||
</MonoLabel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { SOCIAL_LINKS } from "../../../config/links";
|
||||
import { LineMaskHeading, MonoLabel, SectionHeader } from "./ui";
|
||||
|
||||
// /07 — Community. Back to the dark chapter (the boundary sentinel before
|
||||
// this section scrubs the root vars). Two 50/50 hairline cells; the whole
|
||||
// cell is the anchor and inverts to flare on hover.
|
||||
export const SectionCommunity = () => (
|
||||
<>
|
||||
{/* Chapter boundary: paper → dark */}
|
||||
<div data-l4-boundary="dark" aria-hidden className="h-[40vh]" />
|
||||
<section className="mx-auto w-full max-w-[1600px] px-4 py-24 sm:px-8 lg:py-32">
|
||||
<SectionHeader number="07" label="COMMUNITY" right="OPEN SOURCE" />
|
||||
<div className="py-10 lg:py-14">
|
||||
<LineMaskHeading
|
||||
className="text-[clamp(2.8rem,8vw,8.5rem)]"
|
||||
lines={[<>BUILT IN</>, <>THE OPEN.</>]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="l4-line grid grid-cols-1 border sm:grid-cols-2">
|
||||
<a
|
||||
href={SOCIAL_LINKS.GITHUB}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="l4-line hover:bg-flare hover:text-ink group flex flex-col gap-10 border-b p-8 transition-colors sm:border-b-0 sm:border-r sm:p-12"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} className="text-4xl" />
|
||||
<div>
|
||||
<span className="font-brut-display text-3xl font-bold uppercase sm:text-4xl">
|
||||
GitHub <span aria-hidden>↗</span>
|
||||
</span>
|
||||
<MonoLabel className="l4-muted mt-3 block group-hover:text-ink/70">
|
||||
STAR / FORK / CONTRIBUTE
|
||||
</MonoLabel>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={SOCIAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hover:bg-flare hover:text-ink group flex flex-col gap-10 p-8 transition-colors sm:p-12"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className="text-4xl" />
|
||||
<div>
|
||||
<span className="font-brut-display text-3xl font-bold uppercase sm:text-4xl">
|
||||
Discord <span aria-hidden>↗</span>
|
||||
</span>
|
||||
<MonoLabel className="l4-muted mt-3 block group-hover:text-ink/70">
|
||||
SHARE / LEARN / SHIP
|
||||
</MonoLabel>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FEATURES } from "../data";
|
||||
import {
|
||||
HairlineFrame,
|
||||
LazyAutoplayVideo,
|
||||
LineMaskHeading,
|
||||
MonoLabel,
|
||||
SectionHeader,
|
||||
} from "./ui";
|
||||
|
||||
// /03 — Features. Paper chapter. Desktop: an index list on the left with a
|
||||
// sticky hairline-framed preview on the right; an IntersectionObserver with a
|
||||
// center-band root margin marks the row crossing mid-viewport as active,
|
||||
// which drives the preview video swap. Mobile: each row embeds its own video
|
||||
// and the observer never activates (rows are hidden from it via lg gating in
|
||||
// markup, and the sticky preview is display:none).
|
||||
export const SectionFeatures = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const listRef = useRef<HTMLOListElement>(null);
|
||||
const active = FEATURES[activeIndex];
|
||||
|
||||
useEffect(() => {
|
||||
const list = listRef.current;
|
||||
if (!list) return;
|
||||
const rows = Array.from(
|
||||
list.querySelectorAll<HTMLElement>("[data-l4-feature-row]"),
|
||||
);
|
||||
// Center band: a row becomes active while it overlaps the middle 20% of
|
||||
// the viewport. Cheap, and unlike per-row ScrollTriggers it needs no
|
||||
// cleanup coordination with the GSAP context.
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
const index = Number(
|
||||
(entry.target as HTMLElement).dataset.l4FeatureRow,
|
||||
);
|
||||
if (!Number.isNaN(index)) setActiveIndex(index);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-40% 0px -40% 0px" },
|
||||
);
|
||||
rows.forEach((row) => observer.observe(row));
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
data-l4-features
|
||||
className="mx-auto w-full max-w-[1600px] px-4 py-24 sm:px-8 lg:py-32"
|
||||
>
|
||||
<SectionHeader number="03" label="FEATURES" right="07 TOOLS" />
|
||||
<div className="py-10 lg:py-14">
|
||||
<LineMaskHeading
|
||||
className="text-[clamp(2.8rem,8vw,8.5rem)]"
|
||||
lines={[<>THE TOOLKIT</>]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="l4-line grid grid-cols-1 border-t lg:grid-cols-12">
|
||||
{/* Index list */}
|
||||
<ol ref={listRef} className="l4-line lg:col-span-7 lg:border-r">
|
||||
{FEATURES.map((feature, i) => (
|
||||
<li
|
||||
key={feature.title}
|
||||
data-l4-feature-row={i}
|
||||
data-l4-reveal
|
||||
className={`l4-line border-b lg:pr-8 ${
|
||||
i === activeIndex ? "l4-row-active" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-baseline gap-4 py-6 sm:gap-8 lg:py-8">
|
||||
<MonoLabel className="l4-muted w-8">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</MonoLabel>
|
||||
<div>
|
||||
<MonoLabel className="text-ember">{feature.label}</MonoLabel>
|
||||
<h3 className="mt-2 font-brut-display text-[clamp(1.5rem,3vw,2.8rem)] font-semibold uppercase leading-none tracking-[-0.01em]">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="l4-muted mt-3 max-w-lg font-brut-body text-sm leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden
|
||||
className="l4-index-marker font-brut-mono text-xl leading-none"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</div>
|
||||
{/* Mobile-only inline video */}
|
||||
<div className="pb-6 lg:hidden">
|
||||
<HairlineFrame
|
||||
caption={`FIG. ${String(i + 1).padStart(2, "0")} — ${feature.label.toUpperCase()}`}
|
||||
>
|
||||
<LazyAutoplayVideo
|
||||
src={feature.src}
|
||||
className="aspect-[12/10] w-full object-cover"
|
||||
/>
|
||||
</HairlineFrame>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{/* Sticky preview (desktop) */}
|
||||
<div className="hidden lg:col-span-5 lg:block">
|
||||
<div className="sticky top-24 p-8">
|
||||
<HairlineFrame
|
||||
caption={`FIG. ${String(activeIndex + 1).padStart(2, "0")} — ${active.label.toUpperCase()}`}
|
||||
>
|
||||
<div className="aspect-[12/10]">
|
||||
<LazyAutoplayVideo
|
||||
key={active.src}
|
||||
src={active.src}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</HairlineFrame>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faApple,
|
||||
faWindows,
|
||||
faLinux,
|
||||
} from "@fortawesome/free-brands-svg-icons";
|
||||
import { LineMaskHeading, MonoLabel } from "./ui";
|
||||
|
||||
// /08 — Final CTA. Dark chapter, near-full-viewport, giant type with the last
|
||||
// line clipped at the section edge.
|
||||
export const SectionFinalCta = ({
|
||||
onDownloadClick,
|
||||
}: {
|
||||
onDownloadClick: () => void;
|
||||
}) => (
|
||||
<section className="relative flex min-h-[90svh] flex-col justify-between overflow-hidden">
|
||||
<div className="mx-auto w-full max-w-[1600px] px-4 sm:px-8">
|
||||
<div className="l4-line flex items-center justify-between border-t py-3">
|
||||
<MonoLabel className="l4-muted">/08 — DOWNLOAD</MonoLabel>
|
||||
<MonoLabel className="l4-muted">
|
||||
V2.X — PUBLIC BUILD — {new Date().getFullYear()}
|
||||
</MonoLabel>
|
||||
</div>
|
||||
|
||||
<div className="py-16 lg:py-24">
|
||||
<LineMaskHeading
|
||||
className="text-[clamp(4rem,14vw,15rem)]"
|
||||
lines={[
|
||||
<>OWN YOUR</>,
|
||||
<span className="text-flare">TOOLS.</span>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-l4-reveal
|
||||
className="mt-12 flex flex-col gap-3 sm:flex-row sm:items-center"
|
||||
>
|
||||
<button
|
||||
onClick={onDownloadClick}
|
||||
className="bg-flare text-ink hover:bg-paper inline-flex h-14 items-center justify-center gap-3 px-8 font-brut-mono text-[13px] font-bold uppercase tracking-[0.14em] transition-colors"
|
||||
>
|
||||
Download for Desktop <span aria-hidden>→</span>
|
||||
</button>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="l4-line hover:text-flare inline-flex h-14 items-center justify-center border px-8 font-brut-mono text-[13px] uppercase tracking-[0.14em] transition-colors"
|
||||
style={{ color: "var(--l4-ink)" }}
|
||||
>
|
||||
Or use on Web <span aria-hidden className="ml-2">→</span>
|
||||
</Link>
|
||||
<div className="l4-muted flex items-center gap-5 text-lg sm:ml-6">
|
||||
<FontAwesomeIcon icon={faApple} title="macOS" />
|
||||
<FontAwesomeIcon icon={faWindows} title="Windows" />
|
||||
<FontAwesomeIcon icon={faLinux} title="Linux" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faApple,
|
||||
faWindows,
|
||||
faLinux,
|
||||
} from "@fortawesome/free-brands-svg-icons";
|
||||
import { HairlineFrame, LineMaskHeading, MonoLabel, PlusCorners } from "./ui";
|
||||
|
||||
// /01 — Hero. Dark chapter. Swiss grid with visible column rules, stacked
|
||||
// oversized display type, mono metadata at the corners, and a hairline-framed
|
||||
// canvas video in the right column.
|
||||
export const SectionHero = ({
|
||||
onDownloadClick,
|
||||
}: {
|
||||
onDownloadClick: () => void;
|
||||
}) => (
|
||||
<section className="relative flex min-h-[100svh] flex-col overflow-hidden pt-14">
|
||||
{/* Vertical gridlines (desktop only) */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 hidden lg:block"
|
||||
>
|
||||
{[25, 50, 75].map((left) => (
|
||||
<span
|
||||
key={left}
|
||||
className="absolute bottom-0 top-0 w-px"
|
||||
style={{ left: `${left}%`, backgroundColor: "var(--l4-line)" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto flex w-full max-w-[1600px] flex-1 flex-col px-4 sm:px-8">
|
||||
{/* Top metadata row */}
|
||||
<div
|
||||
data-l4-reveal
|
||||
className="l4-line flex items-center justify-between border-b py-3"
|
||||
>
|
||||
<MonoLabel className="l4-muted">EST. 2024 — OPEN SOURCE</MonoLabel>
|
||||
<MonoLabel className="l4-muted hidden sm:inline">
|
||||
IMG / VID / 3D
|
||||
</MonoLabel>
|
||||
<MonoLabel className="l4-muted">N 37°46′ — W 122°25′</MonoLabel>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<div className="relative flex flex-1 flex-col justify-center py-14 lg:py-20">
|
||||
<LineMaskHeading
|
||||
as="h1"
|
||||
className="text-[clamp(3.2rem,12vw,13rem)]"
|
||||
lines={[
|
||||
<>CONTROLLABLE</>,
|
||||
<>
|
||||
<span className="text-flare">AI</span> FOR
|
||||
</>,
|
||||
<span className="l4-stroke">ARTISTS</span>,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
data-l4-reveal
|
||||
className="l4-muted mt-8 max-w-md font-brut-body text-base leading-relaxed"
|
||||
>
|
||||
ArtCraft is an open desktop app for AI image, video, and 3D. Every
|
||||
model, one canvas, zero prompt-wrestling.
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div
|
||||
data-l4-reveal
|
||||
className="mt-10 flex flex-col gap-3 sm:flex-row sm:items-center"
|
||||
>
|
||||
<button
|
||||
onClick={onDownloadClick}
|
||||
className="bg-flare text-ink hover:bg-paper inline-flex h-12 items-center justify-center gap-3 px-7 font-brut-mono text-[13px] font-bold uppercase tracking-[0.14em] transition-colors"
|
||||
>
|
||||
Download_App <span aria-hidden>→</span>
|
||||
</button>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="l4-line hover:text-flare inline-flex h-12 items-center justify-center border px-7 font-brut-mono text-[13px] uppercase tracking-[0.14em] transition-colors"
|
||||
style={{ color: "var(--l4-ink)" }}
|
||||
>
|
||||
Use on Web
|
||||
</Link>
|
||||
<MonoLabel className="l4-muted sm:ml-4">
|
||||
MAC OS — WINDOWS — LINUX
|
||||
</MonoLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom row: figure video right, mono coordinates left */}
|
||||
<div className="l4-line grid grid-cols-1 gap-8 border-t py-8 lg:grid-cols-12">
|
||||
<div
|
||||
data-l4-reveal
|
||||
className="l4-muted flex flex-col justify-end gap-2 lg:col-span-5"
|
||||
>
|
||||
<MonoLabel>ARTCRAFT — DESKTOP + WEB</MonoLabel>
|
||||
<MonoLabel>V2.X — PUBLIC BUILD</MonoLabel>
|
||||
<div className="mt-4 flex items-center gap-5 text-lg opacity-70">
|
||||
<FontAwesomeIcon icon={faApple} title="macOS" />
|
||||
<FontAwesomeIcon icon={faWindows} title="Windows" />
|
||||
<FontAwesomeIcon icon={faLinux} title="Linux" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative lg:col-span-7" data-l4-wipe>
|
||||
<PlusCorners />
|
||||
<HairlineFrame caption="FIG. 01 — CANVAS OUTPUT">
|
||||
<video
|
||||
className="aspect-video w-full object-cover"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
src="/videos/hero-video.mp4"
|
||||
/>
|
||||
</HairlineFrame>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { MADE_WITH_VIDEOS } from "../data";
|
||||
import { HairlineFrame, LineMaskHeading, SectionHeader } from "./ui";
|
||||
|
||||
// /06 — Output. Paper chapter. Asymmetric editorial grid: one large frame plus
|
||||
// two stacked small frames, all lazy YouTube embeds with mono figure captions.
|
||||
export const SectionMadeWith = () => {
|
||||
const [featureFilm, ...smallFilms] = MADE_WITH_VIDEOS;
|
||||
return (
|
||||
<section className="mx-auto w-full max-w-[1600px] px-4 py-24 sm:px-8 lg:py-32">
|
||||
<SectionHeader number="06" label="OUTPUT" right="COMMUNITY FILMS" />
|
||||
<div className="py-10 lg:py-14">
|
||||
<LineMaskHeading
|
||||
className="text-[clamp(2.8rem,8vw,8.5rem)]"
|
||||
lines={[<>MADE WITH</>, <>ARTCRAFT</>]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div data-l4-wipe className="lg:col-span-8">
|
||||
<HairlineFrame caption={featureFilm.caption}>
|
||||
<iframe
|
||||
className="aspect-video w-full"
|
||||
src={featureFilm.embedUrl}
|
||||
title="Made with ArtCraft — feature film"
|
||||
loading="lazy"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</HairlineFrame>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8 lg:col-span-4">
|
||||
{smallFilms.map((film, i) => (
|
||||
<div key={film.embedUrl} data-l4-wipe>
|
||||
<HairlineFrame caption={film.caption}>
|
||||
<iframe
|
||||
className="aspect-video w-full"
|
||||
src={film.embedUrl}
|
||||
title={`Made with ArtCraft — community film ${i + 2}`}
|
||||
loading="lazy"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</HairlineFrame>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Fragment } from "react";
|
||||
import { MANIFESTO_ACCENT_WORDS, MANIFESTO_WORDS } from "../data";
|
||||
import { MonoLabel } from "./ui";
|
||||
|
||||
// /02 — Statement. Dark chapter. On desktop the outer section is tall and the
|
||||
// inner viewport sticks, while a scrubbed ScrollTrigger raises each word from
|
||||
// 18% opacity to full as the user scrolls. Words tagged as accents also flip
|
||||
// to flare. On mobile the section is static (no pin) and gets a simple fade.
|
||||
// The [data-l4-boundary="paper"] spacer after the text drives the dark→paper
|
||||
// chapter transition.
|
||||
export const SectionManifesto = () => (
|
||||
<>
|
||||
<section data-l4-manifesto className="relative lg:h-[250vh]">
|
||||
<div className="flex min-h-[70svh] flex-col justify-center lg:sticky lg:top-0 lg:h-screen">
|
||||
<div className="mx-auto w-full max-w-[1600px] px-4 sm:px-8">
|
||||
<div className="l4-line grid grid-cols-1 gap-8 border-t pt-6 lg:grid-cols-12">
|
||||
<div className="lg:col-span-3" data-l4-reveal>
|
||||
<MonoLabel>
|
||||
/02 <span className="l4-muted ml-3">STATEMENT</span>
|
||||
</MonoLabel>
|
||||
</div>
|
||||
<p className="font-brut-display text-[clamp(1.8rem,4.5vw,4.2rem)] font-medium leading-[1.12] tracking-[-0.02em] lg:col-span-9">
|
||||
{MANIFESTO_WORDS.map((word, i) => (
|
||||
<Fragment key={i}>
|
||||
<span
|
||||
data-l4-word
|
||||
data-l4-word-accent={
|
||||
MANIFESTO_ACCENT_WORDS.has(word) ? "" : undefined
|
||||
}
|
||||
className="inline-block opacity-[0.18]"
|
||||
>
|
||||
{word}
|
||||
</span>{" "}
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* Chapter boundary: scrubbing through this spacer tweens the root CSS
|
||||
vars from the dark palette to paper. */}
|
||||
<div data-l4-boundary="paper" aria-hidden className="h-[60vh]" />
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { PROVIDERS } from "../data";
|
||||
import { LineMaskHeading, MonoLabel, SectionHeader } from "./ui";
|
||||
|
||||
// /04 — Model wall. Paper chapter. Collapsed-border grid of provider marks
|
||||
// (the service SVGs are dark-on-transparent, so they read natively on paper).
|
||||
// Counters tick up once via the [data-l4-counter] hooks in the animations
|
||||
// module.
|
||||
export const SectionModelWall = () => (
|
||||
<section className="mx-auto w-full max-w-[1600px] px-4 py-24 sm:px-8 lg:py-32">
|
||||
<SectionHeader
|
||||
number="04"
|
||||
label="MODELS"
|
||||
right={
|
||||
<>
|
||||
PROVIDERS — <span data-l4-counter="19">19</span> / MODELS —{" "}
|
||||
<span data-l4-counter="60">60</span>+
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="py-10 lg:py-14">
|
||||
<LineMaskHeading
|
||||
className="text-[clamp(2.8rem,8vw,8.5rem)]"
|
||||
lines={[<>EVERY MODEL.</>, <span className="l4-stroke">ONE CANVAS.</span>]}
|
||||
/>
|
||||
<p className="l4-muted mt-8 max-w-md font-brut-body text-base leading-relaxed">
|
||||
Text prompting is neat, but artists crave control. Use every frontier
|
||||
model from one desktop canvas — no tab-hopping, no per-site
|
||||
subscriptions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="l4-line grid grid-cols-3 border-l border-t md:grid-cols-5 lg:grid-cols-7">
|
||||
{PROVIDERS.map((provider) => (
|
||||
<div
|
||||
key={provider.name}
|
||||
data-l4-cell
|
||||
className="l4-line flex aspect-square flex-col items-center justify-center gap-3 border-b border-r p-4"
|
||||
>
|
||||
<img
|
||||
src={provider.src}
|
||||
alt={provider.name}
|
||||
loading="lazy"
|
||||
className="h-8 w-8 object-contain opacity-60 sm:h-10 sm:w-10"
|
||||
/>
|
||||
<MonoLabel className="l4-muted hidden text-center md:block">
|
||||
{provider.name}
|
||||
</MonoLabel>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
data-l4-cell
|
||||
className="border-ink/20 bg-flare text-ink flex aspect-square flex-col items-center justify-center gap-2 border-b border-r p-4 text-center"
|
||||
>
|
||||
<span className="font-brut-display text-3xl font-bold leading-none">
|
||||
+
|
||||
</span>
|
||||
<MonoLabel>MORE EVERY WEEK</MonoLabel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
import { OWNERSHIP_ROWS, RENTAL_ROWS } from "../data";
|
||||
import { LineMaskHeading, MonoLabel, SectionHeader } from "./ui";
|
||||
|
||||
// /05 — Ownership. Paper chapter with the page's strongest hit: a full ink
|
||||
// panel for the ArtCraft column against a paper ledger of rental grievances.
|
||||
export const SectionOwnership = () => (
|
||||
<section className="mx-auto w-full max-w-[1600px] px-4 py-24 sm:px-8 lg:py-32">
|
||||
<SectionHeader number="05" label="OWNERSHIP" right="NO RENT PAYMENTS" />
|
||||
<div className="py-10 lg:py-14">
|
||||
<LineMaskHeading
|
||||
className="text-[clamp(2.8rem,8vw,8.5rem)]"
|
||||
lines={[<>STOP RENTING</>, <span className="l4-stroke">YOUR TOOLS</span>]}
|
||||
/>
|
||||
<p className="l4-muted mt-8 max-w-md font-brut-body text-base leading-relaxed">
|
||||
ArtCraft is yours to own and keep, forever. No subscriptions needed, no
|
||||
aggregator middleman, no rent payments.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="l4-line grid grid-cols-1 border-t lg:grid-cols-2">
|
||||
{/* The subscription web — ledger of grievances */}
|
||||
<div className="l4-line py-8 lg:border-r lg:pr-12">
|
||||
<MonoLabel className="l4-muted">✕ THE SUBSCRIPTION WEB</MonoLabel>
|
||||
<ul className="mt-8">
|
||||
{RENTAL_ROWS.map((row) => (
|
||||
<li
|
||||
key={row}
|
||||
data-l4-reveal
|
||||
className="l4-line flex items-baseline gap-4 border-b py-4"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="l4-muted font-brut-mono text-sm font-bold"
|
||||
>
|
||||
✕
|
||||
</span>
|
||||
<span className="l4-muted font-brut-body text-base">{row}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* ArtCraft — ink block panel */}
|
||||
<div
|
||||
data-l4-panel
|
||||
className="bg-ink text-paper my-8 flex flex-col justify-between p-8 sm:p-10 lg:my-0 lg:ml-12"
|
||||
>
|
||||
<div>
|
||||
<MonoLabel className="text-flare">✓ ARTCRAFT</MonoLabel>
|
||||
<ul className="mt-8">
|
||||
{OWNERSHIP_ROWS.map((row) => (
|
||||
<li
|
||||
key={row}
|
||||
className="flex items-baseline gap-4 border-b border-paper/15 py-4"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="text-flare font-brut-mono text-sm font-bold"
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
<span className="font-brut-body text-base text-paper/85">
|
||||
{row}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<MonoLabel className="mt-10 text-paper/40">
|
||||
LICENSE — YOURS / SERVERS — OPTIONAL
|
||||
</MonoLabel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -0,0 +1,163 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { TICKER_ITEMS } from "../data";
|
||||
|
||||
// Small mono uppercase micro-label, the backbone of the brutalist metadata
|
||||
// treatment (coordinates, figure captions, section eyebrows).
|
||||
export const MonoLabel = ({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => (
|
||||
<span
|
||||
className={`font-brut-mono text-[11px] uppercase tracking-[0.18em] ${className}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
// Numbered section header rail: "/03 — FEATURES" above a hairline.
|
||||
export const SectionHeader = ({
|
||||
number,
|
||||
label,
|
||||
right,
|
||||
}: {
|
||||
number: string;
|
||||
label: string;
|
||||
right?: ReactNode;
|
||||
}) => (
|
||||
<div className="l4-line flex items-baseline justify-between border-b pb-3">
|
||||
<MonoLabel>
|
||||
<span className="opacity-100">/{number}</span>
|
||||
<span className="l4-muted ml-3">{label}</span>
|
||||
</MonoLabel>
|
||||
{right ? <MonoLabel className="l4-muted">{right}</MonoLabel> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Four plus-sign markers pinned at the corners of a relative parent.
|
||||
export const PlusCorners = () => (
|
||||
<>
|
||||
<span aria-hidden className="l4-plus -left-[6px] -top-[6px]" />
|
||||
<span aria-hidden className="l4-plus -right-[6px] -top-[6px]" />
|
||||
<span aria-hidden className="l4-plus -left-[6px] -bottom-[6px]" />
|
||||
<span aria-hidden className="l4-plus -right-[6px] -bottom-[6px]" />
|
||||
</>
|
||||
);
|
||||
|
||||
// 1px-bordered media frame with an optional mono caption strip underneath.
|
||||
export const HairlineFrame = ({
|
||||
children,
|
||||
caption,
|
||||
className = "",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
caption?: string;
|
||||
className?: string;
|
||||
}) => (
|
||||
<figure className={`relative ${className}`}>
|
||||
<div className="l4-line relative overflow-hidden border">{children}</div>
|
||||
{caption ? (
|
||||
<figcaption className="l4-line l4-muted flex items-center justify-between border-x border-b px-3 py-2">
|
||||
<MonoLabel>{caption}</MonoLabel>
|
||||
<MonoLabel aria-hidden>+</MonoLabel>
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
);
|
||||
|
||||
// Headline pre-split into lines, each wrapped in an overflow mask so GSAP can
|
||||
// stagger the inner spans up into view. `lines` accepts ReactNodes so callers
|
||||
// can color or outline individual words. Tag with data-l4-line-group so the
|
||||
// animation module can find each group and its [data-l4-line] children.
|
||||
export const LineMaskHeading = ({
|
||||
lines,
|
||||
className = "",
|
||||
as: Tag = "h2",
|
||||
}: {
|
||||
lines: ReactNode[];
|
||||
className?: string;
|
||||
as?: "h1" | "h2";
|
||||
}) => (
|
||||
<Tag
|
||||
data-l4-line-group
|
||||
className={`font-brut-display font-bold uppercase leading-[0.86] tracking-[-0.02em] ${className}`}
|
||||
>
|
||||
{lines.map((line, i) => (
|
||||
<span key={i} className="l4-line-mask">
|
||||
<span data-l4-line>{line}</span>
|
||||
</span>
|
||||
))}
|
||||
</Tag>
|
||||
);
|
||||
|
||||
// Full-bleed flare marquee band. Content duplicated x2; the CSS animation
|
||||
// translates -50% for a seamless loop (static under prefers-reduced-motion).
|
||||
export const Ticker = () => {
|
||||
const copy = (
|
||||
<div className="flex shrink-0 items-center">
|
||||
{TICKER_ITEMS.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="font-brut-mono px-6 text-[13px] font-bold uppercase tracking-[0.2em]"
|
||||
>
|
||||
{item} <span className="ml-10">✳</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="border-ink/20 bg-flare text-ink flex h-12 items-center overflow-hidden border-y"
|
||||
>
|
||||
<div className="l4-ticker-track">
|
||||
{copy}
|
||||
{copy}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Lazy autoplay video: defers fetch + decoder spin-up until the element nears
|
||||
// the viewport (same pattern as landing3's LazyAutoplayVideo).
|
||||
export const LazyAutoplayVideo = ({
|
||||
src,
|
||||
className,
|
||||
}: {
|
||||
src: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const [shouldLoad, setShouldLoad] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || shouldLoad) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setShouldLoad(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "150% 0px" },
|
||||
);
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, [shouldLoad]);
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={ref}
|
||||
className={className}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload={shouldLoad ? "auto" : "none"}
|
||||
src={shouldLoad ? src : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
// Content data for landing4. Feature copy mirrors landing3's FEATURES array so
|
||||
// the two landings stay in sync on messaging while diverging on presentation.
|
||||
|
||||
export interface Feature {
|
||||
label: string;
|
||||
title: string;
|
||||
description: string;
|
||||
src: string;
|
||||
}
|
||||
|
||||
export const FEATURES: ReadonlyArray<Feature> = [
|
||||
{
|
||||
label: "Worlds",
|
||||
title: "Image to Location",
|
||||
description:
|
||||
"Place virtual actors into physical environments. Establish single-location consistency and film multiple shots in a room without things disappearing.",
|
||||
src: "/videos/features/WorldLabs_Demo_2.webm",
|
||||
},
|
||||
{
|
||||
label: "3D Compositing",
|
||||
title: "Build scenes with depth",
|
||||
description:
|
||||
"Use images, backdrops, foreground elements, and props in scenes with real depth. A couple of images blends naturally into a finished composition.",
|
||||
src: "/videos/features/Panel.webm",
|
||||
},
|
||||
{
|
||||
label: "2D Compositing",
|
||||
title: "Precise layered control",
|
||||
description:
|
||||
"Combine images, background removal, layers, and simple drawing tools to compose a scene exactly the way you imagined it.",
|
||||
src: "/videos/features/Editor.webm",
|
||||
},
|
||||
{
|
||||
label: "3D Mesh",
|
||||
title: "Image to 3D Mesh",
|
||||
description:
|
||||
"Turning images into 3D helps position elements exactingly. Block complex scenes with intentional geometry instead of fighting prompts.",
|
||||
src: "/videos/features/Make_3D.webm",
|
||||
},
|
||||
{
|
||||
label: "Mixed Assets",
|
||||
title: "Mix every kind of asset",
|
||||
description:
|
||||
"Combine image cutouts, worlds, and 3D meshes in one canvas to lay out scenes with precision and intention.",
|
||||
src: "/videos/features/Mixed.webm",
|
||||
},
|
||||
{
|
||||
label: "Posing",
|
||||
title: "Character Posing",
|
||||
description:
|
||||
'Dynamically pose your characters to nail the precise character, scene, and camera blocking before calling "action".',
|
||||
src: "/videos/features/Pose_Second_Version.webm",
|
||||
},
|
||||
{
|
||||
label: "Cutouts",
|
||||
title: "Background Removal",
|
||||
description:
|
||||
"Instantly remove backgrounds from images to create assets for your scenes. Clean, precise, and ready for compositing.",
|
||||
src: "/videos/features/Background.webm",
|
||||
},
|
||||
];
|
||||
|
||||
export const MANIFESTO_WORDS: ReadonlyArray<string> = [
|
||||
"ArtCraft",
|
||||
"brings",
|
||||
"control",
|
||||
"to",
|
||||
"AI",
|
||||
"image",
|
||||
"and",
|
||||
"video",
|
||||
"generation,",
|
||||
"giving",
|
||||
"artists",
|
||||
"like",
|
||||
"you",
|
||||
"full",
|
||||
"power",
|
||||
"over",
|
||||
"every",
|
||||
"shot.",
|
||||
];
|
||||
|
||||
// Words rendered in flare once the manifesto scrub reaches them.
|
||||
export const MANIFESTO_ACCENT_WORDS: ReadonlySet<string> = new Set([
|
||||
"control",
|
||||
"artists",
|
||||
]);
|
||||
|
||||
export const TICKER_ITEMS: ReadonlyArray<string> = [
|
||||
"IMAGE",
|
||||
"VIDEO",
|
||||
"3D",
|
||||
"OPEN SOURCE",
|
||||
"EVERY MODEL",
|
||||
"NO PROMPTS REQUIRED",
|
||||
"MADE BY ARTISTS",
|
||||
];
|
||||
|
||||
export interface Provider {
|
||||
name: string;
|
||||
src: string;
|
||||
}
|
||||
|
||||
export const PROVIDERS: ReadonlyArray<Provider> = [
|
||||
{ name: "OpenAI", src: "/images/services/openai.svg" },
|
||||
{ name: "Google", src: "/images/services/google.svg" },
|
||||
{ name: "Midjourney", src: "/images/services/midjourney.svg" },
|
||||
{ name: "ByteDance", src: "/images/services/bytedance.svg" },
|
||||
{ name: "Kling", src: "/images/services/kling.svg" },
|
||||
{ name: "Black Forest", src: "/images/services/blackforestlabs.svg" },
|
||||
{ name: "World Labs", src: "/images/services/worldlabs.svg" },
|
||||
{ name: "Alibaba", src: "/images/services/alibaba.svg" },
|
||||
{ name: "Tencent", src: "/images/services/tencent.svg" },
|
||||
{ name: "Grok", src: "/images/services/grok.svg" },
|
||||
{ name: "Recraft", src: "/images/services/recraft.svg" },
|
||||
{ name: "Vidu", src: "/images/services/vidu.svg" },
|
||||
{ name: "Suno", src: "/images/services/suno.svg" },
|
||||
{ name: "Krea", src: "/images/services/krea.svg" },
|
||||
{ name: "Higgsfield", src: "/images/services/higgsfield.svg" },
|
||||
{ name: "Replicate", src: "/images/services/replicate.svg" },
|
||||
{ name: "Fal", src: "/images/services/fal.svg" },
|
||||
{ name: "OpenArt", src: "/images/services/openart.svg" },
|
||||
{ name: "TensorArt", src: "/images/services/tensorart.svg" },
|
||||
];
|
||||
|
||||
export interface MadeWithVideo {
|
||||
embedUrl: string;
|
||||
caption: string;
|
||||
}
|
||||
|
||||
export const MADE_WITH_VIDEOS: ReadonlyArray<MadeWithVideo> = [
|
||||
{
|
||||
embedUrl: "https://www.youtube.com/embed/HDdsKJl92H4?si=0Hm4AweSRHq3qRt6",
|
||||
caption: "FILM_001 — COMMUNITY",
|
||||
},
|
||||
{
|
||||
embedUrl: "https://www.youtube.com/embed/oqoCWdOwr2U?si=ILMPk8hGHo9hP8RU",
|
||||
caption: "FILM_002 — COMMUNITY",
|
||||
},
|
||||
{
|
||||
embedUrl: "https://www.youtube.com/embed/H4NFXGMuwpY?si=wPuQl5cJOu1v8MJu",
|
||||
caption: "FILM_003 — COMMUNITY",
|
||||
},
|
||||
];
|
||||
|
||||
export interface LedgerRow {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const RENTAL_ROWS: ReadonlyArray<string> = [
|
||||
"Paying for access, not a product",
|
||||
"Your work lives on someone else's servers",
|
||||
"Monthly fees, forever",
|
||||
"Locked into one provider's models",
|
||||
"History disappears when they do",
|
||||
];
|
||||
|
||||
export const OWNERSHIP_ROWS: ReadonlyArray<string> = [
|
||||
"Download it. It's yours, forever",
|
||||
"Open source, on your desktop",
|
||||
"Your files stay on your machine",
|
||||
"Every model in one canvas",
|
||||
"Bring your own API keys, or use ours",
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./landing4";
|
||||
@@ -0,0 +1,262 @@
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const ACCENT = "#00E5FF";
|
||||
|
||||
const PAPER_VARS = {
|
||||
"--l4-bg": "#F2F1EC",
|
||||
"--l4-ink": "#0A0A0B",
|
||||
"--l4-line": "rgba(10,10,11,0.18)",
|
||||
"--l4-muted": "rgba(10,10,11,0.55)",
|
||||
};
|
||||
|
||||
const DARK_VARS = {
|
||||
"--l4-bg": "#0A0A0B",
|
||||
"--l4-ink": "#F2F1EC",
|
||||
"--l4-line": "rgba(242,241,236,0.16)",
|
||||
"--l4-muted": "rgba(242,241,236,0.55)",
|
||||
};
|
||||
|
||||
// All ScrollTrigger wiring for landing4. Sections carry data-l4-* hooks;
|
||||
// this module owns the motion. Runs inside a gsap.context created by the
|
||||
// page component, so everything reverts on unmount.
|
||||
//
|
||||
// NOTE: entrance animations deliberately create their tweens inside onEnter
|
||||
// callbacks (the landing3 pattern) instead of linking a pre-built tween to
|
||||
// the trigger — pre-linked non-scrub tweens failed to fire on triggers that
|
||||
// were already past their start position at mount.
|
||||
export function initLanding4Animations(root: HTMLElement): void {
|
||||
const reducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
|
||||
if (reducedMotion) {
|
||||
// Everything stays in its final, fully visible state (no initial hidden
|
||||
// states are ever applied). Accent words still get their color, and
|
||||
// chapter colors snap so paper sections stay legible.
|
||||
root.querySelectorAll<HTMLElement>("[data-l4-word]").forEach((word) => {
|
||||
word.style.opacity = "1";
|
||||
if (word.dataset.l4WordAccent !== undefined) {
|
||||
word.style.color = ACCENT;
|
||||
}
|
||||
});
|
||||
setupChapterSnaps(root, true);
|
||||
return;
|
||||
}
|
||||
|
||||
setupLineMasks(root);
|
||||
setupReveals(root);
|
||||
setupWipes(root);
|
||||
setupCounters(root);
|
||||
setupOwnershipPanel(root);
|
||||
|
||||
const mm = gsap.matchMedia();
|
||||
|
||||
mm.add("(min-width: 1024px)", () => {
|
||||
setupManifestoScrub(root);
|
||||
setupChapterScrubs(root);
|
||||
});
|
||||
|
||||
mm.add("(max-width: 1023px)", () => {
|
||||
setupManifestoFade(root);
|
||||
setupChapterSnaps(root, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Headline line reveals: each [data-l4-line-group] staggers its masked lines
|
||||
// up into view when the group enters the viewport.
|
||||
function setupLineMasks(root: HTMLElement) {
|
||||
const groups = root.querySelectorAll<HTMLElement>("[data-l4-line-group]");
|
||||
groups.forEach((group) => {
|
||||
const lines = group.querySelectorAll<HTMLElement>("[data-l4-line]");
|
||||
gsap.set(lines, { yPercent: 110 });
|
||||
ScrollTrigger.create({
|
||||
trigger: group,
|
||||
start: "top 85%",
|
||||
once: true,
|
||||
onEnter: () =>
|
||||
gsap.to(lines, {
|
||||
yPercent: 0,
|
||||
duration: 0.9,
|
||||
ease: "power4.out",
|
||||
stagger: 0.09,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generic fade-up entrances, batched like landing3 so ~30 elements share a
|
||||
// handful of triggers instead of one each.
|
||||
function setupReveals(root: HTMLElement) {
|
||||
const elements = gsap.utils.toArray<HTMLElement>("[data-l4-reveal]", root);
|
||||
if (!elements.length) return;
|
||||
gsap.set(elements, { autoAlpha: 0, y: 20 });
|
||||
ScrollTrigger.batch(elements, {
|
||||
start: "top 88%",
|
||||
once: true,
|
||||
onEnter: (batch) =>
|
||||
gsap.to(batch, {
|
||||
autoAlpha: 1,
|
||||
y: 0,
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
stagger: 0.06,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Media frames open with a left→right clip wipe.
|
||||
function setupWipes(root: HTMLElement) {
|
||||
const elements = gsap.utils.toArray<HTMLElement>("[data-l4-wipe]", root);
|
||||
elements.forEach((el) => {
|
||||
gsap.set(el, { clipPath: "inset(0 100% 0 0)" });
|
||||
ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 85%",
|
||||
once: true,
|
||||
onEnter: () =>
|
||||
gsap.to(el, {
|
||||
clipPath: "inset(0 0% 0 0)",
|
||||
duration: 1.1,
|
||||
ease: "power3.inOut",
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Model-wall counters tick from 0 to their data-l4-counter target once.
|
||||
function setupCounters(root: HTMLElement) {
|
||||
const counters = root.querySelectorAll<HTMLElement>("[data-l4-counter]");
|
||||
counters.forEach((el) => {
|
||||
const target = Number(el.dataset.l4Counter);
|
||||
if (Number.isNaN(target)) return;
|
||||
ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 90%",
|
||||
once: true,
|
||||
onEnter: () => {
|
||||
const state = { value: 0 };
|
||||
gsap.to(state, {
|
||||
value: target,
|
||||
duration: 1.4,
|
||||
ease: "power2.out",
|
||||
onUpdate: () => {
|
||||
el.textContent = String(Math.round(state.value));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Ownership ink panel reveals with a top→bottom wipe.
|
||||
function setupOwnershipPanel(root: HTMLElement) {
|
||||
const panel = root.querySelector<HTMLElement>("[data-l4-panel]");
|
||||
if (!panel) return;
|
||||
gsap.set(panel, { clipPath: "inset(0 0 100% 0)" });
|
||||
ScrollTrigger.create({
|
||||
trigger: panel,
|
||||
start: "top 82%",
|
||||
once: true,
|
||||
onEnter: () =>
|
||||
gsap.to(panel, {
|
||||
clipPath: "inset(0 0 0% 0)",
|
||||
duration: 1.1,
|
||||
ease: "power3.inOut",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Desktop manifesto: the tall section pins its inner viewport via CSS sticky;
|
||||
// this scrub raises each word to full opacity in sequence and flips accent
|
||||
// words to the accent color near their reveal.
|
||||
function setupManifestoScrub(root: HTMLElement) {
|
||||
const section = root.querySelector<HTMLElement>("[data-l4-manifesto]");
|
||||
if (!section) return;
|
||||
const words = section.querySelectorAll<HTMLElement>("[data-l4-word]");
|
||||
if (!words.length) return;
|
||||
|
||||
const timeline = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: "top top",
|
||||
end: "bottom bottom",
|
||||
scrub: true,
|
||||
},
|
||||
});
|
||||
words.forEach((word) => {
|
||||
timeline.to(word, { opacity: 1, duration: 1, ease: "none" }, "<0.35");
|
||||
if (word.dataset.l4WordAccent !== undefined) {
|
||||
timeline.to(word, { color: ACCENT, duration: 0.5 }, "<");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile manifesto: no pin, just a staggered fade of the words.
|
||||
function setupManifestoFade(root: HTMLElement) {
|
||||
const section = root.querySelector<HTMLElement>("[data-l4-manifesto]");
|
||||
if (!section) return;
|
||||
const words = section.querySelectorAll<HTMLElement>("[data-l4-word]");
|
||||
if (!words.length) return;
|
||||
ScrollTrigger.create({
|
||||
trigger: section,
|
||||
start: "top 70%",
|
||||
once: true,
|
||||
onEnter: () => {
|
||||
gsap.to(words, {
|
||||
opacity: 1,
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
stagger: 0.03,
|
||||
});
|
||||
words.forEach((word) => {
|
||||
if (word.dataset.l4WordAccent !== undefined) {
|
||||
gsap.to(word, { color: ACCENT, duration: 0.5, delay: 0.4 });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Desktop chapter transitions: scrubbing through each boundary spacer tweens
|
||||
// the root CSS vars, recoloring everything chapter-aware (navbar, hairlines,
|
||||
// type) as one system.
|
||||
function setupChapterScrubs(root: HTMLElement) {
|
||||
root
|
||||
.querySelectorAll<HTMLElement>("[data-l4-boundary]")
|
||||
.forEach((boundary) => {
|
||||
const vars =
|
||||
boundary.dataset.l4Boundary === "paper" ? PAPER_VARS : DARK_VARS;
|
||||
gsap.to(root, {
|
||||
...vars,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: boundary,
|
||||
start: "top 75%",
|
||||
end: "bottom 25%",
|
||||
scrub: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile / reduced-motion chapter transitions: tween (or snap) the vars when
|
||||
// the boundary enters, and reverse when scrolling back up past it.
|
||||
function setupChapterSnaps(root: HTMLElement, instant: boolean) {
|
||||
root
|
||||
.querySelectorAll<HTMLElement>("[data-l4-boundary]")
|
||||
.forEach((boundary) => {
|
||||
const isPaper = boundary.dataset.l4Boundary === "paper";
|
||||
const forward = isPaper ? PAPER_VARS : DARK_VARS;
|
||||
const backward = isPaper ? DARK_VARS : PAPER_VARS;
|
||||
const duration = instant ? 0 : 0.6;
|
||||
ScrollTrigger.create({
|
||||
trigger: boundary,
|
||||
start: "top 60%",
|
||||
onEnter: () => gsap.to(root, { ...forward, duration }),
|
||||
onLeaveBack: () => gsap.to(root, { ...backward, duration }),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/* Landing4 (brutalist landing) scoped styles. Every class is .l4- prefixed:
|
||||
Vite bundles CSS globally, so unprefixed names would leak into other pages. */
|
||||
|
||||
.l4-root {
|
||||
--l4-bg: #0a0a0b;
|
||||
--l4-ink: #f2f1ec;
|
||||
--l4-line: rgba(242, 241, 236, 0.16);
|
||||
--l4-muted: rgba(242, 241, 236, 0.55);
|
||||
background-color: var(--l4-bg);
|
||||
color: var(--l4-ink);
|
||||
}
|
||||
|
||||
/* Chapter-aware helpers: everything that must recolor when the background
|
||||
scrubs between ink and paper reads these vars instead of fixed colors. */
|
||||
.l4-line {
|
||||
border-color: var(--l4-line);
|
||||
}
|
||||
|
||||
.l4-muted {
|
||||
color: var(--l4-muted);
|
||||
}
|
||||
|
||||
/* Outlined display type (e.g. "ARTISTS", "ONE CANVAS.") */
|
||||
.l4-stroke {
|
||||
-webkit-text-stroke: 2px var(--l4-ink);
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.l4-stroke {
|
||||
-webkit-text-stroke-width: 1.2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Overflow mask for line-by-line headline reveals. The inner span gets
|
||||
translated by GSAP; padding absorbs Bricolage's descender overhang. */
|
||||
.l4-line-mask {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
padding-bottom: 0.08em;
|
||||
margin-bottom: -0.08em;
|
||||
}
|
||||
|
||||
.l4-line-mask > span {
|
||||
display: block;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Marquee ticker: two identical copies of the content; -50% = seamless loop.
|
||||
Reuses the marquee-track keyframes defined in src/styles.css. */
|
||||
.l4-ticker-track {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
animation: marquee-track 28s linear infinite;
|
||||
}
|
||||
|
||||
/* Plus-sign grid intersection markers */
|
||||
.l4-plus {
|
||||
position: absolute;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.l4-plus::before,
|
||||
.l4-plus::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--l4-ink);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.l4-plus::before {
|
||||
left: 6px;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.l4-plus::after {
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 13px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* Feature index rows: the + marker rotates to × when the row is active. */
|
||||
.l4-index-marker {
|
||||
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.l4-row-active .l4-index-marker {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.l4-ticker-track {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.l4-index-marker {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Lenis from "lenis";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import Seo from "../../components/seo";
|
||||
import { DownloadModal } from "../../components/download-modal";
|
||||
import { initLanding4Animations } from "./landing4-animations";
|
||||
import { BrutNavbar } from "./components/brut-navbar";
|
||||
import { BrutFooter } from "./components/brut-footer";
|
||||
import { Ticker } from "./components/ui";
|
||||
import { SectionHero } from "./components/section-hero";
|
||||
import { SectionManifesto } from "./components/section-manifesto";
|
||||
import { SectionFeatures } from "./components/section-features";
|
||||
import { SectionModelWall } from "./components/section-model-wall";
|
||||
import { SectionOwnership } from "./components/section-ownership";
|
||||
import { SectionMadeWith } from "./components/section-made-with";
|
||||
import { SectionCommunity } from "./components/section-community";
|
||||
import { SectionFinalCta } from "./components/section-final-cta";
|
||||
import "./landing4.css";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Landing4 — brutalist grid landing. Chapters: dark (hero, statement) →
|
||||
// paper (toolkit, models, ownership, output) → dark (community, CTA, footer).
|
||||
// Sections are static markup with data-l4-* hooks; all motion lives in
|
||||
// landing4-animations.ts.
|
||||
const Landing4 = () => {
|
||||
const [showDownloadModal, setShowDownloadModal] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Lenis smooth scrolling (same wiring as landing3). Skipped on mobile and
|
||||
// under prefers-reduced-motion.
|
||||
useEffect(() => {
|
||||
if (isMobile) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
smoothWheel: true,
|
||||
lerp: 0.1,
|
||||
});
|
||||
|
||||
let rafId: number;
|
||||
const raf = (time: number) => {
|
||||
lenis.raf(time);
|
||||
rafId = requestAnimationFrame(raf);
|
||||
};
|
||||
rafId = requestAnimationFrame(raf);
|
||||
|
||||
lenis.on("scroll", ScrollTrigger.update);
|
||||
|
||||
const handleResize = () => {
|
||||
lenis.resize();
|
||||
ScrollTrigger.refresh();
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cancelAnimationFrame(rafId);
|
||||
lenis.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
const ctx = gsap.context(
|
||||
() => initLanding4Animations(rootRef.current!),
|
||||
rootRef,
|
||||
);
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
const openDownloadModal = () => setShowDownloadModal(true);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="l4-root font-brut-body">
|
||||
<Seo
|
||||
title="ArtCraft — Controllable AI for Artists"
|
||||
description="Own your tools. ArtCraft is an open desktop app for AI image, video, and 3D — every model, one canvas."
|
||||
/>
|
||||
<BrutNavbar onDownloadClick={openDownloadModal} />
|
||||
|
||||
<main>
|
||||
<SectionHero onDownloadClick={openDownloadModal} />
|
||||
<Ticker />
|
||||
<SectionManifesto />
|
||||
<SectionFeatures />
|
||||
<SectionModelWall />
|
||||
<SectionOwnership />
|
||||
<SectionMadeWith />
|
||||
<SectionCommunity />
|
||||
<SectionFinalCta onDownloadClick={openDownloadModal} />
|
||||
</main>
|
||||
|
||||
<BrutFooter />
|
||||
<DownloadModal
|
||||
isOpen={showDownloadModal}
|
||||
onClose={() => setShowDownloadModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing4;
|
||||
@@ -29,6 +29,15 @@ module.exports = {
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
],
|
||||
// Landing4 (brutalist landing) type system
|
||||
"brut-display": [
|
||||
"Bricolage Grotesque",
|
||||
"Outfit",
|
||||
"system-ui",
|
||||
"sans-serif",
|
||||
],
|
||||
"brut-body": ["Inter", "system-ui", "sans-serif"],
|
||||
"brut-mono": ["Space Mono", "ui-monospace", "monospace"],
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
@@ -90,6 +99,18 @@ module.exports = {
|
||||
950: "#3E3E41",
|
||||
},
|
||||
"base-fg": "rgb(255 255 255 / <alpha-value>)",
|
||||
// Landing4 (brutalist landing) palette
|
||||
ink: {
|
||||
DEFAULT: "#0A0A0B",
|
||||
800: "#141416",
|
||||
700: "#1C1C1F",
|
||||
},
|
||||
paper: {
|
||||
DEFAULT: "#F2F1EC",
|
||||
200: "#E8E6DE",
|
||||
},
|
||||
flare: "#00E5FF",
|
||||
ember: "#F0563A",
|
||||
ui: {
|
||||
// Aligned with artcraft app's theme-gray (frontend/apps/artcraft/app/src/styles/base.css)
|
||||
background: "#242424",
|
||||
|
||||
Reference in New Issue
Block a user