feat: launch Roncarve Harness product page
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user