/* ============================================================ Hero — home Two modes, switchable from the Tweaks panel ("AIR-скролл"): · classic — the original single-screen composition: giant flanking А|Л, frosted chart, scroll affordance. The letters collapse into the top-centre monogram as you scroll out. · air — the aircenter.space-style PINNED scroll act. The chart background pins for the whole section while the content rides over it (parallax). A rotating tagline cycles through three statements, driven directly by scroll, with a numeric counter (01 / 03) + progress bar. A "+" card opens a detail popup on hover. The giant А|Л travels up and collapses across the pin. Reduced-motion always falls back to the classic single screen. ============================================================ */ const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2); const lerp = (a, b, t) => a + (b - a) * t; const clamp01 = (v) => Math.min(1, Math.max(0, v)); /* fixed, scroll-linked А|Л that collapses into a top-CENTER monogram */ function MorphLogo({ progress, dims, onHome, dark }) { const { W, H } = dims; const e = easeInOut(progress); // white while the logo sits over a dark area (hero / Results), black over the white sections const letterColor = dark ? "var(--paper-on-ink)" : "var(--ink)"; const mob = W < 700; // start (hero, giant flanking letters) ⇢ end (small centred А|Л monogram, top centre) const startFS = Math.max(88, Math.min(mob ? 0.30 * W : 0.18 * W, 0.42 * H, 260)); const endFS = mob ? 24 : 28; const fs = lerp(startFS, endFS, e); const xC = W / 2; const gap = mob ? 18 : 21; // half-distance between А and Л centres when collapsed const yEnd = 36; const yA = lerp(0.5 * H, yEnd, e); const xA = lerp((mob ? 0.18 : 0.086) * W, xC - gap, e); const xL = lerp((mob ? 0.82 : 0.9) * W, xC + gap, e); const track = lerp(0.02, -0.01, e); const letter = (ch, x) => ({ position: "fixed", left: x, top: yA, transform: "translate(-50%, -50%)", fontFamily: "var(--font-display)", fontWeight: 300, fontSize: fs, lineHeight: 0.8, letterSpacing: track + "em", color: letterColor, pointerEvents: "none", zIndex: 45, willChange: "left, top, font-size", }); const collapsed = progress > 0.92; return ( {/* А|Л text monogram — letterColor already flips to white over dark sections, so the collapsed mark stays the same size on black as on paper. */} А {/* thin separator bar — fades in only as the monogram forms */} Л {/* invisible hit-area over the collapsed monogram → back to top */} ); } /* ---- hover-expand card (AIR card-team popup) ---- */ function HoverPanelCard({ label, items, light }) { const [open, setOpen] = React.useState(false); const ink = light; const surface = ink ? "rgba(255,255,255,0.72)" : "rgba(16,17,19,0.42)"; const txt = ink ? "var(--ink)" : "var(--paper-on-ink)"; const sub = ink ? "var(--ink-2)" : "rgba(250,250,248,0.74)"; const line = ink ? "var(--hairline-2)" : "rgba(250,250,248,0.18)"; return (
setOpen(true)} onMouseLeave={() => setOpen(false)} style={{ position: "relative" }}> {/* popup */}
{items.map((it, i) => (
{String(i + 1).padStart(2, "0")} {it}
))}
{/* trigger pill */}
); } function Hero({ onContact, onNav, theme = "dark", air = true }) { const reduce = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const airOn = air && !reduce; const { t } = useT(); const secRef = React.useRef(null); const [progress, setProgress] = React.useState(0); // logo-collapse driver (0..1) const [p, setP] = React.useState(0); // pin progress in air mode (0..1) const [dark, setDark] = React.useState(false); const [dims, setDims] = React.useState({ W: window.innerWidth, H: window.innerHeight }); const themeRef = React.useRef(theme); themeRef.current = theme; const airRef = React.useRef(airOn); airRef.current = airOn; const light = theme === "light"; React.useEffect(() => { let raf = 0; const compute = () => { raf = 0; const H = window.innerHeight; const sec = secRef.current; if (airRef.current && sec) { const rect = sec.getBoundingClientRect(); const scrollable = Math.max(1, sec.offsetHeight - H); const pp = clamp01(-rect.top / scrollable); setP(pp); setProgress(pp); // letters collapse across the whole pin } else { const span = Math.max(1, H * 0.72); setProgress(clamp01(window.scrollY / span)); } // is a dark area sitting under the top-left logo? (dark hero, or Results) let isDark = false; const home = document.querySelector('[data-screen-label="Home"]'); if (home && themeRef.current !== "light") { const r = home.getBoundingClientRect(); if (r.bottom > 56) isDark = true; } document.querySelectorAll('[data-screen-label="Results"], [data-screen-label="Call"]').forEach((dk) => { const r = dk.getBoundingClientRect(); if (r.top <= 56 && r.bottom >= 26) isDark = true; }); setDark(isDark); }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(compute); }; const onResize = () => { setDims({ W: window.innerWidth, H: window.innerHeight }); compute(); }; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onResize); compute(); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onResize); cancelAnimationFrame(raf); }; }, []); // re-evaluate when the theme or mode flips (without needing a scroll) React.useEffect(() => { window.dispatchEvent(new Event("scroll")); }, [theme, airOn]); const morph = ( window.scrollTo({ top: 0, behavior: "smooth" })} /> ); /* ---------------- CLASSIC (also reduced-motion) ---------------- */ if (!airOn) { const fade = Math.max(0, 1 - progress * 1.6); return (
0.4 ? "none" : "auto", transition: "opacity 120ms linear" }}> onNav("method")} />
{morph}
); } /* ---------------- AIR pinned scroll act ---------------- */ const roll = t("home.heroRoll") || []; const N = Math.max(1, roll.length); // The first stretch of the pin is a clean "classic" screen — just the giant // А|Л letters over the chart. The AIR roll (taglines, counter, progress bar, // hover card) fades in only once the user starts scrolling. const INTRO = 0.16; const airIn = easeInOut(clamp01(p / INTRO)); // 0 at rest → 1 once scrolling const q = clamp01((p - INTRO) / (1 - INTRO)); // roll progress after the intro const fp = q * (N - 1); const activeIdx = Math.min(N - 1, Math.max(0, Math.round(fp))); const lineColor = light ? "var(--ink)" : "var(--paper-on-ink)"; const eyeColor = light ? "var(--ink-3)" : "rgba(250,250,248,0.62)"; const barTrack = light ? "var(--hairline-2)" : "rgba(250,250,248,0.22)"; const barFill = light ? "var(--ink)" : "var(--paper-on-ink)"; const arrowFade = clamp01(1 - p * 2.4); // affordance retreats as you ride the pin // chart parallax — recedes & lifts a touch so the content rides over it const chartShift = { transform: `translateY(${-p * 7}vh) scale(${1 + p * 0.06})`, transformOrigin: "50% 40%", opacity: 1 - p * 0.18, transition: "none", willChange: "transform" }; const stageItems = t("home.m2.rows"); const cardItems = Array.isArray(stageItems) ? stageItems.slice(0, 4).map((r) => r.title) : ["Чтение рынка", "Smart Money", "Риск-менеджмент"]; return (
{/* pinned viewport-height layer — everything rides on top of the chart */}
{/* parallax background */}
{/* readability veil that deepens slightly as content takes over */}
{/* rotating tagline stack — scroll-driven crossfade */}
{roll.map((m, i) => { const d = fp - i; // <0 below (incoming), >0 above (outgoing) // each label fully fades out before the next begins: visible only within // |d| < 0.46, so adjacent labels never overlap (one clears, then the next arrives) const op = easeInOut(clamp01(1 - Math.abs(d) / 0.46)) * airIn; const ty = -d * 42; return (
{m.eyebrow}

{m.line}

); })}
{/* bottom chrome: hover card (left) · counter + progress + arrow (right) */}
0.5 ? "auto" : "none", opacity: airIn, transform: `translateY(${(1 - airIn) * 10}px)`, transition: "opacity 120ms linear" }} className="hero-hovercard">
{/* counter + progress — only once the AIR act is riding */}
{/* counter */}
{String(activeIdx + 1).padStart(2, "0")} / {String(N).padStart(2, "0")}
{/* progress bar */}
{/* rotating arrow */}
onNav("method")} />
{morph}
); } Object.assign(window, { Hero });