feat: launch Roncarve Harness product page

This commit is contained in:
2026-08-22 21:49:25 +08:00
parent 28574f367a
commit 9cfbb95b1e
18 changed files with 3561 additions and 13 deletions
+471
View File
@@ -0,0 +1,471 @@
import {
ArrowRight,
Brain,
Check,
Code,
Database,
Globe,
Lightning,
List,
MagnifyingGlass,
Plug,
ShieldCheck,
Stack,
UsersThree,
X
} from '@phosphor-icons/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ParticleStage } from './ParticleStage';
gsap.registerPlugin(ScrollTrigger);
const copy = {
zh: {
brand: '荣刻科技',
navProduct: '荣刻驭境',
navEngine: '插件引擎',
navEditions: '版本能力',
navWorkflow: '使用路径',
email: '邮件咨询',
language: 'EN',
menu: '打开导航',
close: '关闭导航',
eyebrow: 'RONCARVE HARNESS / 荣刻驭境',
status: '即将开放',
title: '让 Agent 驾驭\n每一种能力',
summary: '面向大语言模型的可组合 Harness,以定制插件引擎实现精准检索与高效热插拔;个人版在浏览器中操作本地项目,平台版以共享工作空间连接团队协作。',
primary: '邮件咨询',
secondary: '查看版本能力',
proof: ['精准插件检索', '高效热插拔', '个人 / 平台双版本'],
formula: 'PLUGIN IS ALL YOU NEED',
valueTitle: '插件构成能力,\nHarness 组织运行',
valueSummary: '把插件发现、装配、替换和协作放进同一套运行环境,让 Agent 能力持续扩展,而不必反复重建。',
valueCards: [
['海量插件精准检索', '按任务语义、能力类型、兼容性与工作空间策略定位合适插件。'],
['更高效率热插拔', '在明确依赖与生命周期的前提下快速加载、卸载、替换和组合。'],
['本地项目到共享协作', '个人版通过本地客户端操作本地项目;平台版让所有成员进入同一个共享工作空间协同。']
],
scroll: '滚动展开空间',
engineKicker: 'CUSTOM PLUGIN ENGINE',
engineTitle: '为插件规模化而优化的引擎',
engineSummary: '从定制插件数据库中发现能力,在运行时完成装配;个人版作用于本地项目,平台版把有效组合沉淀为团队共享能力。',
demoTitle: '插件检索演示',
demoStatus: '交互演示',
searchPlaceholder: '搜索能力、插件或场景',
categories: ['全部', '知识', '数据', '开发', '网络'],
candidates: '个候选插件',
compose: '装配',
composed: '已装配',
runtime: '当前运行组合',
hotSwap: '点击任一插件可模拟热插拔',
engineSteps: [
['01', '发现', '连接定制插件数据库,建立能力、版本、依赖与兼容性索引。'],
['02', '匹配', '结合任务语义、运行环境与工作空间策略完成候选筛选。'],
['03', '装配', '按需加载、卸载和替换插件,降低重建 Agent 的成本。'],
['04', '观测', '记录插件状态、调用与异常,让有效组合可以复用。']
],
editionsKicker: 'PERSONAL / PLATFORM',
editionsTitle: '从本地项目客户端,\n到团队共享工作空间',
editionsSummary: '同一套插件引擎,两种工作形态:个人版连接本地项目,平台版让所有成员在共享工作空间中协同。产品当前处于即将开放阶段。',
personal: '个人版',
personalEn: 'PERSONAL',
personalDesc: '作为本地客户端运行,并通过浏览器界面操作当前设备上的本地项目。',
personalFeatures: ['本地客户端部署', '浏览器操作本地项目', '本地文件与运行环境保持在当前设备', '完整插件检索与热插拔', '本地项目预设与运行记录'],
platform: '平台版',
platformEn: 'PLATFORM',
platformDesc: '面向团队的共享工作空间,让所有成员围绕同一项目、插件和运行上下文协同。',
platformFeatures: ['团队共享工作空间', '成员与角色协同', '共享项目、插件组合与配置', '组织级插件目录与版本管理', '共享活动记录与运行上下文'],
askPersonal: '邮件咨询个人版',
askPlatform: '邮件咨询平台版',
workflowKicker: 'FROM TASK TO TEAM CAPABILITY',
workflowTitle: '从一个任务,生长为团队能力',
workflowSummary: '一次有效的插件组合,不应止步于一次会话。荣刻驭境让它成为可以保存、复用和协作的工作资产。',
workflowSteps: [
['01', '描述目标', '从具体任务、数据边界和运行环境开始。'],
['02', '检索插件', '从定制数据库中找到符合语义与策略的能力。'],
['03', '运行装配', '在运行中加载、替换并验证插件组合。'],
['04', '共享协同', '个人版保存到本地项目;平台版同步到团队共享工作空间。']
],
ctaKicker: 'RONCARVE HARNESS / COMING SOON',
ctaTitle: '让下一套 Agent 能力,\n从可组合开始',
ctaSummary: '告诉我们你的本地项目形态、团队协作方式与部署环境,一起定义最合适的个人版或平台版方案。',
ctaButton: '发送咨询邮件',
originLine: '基于开源 DeepSeek Harness 二次开发的独立产品。',
copyright: '海南荣刻科技有限公司',
footerSignal: '数据驱动 · AI 赋能 · 安全交付',
policeAlt: '公安备案图标'
},
en: {
brand: 'Roncarve Tech',
navProduct: 'Harness',
navEngine: 'Plugin Engine',
navEditions: 'Editions',
navWorkflow: 'Workflow',
email: 'Email Us',
language: '中文',
menu: 'Open navigation',
close: 'Close navigation',
eyebrow: 'RONCARVE HARNESS',
status: 'COMING SOON',
title: 'Harness every capability\nfor your Agent',
summary: 'A composable Harness for LLMs with precise plugin discovery and efficient hot swapping. Personal is a local client for browser-based work on local projects; Platform gives the whole team one shared workspace.',
primary: 'Email Us',
secondary: 'Compare Editions',
proof: ['Precise plugin discovery', 'Efficient hot swapping', 'Personal / Platform'],
formula: 'PLUGIN IS ALL YOU NEED',
valueTitle: 'Plugins provide capability.\nHarness organizes the work.',
valueSummary: 'Bring discovery, composition, replacement, and collaboration into one runtime so Agents can keep evolving without being rebuilt.',
valueCards: [
['Precise discovery at scale', 'Find the right plugin by task, capability, compatibility, and workspace policy.'],
['Efficient hot swapping', 'Load, unload, replace, and compose plugins with explicit lifecycle and dependency rules.'],
['Local projects to shared collaboration', 'Personal works with local projects through a local client; Platform brings every member into one shared workspace.']
],
scroll: 'Scroll to expand',
engineKicker: 'CUSTOM PLUGIN ENGINE',
engineTitle: 'An engine optimized for plugin scale',
engineSummary: 'Discover and compose capabilities at runtime. Personal applies them to local projects, while Platform turns effective combinations into shared team capabilities.',
demoTitle: 'Plugin discovery demo',
demoStatus: 'Interactive demo',
searchPlaceholder: 'Search capabilities, plugins, or use cases',
categories: ['All', 'Knowledge', 'Data', 'Developer', 'Web'],
candidates: 'candidate plugins',
compose: 'Compose',
composed: 'Composed',
runtime: 'Active runtime',
hotSwap: 'Select a plugin to simulate hot swapping',
engineSteps: [
['01', 'Discover', 'Index capabilities, versions, dependencies, and compatibility.'],
['02', 'Match', 'Filter candidates by intent, environment, and workspace policy.'],
['03', 'Compose', 'Load, unload, or replace plugins without rebuilding the Agent.'],
['04', 'Observe', 'Record plugin state, calls, and errors for repeatable operation.']
],
editionsKicker: 'PERSONAL / PLATFORM',
editionsTitle: 'From a local project client\nto a shared team workspace',
editionsSummary: 'One plugin engine, two operating models: Personal connects to local projects; Platform brings every member into one shared workspace. Roncarve Harness is coming soon.',
personal: 'Personal',
personalEn: 'PERSONAL',
personalDesc: 'Runs as a local client with a browser interface for working on projects stored on the current device.',
personalFeatures: ['Local client deployment', 'Browser-based control of local projects', 'Local files and runtime stay on the current device', 'Full discovery and hot swapping', 'Local project presets and run history'],
platform: 'Platform',
platformEn: 'PLATFORM',
platformDesc: 'A shared workspace where every team member collaborates around the same projects, plugins, and runtime context.',
platformFeatures: ['Shared team workspace', 'Member and role collaboration', 'Shared projects, plugin sets, and configs', 'Organization plugin catalog and version management', 'Shared activity and runtime context'],
askPersonal: 'Ask about Personal',
askPlatform: 'Ask about Platform',
workflowKicker: 'FROM TASK TO TEAM CAPABILITY',
workflowTitle: 'Grow one task into\na team capability',
workflowSummary: 'A successful plugin composition should outlive a single session. Preserve it as a reusable and collaborative working asset.',
workflowSteps: [
['01', 'Describe the goal', 'Start with a real task, data boundary, and runtime.'],
['02', 'Discover plugins', 'Find capabilities that fit intent and policy.'],
['03', 'Compose at runtime', 'Load, replace, and validate a working plugin set.'],
['04', 'Share and collaborate', 'Save to a local project in Personal, or sync into the shared team workspace in Platform.']
],
ctaKicker: 'RONCARVE HARNESS / COMING SOON',
ctaTitle: 'Start your next Agent\nwith composability',
ctaSummary: 'Tell us about your local project model, collaboration workflow, and deployment environment. We will help shape the right Personal or Platform setup.',
ctaButton: 'Send an email',
originLine: 'An independently developed product based on the open-source DeepSeek Harness.',
copyright: 'Hainan Roncarve Technology Co., Ltd',
footerSignal: 'DATA-DRIVEN · AI-POWERED · SECURE DELIVERY',
policeAlt: 'Public security filing icon'
}
};
const pluginCatalog = [
{ id: 'knowledge', category: 1, icon: Brain, zh: ['知识检索增强', '连接知识库与语义索引,为 Agent 提供可追溯的上下文。', '知识'], en: ['Knowledge Retrieval', 'Connect knowledge bases and semantic indexes with traceable context.', 'Knowledge'] },
{ id: 'database', category: 2, icon: Database, zh: ['数据桥接器', '以受控连接访问结构化数据和业务数据源。', '数据'], en: ['Data Bridge', 'Access structured and business data through governed connections.', 'Data'] },
{ id: 'code', category: 3, icon: Code, zh: ['代码执行环境', '在隔离环境中组合代码、命令与文件操作。', '开发'], en: ['Code Runtime', 'Compose code, commands, and file operations in an isolated runtime.', 'Developer'] },
{ id: 'web', category: 4, icon: Globe, zh: ['网页研究工具', '检索、读取并整理可验证的公开网页信息。', '网络'], en: ['Web Research', 'Search, read, and organize verifiable public web information.', 'Web'] },
{ id: 'workflow', category: 3, icon: Stack, zh: ['工作流编排', '把多轮工具调用组合为稳定、可复用的执行路径。', '开发'], en: ['Workflow Composer', 'Turn multi-step tool calls into reusable execution paths.', 'Developer'] },
{ id: 'policy', category: 2, icon: ShieldCheck, zh: ['策略边界', '按工作空间控制插件可见范围与运行权限。', '数据'], en: ['Policy Boundary', 'Control plugin visibility and runtime permissions by workspace.', 'Data'] }
];
function emailHref(language, edition = '') {
const subject = language === 'zh'
? `[荣刻驭境] ${edition ? `${edition}咨询` : '产品咨询'}`
: `[Roncarve Harness] ${edition ? `${edition} inquiry` : 'Product inquiry'}`;
const body = language === 'zh'
? '您好,我想了解荣刻驭境。\n\n关注版本:\n使用场景:\n部署环境:\n团队规模:\n联系人:'
: 'Hello, I would like to learn more about Roncarve Harness.\n\nEdition:\nUse case:\nDeployment:\nTeam size:\nContact:';
return `mailto:contact@roncarve.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
}
function ProductNav({ language, setLanguage }) {
const t = copy[language];
const [open, setOpen] = useState(false);
const close = () => setOpen(false);
return (
<nav className="harness-nav" aria-label={language === 'zh' ? '产品导航' : 'Product navigation'}>
<div className="harness-nav-inner">
<a className="harness-brand" href="/" aria-label={language === 'zh' ? '返回荣刻科技首页' : 'Back to Roncarve home'}>
<img src="/assets/brand-logo-final.png" alt="" />
<span><strong>{t.brand}</strong><small>RONCARVE TECHNOLOGY</small></span>
</a>
<button className="harness-menu-button" type="button" onClick={() => setOpen(!open)} aria-expanded={open} aria-controls="harnessNavLinks" aria-label={open ? t.close : t.menu}>
{open ? <X size={22} /> : <List size={22} />}
</button>
<div className={`harness-nav-links ${open ? 'is-open' : ''}`} id="harnessNavLinks">
<a href="#top" onClick={close}>{t.navProduct}</a>
<a href="#engine" onClick={close}>{t.navEngine}</a>
<a href="#editions" onClick={close}>{t.navEditions}</a>
<a href="#workflow" onClick={close}>{t.navWorkflow}</a>
<button className="language-button" type="button" onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}>{t.language}</button>
<a className="nav-email-button" href={emailHref(language)} onClick={close}>{t.email}<ArrowRight size={16} weight="bold" /></a>
</div>
</div>
</nav>
);
}
function PluginDemo({ language }) {
const t = copy[language];
const [query, setQuery] = useState('');
const [category, setCategory] = useState(0);
const [activePlugin, setActivePlugin] = useState('knowledge');
const filtered = pluginCatalog.filter((plugin) => {
const translated = plugin[language];
const matchesCategory = category === 0 || plugin.category === category;
const haystack = translated.join(' ').toLowerCase();
return matchesCategory && haystack.includes(query.trim().toLowerCase());
});
const active = pluginCatalog.find((plugin) => plugin.id === activePlugin);
return (
<div className="plugin-demo">
<div className="demo-toolbar">
<div><span className="status-dot" />{t.demoStatus}</div>
<span>RONCARVE PLUGIN INDEX</span>
</div>
<div className="demo-search-row">
<label className="demo-search">
<MagnifyingGlass size={19} aria-hidden="true" />
<span className="sr-only">{t.searchPlaceholder}</span>
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={t.searchPlaceholder} />
</label>
<div className="category-tabs" aria-label={language === 'zh' ? '插件分类' : 'Plugin categories'}>
{t.categories.map((label, index) => (
<button key={label} type="button" className={category === index ? 'is-active' : ''} onClick={() => setCategory(index)}>{label}</button>
))}
</div>
</div>
<div className="demo-result-meta"><strong>{filtered.length}</strong> {t.candidates}</div>
<div className="plugin-result-list">
{filtered.map((plugin) => {
const [name, description, tag] = plugin[language];
const Icon = plugin.icon;
const isActive = activePlugin === plugin.id;
return (
<article className={`plugin-result ${isActive ? 'is-active' : ''}`} key={plugin.id}>
<div className="plugin-icon"><Icon size={23} weight="duotone" /></div>
<div className="plugin-copy"><span>{tag}</span><h3>{name}</h3><p>{description}</p></div>
<button type="button" onClick={() => setActivePlugin(plugin.id)} aria-pressed={isActive}>{isActive ? t.composed : t.compose}</button>
</article>
);
})}
{filtered.length === 0 && <div className="empty-result">{language === 'zh' ? '没有匹配结果,请尝试其他关键词。' : 'No matches. Try another keyword.'}</div>}
</div>
<div className="runtime-strip" aria-live="polite">
<div><Lightning size={18} weight="fill" /><span><small>{t.runtime}</small><strong>{active?.[language][0]}</strong></span></div>
<p>{t.hotSwap}</p>
</div>
</div>
);
}
function EditionCard({ title, code, description, features, href, featured, cta }) {
return (
<article className={`edition-card ${featured ? 'is-featured' : ''}`}>
<div className="edition-head"><span>{code}</span>{featured && <UsersThree size={26} weight="duotone" />}</div>
<h3>{title}</h3>
<p>{description}</p>
<ul>{features.map((feature) => <li key={feature}><Check size={17} weight="bold" />{feature}</li>)}</ul>
<a href={href}>{cta}<ArrowRight size={17} weight="bold" /></a>
</article>
);
}
export function HarnessProductPage() {
const [language, setLanguageState] = useState(() => {
try { return localStorage.getItem('site_lang') === 'en' ? 'en' : 'zh'; } catch { return 'zh'; }
});
const storyRef = useRef(null);
const pageRef = useRef(null);
const progressRef = useRef(0);
const t = copy[language];
const setLanguage = (next) => {
setLanguageState(next);
try { localStorage.setItem('site_lang', next); } catch { /* storage may be disabled */ }
};
const meta = useMemo(() => language === 'zh' ? {
title: '荣刻驭境 Roncarve Harness|插件驱动的 Agent 运行环境 - 荣刻科技',
description: '荣刻驭境是一套面向大语言模型与 Agent 的可组合 Harness,通过定制插件引擎提供精准插件检索、高效热插拔,支持浏览器操作本地项目的个人客户端与团队共享工作空间。'
} : {
title: 'Roncarve Harness | A plugin-driven runtime for Agents',
description: 'A composable Harness for LLMs and Agents with precise plugin discovery, efficient hot swapping, a local client for browser-based local projects, and a shared team workspace.'
}, [language]);
useLayoutEffect(() => {
document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
document.title = meta.title;
document.querySelector('meta[name="description"]')?.setAttribute('content', meta.description);
}, [language, meta]);
useLayoutEffect(() => {
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const context = gsap.context(() => {
if (reduced) {
progressRef.current = 0;
gsap.set(['.hero-copy', '.value-copy'], { opacity: 1, y: 0 });
return;
}
const timeline = gsap.timeline({
scrollTrigger: {
trigger: storyRef.current,
start: 'top top',
end: 'bottom bottom',
scrub: 0.85,
onUpdate: (self) => { progressRef.current = self.progress; }
}
});
timeline
.to('.hero-copy', { opacity: 0, y: -92, duration: 0.2, ease: 'none' }, 0.11)
.fromTo('.value-copy', { opacity: 0, y: 80 }, { opacity: 1, y: 0, duration: 0.2, ease: 'none' }, 0.26)
.fromTo('.value-card', { opacity: 0, y: 44 }, { opacity: 1, y: 0, stagger: 0.035, duration: 0.15, ease: 'none' }, 0.39)
.to('.story-scroll-cue', { opacity: 0, duration: 0.08 }, 0.05)
.to('.value-copy', { opacity: 0.82, y: -18, duration: 0.13, ease: 'none' }, 0.84);
gsap.utils.toArray('.reveal-section').forEach((section) => {
gsap.fromTo(section, { opacity: 0, y: 52 }, {
opacity: 1,
y: 0,
duration: 0.85,
ease: 'power3.out',
scrollTrigger: { trigger: section, start: 'top 82%', once: true }
});
});
}, pageRef);
return () => context.revert();
}, [language]);
return (
<div className="harness-page" ref={pageRef}>
<a className="skip-link" href="#main">{language === 'zh' ? '跳到主要内容' : 'Skip to main content'}</a>
<ProductNav language={language} setLanguage={setLanguage} />
<main id="main">
<section className="harness-story" id="top" ref={storyRef}>
<div className="story-sticky">
<div className="story-grid" />
<div className="story-glow" />
<ParticleStage progressRef={progressRef} />
<div className="story-shell">
<div className="hero-copy">
<div className="hero-meta"><span>{t.eyebrow}</span><strong>{t.status}</strong></div>
<h1>{t.title.split('\n').map((line) => <span key={line}>{line}</span>)}</h1>
<p>{t.summary}</p>
<div className="hero-actions">
<a className="button-primary" href={emailHref(language)}>{t.primary}<ArrowRight size={18} weight="bold" /></a>
<a className="button-secondary" href="#editions">{t.secondary}</a>
</div>
<div className="hero-proof">{t.proof.map((item) => <span key={item}>{item}</span>)}</div>
</div>
<div className="value-copy">
<span className="section-kicker">{t.formula}</span>
<h2>{t.valueTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2>
<p>{t.valueSummary}</p>
<div className="value-grid">
{t.valueCards.map(([title, description], index) => {
const Icon = [MagnifyingGlass, Plug, UsersThree][index];
return <article className="value-card" key={title}><Icon size={27} weight="duotone" /><span>0{index + 1}</span><h3>{title}</h3><p>{description}</p></article>;
})}
</div>
</div>
</div>
<div className="story-scroll-cue"><span />{t.scroll}</div>
</div>
</section>
<section className="content-section engine-section" id="engine">
<div className="content-shell">
<header className="section-header reveal-section">
<div><span className="section-kicker">{t.engineKicker}</span><h2>{t.engineTitle}</h2></div>
<p>{t.engineSummary}</p>
</header>
<aside className="opensource-note reveal-section">
<span>{language === 'zh' ? '开源来源' : 'OPEN-SOURCE FOUNDATION'}</span>
<p>{t.originLine}</p>
</aside>
<div className="engine-layout">
<div className="reveal-section"><PluginDemo language={language} /></div>
<div className="engine-steps reveal-section">
{t.engineSteps.map(([number, title, description]) => (
<article key={number}><span>{number}</span><div><h3>{title}</h3><p>{description}</p></div></article>
))}
</div>
</div>
</div>
</section>
<section className="content-section editions-section" id="editions">
<div className="content-shell">
<header className="section-header reveal-section">
<div><span className="section-kicker">{t.editionsKicker}</span><h2>{t.editionsTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2></div>
<p>{t.editionsSummary}</p>
</header>
<div className="edition-grid reveal-section">
<EditionCard title={t.personal} code={t.personalEn} description={t.personalDesc} features={t.personalFeatures} href={emailHref(language, t.personal)} cta={t.askPersonal} />
<EditionCard title={t.platform} code={t.platformEn} description={t.platformDesc} features={t.platformFeatures} href={emailHref(language, t.platform)} cta={t.askPlatform} featured />
</div>
</div>
</section>
<section className="content-section workflow-section" id="workflow">
<div className="content-shell">
<header className="section-header reveal-section">
<div><span className="section-kicker">{t.workflowKicker}</span><h2>{t.workflowTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2></div>
<p>{t.workflowSummary}</p>
</header>
<div className="workflow-grid reveal-section">
{t.workflowSteps.map(([number, title, description], index) => (
<article key={number}><span>{number}</span><div className="workflow-node"><Plug size={20} weight={index === 2 ? 'fill' : 'duotone'} /></div><h3>{title}</h3><p>{description}</p></article>
))}
</div>
</div>
</section>
<section className="product-cta">
<div className="cta-orbit" aria-hidden="true"><img src="/assets/brand-logo-final.png" alt="" /></div>
<div className="content-shell reveal-section">
<span className="section-kicker">{t.ctaKicker}</span>
<h2>{t.ctaTitle.split('\n').map((line) => <span key={line}>{line}</span>)}</h2>
<p>{t.ctaSummary}</p>
<a href={emailHref(language)}>{t.ctaButton}<ArrowRight size={19} weight="bold" /></a>
<strong>contact@roncarve.com</strong>
</div>
</section>
</main>
<footer className="harness-footer">
<div className="content-shell">
<div className="harness-footer-row">
<div className="harness-footer-company">
<span>© {new Date().getFullYear()} {t.copyright}. All Rights Reserved.</span>
<a className="harness-footer-record" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">琼ICP备2026011549号</a>
<a className="harness-footer-record harness-footer-police" href="https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=46010002001924" target="_blank" rel="noopener noreferrer">
<img src="/assets/beian-police.png" alt={t.policeAlt} />
琼公网安备46010002001924号
</a>
</div>
<span className="harness-footer-signal">{t.footerSignal}</span>
</div>
</div>
</footer>
</div>
);
}
+402
View File
@@ -0,0 +1,402 @@
import { Canvas, useFrame } from '@react-three/fiber';
import gsap from 'gsap';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from 'three';
const vertexShader = `
precision highp float;
attribute vec3 aLogoPosition;
attribute vec3 aSpreadPosition;
attribute vec3 aIntroPosition;
attribute float aIntroDelay;
attribute float aIntroBend;
attribute float aSeed;
attribute float aScale;
attribute float aColorMix;
uniform float uTime;
uniform float uScroll;
uniform float uIntro;
uniform float uGather;
uniform float uPixelRatio;
uniform float uPointerActive;
uniform float uAspect;
uniform vec2 uPointer;
varying float vAlpha;
varying float vBrightness;
varying float vColorMix;
mat3 rotateX(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c);
}
mat3 rotateY(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c);
}
mat3 rotateZ(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat3(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0);
}
void main() {
float expansion = smoothstep(0.055, 0.72, uScroll);
float rotationStrength = smoothstep(0.09, 0.48, uScroll);
float settle = smoothstep(0.74, 1.0, uScroll);
float introLinear = clamp((uIntro - aIntroDelay) / 0.38, 0.0, 1.0);
float introProgress = 1.0 - pow(1.0 - introLinear, 3.0);
vec2 introTravel = normalize(aLogoPosition.xy - aIntroPosition.xy + vec2(0.0001));
vec2 introTangent = vec2(-introTravel.y, introTravel.x);
float introArc = sin(introLinear * 3.14159265) * aIntroBend * (1.0 - introProgress * 0.32);
vec3 introPosition = mix(aIntroPosition, aLogoPosition, introProgress);
introPosition.xy += introTangent * introArc;
introPosition.z += sin(introLinear * 3.14159265) * abs(aIntroBend) * 0.68;
float idleStrength = (1.0 - smoothstep(0.045, 0.24, uScroll)) * introProgress;
vec3 position = mix(introPosition, aSpreadPosition, expansion * introProgress);
vec2 logoRadial = normalize(aLogoPosition.xy + vec2(0.0001));
vec2 logoTangent = vec2(-logoRadial.y, logoRadial.x);
float idleBreath = sin(uTime * 0.62) * 0.011;
float idleFloat = sin(uTime * 0.78 + aSeed * 21.0) * 0.032;
float idleSway = cos(uTime * 0.46 + aSeed * 13.0) * 0.016;
position.xy *= 1.0 + idleBreath * idleStrength;
position.xy += (logoRadial * idleFloat + logoTangent * idleSway) * idleStrength;
position.z += sin(uTime * 0.58 + aSeed * 17.0) * 0.11 * idleStrength;
position = rotateZ(sin(uTime * 0.28) * 0.007 * idleStrength) * position;
float drift = sin(uTime * 0.42 + aSeed * 18.0) * 0.12 * rotationStrength;
position += normalize(vec3(position.xy + 0.001, 0.35)) * drift;
float spinY = (uTime * 0.085 + uScroll * 1.18) * rotationStrength;
float spinX = (0.16 + sin(uTime * 0.14) * 0.08) * rotationStrength;
float spinZ = sin(uTime * 0.11) * 0.08 * rotationStrength;
position = rotateY(spinY) * rotateX(spinX) * rotateZ(spinZ) * position;
position.xy *= mix(1.0, 0.82, settle);
position.z -= settle * 0.85;
position = mix(position, aLogoPosition, uGather);
vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
vec4 clipPosition = projectionMatrix * viewPosition;
vec2 particleNdc = clipPosition.xy / max(clipPosition.w, 0.0001);
vec2 pointerDelta = particleNdc - uPointer;
vec2 aspectPointerDelta = vec2(pointerDelta.x * uAspect, pointerDelta.y);
float pointerDistance = length(aspectPointerDelta);
float repel = smoothstep(0.3, 0.0, pointerDistance) * uPointerActive * (1.0 - uGather);
vec2 repelDirection = normalize(aspectPointerDelta + vec2(0.0001));
vec2 ndcDirection = vec2(repelDirection.x / max(uAspect, 0.0001), repelDirection.y);
clipPosition.xy += ndcDirection * repel * 0.16 * clipPosition.w;
float nearFactor = clamp((position.z + 4.5) / 9.0, 0.0, 1.0);
float perspectiveSize = 10.0 / max(3.0, -viewPosition.z);
gl_PointSize = aScale * mix(1.75, 4.35, nearFactor) * uPixelRatio * perspectiveSize * (1.0 + repel * 0.12);
gl_Position = clipPosition;
float contentFade = 1.0 - smoothstep(0.22, 0.48, uScroll) * 0.76;
float idleShimmer = 1.0 + sin(uTime * 0.92 + aSeed * 29.0) * 0.07 * idleStrength;
float introFade = smoothstep(0.0, 0.2, introLinear);
vAlpha = mix(0.28, 0.78, nearFactor) * contentFade * (1.0 - settle * 0.18) * idleShimmer * introFade;
vBrightness = mix(0.46, 1.0, nearFactor) * (1.0 + sin(uTime * 0.54 + aSeed * 9.0) * 0.045 * idleStrength);
vColorMix = aColorMix;
}
`;
const fragmentShader = `
precision highp float;
varying float vAlpha;
varying float vBrightness;
varying float vColorMix;
void main() {
vec2 point = gl_PointCoord - 0.5;
float distanceToCenter = length(point);
float body = 1.0 - smoothstep(0.28, 0.5, distanceToCenter);
float core = 1.0 - smoothstep(0.0, 0.22, distanceToCenter);
if (body <= 0.001) discard;
vec3 cyan = vec3(0.278, 0.898, 0.937);
vec3 blue = vec3(0.259, 0.647, 1.0);
vec3 violet = vec3(0.537, 0.510, 1.0);
vec3 color = mix(cyan, blue, smoothstep(0.18, 0.62, vColorMix));
color = mix(color, violet, smoothstep(0.66, 1.0, vColorMix));
color *= vBrightness + core * 0.22;
gl_FragColor = vec4(color, body * vAlpha);
}
`;
function randomUnit(seed) {
const value = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return value - Math.floor(value);
}
function buildGeometry(image, count) {
const sampleSize = 256;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = sampleSize;
canvas.height = sampleSize;
context.clearRect(0, 0, sampleSize, sampleSize);
context.drawImage(image, 0, 0, sampleSize, sampleSize);
const pixels = context.getImageData(0, 0, sampleSize, sampleSize).data;
const candidates = [];
for (let y = 0; y < sampleSize; y += 1) {
for (let x = 0; x < sampleSize; x += 1) {
const alpha = pixels[(y * sampleSize + x) * 4 + 3];
if (alpha > 34) candidates.push([x, y, alpha / 255]);
}
}
const positions = new Float32Array(count * 3);
const logoPositions = new Float32Array(count * 3);
const spreadPositions = new Float32Array(count * 3);
const introPositions = new Float32Array(count * 3);
const introDelays = new Float32Array(count);
const introBends = new Float32Array(count);
const seeds = new Float32Array(count);
const scales = new Float32Array(count);
const colors = new Float32Array(count);
for (let index = 0; index < count; index += 1) {
const seed = randomUnit(index + 1);
const candidate = candidates[Math.floor(seed * candidates.length)] || [sampleSize / 2, sampleSize / 2, 1];
const jitterX = (randomUnit(index * 3.17 + 4) - 0.5) * 0.032;
const jitterY = (randomUnit(index * 5.23 + 7) - 0.5) * 0.032;
const x = ((candidate[0] / sampleSize) - 0.5) * 7.35 + jitterX;
const y = (0.5 - (candidate[1] / sampleSize)) * 7.35 + jitterY;
const radius = Math.hypot(x, y);
const angle = Math.atan2(y, x);
const orbit = randomUnit(index * 7.91 + 11);
const depth = (randomUnit(index * 11.41 + 3) - 0.5) * 7.8;
const spreadRadius = radius * (0.58 + orbit * 0.58) + orbit * 1.25;
const spreadAngle = angle + (orbit - 0.5) * 1.3;
const offset = index * 3;
positions[offset] = x;
positions[offset + 1] = y;
positions[offset + 2] = 0;
logoPositions[offset] = x;
logoPositions[offset + 1] = y;
logoPositions[offset + 2] = 0;
spreadPositions[offset] = Math.cos(spreadAngle) * spreadRadius + (randomUnit(index + 27) - 0.5) * 1.1;
spreadPositions[offset + 1] = Math.sin(spreadAngle) * spreadRadius * 0.72 + (randomUnit(index + 39) - 0.5) * 1.1;
spreadPositions[offset + 2] = depth;
const introLane = Math.floor(randomUnit(index * 13.77 + 17) * 8);
const introAngle = introLane * (Math.PI / 4) + (randomUnit(index * 4.61 + 29) - 0.5) * 0.52;
const introRadius = 0.78 + randomUnit(index * 9.43 + 47) * 0.5;
introPositions[offset] = Math.cos(introAngle) * 10.8 * introRadius + (randomUnit(index * 2.19 + 61) - 0.5) * 1.2;
introPositions[offset + 1] = Math.sin(introAngle) * 6.7 * introRadius + (randomUnit(index * 8.31 + 73) - 0.5) * 0.9;
introPositions[offset + 2] = (randomUnit(index * 6.07 + 83) - 0.5) * 10.5;
introDelays[index] = randomUnit(index * 5.37 + 97) * 0.6;
const bendSeed = randomUnit(index * 7.13 + 109);
introBends[index] = (bendSeed < 0.5 ? -1 : 1) * (0.34 + Math.abs(bendSeed - 0.5) * 1.8);
seeds[index] = seed;
scales[index] = 0.68 + randomUnit(index * 2.73 + 5) * 1.08;
colors[index] = Math.min(1, Math.max(0, orbit * 0.74 + candidate[2] * 0.24));
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aLogoPosition', new THREE.BufferAttribute(logoPositions, 3));
geometry.setAttribute('aSpreadPosition', new THREE.BufferAttribute(spreadPositions, 3));
geometry.setAttribute('aIntroPosition', new THREE.BufferAttribute(introPositions, 3));
geometry.setAttribute('aIntroDelay', new THREE.BufferAttribute(introDelays, 1));
geometry.setAttribute('aIntroBend', new THREE.BufferAttribute(introBends, 1));
geometry.setAttribute('aSeed', new THREE.BufferAttribute(seeds, 1));
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
geometry.setAttribute('aColorMix', new THREE.BufferAttribute(colors, 1));
geometry.computeBoundingSphere();
return geometry;
}
function LogoParticles({ progressRef, introRef, gatherRef, pointerRef, pointerActiveRef, onReady }) {
const materialRef = useRef(null);
const [geometry, setGeometry] = useState(null);
const particleTransform = useMemo(() => {
if (window.innerWidth > 900) return { position: [1.92, 0, 0], scale: 0.86 };
if (window.innerWidth > 640) return { position: [1.45, 0, 0], scale: 0.5 };
return { position: [0, 0, 0], scale: 1 };
}, []);
const particleCount = useMemo(() => {
if (typeof window === 'undefined') return 18000;
if (window.innerWidth < 480) return 7000;
if (window.innerWidth < 768) return 10000;
if (window.innerWidth < 1200) return 18000;
return 24000;
}, []);
const uniforms = useMemo(() => ({
uTime: { value: 0 },
uScroll: { value: 0 },
uIntro: { value: 0 },
uGather: { value: 0 },
uPixelRatio: { value: Math.min(window.devicePixelRatio || 1, 1.5) },
uPointerActive: { value: 0 },
uAspect: { value: 1 },
uPointer: { value: new THREE.Vector2(20, 20) }
}), []);
useEffect(() => {
let disposed = false;
const image = new Image();
image.decoding = 'async';
image.src = '/assets/brand-logo-final.png';
image.onload = () => {
if (disposed) return;
const nextGeometry = buildGeometry(image, particleCount);
setGeometry(nextGeometry);
onReady?.();
};
return () => {
disposed = true;
};
}, [onReady, particleCount]);
useEffect(() => () => geometry?.dispose(), [geometry]);
useFrame(({ clock, size }) => {
if (!materialRef.current) return;
materialRef.current.uniforms.uTime.value = clock.getElapsedTime();
materialRef.current.uniforms.uScroll.value = progressRef.current;
materialRef.current.uniforms.uIntro.value = introRef.current.value;
materialRef.current.uniforms.uGather.value = gatherRef.current.value;
materialRef.current.uniforms.uAspect.value = size.width / Math.max(size.height, 1);
materialRef.current.uniforms.uPointerActive.value += (
Number(pointerActiveRef.current) - materialRef.current.uniforms.uPointerActive.value
) * 0.18;
materialRef.current.uniforms.uPointer.value.copy(pointerRef.current);
});
if (!geometry) return null;
return (
<points
geometry={geometry}
frustumCulled={false}
position={particleTransform.position}
scale={particleTransform.scale}
>
<shaderMaterial
ref={materialRef}
uniforms={uniforms}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
transparent
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
}
function useReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
const update = () => setReduced(media.matches);
update();
media.addEventListener?.('change', update);
return () => media.removeEventListener?.('change', update);
}, []);
return reduced;
}
export function ParticleStage({ progressRef }) {
const stageRef = useRef(null);
const pointerRef = useRef(new THREE.Vector2(20, 20));
const pointerActiveRef = useRef(false);
const introRef = useRef({ value: 0 });
const gatherRef = useRef({ value: 0 });
const [ready, setReady] = useState(false);
const [active, setActive] = useState(true);
const [failed, setFailed] = useState(false);
const reducedMotion = useReducedMotion();
const handleReady = useCallback(() => setReady(true), []);
useEffect(() => {
if (!ready) return undefined;
gsap.killTweensOf(introRef.current);
if (reducedMotion) {
introRef.current.value = 1;
return undefined;
}
introRef.current.value = 0;
const tween = gsap.to(introRef.current, {
value: 1,
duration: 3.15,
ease: 'none'
});
return () => tween.kill();
}, [ready, reducedMotion]);
useEffect(() => {
if (!stageRef.current) return undefined;
const observer = new IntersectionObserver(([entry]) => setActive(entry.isIntersecting), { rootMargin: '120px 0px' });
observer.observe(stageRef.current);
return () => observer.disconnect();
}, []);
const handlePointerMove = (event) => {
if (reducedMotion || !window.matchMedia('(pointer: fine)').matches) return;
const bounds = stageRef.current.getBoundingClientRect();
const normalizedX = ((event.clientX - bounds.left) / bounds.width) * 2 - 1;
const normalizedY = 1 - ((event.clientY - bounds.top) / bounds.height) * 2;
pointerRef.current.set(normalizedX, normalizedY);
pointerActiveRef.current = true;
};
const handleGather = (event) => {
if (reducedMotion || event.button !== 0) return;
gsap.killTweensOf(introRef.current);
gsap.killTweensOf(gatherRef.current);
gsap.to(introRef.current, { value: 1, duration: 0.48, ease: 'power2.out' });
gsap.timeline()
.to(gatherRef.current, { value: 1, duration: 0.62, ease: 'power3.out' })
.to(gatherRef.current, { value: 1, duration: 0.26 })
.to(gatherRef.current, { value: 0, duration: 0.9, ease: 'power2.inOut' });
};
return (
<div
className={`particle-stage ${ready ? 'is-ready' : ''} ${failed ? 'has-failed' : ''} ${reducedMotion ? 'is-reduced' : ''}`}
ref={stageRef}
onPointerMove={handlePointerMove}
onPointerLeave={() => { pointerActiveRef.current = false; }}
onPointerDown={handleGather}
aria-hidden="true"
>
<img className="particle-static-logo" src="/assets/brand-logo-final.png" alt="" />
{!reducedMotion && !failed && (
<Canvas
className="particle-canvas"
dpr={[1, 1.5]}
camera={{ position: [0, 0, 10], fov: 42, near: 0.1, far: 50 }}
gl={{ alpha: true, antialias: false, powerPreference: 'high-performance' }}
frameloop={active ? 'always' : 'demand'}
onCreated={({ gl }) => {
gl.setClearColor(0x000000, 0);
gl.domElement.addEventListener('webglcontextlost', () => setFailed(true), { once: true });
}}
>
<LogoParticles
progressRef={progressRef}
introRef={introRef}
gatherRef={gatherRef}
pointerRef={pointerRef}
pointerActiveRef={pointerActiveRef}
onReady={handleReady}
/>
</Canvas>
)}
</div>
);
}
+590
View File
@@ -0,0 +1,590 @@
*, *::before, *::after { box-sizing: border-box; }
:root {
color-scheme: dark;
--h-bg: #05070a;
--h-bg-soft: #07131c;
--h-surface: rgba(13, 29, 41, .72);
--h-surface-solid: #0b1822;
--h-text: #f4fbff;
--h-text-soft: rgba(229, 244, 247, .66);
--h-text-faint: rgba(229, 244, 247, .42);
--h-cyan: #47e5ef;
--h-cyan-bright: #8bfbff;
--h-blue: #42a5ff;
--h-violet: #8982ff;
--h-line: rgba(183, 229, 231, .13);
--h-line-strong: rgba(139, 251, 255, .34);
--h-ease: cubic-bezier(.2, .8, .2, 1);
--h-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
--h-shell: min(1200px, calc(100vw - 48px));
}
html { scroll-behavior: smooth; background: var(--h-bg); }
body {
margin: 0;
min-width: 320px;
overflow-x: hidden;
background: var(--h-bg);
color: var(--h-text);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body, button, input { font: inherit; }
a { color: inherit; text-decoration: none; }
button { color: inherit; }
img { display: block; max-width: 100%; }
::selection { color: #031013; background: var(--h-cyan-bright); }
:focus-visible {
outline: 2px solid var(--h-cyan-bright);
outline-offset: 4px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: fixed;
top: 12px;
left: 12px;
z-index: 2000;
padding: 10px 16px;
border-radius: 9px;
color: #031013;
background: var(--h-cyan-bright);
transform: translateY(-160%);
transition: transform .2s ease;
}
.skip-link:focus { transform: translateY(0); }
.content-shell { width: var(--h-shell); margin-inline: auto; }
.harness-page {
min-height: 100vh;
overflow: clip;
background: var(--h-bg);
}
.harness-nav {
position: fixed;
inset: 0 0 auto;
z-index: 1000;
height: 72px;
border-bottom: 1px solid rgba(183, 229, 231, .08);
background: rgba(5, 7, 10, .72);
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
.harness-nav-inner {
width: var(--h-shell);
height: 100%;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 28px;
}
.harness-brand {
display: inline-flex;
align-items: center;
gap: 11px;
flex: 0 0 auto;
}
.harness-brand img {
width: 37px;
height: 37px;
object-fit: contain;
filter: brightness(1.95) saturate(1.15) drop-shadow(0 0 12px rgba(71, 229, 239, .2));
}
.harness-brand span { display: flex; flex-direction: column; line-height: 1.05; }
.harness-brand strong { font-size: 14px; letter-spacing: .04em; }
.harness-brand small { margin-top: 6px; color: var(--h-text-faint); font: 500 8px/1 var(--h-mono); letter-spacing: .14em; }
.harness-nav-links { display: flex; align-items: center; gap: 26px; }
.harness-nav-links > a:not(.nav-email-button) {
position: relative;
color: var(--h-text-soft);
font-size: 14px;
font-weight: 600;
transition: color .2s ease;
}
.harness-nav-links > a:not(.nav-email-button)::after {
content: "";
position: absolute;
inset: auto 50% -10px;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--h-cyan);
opacity: 0;
transform: translateX(-50%) scale(0);
transition: opacity .2s ease, transform .2s ease;
}
.harness-nav-links > a:not(.nav-email-button):hover { color: var(--h-text); }
.harness-nav-links > a:not(.nav-email-button):hover::after { opacity: 1; transform: translateX(-50%) scale(1); }
.language-button {
min-height: 34px;
padding: 0 10px;
border: 1px solid var(--h-line);
border-radius: 8px;
background: rgba(255, 255, 255, .025);
color: var(--h-text-soft);
font: 650 11px/1 var(--h-mono);
cursor: pointer;
}
.language-button:hover { color: var(--h-text); border-color: var(--h-line-strong); }
.nav-email-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 38px;
padding: 0 15px;
border-radius: 9px;
color: #041014;
background: var(--h-cyan);
font-size: 13px;
font-weight: 750;
transition: transform .22s var(--h-ease), box-shadow .22s ease;
}
.nav-email-button:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(71, 229, 239, .18); }
.harness-menu-button {
display: none;
width: 42px;
height: 42px;
place-items: center;
border: 1px solid var(--h-line);
border-radius: 10px;
background: rgba(255, 255, 255, .035);
cursor: pointer;
}
.harness-story { position: relative; height: 220svh; background: var(--h-bg); }
.story-sticky {
position: sticky;
top: 0;
height: 100svh;
min-height: 680px;
overflow: hidden;
isolation: isolate;
background:
radial-gradient(circle at 76% 28%, rgba(66, 165, 255, .14), transparent 24%),
radial-gradient(circle at 58% 54%, rgba(71, 229, 239, .08), transparent 34%),
linear-gradient(180deg, #07111a 0%, #05070a 72%, #05070a 100%);
}
.story-sticky::after {
content: "";
position: absolute;
inset: auto 0 0;
z-index: 5;
height: 180px;
pointer-events: none;
background: linear-gradient(transparent, var(--h-bg));
}
.story-grid {
position: absolute;
inset: 0;
z-index: 0;
opacity: .52;
background-image:
linear-gradient(rgba(183, 229, 231, .045) 1px, transparent 1px),
linear-gradient(90deg, rgba(183, 229, 231, .045) 1px, transparent 1px);
background-size: 72px 72px;
mask-image: radial-gradient(circle at 68% 44%, #000 0%, rgba(0, 0, 0, .82) 42%, transparent 88%);
-webkit-mask-image: radial-gradient(circle at 68% 44%, #000 0%, rgba(0, 0, 0, .82) 42%, transparent 88%);
}
.story-glow {
position: absolute;
z-index: 0;
top: 8%;
right: 3%;
width: min(55vw, 850px);
aspect-ratio: 1;
border-radius: 50%;
opacity: .46;
background: conic-gradient(from 210deg, transparent, rgba(66, 165, 255, .18), rgba(137, 130, 255, .16), transparent 42%);
filter: blur(42px);
pointer-events: none;
}
.particle-stage { position: absolute; inset: 0; z-index: 1; cursor: crosshair; }
.particle-canvas { position: absolute !important; inset: 0; width: 100% !important; height: 100% !important; }
.particle-static-logo {
position: absolute;
top: 50%;
right: 13%;
width: min(45vw, 610px);
transform: translateY(-50%);
opacity: 0;
object-fit: contain;
filter: brightness(1.6) saturate(1.55) hue-rotate(155deg) drop-shadow(0 0 38px rgba(66, 165, 255, .24));
transition: opacity .18s ease;
}
.particle-stage.is-ready .particle-static-logo { opacity: 0; }
.particle-stage.has-failed .particle-static-logo,
.particle-stage.is-reduced .particle-static-logo { opacity: .82; }
.story-shell {
position: relative;
z-index: 3;
width: var(--h-shell);
height: 100%;
margin-inline: auto;
pointer-events: none;
}
.hero-copy {
position: absolute;
top: 50%;
left: 0;
width: min(47%, 560px);
transform: translateY(-48%);
pointer-events: none;
}
.hero-meta { display: flex; align-items: center; gap: 12px; margin-bottom: 26px; }
.hero-meta > span, .section-kicker { color: var(--h-cyan); font: 700 11px/1.35 var(--h-mono); letter-spacing: .13em; }
.hero-meta strong { padding: 7px 10px; border: 1px solid rgba(139, 251, 255, .22); border-radius: 999px; color: var(--h-text); background: rgba(71, 229, 239, .07); font: 650 10px/1 var(--h-mono); letter-spacing: .1em; }
.hero-copy h1, .value-copy h2, .section-header h2, .product-cta h2 {
margin: 0;
color: var(--h-text);
font-weight: 680;
line-height: 1.06;
letter-spacing: -.05em;
}
.hero-copy h1 { font-size: clamp(48px, 5.35vw, 78px); }
.hero-copy h1 span, .value-copy h2 span, .section-header h2 span, .product-cta h2 span { display: block; }
.hero-copy > p {
max-width: 470px;
margin: 28px 0 0;
color: var(--h-text-soft);
font-size: 17px;
line-height: 1.82;
}
.hero-actions { display: flex; gap: 12px; margin-top: 34px; pointer-events: auto; }
.button-primary, .button-secondary, .product-cta a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 50px;
padding: 0 20px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 14px;
font-weight: 750;
transition: transform .24s var(--h-ease), border-color .24s ease, background .24s ease, box-shadow .24s ease;
}
.button-primary, .product-cta a { color: #031013; background: var(--h-cyan); }
.button-primary:hover, .product-cta a:hover { transform: translateY(-3px); box-shadow: 0 16px 40px rgba(71, 229, 239, .2); }
.button-secondary { border-color: var(--h-line); color: var(--h-text); background: rgba(255, 255, 255, .035); }
.button-secondary:hover { transform: translateY(-3px); border-color: var(--h-line-strong); background: rgba(255, 255, 255, .07); }
.hero-proof { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 30px; color: var(--h-text-faint); font-size: 12px; }
.hero-proof span { position: relative; padding-left: 13px; }
.hero-proof span::before { content: ""; position: absolute; left: 0; top: 50%; width: 5px; height: 5px; border-radius: 50%; background: var(--h-cyan); transform: translateY(-50%); box-shadow: 0 0 10px rgba(71, 229, 239, .65); }
.value-copy {
position: absolute;
inset: 15% 0 auto;
opacity: 0;
text-align: center;
pointer-events: none;
}
.value-copy h2 { margin-top: 22px; font-size: clamp(40px, 4.5vw, 64px); }
.value-copy > p { width: min(650px, 80%); margin: 22px auto 0; color: var(--h-text-soft); font-size: 17px; line-height: 1.8; }
.value-copy > .section-kicker { display: inline-flex; align-items: center; gap: 9px; padding: 8px 12px; border: 1px solid rgba(139, 251, 255, .22); border-radius: 999px; background: rgba(71, 229, 239, .06); box-shadow: 0 0 28px rgba(71, 229, 239, .07); letter-spacing: .17em; }
.value-copy > .section-kicker::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--h-cyan); box-shadow: 0 0 10px rgba(71, 229, 239, .75); }
.value-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 48px; text-align: left; }
.value-card {
position: relative;
min-height: 208px;
padding: 24px;
border: 1px solid var(--h-line);
border-radius: 16px;
background: linear-gradient(150deg, rgba(14, 32, 44, .76), rgba(8, 18, 26, .48));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .025);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.value-card > svg { color: var(--h-cyan); }
.value-card > span { position: absolute; top: 24px; right: 24px; color: var(--h-text-faint); font: 600 10px/1 var(--h-mono); }
.value-card h3 { margin: 30px 0 10px; font-size: 19px; letter-spacing: -.025em; }
.value-card p { margin: 0; color: var(--h-text-soft); font-size: 13px; line-height: 1.7; }
.story-scroll-cue {
position: absolute;
z-index: 6;
left: 50%;
bottom: 34px;
display: flex;
align-items: center;
gap: 10px;
color: var(--h-text-faint);
font: 600 9px/1 var(--h-mono);
letter-spacing: .14em;
transform: translateX(-50%);
}
.story-scroll-cue span { width: 34px; height: 1px; background: linear-gradient(90deg, transparent, var(--h-cyan)); animation: cue-pulse 1.8s ease-in-out infinite; }
@keyframes cue-pulse { 50% { width: 52px; opacity: .4; } }
.content-section { position: relative; padding: 132px 0; overflow: hidden; }
.content-section::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .36; background-image: linear-gradient(rgba(183, 229, 231, .035) 1px, transparent 1px), linear-gradient(90deg, rgba(183, 229, 231, .035) 1px, transparent 1px); background-size: 72px 72px; mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent); -webkit-mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent); }
.engine-section { background: linear-gradient(180deg, var(--h-bg), #071019 48%, var(--h-bg)); }
.editions-section { background: radial-gradient(circle at 78% 40%, rgba(137, 130, 255, .09), transparent 30%), var(--h-bg); }
.workflow-section { background: linear-gradient(180deg, var(--h-bg), #07131c); }
.section-header { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(320px, .62fr); align-items: end; gap: 80px; margin-bottom: 62px; }
.section-header h2 { margin-top: 22px; font-size: clamp(38px, 4.2vw, 60px); }
.section-header > p { margin: 0; color: var(--h-text-soft); font-size: 17px; line-height: 1.82; }
.opensource-note { position: relative; z-index: 1; display: flex; align-items: center; gap: 22px; margin: -28px 0 32px; padding: 14px 18px; border: 1px solid var(--h-line); border-radius: 11px; color: var(--h-text-faint); background: rgba(255, 255, 255, .018); }
.opensource-note > span { flex: 0 0 auto; color: var(--h-cyan); font: 650 9px/1.4 var(--h-mono); letter-spacing: .12em; }
.opensource-note > p { margin: 0; font-size: 11px; line-height: 1.6; }
.engine-layout { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0, 1.48fr) minmax(300px, .62fr); gap: 22px; align-items: start; }
.plugin-demo { overflow: hidden; border: 1px solid var(--h-line); border-radius: 20px; background: rgba(7, 18, 27, .87); box-shadow: 0 30px 90px rgba(0, 0, 0, .25), inset 0 1px 0 rgba(255, 255, 255, .03); }
.demo-toolbar { display: flex; align-items: center; justify-content: space-between; min-height: 52px; padding: 0 18px; border-bottom: 1px solid var(--h-line); color: var(--h-text-faint); background: rgba(255, 255, 255, .02); font: 650 9px/1 var(--h-mono); letter-spacing: .12em; }
.demo-toolbar > div { display: flex; align-items: center; gap: 8px; color: var(--h-text-soft); }
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--h-cyan); box-shadow: 0 0 14px rgba(71, 229, 239, .8); }
.demo-search-row { padding: 18px 18px 12px; }
.demo-search { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 14px; border: 1px solid var(--h-line); border-radius: 11px; background: rgba(255, 255, 255, .025); color: var(--h-text-faint); }
.demo-search:focus-within { border-color: var(--h-line-strong); box-shadow: 0 0 0 3px rgba(71, 229, 239, .07); }
.demo-search input { width: 100%; border: 0; outline: 0; color: var(--h-text); background: transparent; font-size: 14px; }
.demo-search input::placeholder { color: var(--h-text-faint); }
.category-tabs { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
.category-tabs button { min-height: 30px; padding: 0 11px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--h-text-faint); font-size: 11px; cursor: pointer; }
.category-tabs button:hover, .category-tabs button.is-active { border-color: var(--h-line); color: var(--h-text); background: rgba(71, 229, 239, .065); }
.demo-result-meta { padding: 0 18px 12px; color: var(--h-text-faint); font: 550 10px/1 var(--h-mono); }
.demo-result-meta strong { color: var(--h-cyan); }
.plugin-result-list { display: grid; gap: 8px; max-height: 416px; padding: 0 18px 18px; overflow: auto; scrollbar-width: thin; scrollbar-color: rgba(139, 251, 255, .2) transparent; }
.plugin-result { display: grid; grid-template-columns: 42px 1fr auto; align-items: center; gap: 12px; min-height: 92px; padding: 13px; border: 1px solid var(--h-line); border-radius: 13px; background: rgba(255, 255, 255, .018); transition: border-color .2s ease, background .2s ease, transform .2s var(--h-ease); }
.plugin-result:hover { transform: translateY(-2px); border-color: rgba(139, 251, 255, .23); background: rgba(255, 255, 255, .032); }
.plugin-result.is-active { border-color: rgba(71, 229, 239, .42); background: linear-gradient(100deg, rgba(71, 229, 239, .08), rgba(66, 165, 255, .035)); }
.plugin-icon { display: grid; place-items: center; width: 42px; height: 42px; border: 1px solid var(--h-line); border-radius: 11px; color: var(--h-cyan); background: rgba(71, 229, 239, .05); }
.plugin-copy > span { color: var(--h-cyan); font: 650 8px/1 var(--h-mono); letter-spacing: .12em; }
.plugin-copy h3 { margin: 7px 0 4px; font-size: 14px; }
.plugin-copy p { margin: 0; color: var(--h-text-faint); font-size: 11px; line-height: 1.5; }
.plugin-result button { min-height: 34px; padding: 0 12px; border: 1px solid var(--h-line); border-radius: 8px; color: var(--h-text-soft); background: rgba(255, 255, 255, .025); font-size: 11px; cursor: pointer; }
.plugin-result button:hover, .plugin-result button[aria-pressed="true"] { border-color: var(--h-line-strong); color: #031013; background: var(--h-cyan); }
.empty-result { padding: 36px 18px; color: var(--h-text-faint); text-align: center; font-size: 13px; }
.runtime-strip { display: flex; align-items: center; justify-content: space-between; gap: 18px; min-height: 68px; padding: 12px 18px; border-top: 1px solid var(--h-line); background: rgba(71, 229, 239, .04); }
.runtime-strip > div { display: flex; align-items: center; gap: 10px; color: var(--h-cyan); }
.runtime-strip > div span { display: flex; flex-direction: column; gap: 5px; }
.runtime-strip small { color: var(--h-text-faint); font: 600 8px/1 var(--h-mono); letter-spacing: .1em; text-transform: uppercase; }
.runtime-strip strong { color: var(--h-text); font-size: 12px; }
.runtime-strip p { margin: 0; color: var(--h-text-faint); font-size: 10px; }
.engine-steps { display: grid; gap: 10px; }
.engine-steps article { display: grid; grid-template-columns: 44px 1fr; gap: 16px; min-height: 135px; padding: 20px; border: 1px solid var(--h-line); border-radius: 15px; background: rgba(255, 255, 255, .018); }
.engine-steps article > span { color: var(--h-cyan); font: 650 10px/1 var(--h-mono); letter-spacing: .1em; }
.engine-steps h3 { margin: 0 0 9px; font-size: 18px; }
.engine-steps p { margin: 0; color: var(--h-text-soft); font-size: 12px; line-height: 1.68; }
.edition-grid { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.edition-card { position: relative; min-height: 570px; padding: 34px; border: 1px solid var(--h-line); border-radius: 22px; background: linear-gradient(155deg, rgba(15, 31, 43, .72), rgba(6, 14, 21, .78)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .03); }
.edition-card.is-featured { border-color: rgba(139, 251, 255, .28); background: radial-gradient(circle at 86% 6%, rgba(137, 130, 255, .15), transparent 28%), linear-gradient(155deg, rgba(15, 34, 47, .85), rgba(6, 14, 21, .82)); }
.edition-head { display: flex; align-items: center; justify-content: space-between; color: var(--h-cyan); }
.edition-head span { font: 700 10px/1 var(--h-mono); letter-spacing: .14em; }
.edition-card h3 { margin: 46px 0 15px; font-size: 34px; letter-spacing: -.035em; }
.edition-card > p { max-width: 510px; margin: 0; color: var(--h-text-soft); font-size: 15px; line-height: 1.72; }
.edition-card ul { display: grid; gap: 14px; margin: 38px 0 0; padding: 28px 0 0; border-top: 1px solid var(--h-line); list-style: none; }
.edition-card li { display: flex; align-items: flex-start; gap: 10px; color: var(--h-text-soft); font-size: 14px; }
.edition-card li svg { flex: 0 0 auto; margin-top: 2px; color: var(--h-cyan); }
.edition-card > a { position: absolute; inset: auto 34px 34px; display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; border: 1px solid var(--h-line); border-radius: 10px; color: var(--h-text); background: rgba(255, 255, 255, .03); font-size: 13px; font-weight: 750; transition: border-color .2s ease, background .2s ease, transform .2s var(--h-ease); }
.edition-card.is-featured > a { color: #031013; border-color: var(--h-cyan); background: var(--h-cyan); }
.edition-card > a:hover { transform: translateY(-2px); border-color: var(--h-line-strong); background: rgba(71, 229, 239, .09); }
.edition-card.is-featured > a:hover { background: var(--h-cyan-bright); }
.workflow-grid { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--h-line); border-radius: 20px; overflow: hidden; background: rgba(5, 13, 19, .7); }
.workflow-grid article { position: relative; min-height: 310px; padding: 28px; }
.workflow-grid article + article { border-left: 1px solid var(--h-line); }
.workflow-grid article > span { color: var(--h-text-faint); font: 650 10px/1 var(--h-mono); letter-spacing: .12em; }
.workflow-node { display: grid; place-items: center; width: 48px; height: 48px; margin-top: 54px; border: 1px solid var(--h-line-strong); border-radius: 50%; color: var(--h-cyan); background: rgba(71, 229, 239, .055); box-shadow: 0 0 28px rgba(71, 229, 239, .08); }
.workflow-grid article:not(:last-child) .workflow-node::after { content: ""; position: absolute; top: 111px; left: 76px; width: calc(100% - 48px); height: 1px; background: linear-gradient(90deg, rgba(71, 229, 239, .4), transparent); }
.workflow-grid h3 { margin: 34px 0 10px; font-size: 19px; }
.workflow-grid p { margin: 0; color: var(--h-text-soft); font-size: 13px; line-height: 1.68; }
.product-cta { position: relative; min-height: 760px; display: grid; place-items: center; overflow: hidden; text-align: center; background: radial-gradient(circle at 50% 48%, rgba(71, 229, 239, .105), transparent 30%), linear-gradient(180deg, #07131c, var(--h-bg)); }
.product-cta::before { content: ""; position: absolute; inset: 0; opacity: .45; background-image: linear-gradient(rgba(183, 229, 231, .04) 1px, transparent 1px), linear-gradient(90deg, rgba(183, 229, 231, .04) 1px, transparent 1px); background-size: 72px 72px; mask-image: radial-gradient(circle, #000 0%, transparent 70%); -webkit-mask-image: radial-gradient(circle, #000 0%, transparent 70%); }
.product-cta .content-shell { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; }
.product-cta h2 { margin-top: 24px; font-size: clamp(43px, 5vw, 70px); }
.product-cta p { width: min(640px, 88%); margin: 26px auto 0; color: var(--h-text-soft); font-size: 17px; line-height: 1.78; }
.product-cta a { margin-top: 34px; }
.product-cta strong { margin-top: 18px; color: var(--h-text-faint); font: 550 11px/1 var(--h-mono); letter-spacing: .08em; }
.cta-orbit { position: absolute; z-index: 1; width: min(68vw, 780px); aspect-ratio: 1; opacity: .11; animation: orbit-rotate 38s linear infinite; }
.cta-orbit img { width: 100%; height: 100%; object-fit: contain; filter: brightness(2.2) saturate(1.4) hue-rotate(155deg); }
@keyframes orbit-rotate { to { transform: rotate(360deg); } }
.harness-footer { padding: 28px 0 32px; border-top: 1px solid var(--h-line); background: var(--h-bg); }
.harness-footer-row { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.harness-footer-company { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; }
.harness-footer-company > span, .harness-footer-signal { color: var(--h-text-faint); font: 550 9px/1.5 var(--h-mono); }
.harness-footer-record { color: var(--h-text-faint); font-size: 11px; line-height: 1.5; transition: color .2s ease; }
.harness-footer-record:hover { color: var(--h-cyan-bright); }
.harness-footer-police { display: inline-flex; align-items: center; gap: 5px; }
.harness-footer-police img { width: 18px; height: 20px; object-fit: contain; }
.harness-footer-signal { flex: 0 0 auto; letter-spacing: .08em; }
@media (min-width: 1500px) {
:root { --h-shell: min(1380px, calc(100vw - 8vw)); }
.hero-copy { width: min(43%, 610px); }
}
@media (max-width: 1050px) {
.harness-nav-links { gap: 18px; }
.harness-nav-links > a:not(.nav-email-button) { font-size: 13px; }
.hero-copy { width: min(50%, 540px); }
.engine-layout { grid-template-columns: 1fr; }
.engine-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.workflow-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.workflow-grid article:nth-child(3) { border-left: 0; border-top: 1px solid var(--h-line); }
.workflow-grid article:nth-child(4) { border-top: 1px solid var(--h-line); }
.workflow-grid article:nth-child(2) .workflow-node::after { display: none; }
}
@media (max-width: 820px) {
:root { --h-shell: min(100% - 36px, 720px); }
.harness-nav { height: 66px; }
.harness-menu-button { display: grid; }
.harness-nav-links {
position: fixed;
inset: 66px 0 auto;
display: grid;
gap: 0;
padding: 16px 18px 20px;
border-bottom: 1px solid var(--h-line);
background: rgba(5, 7, 10, .98);
opacity: 0;
pointer-events: none;
transform: translateY(-12px);
transition: opacity .22s ease, transform .22s var(--h-ease);
}
.harness-nav-links.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }
.harness-nav-links > a:not(.nav-email-button) { padding: 13px 4px; border-bottom: 1px solid var(--h-line); }
.harness-nav-links > a:not(.nav-email-button)::after { display: none; }
.language-button { width: 100%; margin-top: 12px; }
.nav-email-button { margin-top: 10px; }
.story-sticky { min-height: 660px; }
.hero-copy { top: 48%; width: 58%; }
.hero-copy > p { max-width: 330px; }
.hero-copy h1 { font-size: clamp(44px, 8vw, 64px); }
.particle-static-logo { right: -1%; width: 54vw; }
.value-copy { inset: 12% 0 auto; }
.value-grid { gap: 10px; margin-top: 34px; }
.value-card { min-height: 190px; padding: 20px; }
.section-header { grid-template-columns: 1fr; gap: 24px; }
.edition-card { min-height: 590px; padding: 28px; }
.edition-card > a { inset: auto 28px 28px; }
}
@media (max-width: 640px) {
:root { --h-shell: min(100% - 28px, 520px); }
.harness-brand small { display: none; }
.harness-brand img { width: 34px; height: 34px; }
.harness-story { height: 230svh; }
.story-sticky { min-height: 620px; }
.story-grid, .content-section::before, .product-cta::before { background-size: 44px 44px; }
.particle-stage { opacity: .72; }
.particle-static-logo { top: 28%; right: 8%; width: 64vw; }
.hero-copy { top: 47%; width: 100%; transform: translateY(-42%); }
.hero-meta { margin-bottom: 20px; }
.hero-copy h1 { max-width: 500px; font-size: clamp(42px, 13.5vw, 59px); }
.hero-copy > p { margin-top: 22px; font-size: 15px; line-height: 1.72; }
.hero-actions { display: grid; gap: 9px; margin-top: 26px; }
.button-primary, .button-secondary { width: 100%; min-height: 47px; }
.hero-proof { display: grid; gap: 8px; margin-top: 20px; }
.value-copy { inset: 9% 0 auto; }
.value-copy h2 { margin-top: 17px; font-size: clamp(34px, 10vw, 46px); }
.value-copy > p { width: 100%; margin-top: 16px; font-size: 14px; line-height: 1.65; }
.value-grid { grid-template-columns: 1fr; gap: 8px; margin-top: 24px; }
.value-card { display: grid; grid-template-columns: 30px 1fr; align-items: center; gap: 3px 12px; min-height: 0; padding: 14px; }
.value-card > svg { grid-row: 1 / 3; }
.value-card > span { top: 14px; right: 14px; }
.value-card h3 { margin: 0; padding-right: 28px; font-size: 15px; }
.value-card p { margin: 0; font-size: 11px; line-height: 1.45; }
.story-scroll-cue { bottom: 18px; }
.content-section { padding: 92px 0; }
.section-header { margin-bottom: 40px; }
.section-header h2 { font-size: clamp(36px, 10vw, 48px); }
.section-header > p { font-size: 15px; }
.demo-toolbar > span { display: none; }
.demo-search-row { padding: 14px 14px 10px; }
.demo-result-meta { padding-inline: 14px; }
.plugin-result-list { padding: 0 14px 14px; }
.plugin-result { grid-template-columns: 38px 1fr; min-height: 0; }
.plugin-icon { width: 38px; height: 38px; }
.plugin-result button { grid-column: 1 / -1; width: 100%; }
.runtime-strip { align-items: flex-start; flex-direction: column; }
.engine-steps { grid-template-columns: 1fr; }
.engine-steps article { min-height: 0; }
.edition-grid { grid-template-columns: 1fr; }
.edition-card { min-height: 560px; padding: 24px; }
.edition-card > a { inset: auto 24px 24px; }
.workflow-grid { grid-template-columns: 1fr; }
.workflow-grid article { min-height: 250px; }
.workflow-grid article + article { border-left: 0; border-top: 1px solid var(--h-line); }
.workflow-grid article .workflow-node::after { display: none !important; }
.workflow-node { margin-top: 34px; }
.opensource-note { align-items: flex-start; flex-direction: column; gap: 7px; margin-top: -30px; }
.product-cta { min-height: 690px; }
.product-cta h2 { font-size: clamp(40px, 11vw, 54px); }
.product-cta p { width: 100%; font-size: 15px; }
.product-cta a { width: 100%; }
.cta-orbit { width: 100vw; }
.harness-footer-row { align-items: flex-start; flex-direction: column; gap: 14px; }
.harness-footer-company { align-items: flex-start; flex-direction: column; gap: 9px; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
.harness-story { height: auto; }
.story-sticky { position: relative; min-height: 1180px; height: auto; }
.hero-copy { top: 180px; transform: none; }
.value-copy { top: 650px; opacity: 1; }
.particle-static-logo { opacity: .32; }
.reveal-section { opacity: 1 !important; transform: none !important; }
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { HarnessProductPage } from './HarnessProductPage';
import './harness.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<HarnessProductPage />
</StrictMode>
);