Perf: pre-compile JSX, prod React, WebP assets, immutable cache

This is the big "make scrolling smooth" pass — the biggest wins live
in cold-start, not in the scroll loop itself, but a fast cold start
means GSAP starts in a non-thrashing state and stays there.

- Drop @babel/standalone (~3MB) from the page entirely. app.jsx is
  pre-compiled to app.js via deploy/build.sh; the browser loads the
  compiled bundle directly. JSX-in-browser was running babel transform
  on every page load, which is brutal on phones.
- Switch React UMD bundles from .development to .production.min:
  ~1.1MB → ~140KB, no dev-mode warnings/checks in the hot path.
- Add `defer` + `<link rel=preload as=script>` for the React/GSAP CDN
  scripts so they fetch in parallel with HTML parse, execute in order
  after DOM is ready, and don't block first paint.
- Re-encode the three 1.4–1.8MB PNG backgrounds as WebP at full size
  (~190KB total) plus a 900px-wide mobile variant (~52KB total).
  Mobile preload links use `media=` so phones never download the
  full-size variants.
- Move bg-image URLs from inline JSX styles into styles.css so the
  mobile media query can swap them in cleanly.
- nginx: long-cache versioned static assets (Cache-Control immutable,
  1 year) since URLs already carry ?v=… cache busters; keep the HTML
  itself on must-revalidate so the version pointer can update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 03:17:38 +08:00
parent dbe965d0e9
commit b510b33628
13 changed files with 818 additions and 32 deletions

731
app.js Normal file
View File

