2025-12-10 20:52:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="scrollContainer"
|
|
|
|
|
|
class="relative h-screen overflow-y-auto snap-y snap-mandatory scroll-smooth literary-grid literary-paper"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- Fixed scroll indicator -->
|
|
|
|
|
|
<nav class="scroll-indicator">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="(section, index) in sections"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="scroll-indicator-btn group"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
@click="scrollToSection(index)"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
<span class="scroll-indicator-label">{{ section.name }}</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="scroll-indicator-dot"
|
|
|
|
|
|
:class="{ active: currentSection === index }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
|
<header class="fixed top-0 left-0 right-0 z-50 border-b border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-xl transition-all">
|
|
|
|
|
|
<div class="mx-auto max-w-7xl px-6 py-4">
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<!-- Logo & Brand -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex items-center gap-3 group/logo cursor-pointer"
|
|
|
|
|
|
@click="scrollToSection(0)"
|
|
|
|
|
|
>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<HeaderLogo
|
|
|
|
|
|
size="h-9 w-9"
|
|
|
|
|
|
class-name="text-[#191919] dark:text-white"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div class="flex flex-col justify-center">
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
|
|
|
|
|
Aether
|
|
|
|
|
|
</h1>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">API Gateway</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Center Navigation -->
|
|
|
|
|
|
<nav class="hidden md:flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="(section, index) in sections"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="group relative px-3 py-2 text-sm font-medium transition"
|
|
|
|
|
|
:class="currentSection === index
|
|
|
|
|
|
? 'text-[#cc785c] dark:text-[#d4a27f]'
|
|
|
|
|
|
: 'text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white'"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
@click="scrollToSection(index)"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ section.name }}
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="absolute bottom-0 left-0 right-0 h-0.5 rounded-full transition-all duration-300"
|
|
|
|
|
|
:class="currentSection === index ? 'bg-[#cc785c] dark:bg-[#d4a27f] scale-x-100' : 'bg-transparent scale-x-0'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Right Actions -->
|
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
|
|
|
|
|
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
@click="toggleDarkMode"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<SunMoon
|
|
|
|
|
|
v-if="themeMode === 'system'"
|
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Sun
|
|
|
|
|
|
v-else-if="themeMode === 'light'"
|
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Moon
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<RouterLink
|
|
|
|
|
|
v-if="authStore.isAuthenticated"
|
|
|
|
|
|
:to="dashboardPath"
|
|
|
|
|
|
class="rounded-xl bg-[#191919] dark:bg-[#cc785c] px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-[#262625] dark:hover:bg-[#b86d52]"
|
|
|
|
|
|
>
|
|
|
|
|
|
控制台
|
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="rounded-xl bg-[#cc785c] px-4 py-2 text-sm font-medium text-white shadow-lg shadow-[#cc785c]/30 transition hover:bg-[#d4a27f]"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
@click="showLoginDialog = true"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
登录
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
|
<main class="relative z-10">
|
|
|
|
|
|
<!-- Fixed Logo Container -->
|
2025-12-25 00:02:56 +08:00
|
|
|
|
<div class="mt-4 fixed inset-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div
|
2025-12-25 00:02:56 +08:00
|
|
|
|
class="mt-16 transform-gpu logo-container"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:class="[currentSection === SECTIONS.HOME ? 'home-section' : '', `logo-transition-${scrollDirection}`]"
|
|
|
|
|
|
:style="fixedLogoStyle"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Transition :name="logoTransitionName">
|
|
|
|
|
|
<AetherLineByLineLogo
|
|
|
|
|
|
v-if="currentSection === SECTIONS.HOME"
|
|
|
|
|
|
ref="aetherLogoRef"
|
|
|
|
|
|
key="aether-logo"
|
|
|
|
|
|
:size="400"
|
|
|
|
|
|
:line-delay="50"
|
|
|
|
|
|
:stroke-duration="1200"
|
|
|
|
|
|
:fill-duration="1500"
|
|
|
|
|
|
:auto-start="false"
|
|
|
|
|
|
:loop="true"
|
|
|
|
|
|
:loop-pause="800"
|
|
|
|
|
|
:stroke-width="3.5"
|
|
|
|
|
|
:cycle-colors="true"
|
|
|
|
|
|
:is-dark="isDark"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:key="`ripple-wrapper-${currentLogoType}`"
|
|
|
|
|
|
:class="{ 'heartbeat-wrapper': currentSection === SECTIONS.GEMINI && geminiFillComplete }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RippleLogo
|
|
|
|
|
|
ref="rippleLogoRef"
|
|
|
|
|
|
:type="currentLogoType"
|
|
|
|
|
|
:size="320"
|
|
|
|
|
|
:use-adaptive="false"
|
|
|
|
|
|
:disable-ripple="currentSection === SECTIONS.GEMINI || currentSection === SECTIONS.FEATURES"
|
|
|
|
|
|
:anim-delay="logoTransitionDelay"
|
|
|
|
|
|
:static="currentSection === SECTIONS.FEATURES"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
class="logo-active"
|
|
|
|
|
|
:class="[currentLogoClass]"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section 0: Introduction -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<section
|
|
|
|
|
|
ref="section0"
|
|
|
|
|
|
class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div class="max-w-4xl mx-auto text-center">
|
2025-12-25 00:02:56 +08:00
|
|
|
|
<div class="h-80 w-full mb-16 mt-8" />
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<h1
|
|
|
|
|
|
class="mb-6 text-5xl md:text-7xl font-bold text-[#191919] dark:text-white leading-tight transition-all duration-700"
|
|
|
|
|
|
:style="getTitleStyle(SECTIONS.HOME)"
|
|
|
|
|
|
>
|
|
|
|
|
|
欢迎使用 <span class="text-primary">Aether</span>
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p
|
|
|
|
|
|
class="mb-8 text-xl text-[#666663] dark:text-gray-300 max-w-2xl mx-auto transition-all duration-700"
|
|
|
|
|
|
:style="getDescStyle(SECTIONS.HOME)"
|
|
|
|
|
|
>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
AI 开发工具统一接入平台<br>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
整合 Claude Code、Codex CLI、Gemini CLI 等多个 AI 编程助手
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<button
|
2025-12-25 00:02:56 +08:00
|
|
|
|
class="mt-8 transition-all duration-700 cursor-pointer hover:scale-110"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:style="getScrollIndicatorStyle(SECTIONS.HOME)"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
@click="scrollToSection(SECTIONS.CLAUDE)"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
<ChevronDown class="h-8 w-8 mx-auto text-[#91918d] dark:text-muted-foreground/80 animate-bounce" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section 1: Claude Code -->
|
|
|
|
|
|
<CliSection
|
|
|
|
|
|
ref="section1"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
v-model:platform-value="claudePlatform"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
title="Claude Code"
|
|
|
|
|
|
description="直接在您的终端中释放Claude的原始力量。瞬间搜索百万行代码库。将数小时的流程转化为单一命令。您的工具。您的流程。您的代码库,以思维速度进化。"
|
|
|
|
|
|
:badge-icon="Code2"
|
|
|
|
|
|
badge-text="IDE 集成"
|
|
|
|
|
|
badge-class="bg-[#cc785c]/10 dark:bg-amber-900/30 border border-[#cc785c]/20 dark:border-amber-800 text-[#cc785c] dark:text-amber-400"
|
|
|
|
|
|
:platform-options="platformPresets.claude.options"
|
|
|
|
|
|
:install-command="claudeInstallCommand"
|
|
|
|
|
|
:config-files="[{ path: '~/.claude/settings.json', content: claudeConfig, language: 'json' }]"
|
|
|
|
|
|
:badge-style="getBadgeStyle(SECTIONS.CLAUDE)"
|
|
|
|
|
|
:title-style="getTitleStyle(SECTIONS.CLAUDE)"
|
|
|
|
|
|
:desc-style="getDescStyle(SECTIONS.CLAUDE)"
|
|
|
|
|
|
:card-style-fn="(idx) => getCardStyle(SECTIONS.CLAUDE, idx)"
|
|
|
|
|
|
content-position="right"
|
|
|
|
|
|
@copy="copyToClipboard"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section 2: Codex CLI -->
|
|
|
|
|
|
<CliSection
|
|
|
|
|
|
ref="section2"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
v-model:platform-value="codexPlatform"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
title="Codex CLI"
|
|
|
|
|
|
description="Codex CLI 是一款可在本地终端运行的编程助手工具,它能够读取、修改并执行用户指定目录中的代码。"
|
|
|
|
|
|
:badge-icon="Terminal"
|
|
|
|
|
|
badge-text="命令行工具"
|
|
|
|
|
|
badge-class="bg-[#cc785c]/10 dark:bg-emerald-900/30 border border-[#cc785c]/20 dark:border-emerald-800 text-[#cc785c] dark:text-emerald-400"
|
|
|
|
|
|
:platform-options="platformPresets.codex.options"
|
|
|
|
|
|
:install-command="codexInstallCommand"
|
|
|
|
|
|
:config-files="[
|
|
|
|
|
|
{ path: '~/.codex/config.toml', content: codexConfig, language: 'toml' },
|
|
|
|
|
|
{ path: '~/.codex/auth.json', content: codexAuthConfig, language: 'json' }
|
|
|
|
|
|
]"
|
|
|
|
|
|
:badge-style="getBadgeStyle(SECTIONS.CODEX)"
|
|
|
|
|
|
:title-style="getTitleStyle(SECTIONS.CODEX)"
|
|
|
|
|
|
:desc-style="getDescStyle(SECTIONS.CODEX)"
|
|
|
|
|
|
:card-style-fn="(idx) => getCardStyle(SECTIONS.CODEX, idx)"
|
|
|
|
|
|
content-position="left"
|
|
|
|
|
|
@copy="copyToClipboard"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section 3: Gemini CLI -->
|
|
|
|
|
|
<CliSection
|
|
|
|
|
|
ref="section3"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
v-model:platform-value="geminiPlatform"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
title="Gemini CLI"
|
|
|
|
|
|
description="Gemini CLI 是一款开源人工智能代理,可将 Gemini 的强大功能直接带入你的终端。它提供了对 Gemini 的轻量级访问,为你提供了从提示符到我们模型的最直接路径。"
|
|
|
|
|
|
:badge-icon="Sparkles"
|
|
|
|
|
|
badge-text="多模态 AI"
|
|
|
|
|
|
badge-class="bg-[#cc785c]/10 dark:bg-primary/20 border border-[#cc785c]/20 dark:border-primary/30 text-[#cc785c] dark:text-primary"
|
|
|
|
|
|
:platform-options="platformPresets.gemini.options"
|
|
|
|
|
|
:install-command="geminiInstallCommand"
|
|
|
|
|
|
:config-files="[
|
|
|
|
|
|
{ path: '~/.gemini/.env', content: geminiEnvConfig, language: 'dotenv' },
|
|
|
|
|
|
{ path: '~/.gemini/settings.json', content: geminiSettingsConfig, language: 'json' }
|
|
|
|
|
|
]"
|
|
|
|
|
|
:badge-style="getBadgeStyle(SECTIONS.GEMINI)"
|
|
|
|
|
|
:title-style="getTitleStyle(SECTIONS.GEMINI)"
|
|
|
|
|
|
:desc-style="getDescStyle(SECTIONS.GEMINI)"
|
|
|
|
|
|
:card-style-fn="(idx) => getCardStyle(SECTIONS.GEMINI, idx)"
|
|
|
|
|
|
content-position="right"
|
|
|
|
|
|
@copy="copyToClipboard"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #logo>
|
|
|
|
|
|
<GeminiStarCluster :is-visible="currentSection === SECTIONS.GEMINI && sectionVisibility[SECTIONS.GEMINI] > 0.05" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</CliSection>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section 4: Features -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<section
|
|
|
|
|
|
ref="section4"
|
|
|
|
|
|
class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20 relative overflow-hidden"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div class="max-w-4xl mx-auto text-center relative z-10">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="inline-flex items-center gap-2 rounded-full bg-[#cc785c]/10 dark:bg-purple-500/20 border border-[#cc785c]/20 dark:border-purple-500/40 px-4 py-2 text-sm font-medium text-[#cc785c] dark:text-purple-300 mb-6 backdrop-blur-sm transition-all duration-500"
|
|
|
|
|
|
:style="getBadgeStyle(SECTIONS.FEATURES)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Sparkles class="h-4 w-4" />
|
|
|
|
|
|
项目进度
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<h2
|
|
|
|
|
|
class="text-4xl md:text-5xl font-bold text-[#191919] dark:text-white mb-6 transition-all duration-700"
|
|
|
|
|
|
:style="getTitleStyle(SECTIONS.FEATURES)"
|
|
|
|
|
|
>
|
|
|
|
|
|
功能开发进度
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<p
|
|
|
|
|
|
class="text-lg text-[#666663] dark:text-gray-300 mb-12 max-w-2xl mx-auto transition-all duration-700"
|
|
|
|
|
|
:style="getDescStyle(SECTIONS.FEATURES)"
|
|
|
|
|
|
>
|
|
|
|
|
|
核心 API 代理功能已完成,正在载入更多功能
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid md:grid-cols-3 gap-6">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(feature, idx) in featureCards"
|
|
|
|
|
|
:key="idx"
|
|
|
|
|
|
class="bg-white/70 dark:bg-[#262624]/80 backdrop-blur-sm rounded-2xl p-6 border border-[#e5e4df] dark:border-[rgba(227,224,211,0.16)] hover:border-[#cc785c]/30 dark:hover:border-[#d4a27f]/40 transition-all duration-700"
|
|
|
|
|
|
:style="getFeatureCardStyle(SECTIONS.FEATURES, idx)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex h-12 w-12 items-center justify-center rounded-xl mb-4 mx-auto"
|
|
|
|
|
|
:class="feature.status === 'completed'
|
|
|
|
|
|
? 'bg-emerald-500/10 dark:bg-emerald-500/15'
|
|
|
|
|
|
: 'bg-[#cc785c]/10 dark:bg-[#cc785c]/15'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<component
|
|
|
|
|
|
:is="feature.icon"
|
|
|
|
|
|
class="h-6 w-6"
|
|
|
|
|
|
:class="feature.status === 'completed'
|
|
|
|
|
|
? 'text-emerald-500 dark:text-emerald-400'
|
|
|
|
|
|
: 'text-[#cc785c] dark:text-[#d4a27f] animate-spin'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<h3 class="text-lg font-bold text-[#191919] dark:text-white mb-2">
|
|
|
|
|
|
{{ feature.title }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-sm text-[#666663] dark:text-[#c9c3b4]">
|
|
|
|
|
|
{{ feature.desc }}
|
|
|
|
|
|
</p>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="mt-3 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium"
|
|
|
|
|
|
:class="feature.status === 'completed'
|
|
|
|
|
|
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
|
|
|
|
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ feature.status === 'completed' ? '已完成' : '进行中' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="mt-12 transition-all duration-700"
|
|
|
|
|
|
:style="getButtonsStyle(SECTIONS.FEATURES)"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<RouterLink
|
|
|
|
|
|
v-if="authStore.isAuthenticated"
|
|
|
|
|
|
:to="dashboardPath"
|
|
|
|
|
|
class="inline-flex items-center gap-2 rounded-xl bg-primary hover:bg-primary/90 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-primary/30 transition hover:shadow-primary/50 hover:scale-105"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Rocket class="h-5 w-5" />
|
|
|
|
|
|
立即开始使用
|
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="inline-flex items-center gap-2 rounded-xl bg-primary hover:bg-primary/90 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-primary/30 transition hover:shadow-primary/50 hover:scale-105"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
@click="showLoginDialog = true"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Rocket class="h-5 w-5" />
|
|
|
|
|
|
立即开始使用
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Footer -->
|
|
|
|
|
|
<footer class="relative z-10 border-t border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-md py-8">
|
|
|
|
|
|
<div class="mx-auto max-w-7xl px-6">
|
|
|
|
|
|
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<p class="text-sm text-[#91918d] dark:text-muted-foreground">
|
|
|
|
|
|
© 2025 Aether. 团队内部使用
|
|
|
|
|
|
</p>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div class="flex items-center gap-6 text-sm text-[#91918d] dark:text-muted-foreground">
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<a
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
class="transition hover:text-[#191919] dark:hover:text-white"
|
|
|
|
|
|
>使用条款</a>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
class="transition hover:text-[#191919] dark:hover:text-white"
|
|
|
|
|
|
>隐私政策</a>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
class="transition hover:text-[#191919] dark:hover:text-white"
|
|
|
|
|
|
>技术支持</a>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
|
|
|
|
|
<LoginDialog v-model="showLoginDialog" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
|
|
import { RouterLink } from 'vue-router'
|
|
|
|
|
|
import {
|
|
|
|
|
|
ChevronDown,
|
|
|
|
|
|
Code2,
|
|
|
|
|
|
Moon,
|
|
|
|
|
|
Rocket,
|
|
|
|
|
|
Sparkles,
|
|
|
|
|
|
Sun,
|
|
|
|
|
|
SunMoon,
|
|
|
|
|
|
Terminal
|
|
|
|
|
|
} from 'lucide-vue-next'
|
|
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
|
|
import { useDarkMode } from '@/composables/useDarkMode'
|
|
|
|
|
|
import { useClipboard } from '@/composables/useClipboard'
|
|
|
|
|
|
import LoginDialog from '@/features/auth/components/LoginDialog.vue'
|
|
|
|
|
|
import RippleLogo from '@/components/RippleLogo.vue'
|
|
|
|
|
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
|
|
|
|
|
import AetherLineByLineLogo from '@/components/AetherLineByLineLogo.vue'
|
|
|
|
|
|
import GeminiStarCluster from '@/components/GeminiStarCluster.vue'
|
|
|
|
|
|
import CliSection from './CliSection.vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
SECTIONS,
|
|
|
|
|
|
sections,
|
|
|
|
|
|
featureCards,
|
|
|
|
|
|
useCliConfigs,
|
|
|
|
|
|
platformPresets,
|
|
|
|
|
|
getInstallCommand,
|
|
|
|
|
|
getLogoType,
|
|
|
|
|
|
getLogoClass
|
|
|
|
|
|
} from './home-config'
|
|
|
|
|
|
import {
|
|
|
|
|
|
useSectionAnimations,
|
|
|
|
|
|
useLogoPosition,
|
|
|
|
|
|
useLogoTransition
|
|
|
|
|
|
} from './useSectionAnimations'
|
|
|
|
|
|
|
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
|
|
|
|
|
|
const { copyToClipboard } = useClipboard()
|
|
|
|
|
|
|
|
|
|
|
|
const dashboardPath = computed(() =>
|
|
|
|
|
|
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
|
|
|
|
|
|
)
|
|
|
|
|
|
const baseUrl = computed(() => window.location.origin)
|
|
|
|
|
|
|
|
|
|
|
|
// Scroll state
|
|
|
|
|
|
const scrollContainer = ref<HTMLElement | null>(null)
|
|
|
|
|
|
const currentSection = ref(0)
|
|
|
|
|
|
const previousSection = ref(0)
|
|
|
|
|
|
const scrollDirection = ref<'up' | 'down'>('down')
|
|
|
|
|
|
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
|
|
|
|
const sectionVisibility = ref<number[]>([0, 0, 0, 0, 0])
|
|
|
|
|
|
let lastScrollY = 0
|
|
|
|
|
|
|
|
|
|
|
|
// Section refs - section0 and section4 are direct HTML elements, section1-3 are CliSection components
|
|
|
|
|
|
const section0 = ref<HTMLElement | null>(null)
|
|
|
|
|
|
const section1 = ref<InstanceType<typeof CliSection> | null>(null)
|
|
|
|
|
|
const section2 = ref<InstanceType<typeof CliSection> | null>(null)
|
|
|
|
|
|
const section3 = ref<InstanceType<typeof CliSection> | null>(null)
|
|
|
|
|
|
const section4 = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to get DOM element from ref (handles both direct elements and component instances)
|
|
|
|
|
|
const getSectionElement = (index: number): HTMLElement | null => {
|
|
|
|
|
|
switch (index) {
|
|
|
|
|
|
case 0: return section0.value
|
|
|
|
|
|
case 1: return (section1.value?.sectionEl as HTMLElement | null | undefined) ?? null
|
|
|
|
|
|
case 2: return (section2.value?.sectionEl as HTMLElement | null | undefined) ?? null
|
|
|
|
|
|
case 3: return (section3.value?.sectionEl as HTMLElement | null | undefined) ?? null
|
|
|
|
|
|
case 4: return section4.value
|
|
|
|
|
|
default: return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Logo refs
|
|
|
|
|
|
const aetherLogoRef = ref<InstanceType<typeof AetherLineByLineLogo> | null>(null)
|
|
|
|
|
|
const rippleLogoRef = ref<InstanceType<typeof RippleLogo> | null>(null)
|
|
|
|
|
|
const hasLogoAnimationStarted = ref(false)
|
|
|
|
|
|
const geminiFillComplete = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Animation composables
|
|
|
|
|
|
const {
|
|
|
|
|
|
getBadgeStyle,
|
|
|
|
|
|
getTitleStyle,
|
|
|
|
|
|
getDescStyle,
|
|
|
|
|
|
getButtonsStyle,
|
|
|
|
|
|
getScrollIndicatorStyle,
|
|
|
|
|
|
getCardStyle,
|
|
|
|
|
|
getFeatureCardStyle
|
|
|
|
|
|
} = useSectionAnimations(sectionVisibility)
|
|
|
|
|
|
|
|
|
|
|
|
const { fixedLogoStyle } = useLogoPosition(currentSection, windowWidth)
|
|
|
|
|
|
const { logoTransitionName } = useLogoTransition(currentSection, previousSection)
|
|
|
|
|
|
|
|
|
|
|
|
// Logo computed
|
|
|
|
|
|
const currentLogoType = computed(() => getLogoType(currentSection.value))
|
|
|
|
|
|
const currentLogoClass = computed(() => getLogoClass(currentSection.value))
|
|
|
|
|
|
const logoTransitionDelay = computed(() => {
|
|
|
|
|
|
if (currentSection.value === SECTIONS.FEATURES) return 0
|
|
|
|
|
|
if (previousSection.value === SECTIONS.FEATURES) return 200
|
|
|
|
|
|
return 500
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Platform states
|
|
|
|
|
|
const claudePlatform = ref(platformPresets.claude.defaultValue)
|
|
|
|
|
|
const codexPlatform = ref(platformPresets.codex.defaultValue)
|
|
|
|
|
|
const geminiPlatform = ref(platformPresets.gemini.defaultValue)
|
|
|
|
|
|
|
|
|
|
|
|
// Install commands
|
|
|
|
|
|
const claudeInstallCommand = computed(() => getInstallCommand('claude', claudePlatform.value))
|
|
|
|
|
|
const codexInstallCommand = computed(() => getInstallCommand('codex', codexPlatform.value))
|
|
|
|
|
|
const geminiInstallCommand = computed(() => getInstallCommand('gemini', geminiPlatform.value))
|
|
|
|
|
|
|
|
|
|
|
|
// CLI configs
|
|
|
|
|
|
const { claudeConfig, codexConfig, codexAuthConfig, geminiEnvConfig, geminiSettingsConfig } =
|
|
|
|
|
|
useCliConfigs(baseUrl)
|
|
|
|
|
|
|
|
|
|
|
|
// Dialog state
|
|
|
|
|
|
const showLoginDialog = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Scroll handling
|
|
|
|
|
|
let scrollEndTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
|
|
|
|
|
|
const calculateVisibility = (element: HTMLElement | null): number => {
|
|
|
|
|
|
if (!element) return 0
|
|
|
|
|
|
const rect = element.getBoundingClientRect()
|
|
|
|
|
|
const containerHeight = window.innerHeight
|
|
|
|
|
|
if (rect.bottom < 0 || rect.top > containerHeight) return 0
|
|
|
|
|
|
const elementCenter = rect.top + rect.height / 2
|
|
|
|
|
|
const viewportCenter = containerHeight / 2
|
|
|
|
|
|
const distanceFromCenter = Math.abs(elementCenter - viewportCenter)
|
|
|
|
|
|
const maxDistance = containerHeight / 2 + rect.height / 2
|
|
|
|
|
|
return Math.max(0, 1 - distanceFromCenter / maxDistance)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
if (!scrollContainer.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const containerHeight = window.innerHeight
|
|
|
|
|
|
const newScrollY = scrollContainer.value.scrollTop
|
|
|
|
|
|
|
|
|
|
|
|
// Track scroll direction
|
|
|
|
|
|
scrollDirection.value = newScrollY > lastScrollY ? 'down' : 'up'
|
|
|
|
|
|
lastScrollY = newScrollY
|
|
|
|
|
|
|
|
|
|
|
|
// Update visibility
|
|
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
|
|
|
sectionVisibility.value[i] = calculateVisibility(getSectionElement(i))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update current section
|
|
|
|
|
|
const scrollMiddle = newScrollY + containerHeight / 2
|
|
|
|
|
|
for (let i = 4; i >= 0; i--) {
|
|
|
|
|
|
const section = getSectionElement(i)
|
|
|
|
|
|
if (section && section.offsetTop <= scrollMiddle) {
|
|
|
|
|
|
if (currentSection.value !== i) {
|
|
|
|
|
|
previousSection.value = currentSection.value
|
|
|
|
|
|
currentSection.value = i
|
|
|
|
|
|
hasLogoAnimationStarted.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Detect snap complete
|
|
|
|
|
|
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
|
|
|
|
|
scrollEndTimer = setTimeout(() => {
|
|
|
|
|
|
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
|
|
|
|
|
hasLogoAnimationStarted.value = true
|
|
|
|
|
|
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 150)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scrollToSection = (index: number) => {
|
|
|
|
|
|
const target = getSectionElement(index)
|
|
|
|
|
|
if (target) target.scrollIntoView({ behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Watch Gemini fill complete
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => rippleLogoRef.value?.fillComplete,
|
|
|
|
|
|
(val) => {
|
|
|
|
|
|
if (currentSection.value === SECTIONS.GEMINI && val) geminiFillComplete.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(currentSection, (_, old) => {
|
|
|
|
|
|
if (old === SECTIONS.GEMINI) geminiFillComplete.value = false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
|
windowWidth.value = window.innerWidth
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
scrollContainer.value?.addEventListener('scroll', handleScroll, { passive: true })
|
|
|
|
|
|
window.addEventListener('resize', handleResize, { passive: true })
|
|
|
|
|
|
handleScroll()
|
|
|
|
|
|
|
|
|
|
|
|
// Initial animation
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
|
|
|
|
|
hasLogoAnimationStarted.value = true
|
|
|
|
|
|
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 300)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
scrollContainer.value?.removeEventListener('scroll', handleScroll)
|
|
|
|
|
|
window.removeEventListener('resize', handleResize)
|
|
|
|
|
|
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* Typography */
|
|
|
|
|
|
h1, h2, h3 {
|
|
|
|
|
|
font-family: var(--serif);
|
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
|
font-family: var(--serif);
|
|
|
|
|
|
letter-spacing: 0.01em;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
button, nav, a, .inline-flex {
|
|
|
|
|
|
font-family: var(--sans-serif);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Panel styles */
|
|
|
|
|
|
.command-panel-surface {
|
|
|
|
|
|
border-color: var(--color-border);
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
backdrop-filter: blur(12px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dark .command-panel-surface {
|
|
|
|
|
|
background: rgba(38, 38, 36, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Performance */
|
|
|
|
|
|
h1, h2, p {
|
|
|
|
|
|
will-change: transform, opacity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Scroll indicator */
|
|
|
|
|
|
.scroll-indicator {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 2rem;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1023px) {
|
|
|
|
|
|
.scroll-indicator {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scroll-indicator-btn {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
padding: 0.25rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scroll-indicator-label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 1.5rem;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #666663;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
|
border-radius: 0.25rem;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dark .scroll-indicator-label {
|
|
|
|
|
|
color: #a0a0a0;
|
|
|
|
|
|
background: rgba(25, 23, 20, 0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scroll-indicator-btn:hover .scroll-indicator-label {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scroll-indicator-dot {
|
|
|
|
|
|
width: 10px;
|
|
|
|
|
|
height: 10px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 2px solid #d4d4d4;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dark .scroll-indicator-dot {
|
|
|
|
|
|
border-color: #4a4a4a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scroll-indicator-dot.active {
|
|
|
|
|
|
background: #cc785c;
|
|
|
|
|
|
border-color: #cc785c;
|
|
|
|
|
|
transform: scale(1.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Logo transitions */
|
|
|
|
|
|
.logo-scale-enter-active {
|
|
|
|
|
|
transition: opacity 0.5s ease-out, transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-scale-leave-active {
|
|
|
|
|
|
transition: opacity 0.3s ease-in, transform 0.3s ease-in;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-scale-enter-from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: scale(0.6) rotate(-8deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-scale-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: scale(1.2) rotate(8deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-slide-left-enter-active,
|
|
|
|
|
|
.logo-slide-right-enter-active {
|
|
|
|
|
|
transition: opacity 0.4s ease-out, transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-slide-left-leave-active,
|
|
|
|
|
|
.logo-slide-right-leave-active {
|
|
|
|
|
|
transition: opacity 0.25s ease-in, transform 0.3s ease-in;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-slide-left-enter-from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(60px) scale(0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-slide-left-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-60px) scale(0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-slide-right-enter-from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-60px) scale(0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-slide-right-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(60px) scale(0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Logo container */
|
|
|
|
|
|
.logo-container {
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
height: 320px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-container.home-section {
|
|
|
|
|
|
width: 400px;
|
|
|
|
|
|
height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-container > * {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.logo-container {
|
|
|
|
|
|
width: 240px;
|
|
|
|
|
|
height: 240px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.logo-container.home-section {
|
|
|
|
|
|
width: 280px;
|
|
|
|
|
|
height: 280px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Heartbeat animation */
|
|
|
|
|
|
.heartbeat-wrapper {
|
|
|
|
|
|
animation: heartbeat 1.5s ease-in-out infinite;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes heartbeat {
|
|
|
|
|
|
0%, 70%, 100% { transform: scale(1); }
|
|
|
|
|
|
14% { transform: scale(1.06); }
|
|
|
|
|
|
28% { transform: scale(1); }
|
|
|
|
|
|
42% { transform: scale(1.1); }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|