feat(engine-select): Monolith 对角线全屏选择屏

替换原卡片网格为「左上 OpenClaw(石墨黑)vs 右下 Hermes(象牙白)」对角线全屏设计。
启动屏 / 引擎切换时给用户一个有冲击力的「选择时刻」。

## 核心设计
- position: fixed 跳出 #content 范围,覆盖整个 viewport(含 sidebar)
- 双三角形 clip-path: polygon (0 0, 0 100%, 100% 0) / (100% 0, 100% 100%, 0 100%)
- 内容用 absolute 定位到三角形质心(左上 11%/6.5% / 右下 11%/6.5%),永不重叠
- 微妙的 60px 网格纹 + 角落极光(紫蓝 / 橙金)+ 极细中线分割
- clamp(80px, 13vw, 200px) 巨字标题 + 序号 + Logo + tagline + 特性列表 + CTA

## 交互
- hover 联动:用 [data-hover] attribute 替代 :has(),兼容旧 WebKit
  - 鼠标悬停一侧 → 该侧亮起 + 内容平移 + CTA 反白;另一侧变暗 + 内容模糊缩小
- 点击三角形 → 三角形 clip-path 扩满(0.8s)→ 中心圆扩散(0.9s)→ 进入主页
- reveal 节点 attach 到 body,跨路由切换存活,新页面渲染后再淡出

## Both / Later 处理
- 两个次级选项保留,做成底部居中的玻璃 pill 链接(不抢戏)
- 不走对角线扩散动画,点击后直接 applyEngineSelection + navigate

## 兼容性
- prefers-reduced-motion: reduce → 关闭所有动画
- 移动端响应式:< 760px 调整字号 / 边距 / 角标
- 用 Vite define 注入的 __APP_VERSION__ 显示版本号(与 main.js / sidebar.js 一致)

## i18n
- engine.choiceTopBanner / choiceCtaEnter
- choiceOpenclaw{Tagline,Feat1,Feat2,Feat3,Category}
- choiceHermes{Tagline,Feat1,Feat2,Feat3,Category}
- choiceSecondary{Both,Later}
- 三语完整(zh-CN / en / zh-TW)

## 抽卡 prototype
保留 docs/engine-select-mockups/ 下的 V2 4 张设计 + 索引页(v2-monolith.html
即本次接入的最终版本)。
This commit is contained in:
晴天
2026-05-14 06:22:32 +08:00
parent cc19a07999
commit 6a0d87479c
12 changed files with 3330 additions and 223 deletions

View File