@@ -0,0 +1,731 @@
/* Facere — cinematic scroll landing
Three sections, three production-art backgrounds, one continuous camera push. */
const {
useEffect,
useRef,
useState
} = React;
// Mobile heuristic — captured once at boot. Used to skip expensive
// effects (extra particles, heavy scrub smoothing) on phones.
const IS_MOBILE = typeof window !== "undefined" && window.matchMedia("(max-width: 760px), (pointer: coarse) and (max-width: 900px)").matches;
/* ───────────────── PixelLogo ─────────────────
Bitmap glyphs rendered as a CSS grid of <span>s, so we control individual
pixel colors / glow / animations. Original blocky design (no copyrighted mark). */
// 7 wide × 9 tall chunky glyphs, designed in the style of the reference:
// thick stems, rounded/beveled corners on C/E/R, no diagonals.
const GLYPH_W = 7;
const GLYPH_H = 9;
const GLYPHS = {
F: ["#######", "#######", "##.....", "##.....", "######.", "######.", "##.....", "##.....", "##....."],
A: [".#####.", "#######", "##...##", "##...##", "#######", "#######", "##...##", "##...##", "##...##"],
C: [".######", "#######", "##.....", "##.....", "##.....", "##.....", "##.....", "#######", ".######"],
E: ["#######", "#######", "##.....", "##.....", "######.", "######.", "##.....", "#######", "#######"],
R: ["######.", "#######", "##...##", "##...##", "######.", "#####..", "##.##..", "##..##.", "##...##"]
};
function useResponsiveLogoScale(word = "FACERE") {
const [scale, setScale] = useState(() => computeLogoScale(word));
useEffect(() => {
let raf = 0;
const onResize = () => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => setScale(computeLogoScale(word)));
};
window.addEventListener("resize", onResize);
window.addEventListener("orientationchange", onResize);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", onResize);
window.removeEventListener("orientationchange", onResize);
};
}, [word]);
return scale;
}
function computeLogoScale(word) {
// Total logo width = letters * GLYPH_W * scale + (letters-1) * scale * 1.6
// Solve for scale that fits the viewport with side margins.
if (typeof window === "undefined") return 11;
const vw = window.innerWidth;
const letters = word.length;
const sideMargin = vw < 480 ? 0.10 : vw < 760 ? 0.12 : 0.16; // each side
const usable = vw * (1 - 2 * sideMargin);
const denom = letters * GLYPH_W + (letters - 1) * 1.6;
const fit = Math.floor(usable / denom);
// Clamp so it doesn't get too small or absurdly large on ultrawide.
return Math.max(4, Math.min(11, fit));
}
function PixelLogo({
word = "FACERE",
scale,
gap = 0
}) {
const responsiveScale = useResponsiveLogoScale(word);
const px = scale ?? responsiveScale;
const letters = word.split("");
return /*#__PURE__*/React.createElement("div", {
className: "pixel-logo",
"data-text": word
}, /*#__PURE__*/React.createElement("div", {
className: "pixel-logo-grid",
style: {
gap: `0 ${px * 1.6}px`
}
}, letters.map((ch, li) => {
const g = GLYPHS[ch];
if (!g) return null;
return /*#__PURE__*/React.createElement("div", {
key: li,
className: "pixel-letter",
style: {
gridTemplateColumns: `repeat(${GLYPH_W}, ${px}px)`,
gridTemplateRows: `repeat(${GLYPH_H}, ${px}px)`,
gap: `${gap}px`
}
}, g.flatMap((row, ri) => row.split("").map((c, ci) => /*#__PURE__*/React.createElement("span", {
key: `${ri}-${ci}`,
className: c === "#" ? "px on" : "px"
}))));
})), /*#__PURE__*/React.createElement("div", {
className: "pixel-logo-ghost ghost-r",
"aria-hidden": "true"
}, /*#__PURE__*/React.createElement(PixelLogoStatic, {
word: word,
scale: px,
gap: gap,
color: "rgba(255,80,120,0.45)"
})), /*#__PURE__*/React.createElement("div", {
className: "pixel-logo-ghost ghost-c",
"aria-hidden": "true"
}, /*#__PURE__*/React.createElement(PixelLogoStatic, {
word: word,
scale: px,
gap: gap,
color: "rgba(33,234,255,0.55)"
})));
}
function PixelLogoStatic({
word,
scale,
gap,
color
}) {
const letters = word.split("");
return /*#__PURE__*/React.createElement("div", {
className: "pixel-logo-grid",
style: {
gap: `0 ${scale * 1.6}px`
}
}, letters.map((ch, li) => {
const g = GLYPHS[ch];
if (!g) return null;
return /*#__PURE__*/React.createElement("div", {
key: li,
className: "pixel-letter",
style: {
gridTemplateColumns: `repeat(${GLYPH_W}, ${scale}px)`,
gridTemplateRows: `repeat(${GLYPH_H}, ${scale}px)`,
gap: `${gap}px`
}
}, g.flatMap((row, ri) => row.split("").map((c, ci) => /*#__PURE__*/React.createElement("span", {
key: `${ri}-${ci}`,
className: "px",
style: c === "#" ? {
background: color,
boxShadow: "none"
} : {
background: "transparent"
}
}))));
}));
}
/* ───────────────── SmokeLayer ─────────────────
Pixel-art smoke puffs above each chimney, positioned as % over the bg image.
Each puff is a chunky, low-res-feeling translucent disc that drifts up + fades. */
function SmokeLayer({
chimneys,
scale = 1,
intensity = 1
}) {
// Each chimney emits N puffs on staggered phases.
const PUFFS_PER = IS_MOBILE ? 2 : 3;
const puffs = [];
chimneys.forEach((ch, ci) => {
for (let p = 0; p < PUFFS_PER; p++) {
const delay = p / PUFFS_PER * ch.dur + ci * 0.3;
puffs.push({
id: `${ci}-${p}`,
x: ch.x,
y: ch.y,
size: ch.size * (0.85 + p % 2 * 0.25),
dur: ch.dur,
delay,
drift: ch.drift + (p % 3 - 1) * 0.4
});
}
});
return /*#__PURE__*/React.createElement("div", {
className: "smoke-layer",
style: {
"--smoke-scale": scale,
"--smoke-intensity": intensity
}
}, puffs.map(p => /*#__PURE__*/React.createElement("span", {
key: p.id,
className: "smoke-puff",
style: {
left: `${p.x}%`,
top: `${p.y}%`,
width: `${p.size}px`,
height: `${p.size}px`,
animationDuration: `${p.dur}s`,
animationDelay: `-${p.delay}s`,
"--drift": `${p.drift}rem`
}
})));
}
/* ───────────────── EmberLayer ─────────────────
Tiny pulsing orange points scattered across the horizon — gives the city
/factory a flicker without recreating any structure in CSS. */
function EmberLayer({
count = 18,
area
}) {
const embers = useRef(null);
if (!embers.current) {
const arr = [];
for (let i = 0; i < count; i++) {
arr.push({
x: area.x + Math.random() * area.w,
y: area.y + Math.random() * area.h,
size: 2 + Math.random() * 2,
dur: 1.6 + Math.random() * 3,
delay: Math.random() * 4,
hue: Math.random() > 0.7 ? "warm" : "soft"
});
}
embers.current = arr;
}
return /*#__PURE__*/React.createElement("div", {
className: "ember-layer"
}, embers.current.map((e, i) => /*#__PURE__*/React.createElement("span", {
key: i,
className: `ember ember-${e.hue}`,
style: {
left: `${e.x}%`,
top: `${e.y}%`,
width: `${e.size}px`,
height: `${e.size}px`,
animationDuration: `${e.dur}s`,
animationDelay: `-${e.delay}s`
}
})));
}
/* ───────────────── SparkLayer ─────────────────
For factory interior — small orange sparks bursting from the welding zone. */
function SparkLayer() {
const sparks = useRef(null);
if (!sparks.current) {
const arr = [];
const count = IS_MOBILE ? 7 : 14;
for (let i = 0; i < count; i++) {
arr.push({
a: -90 + (Math.random() - 0.5) * 160,
// angle deg
d: 18 + Math.random() * 36,
// distance
size: 2 + Math.random() * 2,
dur: 0.6 + Math.random() * 1.0,
delay: Math.random() * 1.6
});
}
sparks.current = arr;
}
return /*#__PURE__*/React.createElement("div", {
className: "spark-layer"
}, /*#__PURE__*/React.createElement("span", {
className: "spark-core"
}), sparks.current.map((s, i) => {
const rad = s.a * Math.PI / 180;
const tx = Math.cos(rad) * s.d;
const ty = Math.sin(rad) * s.d;
return /*#__PURE__*/React.createElement("span", {
key: i,
className: "spark",
style: {
"--tx": `${tx}px`,
"--ty": `${ty}px`,
width: `${s.size}px`,
height: `${s.size}px`,
animationDuration: `${s.dur}s`,
animationDelay: `-${s.delay}s`
}
});
}));
}
/* ───────────────── ChipPulses ─────────────────
Subtle pulsing orange dots over the PCB area. */
function ChipPulses() {
// hand-placed % over the chip image
const allDots = [{
x: 76,
y: 38,
dur: 2.4,
delay: 0
}, {
x: 82,
y: 28,
dur: 2.0,
delay: 0.7
}, {
x: 88,
y: 33,
dur: 2.8,
delay: 1.2
}, {
x: 73,
y: 60,
dur: 2.2,
delay: 0.4
}, {
x: 80,
y: 75,
dur: 2.6,
delay: 1.0
}, {
x: 92,
y: 55,
dur: 2.0,
delay: 1.6
}, {
x: 70,
y: 48,
dur: 3.0,
delay: 0.2
}, {
x: 95,
y: 70,
dur: 2.4,
delay: 0.9
}];
const dots = IS_MOBILE ? allDots.filter((_, i) => i % 2 === 0) : allDots;
return /*#__PURE__*/React.createElement("div", {
className: "chip-pulses"
}, dots.map((d, i) => /*#__PURE__*/React.createElement("span", {
key: i,
className: "chip-pulse",
style: {
left: `${d.x}%`,
top: `${d.y}%`,
animationDuration: `${d.dur}s`,
animationDelay: `-${d.delay}s`
}
})), /*#__PURE__*/React.createElement("span", {
className: "chip-glow"
}));
}
/* ───────────────── HeroSection ───────────────── */
function HeroSection({
bgRef,
contentRef
}) {
// Chimney positions (% over the exterior factory image) — hand-tuned to artwork.
// The three tall chimneys cluster at ~74%, 78%, 83% with tops near y≈49.5%.
// Smaller stacks at 71%, 80%, 85.5%.
// Image 1: assets/exterior-factory.png
const chimneys = [{
x: 71.5,
y: 51.0,
size: 22,
dur: 7.2,
drift: -0.3
}, {
x: 74.5,
y: 49.5,
size: 32,
dur: 8.0,
drift: -0.4
}, {
x: 78.5,
y: 50.4,
size: 30,
dur: 7.6,
drift: 0.2
}, {
x: 80.5,
y: 51.0,
size: 22,
dur: 7.0,
drift: 0.3
}, {
x: 83.5,
y: 49.8,
size: 32,
dur: 8.2,
drift: -0.2
}, {
x: 86.0,
y: 51.4,
size: 22,
dur: 7.4,
drift: 0.4
}];
return /*#__PURE__*/React.createElement("section", {
className: "scene scene-hero",
"data-screen-label": "01 Hero"
}, /*#__PURE__*/React.createElement("div", {
className: "bg-wrap",
ref: bgRef
}, /*#__PURE__*/React.createElement("div", {
className: "image-frame"
}, /*#__PURE__*/React.createElement("div", {
className: "bg-image"
}), /*#__PURE__*/React.createElement(SmokeLayer, {
chimneys: IS_MOBILE ? chimneys.filter((_, i) => i % 2 === 0) : chimneys
}), /*#__PURE__*/React.createElement(EmberLayer, {
count: IS_MOBILE ? 10 : 22,
area: {
x: 32,
y: 48,
w: 65,
h: 8
}
})), /*#__PURE__*/React.createElement("div", {
className: "vignette"
}), /*#__PURE__*/React.createElement("div", {
className: "haze"
})), /*#__PURE__*/React.createElement("div", {
className: "content content-hero",
ref: contentRef
}, /*#__PURE__*/React.createElement("div", {
className: "logo-wrap"
}, /*#__PURE__*/React.createElement(PixelLogo, {
word: "FACERE"
}), /*#__PURE__*/React.createElement("div", {
className: "logo-scanlines"
}))), /*#__PURE__*/React.createElement("div", {
className: "scroll-cue",
"aria-hidden": "true"
}, /*#__PURE__*/React.createElement("span", {
className: "scroll-cue-line"
}), /*#__PURE__*/React.createElement("span", {
className: "scroll-cue-label"
}, "SCROLL")));
}
/* ───────────────── ProductSection ───────────────── */
function ProductSection({
bgRef,
contentRef
}) {
return /*#__PURE__*/React.createElement("section", {
className: "scene scene-product",
"data-screen-label": "02 Product"
}, /*#__PURE__*/React.createElement("div", {
className: "bg-wrap",
ref: bgRef
}, /*#__PURE__*/React.createElement("div", {
className: "image-frame"
}, /*#__PURE__*/React.createElement("div", {
className: "bg-image"
}), /*#__PURE__*/React.createElement("div", {
className: "arm-pivot arm-left"
}), /*#__PURE__*/React.createElement("div", {
className: "arm-pivot arm-right"
}), /*#__PURE__*/React.createElement(SparkLayer, null), /*#__PURE__*/React.createElement("div", {
className: "conveyor-pulse"
})), /*#__PURE__*/React.createElement("div", {
className: "vignette product-vignette"
}), /*#__PURE__*/React.createElement("div", {
className: "haze"
})), /*#__PURE__*/React.createElement("div", {
className: "content content-product",
ref: contentRef
}, /*#__PURE__*/React.createElement("div", {
className: "tag"
}, /*#__PURE__*/React.createElement("span", {
className: "tag-dot"
}), /*#__PURE__*/React.createElement("span", null, "SYSTEM ONLINE \u2014 NODE 02")), /*#__PURE__*/React.createElement("h1", {
className: "headline"
}, "Facere is a", /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("span", {
className: "hl"
}, "Hardware Design Agent")), /*#__PURE__*/React.createElement("p", {
className: "body"
}, "From natural-language requirements to engineering files, Facere executes an end-to-end agent workflow")));
}
/* ───────────────── InstallSection ───────────────── */
function InstallSection({
bgRef,
contentRef
}) {
const [copied, setCopied] = useState(false);
const cmd = "curl -fsSL https://facere.ai/install.sh | sh";
const copy = async () => {
try {
await navigator.clipboard.writeText(cmd);
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch (e) {
setCopied(true);
setTimeout(() => setCopied(false), 1600);
}
};
return /*#__PURE__*/React.createElement("section", {
className: "scene scene-install",
"data-screen-label": "03 Install"
}, /*#__PURE__*/React.createElement("div", {
className: "bg-wrap",
ref: bgRef
}, /*#__PURE__*/React.createElement("div", {
className: "image-frame"
}, /*#__PURE__*/React.createElement("div", {
className: "bg-image"
}), /*#__PURE__*/React.createElement(ChipPulses, null)), /*#__PURE__*/React.createElement("div", {
className: "vignette"
}), /*#__PURE__*/React.createElement("div", {
className: "haze haze-deep"
})), /*#__PURE__*/React.createElement("div", {
className: "content content-install",
ref: contentRef
}, /*#__PURE__*/React.createElement("div", {
className: "tag"
}, /*#__PURE__*/React.createElement("span", {
className: "tag-dot"
}), /*#__PURE__*/React.createElement("span", null, "NODE 03 \u2014 SUBSTRATE")), /*#__PURE__*/React.createElement("h1", {
className: "headline"
}, "Get started with Facere"), /*#__PURE__*/React.createElement("p", {
className: "subheadline"
}, "Install the CLI"), /*#__PURE__*/React.createElement("div", {
className: `cmd-box ${copied ? "copied" : ""}`,
onClick: copy
}, /*#__PURE__*/React.createElement("div", {
className: "cmd-bar"
}, /*#__PURE__*/React.createElement("span", {
className: "cmd-led"
}), /*#__PURE__*/React.createElement("span", {
className: "cmd-led led-amber"
}), /*#__PURE__*/React.createElement("span", {
className: "cmd-led led-cyan"
}), /*#__PURE__*/React.createElement("span", {
className: "cmd-title"
}, "~/facere \u2014 bash")), /*#__PURE__*/React.createElement("div", {
className: "cmd-body"
}, /*#__PURE__*/React.createElement("span", {
className: "cmd-prompt"
}, "$"), /*#__PURE__*/React.createElement("span", {
className: "cmd-text"
}, cmd), /*#__PURE__*/React.createElement("span", {
className: "cmd-cursor"
})), /*#__PURE__*/React.createElement("div", {
className: "cmd-hint"
}, copied ? "✓ COPIED" : "CLICK TO COPY")), /*#__PURE__*/React.createElement("a", {
className: "docs-link",
href: "#",
onClick: e => e.preventDefault()
}, "docs.facere.ai/cli ", /*#__PURE__*/React.createElement("span", {
className: "arrow"
}, "\u2192"))));
}
/* ───────────────── App / ScrollScene ─────────────────
Pin the stage, push the camera deeper through each section.
Each section's bg scales up while the next fades in over it. */
function App() {
const stageRef = useRef(null);
const heroBg = useRef(null),
heroContent = useRef(null);
const prodBg = useRef(null),
prodContent = useRef(null);
const instBg = useRef(null),
instContent = useRef(null);
const heroSceneRef = useRef(null);
const prodSceneRef = useRef(null);
const instSceneRef = useRef(null);
const [progress, setProgress] = useState(0);
const lastDepth = useRef(-1);
useEffect(() => {
if (!window.gsap || !window.ScrollTrigger) return;
const {
gsap,
ScrollTrigger
} = window;
gsap.registerPlugin(ScrollTrigger);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Initial state: only hero visible
gsap.set(prodSceneRef.current, {
opacity: 0
});
gsap.set(instSceneRef.current, {
opacity: 0
});
const ctx = gsap.context(() => {
// Master timeline pinned to stage; scrub linked to scroll.
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".scroll-track",
start: "top top",
end: "bottom bottom",
// On mobile, scrub:true (no smoothing lag) feels snappier and
// skips an extra rAF interpolation per frame.
scrub: IS_MOBILE ? true : 0.6,
onUpdate: self => {
// Re-render only when the displayed depth (0..100) changes —
// otherwise we re-render the whole HUD on every scroll frame.
const depth = Math.round(self.progress * 100);
if (depth !== lastDepth.current) {
lastDepth.current = depth;
setProgress(self.progress);
}
}
}
});
// Timing map (extra-long product dwell):
// 0.00 → 0.20 Hero camera push
// 0.20 → 0.25 Hero → Product cross-fade
// 0.25 → 0.82 Product (very long dwell)
// 0.82 → 0.86 Product → Install cross-fade
// 0.86 → 1.00 Install settle
// Phase 1: Hero camera push-in
tl.to(heroBg.current, {
scale: reduceMotion ? 1.05 : 1.35,
ease: "none"
}, 0);
tl.to(heroContent.current, {
scale: reduceMotion ? 1.02 : 1.18,
opacity: 0,
ease: "none"
}, 0);
// Phase 2: cross-fade hero → product
tl.to(heroSceneRef.current, {
opacity: 0,
ease: "none"
}, 0.20);
tl.fromTo(prodSceneRef.current, {
opacity: 0
}, {
opacity: 1,
ease: "none"
}, 0.20);
tl.fromTo(prodBg.current, {
scale: reduceMotion ? 1.05 : 1.18
}, {
scale: 1,
ease: "none"
}, 0.20);
tl.fromTo(prodContent.current, {
y: 40,
opacity: 0
}, {
y: 0,
opacity: 1,
ease: "none"
}, 0.23);
// Phase 3: Long product dwell with slow camera creep
tl.to(prodBg.current, {
scale: reduceMotion ? 1.05 : 1.22,
ease: "none"
}, 0.27);
tl.to(prodContent.current, {
scale: reduceMotion ? 1.02 : 1.08,
opacity: 0,
ease: "none",
duration: 0.06
}, 0.78);
// Phase 4: cross-fade product → install
tl.to(prodSceneRef.current, {
opacity: 0,
ease: "none"
}, 0.82);
tl.fromTo(instSceneRef.current, {
opacity: 0
}, {
opacity: 1,
ease: "none"
}, 0.82);
tl.fromTo(instBg.current, {
scale: reduceMotion ? 1.05 : 1.18
}, {
scale: 1,
ease: "none"
}, 0.82);
tl.fromTo(instContent.current, {
y: 40,
opacity: 0
}, {
y: 0,
opacity: 1,
ease: "none"
}, 0.86);
// Phase 5: Install gentle settle
tl.to(instBg.current, {
scale: reduceMotion ? 1.0 : 1.05,
ease: "none"
}, 0.90);
}, stageRef);
return () => ctx.revert();
}, []);
// Section indicator
const section = progress < 0.22 ? 0 : progress < 0.84 ? 1 : 2;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
className: "hud"
}, /*#__PURE__*/React.createElement("div", {
className: "hud-brand"
}, "FACERE"), /*#__PURE__*/React.createElement("div", {
className: "hud-nav",
"aria-hidden": "true"
}, ["EXT", "INT", "SUB"].map((label, i) => /*#__PURE__*/React.createElement("div", {
key: i,
className: `hud-step ${section === i ? "is-active" : section > i ? "is-past" : ""}`
}, /*#__PURE__*/React.createElement("span", {
className: "hud-step-dot"
}), /*#__PURE__*/React.createElement("span", {
className: "hud-step-label"
}, label)))), /*#__PURE__*/React.createElement("div", {
className: "hud-meta"
}, /*#__PURE__*/React.createElement("span", {
className: "hud-meta-line"
}), /*#__PURE__*/React.createElement("span", null, "DEPTH ", Math.round(progress * 100).toString().padStart(2, "0")))), /*#__PURE__*/React.createElement("div", {
className: "scroll-track"
}, /*#__PURE__*/React.createElement("div", {
className: "stage",
ref: stageRef
}, /*#__PURE__*/React.createElement("div", {
className: "scene-stack"
}, /*#__PURE__*/React.createElement("div", {
className: "scene-slot",
ref: heroSceneRef
}, /*#__PURE__*/React.createElement(HeroSection, {
bgRef: heroBg,
contentRef: heroContent
})), /*#__PURE__*/React.createElement("div", {
className: "scene-slot",
ref: prodSceneRef
}, /*#__PURE__*/React.createElement(ProductSection, {
bgRef: prodBg,
contentRef: prodContent
})), /*#__PURE__*/React.createElement("div", {
className: "scene-slot",
ref: instSceneRef
}, /*#__PURE__*/React.createElement(InstallSection, {
bgRef: instBg,
contentRef: instContent
}))), /*#__PURE__*/React.createElement("div", {
className: "grain"
}))));
}
ReactDOM.createRoot(document.getElementById("root")).render(/*#__PURE__*/React.createElement(App, null));