1# RECREATE THIS EXACT LOVABLE LANDING PAGE 1:1
2
3RECREATE THIS EXACT LOVABLE LANDING PAGE 1:1. THIS IS NOT AN INSPIRATION REQUEST. DO NOT REDESIGN, SIMPLIFY, APPROXIMATE, SUBSTITUTE, GENERATE NEW IMAGES, OR CREATE A SIMILAR LANDING PAGE. BUILD THE EXACT SAME DESIGN, WEBGL HERO, NAV INTERACTIONS, PROMPT INPUT ANIMATION, AND TRUSTED-BY LOGO CAROUSEL DESCRIBED BELOW.
4
5## CRITICAL REQUIREMENTS
6
7- THE FIRST SCREEN MUST BE A BLACK FULL-VIEWPORT WEBGL HERO WITH THE REAL ANIMATED SHADER BEHIND THE CONTENT.
8- THE WEBGL MUST USE THE EXACT CORERENDERER RUNTIME AND EXACT HERO PROJECT DATA LISTED IN THE ASSET MANIFEST. DO NOT REPLACE IT WITH CSS GRADIENTS, CANVAS NOISE, VIDEO, SVG, THREE.JS, OR A GENERATED IMAGE.
9- THE USER MUST NOT PROVIDE FILES MANUALLY. YOU MUST CREATE THE FILES IN THE PROJECT AND FETCH/COPY THE EXACT ASSETS FROM THE ASSET MANIFEST INTO `public/vendor/` USING YOUR INTERNAL FILE TOOLS. DO NOT PASTE A BASH SCRIPT INTO THE APP OR ASK THE USER TO RUN ONE.
10- USE TANSTACK START / REACT / TAILWIND V4 AS THE LOVABLE DEFAULT STACK.
11- ADD `framer-motion` AND `lucide-react` IF THE PROJECT DOES NOT ALREADY HAVE THEM.
12- REPLACE THE HOME ROUTE WITH THIS IMPLEMENTATION.
13- KEEP ALL MEASUREMENTS, COLORS, TIMINGS, EASING, HOVER BEHAVIOR, AND ASSET PATHS EXACTLY AS SPECIFIED.
14
15## ASSET MANIFEST — SAVE EACH ASSET UNDER `public/vendor/<filename>` WITH THE EXACT FILENAME
16
17- `public/vendor/core-renderer.js` FROM `https://qclay.design/lovable/webgl/core-renderer.js`
18- `public/vendor/hero-project.js` FROM `https://qclay.design/lovable/webgl/hero-project.js`
19- `public/vendor/back-gl-3.png` FROM `https://qclay.design/lovable/webgl/back-gl-3.png`
20- `public/vendor/back-gl.png` FROM `https://qclay.design/lovable/webgl/back-gl.png`
21- `public/vendor/back-gl-2.png` FROM `https://qclay.design/lovable/webgl/back-gl-2.png`
22- `public/vendor/logo.svg` FROM `https://qclay.design/lovable/webgl/logo.svg`
23- `public/vendor/arrow-right.svg` FROM `https://qclay.design/lovable/webgl/arrow-right.svg`
24- `public/vendor/arrow-up.svg` FROM `https://qclay.design/lovable/webgl/arrow-up.svg`
25- `public/vendor/ai-select.svg` FROM `https://qclay.design/lovable/webgl/ai-select.svg`
26- `public/vendor/dots.svg` FROM `https://qclay.design/lovable/webgl/dots.svg`
27- `public/vendor/announcement.jpg` FROM `https://qclay.design/lovable/webgl/announcement.jpg`
28- `public/vendor/logo-1.svg` FROM `https://qclay.design/lovable/webgl/logo-1.svg`
29- `public/vendor/logo-2.svg` FROM `https://qclay.design/lovable/webgl/logo-2.svg`
30- `public/vendor/logo-3.svg` FROM `https://qclay.design/lovable/webgl/logo-3.svg`
31- `public/vendor/logo-4.svg` FROM `https://qclay.design/lovable/webgl/logo-4.svg`
32- `public/vendor/logo-5.svg` FROM `https://qclay.design/lovable/webgl/logo-5.svg`
33- `public/vendor/logo-6.svg` FROM `https://qclay.design/lovable/webgl/logo-6.svg`
34
35## IMPORTANT ASSET RULES
36
37- `core-renderer.js`, `hero-project.js`, AND `back-gl-3.png` ARE REQUIRED FOR THE WEBGL HERO.
38- `hero-project.js` CONTAINS `/vendor/back-gl-3.png` REFERENCES, SO THE LOCAL PATH MUST EXIST EXACTLY.
39- `dots.svg`, `arrow-up.svg`, AND `ai-select.svg` ARE REQUIRED FOR THE PROMPT SEND BUTTON.
40- `logo.svg`, `arrow-right.svg`, AND `announcement.jpg` ARE REQUIRED FOR THE NAVBAR AND DROPDOWN.
41- `logo-1.svg` THROUGH `logo-6.svg` ARE REQUIRED FOR THE ROTATING TRUSTED-BY CAROUSEL.
42- DO NOT INLINE, REDRAW, COMPRESS, OR CONVERT THESE ASSETS.
43
44## CREATE/REPLACE THESE FILES EXACTLY
45
46FILE: `src/routes/index.tsx`
47
48```tsx
49import { createFileRoute } from "@tanstack/react-router";
50import { ArrowRight } from "lucide-react";
51import HeroShader from "@/components/HeroShader";
52import HeroNavbar from "@/components/HeroNavbar";
53import PromptInput from "@/components/PromptInput";
54import SectionTrustedBy from "@/components/SectionTrustedBy";
55
56export const Route = createFileRoute("/")({
57 component: Index,
58});
59
60function Index() {
61 return (
62 <div className="relative w-full bg-black text-white">
63 <HeroNavbar />
64 <div className="relative w-full overflow-hidden" style={{ height: "100vh" }}>
65 <HeroShader />
66 <div className="absolute inset-0 flex items-center justify-center px-4 pointer-events-none">
67 <div
68 className="pointer-events-auto w-full flex flex-col items-center"
69 style={{ fontFamily: '"Inter Tight", sans-serif' }}
70 >
71 <a
72 href="#"
73 style={{
74 display: "inline-flex",
75 alignItems: "center",
76 gap: 12,
77 padding: "6px 16px 6px 6px",
78 borderRadius: 999,
79 background: "rgba(15,15,15,0.6)",
80 border: "1px solid rgba(255,255,255,0.08)",
81 backdropFilter: "blur(18px)",
82 WebkitBackdropFilter: "blur(18px)",
83 color: "#fff",
84 fontSize: 14,
85 textDecoration: "none",
86 marginBottom: 28,
87 }}
88 >
89 <span
90 style={{
91 background: "#2F6BFF",
92 color: "#fff",
93 fontSize: 12,
94 fontWeight: 500,
95 padding: "4px 10px",
96 borderRadius: 999,
97 }}
98 >
99 New
100 </span>
101 <span style={{ color: "rgba(255,255,255,0.92)" }}>
102 Better SEO – Apps built to be found
103 </span>
104 <ArrowRight size={16} color="rgba(255,255,255,0.85)" />
105 </a>
106 <h1
107 style={{
108 fontSize: "clamp(2.5rem, 6vw, 4.5rem)",
109 fontWeight: 600,
110 letterSpacing: "-0.03em",
111 lineHeight: 1.15,
112 paddingBottom: "0.15em",
113 margin: 0,
114 textAlign: "center",
115 }}
116 className="gradient-text-animate"
117 >
118 Build something Lovable
119 </h1>
120 <p
121 style={{
122 fontSize: "clamp(1rem, 1.6vw, 1.35rem)",
123 color: "rgba(255,255,255,0.65)",
124 marginTop: 6,
125 marginBottom: 36,
126 textAlign: "center",
127 }}
128 >
129 Create apps and websites by chatting with AI
130 </p>
131 <PromptInput />
132 </div>
133 </div>
134 </div>
135 <SectionTrustedBy />
136 </div>
137 );
138}
139
140```
141
142FILE: `src/components/HeroShader.tsx`
143
144```tsx
145import { useEffect, useRef, useState } from "react";
146
147declare global {
148 interface Window {
149 _heroProjectData?: unknown;
150 CoreRenderer?: { init: () => Promise<void> };
151 }
152}
153
154async function loadScript(src: string) {
155 const cacheKey = `__loaded_${src}`;
156 if ((window as unknown as Record<string, boolean>)[cacheKey]) return;
157 const res = await fetch(src, { credentials: "include" });
158 if (!res.ok) throw new Error(`fetch ${src} -> ${res.status}`);
159 const text = await res.text();
160 // Execute in global scope so it can assign to window.* like a normal <script>.
161 // eslint-disable-next-line @typescript-eslint/no-implied-eval
162 new Function(text).call(window);
163 (window as unknown as Record<string, boolean>)[cacheKey] = true;
164}
165
166export default function HeroShader({ className = "" }: { className?: string }) {
167 const containerRef = useRef<HTMLDivElement | null>(null);
168 const [ready, setReady] = useState(false);
169
170 useEffect(() => {
171 let cancelled = false;
172 let blobUrl: string | null = null;
173
174 (async () => {
175 try {
176 await loadScript("/vendor/core-renderer.js");
177 await loadScript("/vendor/hero-project.js");
178 if (cancelled) return;
179
180 const projectData = window._heroProjectData;
181 const container = containerRef.current;
182 if (!projectData || !container || !window.CoreRenderer) return;
183
184 const blob = new Blob([JSON.stringify(projectData)], { type: "application/json" });
185 blobUrl = URL.createObjectURL(blob);
186 container.setAttribute("data-cr-project-src", blobUrl);
187
188 await window.CoreRenderer.init();
189 if (blobUrl) URL.revokeObjectURL(blobUrl);
190
191 if (!cancelled) {
192 // wait for renderer to paint a few frames before fading in
193 setTimeout(() => {
194 if (!cancelled) setReady(true);
195 }, 250);
196 }
197
198 // Nudge a mousemove so any mouse-tracking layers initialize.
199 window.dispatchEvent(
200 new MouseEvent("mousemove", {
201 clientX: window.innerWidth / 2,
202 clientY: window.innerHeight / 2,
203 bubbles: true,
204 }),
205 );
206 } catch (err) {
207 console.error("[HeroShader] init failed", err);
208 }
209 })();
210
211 return () => {
212 cancelled = true;
213 if (blobUrl) URL.revokeObjectURL(blobUrl);
214 };
215 }, []);
216
217 return (
218 <div
219 id="hero-canvas"
220 ref={containerRef}
221 className={className}
222 style={{
223 position: "absolute",
224 inset: 0,
225 width: "100vw",
226 height: "100vh",
227 zIndex: 0,
228 overflow: "hidden",
229 opacity: ready ? 1 : 0,
230 transform: ready ? "translateY(0)" : "translateY(40px)",
231 transition: "opacity 1.4s cubic-bezier(0.22, 1, 0.36, 1), transform 1.4s cubic-bezier(0.22, 1, 0.36, 1)",
232 willChange: "opacity, transform",
233 }}
234 />
235 );
236}
237```
238
239FILE: `src/components/HeroNavbar.tsx`
240
241```tsx
242import { ChevronDown } from "lucide-react";
243import { useState } from "react";
244
245type NavItem = { label: string; description?: string; external?: boolean };
246type NavColumn = { heading: string; items: NavItem[]; groups?: NavItem[][] };
247type Announcement = {
248 eyebrow: string;
249 image: string;
250 title: string;
251 ctaLabel: string;
252};
253type NavLink = {
254 label: string;
255 active?: boolean;
256 dropdown?: boolean;
257 columns?: NavColumn[];
258 extra?: NavColumn;
259 announcement?: Announcement;
260};
261
262const navLinks: NavLink[] = [
263 {
264 label: "Solution",
265 dropdown: true,
266 columns: [
267 {
268 heading: "Who is it for?",
269 items: [
270 { label: "Founders", description: "Ship before you pitch." },
271 { label: "Sales", description: "Build the demo live." },
272 { label: "Product managers", description: "Prototype, don't spec." },
273 { label: "Designers", description: "Your designs, built." },
274 { label: "Marketers", description: "Launch pages in minutes." },
275 { label: "Ops", description: "Tools that fit your flow." },
276 { label: "People", description: "HR tools your team loves." },
277 ],
278 },
279 ],
280 extra: {
281 heading: "Use cases",
282 items: [
283 { label: "Prototyping", description: "Proof of concept in hours." },
284 { label: "Internal tools", description: "Built for your team." },
285 ],
286 },
287 },
288 {
289 label: "Resources",
290 dropdown: true,
291 columns: [
292 {
293 heading: "Resources",
294 items: [],
295 groups: [
296 [
297 { label: "Blog", description: "Ideas, updates, stories." },
298 { label: "Partners", description: "Build more together." },
299 { label: "Templates", description: "Begin with a template." },
300 { label: "Guides", description: "Learn as you build." },
301 ],
302 [
303 { label: "Connectors", description: "Build from what you already use." },
304 { label: "Docs", description: "Everything under the hood." },
305 ],
306 ],
307 },
308 ],
309 announcement: {
310 eyebrow: "Announcement",
311 image:
312 "/vendor/announcement.jpg",
313 title: "Building is just the beginning: Introducing Discoverability",
314 ctaLabel: "Learn more",
315 },
316 },
317 { label: "Community" },
318 { label: "Enterprise" },
319 { label: "Pricing" },
320 { label: "Security" },
321];
322
323const ARROW_SRC = "/vendor/arrow-right.svg";
324
325const ArrowButton = ({ children }: { children: React.ReactNode }) => (
326 <button
327 className="group relative inline-flex items-center justify-center overflow-hidden"
328 style={{
329 height: 38,
330 padding: "12px 16px",
331 gap: 10,
332 borderRadius: 9,
333 border: "1px solid rgba(250,250,250,0.20)",
334 background: "#FFF",
335 color: "#111111",
336 fontFamily: '"Inter Tight", sans-serif',
337 fontSize: 14,
338 fontWeight: 500,
339 cursor: "pointer",
340 }}
341 >
342 <span>{children}</span>
343 <span style={{ position: "relative", width: 14, height: 14, overflow: "hidden", display: "inline-block" }}>
344 <img
345 src={ARROW_SRC}
346 width={14}
347 height={14}
348 alt=""
349 className="absolute inset-0 translate-x-0 group-hover:translate-x-[150%]"
350 style={{ transition: "transform 500ms cubic-bezier(0.65,0,0.35,1)" }}
351 />
352 <img
353 src={ARROW_SRC}
354 width={14}
355 height={14}
356 alt=""
357 className="absolute inset-0 -translate-x-[150%] group-hover:translate-x-0"
358 style={{ transition: "transform 500ms cubic-bezier(0.65,0,0.35,1)" }}
359 />
360 </span>
361 </button>
362);
363
364const HeroNavbar = () => {
365 const [aboutOpen, setAboutOpen] = useState(false);
366 const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
367 const currentDropdownLink = navLinks.find((l) => l.label === activeDropdown) as
368 | (NavLink & { columns: NavColumn[]; extra?: NavColumn; announcement?: Announcement })
369 | undefined;
370 const aboutLink = navLinks.find((l) => l.label === "Solution") as NavLink & {
371 columns: NavColumn[];
372 extra: NavColumn;
373 };
374 void aboutLink;
375
376 return (
377 <>
378 <div
379 aria-hidden
380 style={{
381 position: "fixed",
382 inset: 0,
383 zIndex: 40,
384 pointerEvents: "none",
385 backdropFilter: aboutOpen ? "blur(8px)" : "blur(0px)",
386 WebkitBackdropFilter: aboutOpen ? "blur(8px)" : "blur(0px)",
387 background: aboutOpen ? "rgba(0,0,0,0.25)" : "rgba(0,0,0,0)",
388 transition:
389 "backdrop-filter 280ms ease, -webkit-backdrop-filter 280ms ease, background 280ms ease",
390 }}
391 />
392
393 <nav
394 className="fixed top-0 left-0 right-0 z-50 grid items-center hero-nav"
395 style={{
396 gridTemplateColumns: "1fr auto 1fr",
397 background: "color-mix(in oklab, #1c1c1c 75%, transparent)",
398 backdropFilter: "blur(12px)",
399 WebkitBackdropFilter: "blur(12px)",
400 borderBottom: "1px solid rgba(255,255,255,0.06)",
401 padding: "0 clamp(1rem, 3vw, 2rem)",
402 height: 70,
403 fontFamily: '"Inter Tight", sans-serif',
404 }}
405 >
406 <style>{`
407 @media (max-width: 1024px) {
408 .hero-nav-links { display: none !important; }
409 .hero-nav { grid-template-columns: 1fr 1fr !important; }
410 }
411 @media (max-width: 600px) {
412 .hero-nav-contact { display: none !important; }
413 }
414 .about-panel { opacity: 0; pointer-events: none; transition: opacity 220ms ease; }
415 .about-panel[data-open="true"] { opacity: 1; pointer-events: auto; }
416 .about-panel-inner {
417 opacity: 0;
418 transform: translateY(-12px);
419 transition: opacity 320ms ease, transform 380ms cubic-bezier(0.22, 1, 0.36, 1);
420 }
421 .about-panel[data-open="true"] .about-panel-inner {
422 opacity: 1;
423 transform: translateY(0);
424 transition-delay: 80ms;
425 }
426 .about-item {
427 color: rgba(255,255,255,0.92);
428 font-family: "Inter Tight", sans-serif;
429 font-size: 15px;
430 font-weight: 600;
431 background: transparent;
432 border: none;
433 padding: 10px 0;
434 cursor: pointer;
435 text-align: left;
436 display: flex;
437 flex-direction: column;
438 align-items: flex-start;
439 gap: 4px;
440 transition: color 180ms ease;
441 break-inside: avoid;
442 }
443 .about-item:hover { color: #fff; }
444 .about-item-desc {
445 color: rgba(255,255,255,0.55);
446 font-size: 13px;
447 font-weight: 400;
448 }
449 .about-heading {
450 color: rgba(255,255,255,0.45);
451 font-family: "Inter Tight", sans-serif;
452 font-size: 12px;
453 font-weight: 500;
454 margin-bottom: 14px;
455 letter-spacing: 0.02em;
456 }
457 .logo-link {
458 position: relative;
459 display: inline-block;
460 line-height: 0;
461 cursor: pointer;
462 }
463 .logo-img {
464 height: 1.1rem;
465 width: auto;
466 display: block;
467 }
468 .logo-overlay {
469 position: absolute;
470 inset: 0;
471 pointer-events: none;
472 opacity: 0;
473 -webkit-mask: url(/vendor/logo.svg) no-repeat center / contain;
474 mask: url(/vendor/logo.svg) no-repeat center / contain;
475 background-image: linear-gradient(
476 90deg,
477 #ffffff 0,
478 #ffffff 33.33%,
479 #82BCFF 40%,
480 #2483FF 45%,
481 #FF66F4 50%,
482 #FF3029 55%,
483 #FE7B02 60%,
484 #ffffff 66.67%,
485 #ffffff
486 );
487 background-size: 300% 100%;
488 background-position: 100% 0;
489 }
490 .logo-link:hover .logo-overlay {
491 opacity: 1;
492 animation: gradient-sweep 1.2s cubic-bezier(0.455, 0.03, 0.515, 0.955) forwards;
493 }
494 `}</style>
495
496 <div className="flex items-center">
497 <a href="/" className="logo-link" aria-label="Lovable">
498 <img
499 src="/vendor/logo.svg"
500 alt="Lovable"
501 className="logo-img"
502 />
503 <span className="logo-overlay" aria-hidden="true" />
504 </a>
505 </div>
506
507 <div
508 className="flex items-center justify-center hero-nav-links"
509 style={{ gap: 4, position: "relative" }}
510 >
511 {navLinks.map((link) => {
512 const hasDropdown = !!link.dropdown;
513 const isOpen = hasDropdown && activeDropdown === link.label;
514 const highlighted = link.active || isOpen;
515 return (
516 <button
517 key={link.label}
518 className="flex items-center transition-colors duration-200"
519 style={{
520 gap: 4,
521 fontFamily: '"Inter Tight", sans-serif',
522 fontSize: "0.8rem",
523 fontWeight: 500,
524 padding: "6px 14px",
525 cursor: "pointer",
526 background: "transparent",
527 border: "none",
528 color: highlighted ? "rgba(255,255,255,0.95)" : "rgba(255,255,255,0.65)",
529 }}
530 onMouseEnter={(e) => {
531 if (hasDropdown) {
532 setActiveDropdown(link.label);
533 setAboutOpen(true);
534 } else {
535 setActiveDropdown(null);
536 setAboutOpen(false);
537 }
538 if (!link.active) e.currentTarget.style.color = "rgba(255,255,255,0.95)";
539 }}
540 onMouseLeave={(e) => {
541 if (!link.active && !isOpen)
542 e.currentTarget.style.color = "rgba(255,255,255,0.65)";
543 }}
544 >
545 {link.label}
546 {link.dropdown && (
547 <ChevronDown
548 size={11}
549 color="rgba(255,255,255,0.65)"
550 style={{
551 transition: "transform 220ms ease",
552 transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
553 }}
554 />
555 )}
556 </button>
557 );
558 })}
559 </div>
560
561 <div className="flex items-center justify-end" style={{ gap: "1rem" }}>
562 <ArrowButton>Open Lovable</ArrowButton>
563 </div>
564 </nav>
565
566 <div
567 className="about-panel"
568 data-open={aboutOpen}
569 onMouseEnter={() => setAboutOpen(true)}
570 onMouseLeave={() => {
571 setAboutOpen(false);
572 setActiveDropdown(null);
573 }}
574 style={{
575 position: "fixed",
576 top: 70,
577 left: 0,
578 right: 0,
579 background: "rgba(15,15,15,0.85)",
580 backdropFilter: "blur(18px)",
581 WebkitBackdropFilter: "blur(18px)",
582 padding: "32px 0 40px",
583 zIndex: 60,
584 }}
585 >
586 <div
587 className="about-panel-inner"
588 style={{ maxWidth: 1100, margin: "0 auto", padding: "0 32px" }}
589 >
590 {currentDropdownLink && (
591 <>
592 <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 64 }}>
593 {currentDropdownLink.columns.map((col) => (
594 <div key={col.heading} style={{ display: "flex", flexDirection: "column", borderRight: "1px solid rgba(255,255,255,0.08)", paddingRight: 32 }}>
595 <div className="about-heading">{col.heading}</div>
596 {col.groups ? (
597 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 48 }}>
598 {col.groups.map((group, gi) => (
599 <div key={gi} style={{ display: "flex", flexDirection: "column" }}>
600 {group.map((it) => (
601 <button key={it.label} className="about-item" style={{ width: "100%" }}>
602 <span>{it.label}</span>
603 {it.description && <span className="about-item-desc">{it.description}</span>}
604 </button>
605 ))}
606 </div>
607 ))}
608 </div>
609 ) : (
610 <div style={{ columnCount: 2, columnGap: 48 }}>
611 {col.items.map((it) => (
612 <button key={it.label} className="about-item" style={{ width: "100%" }}>
613 <span>{it.label}</span>
614 {it.description && <span className="about-item-desc">{it.description}</span>}
615 </button>
616 ))}
617 </div>
618 )}
619 </div>
620 ))}
621 {currentDropdownLink.announcement ? (
622 <div style={{ display: "flex", flexDirection: "column" }}>
623 <div className="about-heading">{currentDropdownLink.announcement.eyebrow}</div>
624 <button
625 className="about-item"
626 style={{ padding: 0, gap: 14, alignItems: "stretch" }}
627 >
628 <div
629 style={{
630 width: "100%",
631 aspectRatio: "16 / 9",
632 borderRadius: 10,
633 overflow: "hidden",
634 background:
635 "linear-gradient(135deg, #2a1530 0%, #1a1a1a 60%, #2a1a2a 100%)",
636 }}
637 >
638 <img
639 src={currentDropdownLink.announcement.image}
640 alt=""
641 onError={(e) => {
642 (e.currentTarget as HTMLImageElement).style.display = "none";
643 }}
644 style={{
645 width: "100%",
646 height: "100%",
647 objectFit: "cover",
648 display: "block",
649 }}
650 />
651 </div>
652 <span style={{ fontSize: 15, fontWeight: 600, lineHeight: 1.35 }}>
653 {currentDropdownLink.announcement.title}
654 </span>
655 <span
656 style={{
657 display: "inline-flex",
658 alignItems: "center",
659 gap: 6,
660 color: "rgba(255,255,255,0.85)",
661 fontSize: 13,
662 fontWeight: 500,
663 }}
664 >
665 {currentDropdownLink.announcement.ctaLabel}
666 <span style={{ transform: "translateY(-1px)" }}>›</span>
667 </span>
668 </button>
669 </div>
670 ) : currentDropdownLink.extra ? (
671 <div style={{ display: "flex", flexDirection: "column" }}>
672 <div className="about-heading">{currentDropdownLink.extra.heading}</div>
673 {currentDropdownLink.extra.items.map((it) => (
674 <button key={it.label} className="about-item">
675 <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
676 {it.label}
677 {it.external && <span style={{ transform: "translateY(-1px)" }}>↗</span>}
678 </span>
679 {it.description && <span className="about-item-desc">{it.description}</span>}
680 </button>
681 ))}
682 </div>
683 ) : null}
684 </div>
685 </>
686 )}
687 </div>
688 </div>
689 </>
690 );
691};
692
693export default HeroNavbar;
694```
695
696FILE: `src/components/PromptInput.tsx`
697
698```tsx
699import { useEffect, useRef, useState } from "react";
700import { motion } from "framer-motion";
701import { ChevronDown } from "lucide-react";
702
703const A = "/vendor";
704
705const PHRASES = [
706 "Create a finance dashboard design",
707 "Branding with M letter",
708 "Liquid glass effect",
709 "Loader animation",
710 "SaaS landing page",
711];
712
713type Mode = "typing" | "pausing" | "deleting";
714
715const TypewriterPrompt = () => {
716 const [text, setText] = useState("");
717 const [phraseIdx, setPhraseIdx] = useState(0);
718 const [mode, setMode] = useState<Mode>("typing");
719
720 useEffect(() => {
721 const phrase = PHRASES[phraseIdx];
722 let t: ReturnType<typeof setTimeout>;
723 if (mode === "typing") {
724 if (text.length < phrase.length) {
725 t = setTimeout(
726 () => setText(phrase.slice(0, text.length + 1)),
727 22 + Math.random() * 25,
728 );
729 } else {
730 t = setTimeout(() => setMode("pausing"), 30);
731 }
732 } else if (mode === "pausing") {
733 t = setTimeout(() => setMode("deleting"), 1400);
734 } else {
735 if (text.length > 0) {
736 t = setTimeout(() => setText(text.slice(0, -1)), 14);
737 } else {
738 setPhraseIdx((i) => (i + 1) % PHRASES.length);
739 setMode("typing");
740 return;
741 }
742 }
743 return () => clearTimeout(t);
744 }, [text, mode, phraseIdx]);
745
746 return (
747 <div
748 style={{
749 width: "100%",
750 fontFamily: '"Inter Tight"',
751 fontSize: 15,
752 fontWeight: 400,
753 color: "rgba(255,255,255,0.60)",
754 padding: "0 0 10px 0",
755 display: "flex",
756 alignItems: "center",
757 height: 32,
758 boxSizing: "border-box",
759 lineHeight: "22px",
760 flexShrink: 0,
761 transform: "translate(1%, 18%)",
762 }}
763 >
764 <style>{`@keyframes promptCaretBlink { 0%,49% { opacity:1 } 50%,100% { opacity:0 } }`}</style>
765 <span style={{ whiteSpace: "pre" }}>{text}</span>
766 <span
767 style={{
768 display: "inline-block",
769 width: 2,
770 height: 18,
771 marginLeft: 2,
772 background: "rgba(255,255,255,0.85)",
773 animation: "promptCaretBlink 1s steps(1) infinite",
774 flexShrink: 0,
775 }}
776 />
777 </div>
778 );
779};
780
781const SendButton = () => {
782 const [hovered, setHovered] = useState(false);
783 const [arrowToggle, setArrowToggle] = useState(0);
784 const ringRef = useRef<HTMLDivElement | null>(null);
785 const stateRef = useRef({ angle: 0, speed: 0, last: 0, raf: 0, hovered: false });
786
787 useEffect(() => {
788 stateRef.current.hovered = hovered;
789 const tick = (now: number) => {
790 const s = stateRef.current;
791 const dt = s.last ? now - s.last : 16;
792 s.last = now;
793 const target = s.hovered ? 360 / 1500 : 0;
794 const tau = s.hovered ? 250 : 700;
795 const k = 1 - Math.exp(-dt / tau);
796 s.speed += (target - s.speed) * k;
797 s.angle = (s.angle + s.speed * dt) % 360;
798 if (ringRef.current) {
799 ringRef.current.style.transform = `rotate(${s.angle}deg)`;
800 }
801 if (!s.hovered && Math.abs(s.speed) < 0.0005) {
802 s.raf = 0;
803 s.last = 0;
804 return;
805 }
806 s.raf = requestAnimationFrame(tick);
807 };
808 if (!stateRef.current.raf) {
809 stateRef.current.last = 0;
810 stateRef.current.raf = requestAnimationFrame(tick);
811 }
812 return () => {
813 if (stateRef.current.raf) {
814 cancelAnimationFrame(stateRef.current.raf);
815 stateRef.current.raf = 0;
816 }
817 };
818 }, [hovered]);
819
820 return (
821 <motion.div
822 onHoverStart={() => {
823 setArrowToggle((v) => v + 1);
824 setHovered(true);
825 }}
826 onHoverEnd={() => setHovered(false)}
827 animate={{ scale: hovered ? 1.05 : 1 }}
828 transition={{ duration: 0.2 }}
829 style={{
830 position: "relative",
831 width: 44,
832 height: 44,
833 display: "flex",
834 alignItems: "center",
835 justifyContent: "center",
836 flexShrink: 0,
837 cursor: "pointer",
838 }}
839 >
840 <div
841 style={{
842 position: "absolute",
843 inset: 0,
844 borderRadius: 15,
845 background: "rgb(95 126 167 / 15%)",
846 width: 44,
847 height: 44,
848 zIndex: 1,
849 }}
850 />
851 <div
852 style={{
853 position: "absolute",
854 width: 36,
855 height: 36,
856 borderRadius: 12,
857 background:
858 "linear-gradient(var(--send-btn-angle, 180deg), #ff660e 0%, #646aed 100%)",
859 animation: "send-btn-bg-rotate 4s linear infinite",
860 boxShadow:
861 "inset 0 1px 18px 2px rgba(173,208,255,0.20), inset 0 1px 4px 2px rgba(222,236,255,0.80), 0 42px 107px 0 rgba(61,130,222,0.34), 0 10px 10px 0 rgba(61,130,222,0.20), 0 3.714px 4.846px 0 rgba(61,130,222,0.15)",
862 display: "flex",
863 alignItems: "center",
864 justifyContent: "center",
865 overflow: "hidden",
866 padding: 8,
867 zIndex: 2,
868 }}
869 >
870 <div
871 style={{
872 position: "absolute",
873 inset: -1,
874 borderRadius: 13,
875 padding: 1,
876 zIndex: 3,
877 pointerEvents: "none",
878 background:
879 "conic-gradient(from 0deg, rgba(255,255,255,0) 0deg, #FFFFFF 60deg, #9EC7FF 120deg, rgba(255,255,255,0) 200deg, rgba(255,255,255,0) 360deg)",
880 WebkitMask:
881 "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
882 WebkitMaskComposite: "xor",
883 mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
884 maskComposite: "exclude",
885 }}
886 >
887 <div
888 ref={ringRef}
889 style={{ width: "100%", height: "100%", borderRadius: 13 }}
890 />
891 </div>
892 <div
893 style={{
894 position: "absolute",
895 inset: 0,
896 borderRadius: 12,
897 border: "1px solid #9EC7FF",
898 pointerEvents: "none",
899 zIndex: 4,
900 }}
901 />
902 <div
903 style={{
904 position: "absolute",
905 inset: 0,
906 zIndex: 2,
907 pointerEvents: "none",
908 }}
909 >
910 <img
911 src={`${A}/dots.svg`}
912 style={{ width: "100%", height: "100%", objectFit: "cover", opacity: 0.7 }}
913 alt=""
914 />
915 </div>
916 {arrowToggle > 0 && (
917 <motion.div
918 key={`blink-${arrowToggle}`}
919 initial={{ x: "-120%" }}
920 animate={{ x: "120%" }}
921 transition={{ duration: 0.7, ease: [0.4, 0, 0.2, 1] }}
922 style={{
923 position: "absolute",
924 inset: 0,
925 zIndex: 4,
926 pointerEvents: "none",
927 background:
928 "linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.55) 50%, transparent 70%)",
929 mixBlendMode: "screen",
930 }}
931 />
932 )}
933 <div
934 style={{
935 position: "relative",
936 width: 16,
937 height: 16,
938 zIndex: 5,
939 overflow: "hidden",
940 }}
941 >
942 <motion.img
943 key={`out-${arrowToggle}`}
944 src={`${A}/arrow-up.svg`}
945 initial={{ y: 0, opacity: 1 }}
946 animate={{ y: -16, opacity: 0 }}
947 transition={{ duration: 0.32, ease: [0.65, 0, 0.35, 1] }}
948 style={{
949 position: "absolute",
950 inset: 0,
951 width: 16,
952 height: 16,
953 objectFit: "contain",
954 }}
955 alt=""
956 />
957 <motion.img
958 key={`in-${arrowToggle}`}
959 src={`${A}/arrow-up.svg`}
960 initial={{ y: 16, opacity: 0 }}
961 animate={{ y: 0, opacity: 1 }}
962 transition={{ duration: 0.32, ease: [0.65, 0, 0.35, 1] }}
963 style={{
964 position: "absolute",
965 inset: 0,
966 width: 16,
967 height: 16,
968 objectFit: "contain",
969 }}
970 alt=""
971 />
972 </div>
973 </div>
974 </motion.div>
975 );
976};
977
978const PromptInput = () => {
979 return (
980 <motion.div
981 initial={{ opacity: 0, y: 16, filter: "blur(4px)" }}
982 animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
983 transition={{ duration: 0.6, delay: 0.55, ease: "easeOut" }}
984 style={{
985 width: 702,
986 maxWidth: "100%",
987 margin: "0 auto",
988 padding: 4,
989 borderRadius: 34,
990 opacity: 1,
991 }}
992 >
993 <div
994 style={{
995 width: "100%",
996 height: 160,
997 background:
998 "linear-gradient(180.9deg, rgb(118 100 50 / 23%) -0.58%, rgba(53, 53, 56, 0.7) 66.34%, rgba(38, 38, 39, 0.7) 101.25%), rgb(43 38 38 / 67%)",
999 boxShadow:
1000 "rgba(0, 0, 0, 0.5) 0px 118px 112px, rgba(0, 0, 0, 0.36) 0px 69.4784px 58.4192px, rgba(0, 0, 0, 0.282) 0px 35.6832px 27.4176px, rgba(255, 255, 255, 0.32) 0.5px 0.5px 0.5px inset, rgba(255, 255, 255, 0.05) 0.5px -0.5px 0.5px inset",
1001 borderRadius: 30,
1002 padding: "14px 14px 12px 16px",
1003 position: "relative",
1004 display: "flex",
1005 flexDirection: "column",
1006 }}
1007 >
1008 <TypewriterPrompt />
1009 <div
1010 style={{
1011 display: "flex",
1012 alignItems: "center",
1013 justifyContent: "space-between",
1014 gap: 8,
1015 marginTop: "auto",
1016 transform: "translateY(0%)",
1017 }}
1018 >
1019 <div
1020 style={{
1021 display: "flex",
1022 alignItems: "center",
1023 gap: 6,
1024 transform: "translateX(1%)",
1025 }}
1026 >
1027 <button
1028 style={{
1029 width: 28,
1030 height: 28,
1031 borderRadius: 6,
1032 border: "1px solid rgba(255,255,255,0.10)",
1033 background: "transparent",
1034 display: "inline-flex",
1035 alignItems: "center",
1036 justifyContent: "center",
1037 cursor: "pointer",
1038 color: "rgba(255,255,255,0.50)",
1039 padding: 0,
1040 }}
1041 aria-label="Add"
1042 >
1043 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
1044 <path d="M6 1V11M1 6H11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
1045 </svg>
1046 </button>
1047 <div
1048 style={{
1049 width: 1,
1050 height: 18,
1051 background: "rgba(255,255,255,0.12)",
1052 margin: "0 2px",
1053 }}
1054 />
1055 <div
1056 style={{
1057 display: "inline-flex",
1058 alignItems: "center",
1059 gap: 6,
1060 width: 83,
1061 height: 30,
1062 background: "hsl(0deg 0% 43.53% / 14.9%)",
1063 borderRadius: 8,
1064 padding: "0 8px",
1065 cursor: "pointer",
1066 flexShrink: 0,
1067 }}
1068 >
1069 <div
1070 style={{
1071 width: 14,
1072 height: 14,
1073 borderRadius: 4,
1074 background:
1075 "linear-gradient(166deg, #A0E4FF 9.8%, #9CA4FB 184.41%)",
1076 display: "flex",
1077 alignItems: "center",
1078 justifyContent: "center",
1079 }}
1080 >
1081 <img src={`${A}/ai-select.svg`} width={8} height={8} alt="" />
1082 </div>
1083 <span
1084 style={{
1085 fontFamily: '"Inter Tight"',
1086 fontSize: 12,
1087 fontWeight: 400,
1088 lineHeight: "16px",
1089 color: "#fff",
1090 textAlign: "center",
1091 flex: 1,
1092 whiteSpace: "nowrap",
1093 }}
1094 >
1095 Build
1096 </span>
1097 <ChevronDown size={12} stroke="#fff" style={{ flexShrink: 0 }} />
1098 </div>
1099 </div>
1100 <div style={{ display: "flex", alignItems: "center", gap: 8, transform: "translateX(-1%)" }}>
1101 <button
1102 aria-label="Voice input"
1103 style={{
1104 width: 36,
1105 height: 36,
1106 borderRadius: 10,
1107 border: "none",
1108 background: "transparent",
1109 display: "flex",
1110 alignItems: "center",
1111 justifyContent: "center",
1112 cursor: "pointer",
1113 color: "rgba(255,255,255,0.70)",
1114 padding: 0,
1115 }}
1116 >
1117 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" width={20} height={20} aria-hidden="true">
1118 <path fill="currentColor" d="M11.25 21v-2.286a8.25 8.25 0 0 1-7.5-8.214.75.75 0 0 1 1.5 0 6.75 6.75 0 0 0 13.5 0 .75.75 0 0 1 1.5 0 8.25 8.25 0 0 1-7.5 8.214V21a.75.75 0 0 1-1.5 0m4-14a3.25 3.25 0 0 0-6.5 0v3a3.25 3.25 0 0 0 6.5 0zm1.5 3a4.75 4.75 0 1 1-9.5 0V7a4.75 4.75 0 0 1 9.5 0z"></path>
1119 </svg>
1120 </button>
1121 <SendButton />
1122 </div>
1123 </div>
1124 </div>
1125 </motion.div>
1126 );
1127};
1128
1129export default PromptInput;
1130```
1131
1132FILE: `src/components/SectionTrustedBy.tsx`
1133
1134```tsx
1135import { motion } from "framer-motion";
1136import { useState, useEffect, useRef } from "react";
1137
1138const LOGO_BASE = "/vendor/";
1139const LOGOS = [
1140 "logo-1.svg",
1141 "logo-2.svg",
1142 "logo-3.svg",
1143 "logo-4.svg",
1144 "logo-5.svg",
1145 "logo-6.svg",
1146];
1147
1148const useIsMobile = () => {
1149 const [m, setM] = useState(false);
1150 useEffect(() => {
1151 const mq = window.matchMedia("(max-width: 640px)");
1152 const on = () => setM(mq.matches);
1153 on();
1154 mq.addEventListener("change", on);
1155 return () => mq.removeEventListener("change", on);
1156 }, []);
1157 return m;
1158};
1159
1160const SectionTrustedBy = () => {
1161 const isMobile = useIsMobile();
1162 const containerRef = useRef<HTMLDivElement>(null);
1163 const trackRef = useRef<HTMLDivElement>(null);
1164 const innerRefs = useRef<HTMLDivElement[]>([]);
1165 const slotRefs = useRef<HTMLDivElement[]>([]);
1166 const offsetRef = useRef(0);
1167 const pausedRef = useRef(false);
1168 const setWidthRef = useRef(0);
1169
1170 // Repeat the logos to ensure a continuous strip wider than the viewport
1171 const REPEATS = 6;
1172 const strip = Array.from({ length: REPEATS }).flatMap((_, r) =>
1173 LOGOS.map((file, i) => ({ file, key: `${r}-${i}-${file}`, idx: r * LOGOS.length + i })),
1174 );
1175
1176 // Measure one logical "set" width from the unscaled slot widths so the loop
1177 // distance stays constant regardless of per-item transforms.
1178 const measure = () => {
1179 const slots = slotRefs.current;
1180 if (!slots.length) return;
1181 const perSet = LOGOS.length;
1182 let w = 0;
1183 for (let i = 0; i < perSet; i++) {
1184 const s = slots[i];
1185 if (!s) return;
1186 const r = s.getBoundingClientRect();
1187 w += r.width;
1188 }
1189 // include one gap per item (flex gap is between items)
1190 const gap = isMobile ? 56 : 88;
1191 w += gap * perSet;
1192 setWidthRef.current = w;
1193 };
1194
1195 useEffect(() => {
1196 measure();
1197 const onResize = () => measure();
1198 window.addEventListener("resize", onResize);
1199 return () => window.removeEventListener("resize", onResize);
1200 // eslint-disable-next-line react-hooks/exhaustive-deps
1201 }, [isMobile]);
1202
1203 useEffect(() => {
1204 let raf = 0;
1205 let last = performance.now();
1206 const speed = isMobile ? 55 : 80; // px/s
1207
1208 const tick = (now: number) => {
1209 const dt = Math.min(0.05, (now - last) / 1000);
1210 last = now;
1211 if (!pausedRef.current) offsetRef.current += speed * dt;
1212
1213 const container = containerRef.current;
1214 const track = trackRef.current;
1215 const setWidth = setWidthRef.current;
1216 if (!container || !track) {
1217 raf = requestAnimationFrame(tick);
1218 return;
1219 }
1220 // seamless wrap using measured set width (stable, never glitches)
1221 if (setWidth > 0) {
1222 offsetRef.current = ((offsetRef.current % setWidth) + setWidth) % setWidth;
1223 }
1224 track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
1225
1226 const cw = container.clientWidth;
1227 const cx = cw / 2;
1228 const cLeft = container.getBoundingClientRect().left;
1229 const slots = slotRefs.current;
1230 const inners = innerRefs.current;
1231
1232 for (let i = 0; i < slots.length; i++) {
1233 const slot = slots[i];
1234 const inner = inners[i];
1235 if (!slot || !inner) continue;
1236 const r = slot.getBoundingClientRect();
1237 const elCenter = r.left + r.width / 2 - cLeft;
1238 // normalized position across viewport: -1 (left) .. 1 (right)
1239 const t = (elCenter - cx) / (cx || 1);
1240 const ct = Math.max(-1.3, Math.min(1.3, t));
1241 // smooth bell — modern orbit-like depth curve
1242 const bulge = Math.cos(Math.max(-1, Math.min(1, ct)) * (Math.PI / 2));
1243 // subtle orbit: gentle rotateY + tiny y arc
1244 const rotY = -ct * 26; // deg
1245 const yArc = (1 - bulge) * -8; // lift edges slightly
1246 const tz = bulge * 140;
1247 const scale = 0.72 + bulge * 0.5; // 0.72 .. 1.22
1248 const edge = Math.max(0, 1 - Math.pow(Math.abs(ct), 1.5));
1249 const opacity = edge * (0.5 + bulge * 0.5);
1250 const blur = (1 - bulge) * 1.4;
1251
1252 inner.style.transform =
1253 `translateY(${yArc}px) translateZ(${tz}px) rotateY(${rotY}deg) scale(${scale})`;
1254 inner.style.opacity = String(opacity);
1255 inner.style.filter = `brightness(0) invert(1) blur(${blur}px)`;
1256 }
1257 raf = requestAnimationFrame(tick);
1258 };
1259 raf = requestAnimationFrame(tick);
1260 return () => cancelAnimationFrame(raf);
1261 }, [isMobile]);
1262
1263 return (
1264 <section
1265 style={{
1266 background: "#1c1c1c",
1267 color: "#fff",
1268 padding: "70px 24px",
1269 width: "100%",
1270 }}
1271 >
1272 <div
1273 style={{
1274 maxWidth: 1280,
1275 margin: "0 auto",
1276 display: "flex",
1277 flexDirection: "column",
1278 alignItems: "center",
1279 gap: 56,
1280 }}
1281 >
1282 <motion.div
1283 initial={{ opacity: 0, y: 24, filter: "blur(8px)" }}
1284 whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
1285 viewport={{ once: true, amount: 0.4 }}
1286 transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
1287 style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 28 }}
1288 >
1289 <h2
1290 style={{
1291 fontFamily: '"Inter Tight", sans-serif',
1292 fontWeight: 300,
1293 fontSize: "1rem",
1294 lineHeight: 1.4,
1295 color: "#fff",
1296 opacity: 0.6,
1297 textAlign: "center",
1298 maxWidth: 720,
1299 margin: 0,
1300 }}
1301 >
1302 Teams from top companies build with Lovable
1303 </h2>
1304 </motion.div>
1305
1306 <motion.div
1307 ref={containerRef}
1308 initial={{ opacity: 0, filter: "blur(8px)" }}
1309 whileInView={{ opacity: 1, filter: "blur(0px)" }}
1310 viewport={{ once: true, amount: 0.4 }}
1311 transition={{ duration: 0.8, delay: 0.2, ease: [0.22, 1, 0.36, 1] }}
1312 style={{
1313 width: "100%",
1314 height: isMobile ? 110 : 140,
1315 position: "relative",
1316 overflow: "hidden",
1317 perspective: "1200px",
1318 perspectiveOrigin: "50% 50%",
1319 maskImage:
1320 "linear-gradient(90deg, transparent 0%, #000 20%, #000 80%, transparent 100%)",
1321 WebkitMaskImage:
1322 "linear-gradient(90deg, transparent 0%, #000 20%, #000 80%, transparent 100%)",
1323 }}
1324 onMouseEnter={() => { pausedRef.current = true; }}
1325 onMouseLeave={() => { pausedRef.current = false; }}
1326 >
1327 <div
1328 ref={trackRef}
1329 style={{
1330 display: "flex",
1331 alignItems: "center",
1332 gap: isMobile ? 56 : 88,
1333 width: "max-content",
1334 height: "100%",
1335 transformStyle: "preserve-3d",
1336 willChange: "transform",
1337 }}
1338 >
1339 {strip.map(({ file, key, idx }) => (
1340 <div
1341 key={key}
1342 ref={(el) => { if (el) slotRefs.current[idx] = el; }}
1343 style={{
1344 flex: "0 0 auto",
1345 height: isMobile ? 28 : 38,
1346 display: "flex",
1347 alignItems: "center",
1348 justifyContent: "center",
1349 transformStyle: "preserve-3d",
1350 pointerEvents: "none",
1351 }}
1352 >
1353 <div
1354 ref={(el) => { if (el) innerRefs.current[idx] = el; }}
1355 style={{
1356 transformStyle: "preserve-3d",
1357 willChange: "transform, opacity, filter",
1358 transformOrigin: "center center",
1359 }}
1360 >
1361 <img
1362 src={LOGO_BASE + file}
1363 alt=""
1364 draggable={false}
1365 style={{
1366 height: isMobile ? 28 : 38,
1367 width: "auto",
1368 display: "block",
1369 userSelect: "none",
1370 }}
1371 onLoad={measure}
1372 />
1373 </div>
1374 </div>
1375 ))}
1376 </div>
1377 </motion.div>
1378 </div>
1379 </section>
1380 );
1381};
1382
1383export default SectionTrustedBy;
1384
1385```
1386
1387FILE: `src/styles.css`
1388
1389```css
1390@import "tailwindcss" source(none);
1391@source "../src";
1392@import "tw-animate-css";
1393
1394@custom-variant dark (&:is(.dark *));
1395
1396/*
1397 * Design system definition.
1398 *
1399 * The @theme inline block maps CSS custom properties to Tailwind utility
1400 * classes (e.g. --color-primary -> bg-primary, text-primary).
1401 *
1402 * The :root and .dark blocks define the actual color values using oklch.
1403 * All colors MUST use oklch format.
1404 *
1405 * To add a new semantic color:
1406 * 1. Add the variable to :root (light value) and .dark (dark value)
1407 * 2. Register it in @theme inline as --color-<name>: var(--<name>)
1408 */
1409
1410@theme inline {
1411 --radius-sm: calc(var(--radius) - 4px);
1412 --radius-md: calc(var(--radius) - 2px);
1413 --radius-lg: var(--radius);
1414 --radius-xl: calc(var(--radius) + 4px);
1415 --radius-2xl: calc(var(--radius) + 8px);
1416 --radius-3xl: calc(var(--radius) + 12px);
1417 --radius-4xl: calc(var(--radius) + 16px);
1418 --color-background: var(--background);
1419 --color-foreground: var(--foreground);
1420 --color-card: var(--card);
1421 --color-card-foreground: var(--card-foreground);
1422 --color-popover: var(--popover);
1423 --color-popover-foreground: var(--popover-foreground);
1424 --color-primary: var(--primary);
1425 --color-primary-foreground: var(--primary-foreground);
1426 --color-secondary: var(--secondary);
1427 --color-secondary-foreground: var(--secondary-foreground);
1428 --color-muted: var(--muted);
1429 --color-muted-foreground: var(--muted-foreground);
1430 --color-accent: var(--accent);
1431 --color-accent-foreground: var(--accent-foreground);
1432 --color-destructive: var(--destructive);
1433 --color-destructive-foreground: var(--destructive-foreground);
1434 --color-border: var(--border);
1435 --color-input: var(--input);
1436 --color-ring: var(--ring);
1437 --color-ring-offset-background: var(--background);
1438 --color-chart-1: var(--chart-1);
1439 --color-chart-2: var(--chart-2);
1440 --color-chart-3: var(--chart-3);
1441 --color-chart-4: var(--chart-4);
1442 --color-chart-5: var(--chart-5);
1443 --color-sidebar: var(--sidebar);
1444 --color-sidebar-foreground: var(--sidebar-foreground);
1445 --color-sidebar-primary: var(--sidebar-primary);
1446 --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
1447 --color-sidebar-accent: var(--sidebar-accent);
1448 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
1449 --color-sidebar-border: var(--sidebar-border);
1450 --color-sidebar-ring: var(--sidebar-ring);
1451}
1452
1453:root {
1454 --radius: 0.625rem;
1455 --background: oklch(1 0 0);
1456 --foreground: oklch(0.129 0.042 264.695);
1457 --card: oklch(1 0 0);
1458 --card-foreground: oklch(0.129 0.042 264.695);
1459 --popover: oklch(1 0 0);
1460 --popover-foreground: oklch(0.129 0.042 264.695);
1461 --primary: oklch(0.208 0.042 265.755);
1462 --primary-foreground: oklch(0.984 0.003 247.858);
1463 --secondary: oklch(0.968 0.007 247.896);
1464 --secondary-foreground: oklch(0.208 0.042 265.755);
1465 --muted: oklch(0.968 0.007 247.896);
1466 --muted-foreground: oklch(0.554 0.046 257.417);
1467 --accent: oklch(0.968 0.007 247.896);
1468 --accent-foreground: oklch(0.208 0.042 265.755);
1469 --destructive: oklch(0.577 0.245 27.325);
1470 --destructive-foreground: oklch(0.984 0.003 247.858);
1471 --border: oklch(0.929 0.013 255.508);
1472 --input: oklch(0.929 0.013 255.508);
1473 --ring: oklch(0.704 0.04 256.788);
1474 --chart-1: oklch(0.646 0.222 41.116);
1475 --chart-2: oklch(0.6 0.118 184.704);
1476 --chart-3: oklch(0.398 0.07 227.392);
1477 --chart-4: oklch(0.828 0.189 84.429);
1478 --chart-5: oklch(0.769 0.188 70.08);
1479 --sidebar: oklch(0.984 0.003 247.858);
1480 --sidebar-foreground: oklch(0.129 0.042 264.695);
1481 --sidebar-primary: oklch(0.208 0.042 265.755);
1482 --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
1483 --sidebar-accent: oklch(0.968 0.007 247.896);
1484 --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
1485 --sidebar-border: oklch(0.929 0.013 255.508);
1486 --sidebar-ring: oklch(0.704 0.04 256.788);
1487}
1488
1489.dark {
1490 --background: oklch(0.129 0.042 264.695);
1491 --foreground: oklch(0.984 0.003 247.858);
1492 --card: oklch(0.208 0.042 265.755);
1493 --card-foreground: oklch(0.984 0.003 247.858);
1494 --popover: oklch(0.208 0.042 265.755);
1495 --popover-foreground: oklch(0.984 0.003 247.858);
1496 --primary: oklch(0.929 0.013 255.508);
1497 --primary-foreground: oklch(0.208 0.042 265.755);
1498 --secondary: oklch(0.279 0.041 260.031);
1499 --secondary-foreground: oklch(0.984 0.003 247.858);
1500 --muted: oklch(0.279 0.041 260.031);
1501 --muted-foreground: oklch(0.704 0.04 256.788);
1502 --accent: oklch(0.279 0.041 260.031);
1503 --accent-foreground: oklch(0.984 0.003 247.858);
1504 --destructive: oklch(0.704 0.191 22.216);
1505 --destructive-foreground: oklch(0.984 0.003 247.858);
1506 --border: oklch(1 0 0 / 10%);
1507 --input: oklch(1 0 0 / 15%);
1508 --ring: oklch(0.551 0.027 264.364);
1509 --chart-1: oklch(0.488 0.243 264.376);
1510 --chart-2: oklch(0.696 0.17 162.48);
1511 --chart-3: oklch(0.769 0.188 70.08);
1512 --chart-4: oklch(0.627 0.265 303.9);
1513 --chart-5: oklch(0.645 0.246 16.439);
1514 --sidebar: oklch(0.208 0.042 265.755);
1515 --sidebar-foreground: oklch(0.984 0.003 247.858);
1516 --sidebar-primary: oklch(0.488 0.243 264.376);
1517 --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
1518 --sidebar-accent: oklch(0.279 0.041 260.031);
1519 --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
1520 --sidebar-border: oklch(1 0 0 / 10%);
1521 --sidebar-ring: oklch(0.551 0.027 264.364);
1522}
1523
1524@layer base {
1525 * {
1526 border-color: var(--color-border);
1527 }
1528
1529 body {
1530 background-color: var(--color-background);
1531 color: var(--color-foreground);
1532 }
1533}
1534
1535@keyframes gradient-sweep {
1536 from { background-position: 100% 0; }
1537 to { background-position: 0 0; }
1538}
1539
1540.gradient-text-animate {
1541 background-image: linear-gradient(
1542 90deg,
1543 #ffffff 0,
1544 #ffffff 33.33%,
1545 #82BCFF 40%,
1546 #2483FF 45%,
1547 #FF66F4 50%,
1548 #FF3029 55%,
1549 #FE7B02 60%,
1550 transparent 66.67%,
1551 transparent
1552 );
1553 background-size: 300% 100%;
1554 background-position: 100% 0;
1555 -webkit-background-clip: text;
1556 background-clip: text;
1557 -webkit-text-fill-color: transparent;
1558 color: transparent;
1559 animation-name: gradient-sweep;
1560 animation-duration: 1.2s;
1561 animation-delay: 0.1s;
1562 animation-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
1563 animation-fill-mode: forwards;
1564}
1565
1566@property --send-btn-angle {
1567 syntax: "<angle>";
1568 initial-value: 180deg;
1569 inherits: false;
1570}
1571
1572@keyframes send-btn-bg-rotate {
1573 from { --send-btn-angle: 0deg; }
1574 to { --send-btn-angle: 360deg; }
1575}
1576
1577@keyframes marquee-x {
1578 0% { transform: translateX(0); }
1579 100% { transform: translateX(-50%); }
1580}
1581
1582@keyframes rec-blink {
1583 0%, 100% { opacity: 1; }
1584 50% { opacity: 0.3; }
1585}
1586
1587```
1588
1589## PIXEL/FUNCTIONALITY VERIFICATION BEFORE FINISHING
1590
1591- AT `/`, THE TOP NAV IS FIXED, 70PX TALL, DARK GLASS, WITH LOVABLE LOGO ON THE LEFT, CENTERED NAV ITEMS, AND THE OPEN LOVABLE BUTTON ON THE RIGHT.
1592- HOVERING "SOLUTION" AND "RESOURCES" OPENS THE FULL-WIDTH BLURRED DROPDOWN BELOW THE NAVBAR; CHEVRONS ROTATE.
1593- THE HERO WEBGL FADES IN AFTER INITIALIZATION AND FILLS THE VIEWPORT BEHIND THE CONTENT.
1594- THE ANNOUNCEMENT PILL SAYS "NEW" AND "BETTER SEO – APPS BUILT TO BE FOUND".
1595- H1 TEXT IS EXACTLY "BUILD SOMETHING LOVABLE" WITH THE 1.2S MULTICOLOR GRADIENT SWEEP.
1596- SUBTITLE IS EXACTLY "CREATE APPS AND WEBSITES BY CHATTING WITH AI".
1597- PROMPT BOX IS 702PX WIDE MAX, 160PX TALL, ROUNDED 30PX, WITH THE TYPEWRITER PHRASES AND BLINKING CARET.
1598- SEND BUTTON HAS THE ROTATING GRADIENT, DOTTED TEXTURE, HOVER SCALE, ARROW ANIMATION, AND SHINE SWEEP.
1599- THE NEXT SECTION BACKGROUND IS `#1c1c1c`, TEXT SAYS "TEAMS FROM TOP COMPANIES BUILD WITH LOVABLE", AND THE 3D LOGO CAROUSEL MOVES CONTINUOUSLY WITH DEPTH/BLUR/SCALE CHANGES.
1600- NO PLACEHOLDER CONTENT, NO GENERIC HERO, NO GENERATED REPLACEMENT IMAGERY, NO MISSING ASSETS, AND NO CONSOLE ERRORS.
1601