@@ -29,6 +29,21 @@ export default {
choiceLaterDesc: _('先停留在引擎选择页,不立即安装任何引擎。准备好后再选择即可。', 'Stay on the engine chooser without installing an engine now. Pick one when you are ready.', '先停留在引擎選擇頁,不立即安裝任何引擎。準備好後再選擇即可。'),
choiceLaterMeta: _('不会启动安装流程', 'No setup flow will start', '不會啟動安裝流程'),
choiceLaterBadge: _('跳过', 'Skip', '跳過'),
// —— Monolith 启动选择屏(对角线分割版) ——
choiceTopBanner: _('— 选择你的引擎 —', '— Choose your engine —', '— 選擇你的引擎 —'),
choiceCtaEnter: _('立即进入', 'Enter', '立即進入'),
choiceOpenclawTagline: _('从模型到智能体,一站式打造你的 AI 工作台。', 'From models to agents — your full AI workbench in one panel.', '從模型到智能體,一站式打造你的 AI 工作台。'),
choiceOpenclawFeat1: _('多模型 / 多渠道并行管理', 'Multi-model · multi-channel orchestration', '多模型 / 多渠道並行管理'),
choiceOpenclawFeat2: _('持久化记忆 + 上下文工程', 'Persistent memory + context engineering', '持久化記憶 + 上下文工程'),
choiceOpenclawFeat3: _('无代码搭建智能体', 'No-code agent builder', '無程式碼搭建智能體'),
choiceOpenclawCategory: _('通用助理', 'Universal Assistant', '通用助理'),
choiceHermesTagline: _('让 Agent 真正能干活。工具调用、Profile、Kanban 一应俱全。', 'Make agents actually work — tool calling, profiles, Kanban built-in.', '讓 Agent 真正能幹活。工具調用、Profile、Kanban 一應俱全。'),
choiceHermesFeat1: _('原生工具调用 + Approval Flow', 'Native tool calling + approval flow', '原生工具調用 + Approval Flow'),
choiceHermesFeat2: _('多 Profile 隔离 + 多 Gateway', 'Multi-profile isolation + multi-gateway', '多 Profile 隔離 + 多 Gateway'),
choiceHermesFeat3: _('内置 Kanban / Skills / OAuth', 'Kanban / Skills / OAuth built-in', '內建 Kanban / Skills / OAuth'),
choiceHermesCategory: _('Agent 工作流', 'Agent Workflow', 'Agent 工作流'),
choiceSecondaryBoth: _('两个都要 ↗', 'Use both ↗', '兩個都要 ↗'),
choiceSecondaryLater: _('稍后再说', 'Decide later', '稍後再說'),
hermesSetupDesc: _('安装并配置 Hermes Agent', 'Install and configure Hermes Agent', '安裝並配置 Hermes Agent'),
hermesPhaseClickHint: _('点击可返回此步骤', 'Click to go back to this step', '點擊可返回此步驟', 'このステップに戻るにはクリック', '이 단계로 돌아가려면 클릭'),
hermesSetupIntro: _(

View File

@@ -3,27 +3,24 @@ import { t } from '../lib/i18n.js'
import { applyEngineSelection } from '../lib/engine-manager.js'
import { toast } from '../components/toast.js'
const OPTIONS = [
const PRIMARY_OPTIONS = [
{
id: 'openclaw',
key: 'Openclaw',
icon: 'layers',
activeEngineId: 'openclaw',
enabledEngineIds: ['openclaw'],
targetRoute: '/setup',
},
{
id: 'hermes',
key: 'Hermes',
icon: 'bolt',
activeEngineId: 'hermes',
enabledEngineIds: ['hermes'],
targetRoute: '/h/setup',
},
]
const SECONDARY_OPTIONS = [
{
id: 'both',
key: 'Both',
icon: 'spark',
activeEngineId: 'openclaw',
enabledEngineIds: ['openclaw', 'hermes'],
engineMode: 'both',
@@ -31,8 +28,6 @@ const OPTIONS = [
},
{
id: 'later',
key: 'Later',
icon: 'clock',
activeEngineId: 'openclaw',
enabledEngineIds: [],
deferred: true,
@@ -41,83 +36,212 @@ const OPTIONS = [
]
const ICONS = {
layers: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
bolt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>',
spark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2l1.7 6.3L20 10l-6.3 1.7L12 18l-1.7-6.3L4 10l6.3-1.7L12 2z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15z"/></svg>',
clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
openclaw: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
hermes: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>',
}
let _busy = false
let _revealEl = null
let _homeEl = null
export async function render() {
const page = document.createElement('div')
page.className = 'page engine-select-page'
page.className = 'page engine-select-page es-monolith'
page.innerHTML = `
<section class="engine-select-hero">
<div class="engine-select-kicker">${esc(t('engine.choiceKicker'))}</div>
<h1>${esc(t('engine.choiceTitle'))}</h1>
<p>${esc(t('engine.choiceSubtitle'))}</p>
</section>
<section class="engine-choice-grid">
${OPTIONS.map(renderOption).join('')}
</section>
<section class="engine-choice-note">
<div class="engine-choice-note-title">${esc(t('engine.choiceNoteTitle'))}</div>
<div>${esc(t('engine.choiceNoteDesc'))}</div>
</section>
<div class="es-stage">
<div class="es-panel es-panel-openclaw" data-engine="openclaw">
<div class="es-glow es-glow-openclaw"></div>
</div>
<div class="es-panel es-panel-hermes" data-engine="hermes">
<div class="es-glow es-glow-hermes"></div>
</div>
<div class="es-divider"></div>
<div class="es-top-banner">${esc(t('engine.choiceTopBanner'))}</div>
<div class="es-corner-mark es-corner-tl">CLAWPANEL</div>
<div class="es-corner-mark es-corner-br" data-version-tag>v—</div>
${renderContent('openclaw')}
${renderContent('hermes')}
<div class="es-secondary">
<button type="button" class="es-secondary-link" data-secondary="both">${esc(t('engine.choiceSecondaryBoth'))}</button>
<span class="es-secondary-sep" aria-hidden="true">·</span>
<button type="button" class="es-secondary-link" data-secondary="later">${esc(t('engine.choiceSecondaryLater'))}</button>
</div>
</div>
`
page.addEventListener('click', async (event) => {
const card = event.target.closest('.engine-choice-card')
if (!card) return
const option = OPTIONS.find(item => item.id === card.dataset.choice)
if (!option || card.classList.contains('loading')) return
await chooseOption(page, card, option)
})
// 注入版本号package.json 同步Vite define 注入)
try {
const v = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : ''
const tag = page.querySelector('[data-version-tag]')
if (tag && v) tag.textContent = `v${v}`
} catch (_) {}
bindHover(page)
bindClick(page)
return page
}
function renderOption(option) {
const badge = t(`engine.choice${option.key}Badge`)
function renderContent(id) {
const num = id === 'openclaw' ? '01' : '02'
const cap = id === 'openclaw' ? 'OpenClaw' : 'Hermes'
const cat = id === 'openclaw' ? t('engine.choiceOpenclawCategory') : t('engine.choiceHermesCategory')
const tagline = id === 'openclaw' ? t('engine.choiceOpenclawTagline') : t('engine.choiceHermesTagline')
const feats = id === 'openclaw'
? [t('engine.choiceOpenclawFeat1'), t('engine.choiceOpenclawFeat2'), t('engine.choiceOpenclawFeat3')]
: [t('engine.choiceHermesFeat1'), t('engine.choiceHermesFeat2'), t('engine.choiceHermesFeat3')]
const cta = `${t('engine.choiceCtaEnter')} ${cap}`
// OpenClaw左上序号在前 / Hermes右下序号在后
const productRow = id === 'openclaw'
? `<span class="es-product-icon">${ICONS[id]}</span><span class="es-product-tag">${esc(num)} · ${esc(cat)}</span>`
: `<span class="es-product-tag">${esc(cat)} · ${esc(num)}</span><span class="es-product-icon">${ICONS[id]}</span>`
return `
<button class="engine-choice-card" data-choice="${option.id}">
<span class="engine-choice-icon">${ICONS[option.icon] || ''}</span>
<span class="engine-choice-content">
<span class="engine-choice-title-row">
<span class="engine-choice-title">${esc(t(`engine.choice${option.key}Title`))}</span>
${badge && badge !== `engine.choice${option.key}Badge` ? `<span class="engine-choice-badge">${esc(badge)}</span>` : ''}
</span>
<span class="engine-choice-desc">${esc(t(`engine.choice${option.key}Desc`))}</span>
<span class="engine-choice-meta">${esc(t(`engine.choice${option.key}Meta`))}</span>
</span>
<span class="engine-choice-arrow">→</span>
</button>
<div class="es-content es-content-${id}" data-engine-content="${id}">
<div class="es-product-row">${productRow}</div>
<div class="es-title">${esc(cap)}</div>
<div class="es-tagline">${esc(tagline)}</div>
<ul class="es-feature-list">
${feats.map(f => `<li>${esc(f)}</li>`).join('')}
</ul>
<button type="button" class="es-cta" data-engine-cta="${id}" tabindex="-1">
${id === 'openclaw' ? `<span>${esc(cta)}</span><span class="es-cta-arrow">→</span>` : `<span class="es-cta-arrow">→</span><span>${esc(cta)}</span>`}
</button>
</div>
`
}
async function chooseOption(page, card, option) {
setBusy(page, card, true)
try {
await applyEngineSelection({
activeEngineId: option.activeEngineId,
enabledEngineIds: option.enabledEngineIds,
deferred: !!option.deferred,
choice: option.id,
engineMode: option.engineMode || '',
function bindHover(page) {
// 用 attribute 替代 :has() — 兼容性更好(旧 WebKit / Linux WebKitGTK
const stage = page.querySelector('.es-stage')
page.querySelectorAll('.es-panel').forEach(panel => {
const engine = panel.dataset.engine
panel.addEventListener('mouseenter', () => {
if (_busy) return
stage.dataset.hover = engine
})
toast(t('engine.choiceSaved'), 'success')
navigate(option.targetRoute)
} catch (error) {
console.error('[engine-select] choose failed:', error)
toast(t('engine.choiceSaveFailed'), 'error')
setBusy(page, card, false)
panel.addEventListener('mouseleave', () => {
if (_busy) return
delete stage.dataset.hover
})
})
}
function bindClick(page) {
const stage = page.querySelector('.es-stage')
// 主区:点击三角形选引擎
stage.addEventListener('click', (event) => {
if (_busy) return
const panel = event.target.closest('.es-panel')
if (!panel) return
const engine = panel.dataset.engine
const option = PRIMARY_OPTIONS.find(o => o.id === engine)
if (option) chooseWithAnimation(page, panel, option, engine)
})
// 次级链接:两个都要 / 稍后再说(无对角线动画,直接走选择)
page.querySelectorAll('[data-secondary]').forEach(btn => {
btn.addEventListener('click', async (event) => {
event.stopPropagation()
if (_busy) return
const id = btn.dataset.secondary
const option = SECONDARY_OPTIONS.find(o => o.id === id)
if (!option) return
_busy = true
btn.classList.add('loading')
try {
await applyEngineSelection({
activeEngineId: option.activeEngineId,
enabledEngineIds: option.enabledEngineIds,
deferred: !!option.deferred,
choice: option.id,
engineMode: option.engineMode || '',
})
toast(t('engine.choiceSaved'), 'success')
navigate(option.targetRoute)
} catch (error) {
console.error('[engine-select] secondary choose failed:', error)
toast(t('engine.choiceSaveFailed'), 'error')
_busy = false
btn.classList.remove('loading')
}
})
})
}
async function chooseWithAnimation(page, panel, option, engine) {
_busy = true
const stage = page.querySelector('.es-stage')
delete stage.dataset.hover
stage.dataset.expanding = engine
// 先把 reveal / home mock 节点 attach 到 body — 路由切换时它们不会被销毁
ensureRevealNodes()
// 阶段 1: 三角形扩满CSS 通过 [data-expanding] 触发 clip-path 变化)
// 阶段 2: 600ms 后开始中心圆扩散
setTimeout(() => {
_revealEl.dataset.engine = engine
_revealEl.classList.add('es-reveal-active')
}, 600)
// 阶段 3: 1300ms 后保存选择 + 切换路由
setTimeout(async () => {
try {
await applyEngineSelection({
activeEngineId: option.activeEngineId,
enabledEngineIds: option.enabledEngineIds,
deferred: !!option.deferred,
choice: option.id,
engineMode: option.engineMode || '',
})
navigate(option.targetRoute)
// 给新页面一点渲染时间后淡出 reveal 层
setTimeout(() => {
if (_revealEl) {
_revealEl.classList.add('es-reveal-fadeout')
setTimeout(() => removeRevealNodes(), 600)
}
}, 280)
} catch (error) {
console.error('[engine-select] choose failed:', error)
toast(t('engine.choiceSaveFailed'), 'error')
// 失败回退:移除动画层 + 解除 busy
removeRevealNodes()
delete stage.dataset.expanding
_busy = false
}
}, 1300)
}
function ensureRevealNodes() {
if (!_revealEl) {
_revealEl = document.createElement('div')
_revealEl.className = 'es-reveal'
document.body.appendChild(_revealEl)
}
if (!_homeEl) {
_homeEl = document.createElement('div')
_homeEl.className = 'es-reveal-home'
document.body.appendChild(_homeEl)
}
}
function setBusy(page, activeCard, busy) {
page.querySelectorAll('.engine-choice-card').forEach(card => {
card.disabled = busy
card.classList.toggle('loading', busy && card === activeCard)
})
function removeRevealNodes() {
if (_revealEl) { _revealEl.remove(); _revealEl = null }
if (_homeEl) { _homeEl.remove(); _homeEl = null }
_busy = false
}
export function cleanup() {
// 路由切走时不主动销毁 reveal 节点(动画完成后会自行淡出)
// 这里仅重置 busy防卡死
_busy = false
}
function esc(value) {

View File

@@ -2657,189 +2657,399 @@
engines/hermes/style/hermes.css。原来 .hm-memory-* 那一坨规则已无任何
JS / CSS 引用,已在此处删除。 */
.engine-select-page {
max-width: 1120px;
margin: 0 auto;
padding: 48px 32px;
/* === Engine Select · Monolith 对角线全屏 ===
左上三角 OpenClaw石墨黑vs 右下三角 Hermes象牙白
用 position: fixed 跳出 #content 范围,覆盖整个 viewport含 sidebar。 */
.engine-select-page.es-monolith {
position: fixed;
inset: 0;
z-index: 1000;
margin: 0;
padding: 0;
max-width: none;
overflow: hidden;
background: #f5f3ee; /* 兜底色 */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
animation: es-fadein 0.5s ease;
}
@keyframes es-fadein { from { opacity: 0 } to { opacity: 1 } }
.es-stage { position: absolute; inset: 0 }
/* —— 两个三角形 —— */
.es-panel {
position: absolute;
inset: 0;
cursor: pointer;
transition: filter 0.6s ease, clip-path 0.8s cubic-bezier(0.7, 0, 0.3, 1);
}
.es-panel-openclaw {
clip-path: polygon(0 0, 0 100%, 100% 0);
background: #0c0d12;
}
/* 微妙网格纹OpenClaw 侧) */
.es-panel-openclaw::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 60px 60px;
background-position: -1px -1px;
pointer-events: none;
}
.engine-select-hero {
max-width: 720px;
.es-panel-hermes {
clip-path: polygon(100% 0, 100% 100%, 0 100%);
background: #f5f3ee;
}
.es-panel-hermes::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(0, 0, 0, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.04) 1px, transparent 1px);
background-size: 60px 60px;
background-position: -1px -1px;
pointer-events: none;
}
/* —— 角落极光发光 —— */
.es-glow {
position: absolute;
pointer-events: none;
transition: opacity 0.5s ease;
}
.es-glow-openclaw {
top: -10%;
left: -10%;
width: 50%;
height: 50%;
background: radial-gradient(circle, rgba(110, 100, 255, 0.4) 0%, transparent 60%);
filter: blur(40px);
clip-path: polygon(0 0, 100% 0, 0 100%);
}
.es-glow-hermes {
bottom: -10%;
right: -10%;
width: 50%;
height: 50%;
background: radial-gradient(circle, rgba(220, 130, 60, 0.28) 0%, transparent 60%);
filter: blur(40px);
clip-path: polygon(100% 100%, 0 100%, 100% 0);
}
/* —— 中线(极细发光) —— */
.es-divider {
position: absolute;
inset: 0;
background: linear-gradient(45deg, transparent calc(50% - 0.5px), rgba(180, 180, 180, 0.6) 50%, transparent calc(50% + 0.5px));
pointer-events: none;
transition: opacity 0.5s ease;
}
/* —— 顶部 banner + 角标 —— */
.es-top-banner {
position: absolute;
top: 32px;
left: 50%;
transform: translateX(-50%);
color: rgba(180, 180, 180, 0.78);
font-size: 11px;
letter-spacing: 0.4em;
text-transform: uppercase;
z-index: 5;
pointer-events: none;
font-weight: 500;
transition: opacity 0.5s ease;
}
.es-corner-mark {
position: absolute;
font-size: 11px;
letter-spacing: 0.32em;
text-transform: uppercase;
pointer-events: none;
z-index: 5;
font-weight: 500;
transition: opacity 0.5s ease;
}
.es-corner-tl { top: 32px; left: 36px; color: rgba(255, 255, 255, 0.45) }
.es-corner-br { bottom: 32px; right: 36px; color: rgba(0, 0, 0, 0.45) }
/* —— 内容定位(重心放在三角形质心) —— */
.es-content {
position: absolute;
pointer-events: none;
z-index: 3;
transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s, filter 0.5s;
}
.es-content-openclaw {
top: 11%;
left: 6.5%;
color: #fafafa;
text-align: left;
max-width: 44vw;
}
.es-content-hermes {
bottom: 11%;
right: 6.5%;
color: #18171a;
text-align: right;
max-width: 44vw;
}
/* —— hover 联动(用 [data-hover] attribute 替代 :has() —— */
.es-stage[data-hover='openclaw'] .es-panel-hermes { filter: brightness(0.94) saturate(0.85) }
.es-stage[data-hover='hermes'] .es-panel-openclaw { filter: brightness(0.6) }
.es-stage[data-hover='openclaw'] .es-content-hermes,
.es-stage[data-hover='hermes'] .es-content-openclaw {
opacity: 0.25;
filter: blur(2px);
}
.es-stage[data-hover='openclaw'] .es-content-openclaw { transform: translateX(8px) }
.es-stage[data-hover='hermes'] .es-content-hermes { transform: translateX(-8px) }
/* —— 顶部 logo + 序号 —— */
.es-product-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 28px;
}
.es-content-hermes .es-product-row { flex-direction: row-reverse }
.es-product-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
display: grid;
place-items: center;
}
.es-content-hermes .es-product-icon {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.12);
}
.es-product-icon svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.6 }
.es-product-tag {
font-size: 12px;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
opacity: 0.55;
}
/* —— 巨字标题 —— */
.es-title {
font-size: clamp(80px, 13vw, 200px);
font-weight: 200;
letter-spacing: -0.055em;
line-height: 0.92;
margin-bottom: 28px;
}
.engine-select-kicker {
/* —— 副标题 —— */
.es-tagline {
font-size: clamp(20px, 1.8vw, 28px);
line-height: 1.4;
font-weight: 300;
max-width: 540px;
margin-bottom: 32px;
opacity: 0.78;
letter-spacing: -0.01em;
}
.es-content-hermes .es-tagline { margin-left: auto }
/* —— 特性列表 —— */
.es-feature-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 44px;
font-size: 14px;
font-weight: 400;
line-height: 1.6;
list-style: none;
padding: 0;
}
.es-content-hermes .es-feature-list { align-items: flex-end }
.es-feature-list li {
display: flex;
align-items: center;
gap: 12px;
opacity: 0.7;
}
.es-content-hermes .es-feature-list li { flex-direction: row-reverse }
.es-feature-list li::before {
content: '';
width: 16px;
height: 1px;
background: currentColor;
opacity: 0.4;
}
/* —— CTA 按钮 —— */
.es-cta {
display: inline-flex;
align-items: center;
padding: 4px 10px;
margin-bottom: 14px;
border: 1px solid var(--border-primary);
border-radius: 999px;
color: var(--accent);
background: var(--bg-glass);
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.engine-select-hero h1 {
margin: 0 0 12px;
color: var(--text-primary);
font-size: clamp(28px, 4vw, 44px);
line-height: 1.1;
letter-spacing: -0.03em;
}
.engine-select-hero p {
margin: 0;
color: var(--text-secondary);
font-size: 16px;
line-height: 1.7;
}
.engine-choice-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.engine-choice-card {
position: relative;
display: flex;
align-items: flex-start;
gap: 16px;
width: 100%;
min-height: 172px;
padding: 22px;
border: 1px solid var(--border-primary);
border-radius: 22px;
background:
radial-gradient(circle at top right, rgba(99, 102, 241, 0.12), transparent 34%),
var(--bg-card);
color: var(--text-primary);
text-align: left;
gap: 12px;
padding: 16px 28px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.04em;
cursor: pointer;
transition: transform .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease;
pointer-events: none;
transition: all 0.35s ease;
font-family: inherit;
}
.engine-choice-card:hover {
transform: translateY(-2px);
border-color: var(--accent);
background:
radial-gradient(circle at top right, rgba(99, 102, 241, 0.18), transparent 38%),
var(--bg-card-hover);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.12);
.es-content-openclaw .es-cta {
background: rgba(255, 255, 255, 0.08);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.18);
}
.engine-choice-card:disabled {
cursor: wait;
opacity: .72;
.es-content-hermes .es-cta {
background: rgba(0, 0, 0, 0.06);
color: #18171a;
border: 1px solid rgba(0, 0, 0, 0.18);
flex-direction: row-reverse;
}
.engine-choice-card.loading .engine-choice-arrow {
animation: engine-choice-pulse .8s ease-in-out infinite alternate;
.es-stage[data-hover='openclaw'] .es-content-openclaw .es-cta {
background: #fff;
color: #0a0a0a;
border-color: #fff;
}
.engine-choice-icon {
.es-stage[data-hover='hermes'] .es-content-hermes .es-cta {
background: #0a0a0a;
color: #fff;
border-color: #0a0a0a;
}
.es-cta-arrow {
width: 18px;
height: 18px;
display: grid;
place-items: center;
width: 48px;
height: 48px;
flex: 0 0 auto;
border-radius: 16px;
color: var(--accent);
background: var(--bg-glass);
border: 1px solid var(--border-primary);
transition: transform 0.3s;
font-weight: 400;
}
.es-stage[data-hover='openclaw'] .es-content-openclaw .es-cta-arrow { transform: translateX(4px) }
.es-stage[data-hover='hermes'] .es-content-hermes .es-cta-arrow { transform: translateX(-4px) }
.engine-choice-icon svg {
width: 24px;
height: 24px;
}
.engine-choice-content {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 10px;
}
.engine-choice-title-row {
/* —— 底部次级链接(两个都要 / 稍后再说) —— */
.es-secondary {
position: absolute;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.engine-choice-title {
font-size: 18px;
font-weight: 800;
letter-spacing: -0.02em;
}
.engine-choice-badge {
padding: 2px 8px;
gap: 14px;
z-index: 4;
padding: 8px 18px;
border-radius: 999px;
color: var(--accent);
background: var(--accent-muted, rgba(99, 102, 241, 0.12));
background: rgba(150, 150, 150, 0.08);
backdrop-filter: blur(20px) saturate(1.4);
-webkit-backdrop-filter: blur(20px) saturate(1.4);
border: 1px solid rgba(180, 180, 180, 0.18);
transition: opacity 0.5s ease;
}
.es-secondary-link {
background: transparent;
border: 0;
font-family: inherit;
font-size: 12.5px;
letter-spacing: 0.04em;
color: rgba(150, 150, 150, 0.85);
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
transition: color 0.2s ease, background 0.2s ease;
}
.es-secondary-link:hover {
color: rgba(20, 20, 20, 0.95);
background: rgba(180, 180, 180, 0.18);
}
.es-secondary-link.loading {
opacity: 0.5;
cursor: wait;
}
.es-secondary-sep {
color: rgba(150, 150, 150, 0.5);
font-size: 11px;
font-weight: 700;
user-select: none;
}
.engine-choice-desc {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.6;
/* —— 选中态:三角形扩满(数据属性触发) —— */
.es-stage[data-expanding='openclaw'] .es-panel-openclaw,
.es-stage[data-expanding='hermes'] .es-panel-hermes {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
z-index: 5;
}
.es-stage[data-expanding='openclaw'] .es-panel-hermes,
.es-stage[data-expanding='hermes'] .es-panel-openclaw {
opacity: 0;
transition: opacity 0.4s ease;
}
.es-stage[data-expanding] .es-content,
.es-stage[data-expanding] .es-divider,
.es-stage[data-expanding] .es-top-banner,
.es-stage[data-expanding] .es-corner-mark,
.es-stage[data-expanding] .es-secondary {
opacity: 0;
}
.engine-choice-meta {
margin-top: auto;
color: var(--text-tertiary);
font-size: 12px;
}
.engine-choice-arrow {
margin-left: auto;
color: var(--text-tertiary);
font-size: 22px;
line-height: 1;
}
.engine-choice-note {
margin-top: 18px;
padding: 16px 18px;
border: 1px solid var(--border-primary);
border-radius: 16px;
background: var(--bg-glass);
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
}
.engine-choice-note-title {
margin-bottom: 4px;
color: var(--text-primary);
font-weight: 700;
}
@keyframes engine-choice-pulse {
from { transform: translateX(0); opacity: .5; }
to { transform: translateX(4px); opacity: 1; }
/* —— 全屏 revealattach 到 body跨路由切换存活 —— */
.es-reveal {
position: fixed;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 9000;
pointer-events: none;
background: #0c0d12;
transition: width 0.9s cubic-bezier(0.65, 0, 0.35, 1), height 0.9s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.6s ease;
}
.es-reveal[data-engine='openclaw'] { background: #0c0d12 }
.es-reveal[data-engine='hermes'] { background: #f5f3ee }
.es-reveal.es-reveal-active { width: 260vmax; height: 260vmax }
.es-reveal.es-reveal-fadeout { opacity: 0 }
/* —— 移动端响应式 —— */
@media (max-width: 760px) {
.engine-select-page {
padding: 28px 18px;
}
.es-content-openclaw { top: 8%; left: 5% }
.es-content-hermes { bottom: 8%; right: 5% }
.es-title { font-size: clamp(56px, 14vw, 120px) }
.es-tagline { font-size: 16px; margin-bottom: 20px }
.es-feature-list { gap: 8px; font-size: 12.5px; margin-bottom: 28px }
.es-cta { padding: 12px 22px; font-size: 12px }
.es-corner-tl, .es-corner-br { font-size: 10px; letter-spacing: 0.24em }
.es-corner-tl { top: 16px; left: 18px }
.es-corner-br { bottom: 16px; right: 18px }
.es-top-banner { top: 18px; font-size: 10px; letter-spacing: 0.32em }
.es-secondary { bottom: 16px }
}
.engine-choice-grid {
grid-template-columns: 1fr;
}
.engine-choice-card {
min-height: auto;
padding: 18px;
@media (prefers-reduced-motion: reduce) {
.engine-select-page.es-monolith,
.es-panel,
.es-content,
.es-cta,
.es-glow,
.es-divider,
.es-reveal,
.es-top-banner,
.es-corner-mark,
.es-secondary {
transition: none !important;
animation: none !important;
}
}