Publish Xiaomi subsite without APK artifact
@@ -5,6 +5,7 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'site/**'
|
||||
- 'site-internal/**'
|
||||
- '.github/workflows/deploy-site.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -26,8 +27,12 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
cache-dependency-path: |
|
||||
site/package-lock.json
|
||||
site-internal/package-lock.json
|
||||
- run: cd site && npm install && VITE_BASE=/multi-casual-android/ npm run build
|
||||
- run: cd site-internal && npm install && VITE_BASE=/multi-casual-android/xiaomi/ npm run build
|
||||
- run: mkdir -p site/dist/xiaomi && cp -R site-internal/dist/. site/dist/xiaomi/
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: site/dist
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.edgeone/
|
||||
*.tsbuildinfo
|
||||
public/downloads/*.apk
|
||||
@@ -0,0 +1,78 @@
|
||||
# site-internal — Multica Xiaomi internal landing page
|
||||
|
||||
A separate Vite + React landing page for the **Multica Xiaomi** internal
|
||||
Android build. The public site at [`../site/`](../site/) targets the
|
||||
Multi-Casual public release; this directory is a sibling project
|
||||
published under the same GitHub Pages site at `/xiaomi/`.
|
||||
|
||||
## Visual scope
|
||||
|
||||
This page is a near-clone of the public site (`site/src/`) with three
|
||||
intentional differences:
|
||||
|
||||
1. The "Relationship to Multica" section is removed.
|
||||
2. The source link points to the private company GitLab project.
|
||||
The signed APK download points to a separate artifact-only GitHub
|
||||
release repository.
|
||||
3. The iOS App button is left untouched and still points at the
|
||||
public iOS repo — the same `github.com/park0er/multi-casual-ios-app`
|
||||
source-of-truth is reused on purpose.
|
||||
|
||||
All screenshots, the promo video, the journey / workspace interactions,
|
||||
the language toggle, and the layout are reused from `site/src/assets/`
|
||||
so the visual stays in sync with the public site.
|
||||
|
||||
## Release metadata
|
||||
|
||||
`src/App.tsx` points the primary APK download button at the static
|
||||
GitHub release asset:
|
||||
|
||||
```text
|
||||
https://github.com/park0er/multica-xiaomi-apk/releases/download/v0.1.5-xiaomi/Multica-Xiaomi-v0.1.5.apk
|
||||
```
|
||||
|
||||
The APK is copied from `app/build/outputs/apk/xiaomi/release/` into
|
||||
`site-internal/public/downloads/` before deployment. Current metadata:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `release.sha256` | `13e004472d33e66aa40aa7ebd58f75e22cfde8cce3a153729481f1be4d2d1414` |
|
||||
| `release.gitlabUrl` | `https://git.n.xiaomi.com/zhaoxisheng/multica-xiaomi-android` |
|
||||
| `release.releasesUrl` | `https://github.com/park0er/multica-xiaomi-apk/releases/download/v0.1.5-xiaomi/Multica-Xiaomi-v0.1.5.apk` |
|
||||
|
||||
Also confirm the `release.apiUrl` matches the staging or production
|
||||
Xiaomi Multica endpoint you want this build to talk to
|
||||
(`http://staging-multica.ad.xiaomi.srv` is the build.gradle default).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd site-internal
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output goes to `site-internal/dist/` and is gitignored.
|
||||
|
||||
## Deploy
|
||||
|
||||
The site is hosted as a subdirectory of the public GitHub Pages site:
|
||||
|
||||
```
|
||||
https://park0er.github.io/multi-casual-android/xiaomi/
|
||||
```
|
||||
|
||||
GitHub Actions builds the public site into the Pages root and this
|
||||
internal Xiaomi site into `dist/xiaomi/` before deploying.
|
||||
|
||||
### Publish flow
|
||||
|
||||
1. Copy `site-internal/` into the public repo.
|
||||
2. Commit and push the public repo.
|
||||
3. GitHub Actions deploys both `site/` and `site-internal/`.
|
||||
|
||||
### EdgeOne status
|
||||
|
||||
Tencent EdgeOne hosting was attempted first but proved unreliable for
|
||||
this internal distribution. The current source of truth for downloads is
|
||||
GitHub Pages.
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Multica Xiaomi is the Xiaomi-flavor Android build of the Multica companion app, distributed internally via the company GitLab." />
|
||||
<link rel="icon" href="./favicon.png" type="image/png" />
|
||||
<title>Multica Xiaomi · Internal Build</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "multica-xiaomi-internal-site",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview --host 127.0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "latest",
|
||||
"motion": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"typescript": "latest",
|
||||
"vite": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.16",
|
||||
"@types/react-dom": "^19.2.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="refresh" content="0.8; url=https://park0er.github.io/multi-casual-android/xiaomi/" />
|
||||
<title>跳转到 Multica Xiaomi</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #101218;
|
||||
--muted: #5b6472;
|
||||
--blue: #1647ff;
|
||||
--paper: #f6f8fc;
|
||||
--card: #ffffff;
|
||||
--line: rgba(16, 18, 24, 0.12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
main {
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
padding: 32px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--card);
|
||||
box-shadow: 0 24px 70px rgba(16, 18, 24, 0.12);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 26px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 24px;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
font-size: 15px;
|
||||
}
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
background: var(--blue);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
code {
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>正在跳转到 Multica Xiaomi</h1>
|
||||
<p>这是小米内网入口,目标页面托管在 GitHub Pages。</p>
|
||||
<a href="https://park0er.github.io/multi-casual-android/xiaomi/">立即打开</a>
|
||||
<code>https://park0er.github.io/multi-casual-android/xiaomi/</code>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,662 @@
|
||||
import logoImg from "./assets/logo.png";
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { motion, AnimatePresence, useMotionValue, useTransform, useReducedMotion } from "motion/react";
|
||||
import inboxImg from "./assets/screenshots/01-inbox.jpg";
|
||||
import issuesListImg from "./assets/screenshots/02-issues-list.jpg";
|
||||
import issuesBoardImg from "./assets/screenshots/03-issues-board.jpg";
|
||||
import issueDetailImg from "./assets/screenshots/04-issue-detail.jpg";
|
||||
import issueSubImg from "./assets/screenshots/05-issue-detail-subissues.jpg";
|
||||
import commentImg from "./assets/screenshots/06-comment-composer.jpg";
|
||||
import mentionImg from "./assets/screenshots/07-mention-picker.jpg";
|
||||
import projectsImg from "./assets/screenshots/08-projects-list.jpg";
|
||||
import projectDetailImg from "./assets/screenshots/09-project-detail.jpg";
|
||||
import agentsImg from "./assets/screenshots/10-agents-list.jpg";
|
||||
import runtimesImg from "./assets/screenshots/11-runtimes-list.jpg";
|
||||
import skillsImg from "./assets/screenshots/12-skills-list.jpg";
|
||||
import skillDetailImg from "./assets/screenshots/13-skill-detail.jpg";
|
||||
import settingsImg from "./assets/screenshots/14-settings.jpg";
|
||||
import promoVideo from "./assets/video/multica-android-promo-draft.mp4";
|
||||
|
||||
const release = {
|
||||
packageName: "ai.multica.app.xiaomi",
|
||||
sha256: "13e004472d33e66aa40aa7ebd58f75e22cfde8cce3a153729481f1be4d2d1414",
|
||||
gitlabUrl: "https://git.n.xiaomi.com/zhaoxisheng/multica-xiaomi-android",
|
||||
releasesUrl: "https://github.com/park0er/multica-xiaomi-apk/releases/download/v0.1.5-xiaomi/Multica-Xiaomi-v0.1.5.apk",
|
||||
iosUrl: "https://github.com/park0er/multi-casual-ios-app",
|
||||
multicaUrl: "https://multica.ai",
|
||||
apiUrl: "http://staging-multica.ad.xiaomi.srv",
|
||||
};
|
||||
|
||||
type Locale = "en" | "zh";
|
||||
|
||||
const copy: Record<Locale, {
|
||||
nav: { brand: string; download: string; trust: string; language: string };
|
||||
hero: { eyebrow: string; title: string; subtitle: string; download: string; gitlab: string; multica: string; ios: string };
|
||||
// relationship section intentionally removed for the internal build
|
||||
journey: { kicker: string; title: string; subtitle: string; cta: string; steps: Array<{ title: string; subtitle: string }> };
|
||||
workspace: { kicker: string; title: string; subtitle: string; items: Array<{ title: string; subtitle: string }> };
|
||||
video: { kicker: string; title: string; body: string };
|
||||
download: { kicker: string; title: string; package: string; api: string; sha: string; latest: string; source: string; note: string };
|
||||
trust: { kicker: string; items: string[] };
|
||||
footer: { tagline: string; multica: string; gitlab: string; ios: string; download: string };
|
||||
}> = {
|
||||
en: {
|
||||
nav: { brand: "Multica Xiaomi", download: "Download", trust: "Scope", language: "中文" },
|
||||
hero: {
|
||||
eyebrow: "Internal Xiaomi build · Company-only distribution",
|
||||
title: "Multica Xiaomi for the Xiaomi workspace.",
|
||||
subtitle: "An Android app independently built from scratch by Zhao Xisheng in Xiaomi's Commercial Platform Department. Multica has not provided an official Android app to date; this internal build gives Xiaomi users a native path today.",
|
||||
download: "Download APK",
|
||||
gitlab: "GitLab",
|
||||
multica: "Visit Multica",
|
||||
ios: "iOS App",
|
||||
},
|
||||
journey: {
|
||||
kicker: "Interactive walkthrough",
|
||||
title: "Try the flow.",
|
||||
subtitle: "Click each step or scroll to see the app in action.",
|
||||
cta: "Start the walkthrough",
|
||||
steps: [
|
||||
{ title: "Inbox notifications", subtitle: "Stay on top of activity — new issues, comments, and agent progress show up as they happen." },
|
||||
{ title: "Browse issues", subtitle: "See all tasks in a clean list view, ready for triage. Filter, search, and tap into any issue." },
|
||||
{ title: "Switch to board view", subtitle: "Tap the view toggle to switch to a kanban board that shows status flow at a glance." },
|
||||
{ title: "Open issue detail", subtitle: "Tap an issue card to see the full detail: title, status, labels, assignee, and body text." },
|
||||
{ title: "Explore context", subtitle: "Context guide points zoom into focus areas — title and status, body content, then subtasks and comments — and return to the full view between each." },
|
||||
{ title: "Scroll for more", subtitle: "Keep scrolling to see subtasks, subscribers, and the comment thread — all the structured context stays on one page." },
|
||||
{ title: "Draft a comment", subtitle: "Tap the comment box. The keyboard appears and a comment types out character by character." },
|
||||
{ title: "Mention anyone", subtitle: "Type @ and a picker appears with people, agents, and squads. Choose who to notify." },
|
||||
{ title: "Workspace overview", subtitle: "Switch between Projects, Agents, Runtimes, Skills, and Settings with workspace chips." },
|
||||
],
|
||||
},
|
||||
workspace: {
|
||||
kicker: "Workspace assets",
|
||||
title: "Projects, agents, and everything in between.",
|
||||
subtitle: "Switch between workspace views to see the full picture.",
|
||||
items: [
|
||||
{ title: "Projects", subtitle: "Browse projects with progress tracking" },
|
||||
{ title: "Project detail", subtitle: "Task stats, status fields, and resources" },
|
||||
{ title: "Agents", subtitle: "View agent status and recent activity" },
|
||||
{ title: "Runtimes", subtitle: "Check runtime availability and versions" },
|
||||
{ title: "Skills", subtitle: "Browse the workspace agent skill library" },
|
||||
{ title: "Skill detail", subtitle: "Inspect skill structure and documentation" },
|
||||
{ title: "Settings", subtitle: "Account, workspace, and preference controls" },
|
||||
],
|
||||
},
|
||||
video: { kicker: "Product demo", title: "See it in 30 seconds.", body: "A fast walkthrough of inbox, issues, context, comments, mentions, and workspace views." },
|
||||
download: { kicker: "Internal build", title: "Download the Xiaomi internal APK from GitHub Pages.", package: "Package", api: "Service API", sha: "APK SHA256", latest: "Download APK", source: "View source", note: "Internal distribution only — not for external release." },
|
||||
trust: {
|
||||
kicker: "Internal build scope",
|
||||
items: [
|
||||
"Designed and implemented from scratch by Zhao Xisheng of Xiaomi's Commercial Platform Department, with no upstream Android app to inherit from.",
|
||||
"Multica has not provided an official Android app to date; this project is an independent Xiaomi internal Android client for Multica workflows.",
|
||||
"Company-internal distribution only. APK download is served from a separate GitHub release artifact repository after EdgeOne proved unreliable.",
|
||||
"Connects to the Xiaomi Multica service endpoint, with analytics routed through OneTrack and disabled for Umeng / PostHog.",
|
||||
"Source is kept in the private company GitLab project; the signed APK is distributed from GitHub Pages.",
|
||||
],
|
||||
},
|
||||
footer: { tagline: "Built from scratch by Zhao Xisheng for the Xiaomi workspace. APK served via GitHub Pages.", multica: "Multica website", gitlab: "GitLab", ios: "iOS App", download: "Download APK" },
|
||||
},
|
||||
zh: {
|
||||
nav: { brand: "Multica Xiaomi", download: "下载", trust: "分发说明", language: "English" },
|
||||
hero: {
|
||||
eyebrow: "小米内部构建 · 仅限公司内部分发",
|
||||
title: "面向小米工作区的 Multica Xiaomi 内部版。",
|
||||
subtitle: "由小米商业平台部赵锡盛完全从头自研的 Android App。Multica 官方到目前为止还没有提供 Android App;这套内部构建先为小米用户补上原生移动端入口。",
|
||||
download: "下载 APK",
|
||||
gitlab: "GitLab",
|
||||
multica: "访问 Multica",
|
||||
ios: "iOS App",
|
||||
},
|
||||
journey: {
|
||||
kicker: "交互式演示",
|
||||
title: "试试这个流程。",
|
||||
subtitle: "点击每个步骤,或滚动页面查看 app 实际操作。",
|
||||
cta: "开始演示",
|
||||
steps: [
|
||||
{ title: "Inbox 通知", subtitle: "新 issues、评论和 agent 进展会实时出现在收件箱,不错过任何动态。" },
|
||||
{ title: "浏览 Issues", subtitle: "在列表视图中查看所有任务,快速筛选和进入任意 issue。" },
|
||||
{ title: "切换看板视图", subtitle: "点击视图切换按钮,看板视图让你一眼看到任务状态流。" },
|
||||
{ title: "打开 Issue 详情", subtitle: "点击 issue 卡片查看完整详情:标题、状态、标签、负责人和正文。" },
|
||||
{ title: "探索上下文", subtitle: "Context 引导点依次放大标题与状态、正文内容、子任务与评论,然后缩回全貌。" },
|
||||
{ title: "滚动查看更多", subtitle: "继续下滑查看子任务、订阅者和评论线程 —— 结构化上下文都在同一页。" },
|
||||
{ title: "草拟评论", subtitle: "点击评论框,键盘出现,评论逐字输入。" },
|
||||
{ title: "@ 提及任何人", subtitle: "输入 @ 弹出选择器,可选人、agent 或小队。" },
|
||||
{ title: "Workspace 总览", subtitle: "通过 workspace chips 切换查看 Projects、Agents、Runtimes、Skills 和 Settings。" },
|
||||
],
|
||||
},
|
||||
workspace: {
|
||||
kicker: "Workspace 资产",
|
||||
title: "项目、agents,以及它们之间的一切。",
|
||||
subtitle: "切换 workspace 视图,掌握全貌。",
|
||||
items: [
|
||||
{ title: "Projects", subtitle: "浏览项目和进度追踪" },
|
||||
{ title: "项目详情", subtitle: "任务统计、状态字段和资源入口" },
|
||||
{ title: "Agents", subtitle: "查看 agent 状态和最近活动" },
|
||||
{ title: "Runtimes", subtitle: "检查运行时可用性和版本" },
|
||||
{ title: "Skills", subtitle: "浏览 workspace agent 技能库" },
|
||||
{ title: "技能详情", subtitle: "查看技能结构和文档" },
|
||||
{ title: "Settings", subtitle: "账号、工作区和偏好控制" },
|
||||
],
|
||||
},
|
||||
video: { kicker: "产品演示", title: "30 秒看完核心流程。", body: "快速演示 inbox 通知、issues 看板、上下文探索、评论回复、@ 提及和 workspace 视图切换。" },
|
||||
download: { kicker: "内部构建", title: "从 GitHub Pages 下载小米内部 APK。", package: "Package", api: "Service API", sha: "APK SHA256", latest: "下载 APK", source: "查看源码", note: "仅供公司内部分发,不对外发布。" },
|
||||
trust: {
|
||||
kicker: "内部构建说明",
|
||||
items: [
|
||||
"由小米商业平台部赵锡盛完全从头设计和实现,不是基于 Multica 官方 Android 客户端改造。",
|
||||
"Multica 官方到目前为止还没有提供 Android App;本项目是面向小米内部 Multica 工作流的独立 Android 客户端。",
|
||||
"仅供公司内部分发;由于腾讯云 EdgeOne 访问不稳定,APK 下载改由单独的 GitHub Release 产物仓库托管提供。",
|
||||
"连接小米 Multica 服务端点,统计走 OneTrack,Umeng / PostHog 已关闭。",
|
||||
"源码保存在私有公司 GitLab 项目中;签名 APK 从 GitHub Pages 分发。",
|
||||
],
|
||||
},
|
||||
footer: { tagline: "商业平台部赵锡盛从头自研,APK 由 GitHub Pages 分发。", multica: "Multica 官网", gitlab: "GitLab", ios: "iOS App", download: "下载 APK" },
|
||||
},
|
||||
};
|
||||
|
||||
const JOURNEY_STEPS = 9;
|
||||
const TYPING_TEXT = "Looks good. I'll ask the agent to check the failing case and post a follow-up.";
|
||||
const MENTION_ITEMS = [
|
||||
{ name: "codex", color: "#1647ff" },
|
||||
{ name: "Opus", color: "#7c3aed" },
|
||||
{ name: "RollieOC", color: "#059669" },
|
||||
];
|
||||
|
||||
const WORKSPACE_SCREENSHOTS = [projectsImg, projectDetailImg, agentsImg, runtimesImg, skillsImg, skillDetailImg, settingsImg];
|
||||
|
||||
const fade = (reduced: boolean, delay = 0) => ({
|
||||
initial: reduced ? {} : { opacity: 0, y: 18 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-60px" },
|
||||
transition: { duration: 0.65, delay, ease: [0.25, 0.1, 0.25, 1] as const },
|
||||
});
|
||||
|
||||
const reveal = (reduced: boolean) => ({
|
||||
initial: reduced ? {} : { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-80px" },
|
||||
transition: { duration: 0.7, ease: [0.25, 0.1, 0.25, 1] as const },
|
||||
});
|
||||
|
||||
/* ── PhoneDemo ── */
|
||||
function PhoneDemo({
|
||||
src,
|
||||
reduced,
|
||||
zoomScale = 1,
|
||||
zoomX = 0,
|
||||
zoomY = 0,
|
||||
children,
|
||||
}: {
|
||||
src: string;
|
||||
reduced: boolean;
|
||||
zoomScale?: number;
|
||||
zoomX?: number;
|
||||
zoomY?: number;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="phone-shell-compact">
|
||||
<div className="speaker" />
|
||||
<div className="phone-screen-compact">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.img
|
||||
key={src}
|
||||
src={src}
|
||||
alt="App screenshot"
|
||||
initial={reduced ? {} : { opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: zoomScale,
|
||||
x: zoomX,
|
||||
y: zoomY,
|
||||
}}
|
||||
exit={reduced ? {} : { opacity: 0 }}
|
||||
transition={{ duration: reduced ? 0.1 : 0.4, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Typewriter effect ── */
|
||||
function TypewriterEffect({ active, text, reduced }: { active: boolean; text: string; reduced: boolean }) {
|
||||
const charCount = useMotionValue(0);
|
||||
const chars = useMemo(() => text.split(""), [text]);
|
||||
const charRefs = useRef<(HTMLSpanElement | null)[]>([]);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Imperatively update char opacity from motion value (no hooks in loops)
|
||||
useEffect(() => {
|
||||
const unsubscribe = charCount.on("change", (v) => {
|
||||
const count = Math.round(v);
|
||||
charRefs.current.forEach((el, i) => {
|
||||
if (el) el.style.opacity = i < count ? "1" : "0";
|
||||
});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [charCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
if (!active) { charCount.set(0); charRefs.current.forEach((el) => { if (el) el.style.opacity = "0"; }); return; }
|
||||
if (reduced) {
|
||||
charCount.set(chars.length);
|
||||
charRefs.current.forEach((el, i) => { if (el) el.style.opacity = i < chars.length ? "1" : "0"; });
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
const interval = setInterval(() => {
|
||||
i++;
|
||||
charCount.set(i);
|
||||
if (i >= chars.length) clearInterval(interval);
|
||||
}, 42);
|
||||
intervalRef.current = interval;
|
||||
return () => clearInterval(interval);
|
||||
}, [active, reduced, chars.length]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="typing-line"
|
||||
animate={active ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{chars.map((char, i) => (
|
||||
<span
|
||||
key={i}
|
||||
ref={(el) => { charRefs.current[i] = el; }}
|
||||
style={{ opacity: 0 }}
|
||||
>
|
||||
{char === " " ? "\u00A0" : char}
|
||||
</span>
|
||||
))}
|
||||
<motion.span
|
||||
className="typing-cursor"
|
||||
animate={active ? { opacity: [1, 0.18] } : { opacity: 0 }}
|
||||
transition={active ? { duration: 0.64, repeat: Infinity, repeatType: "reverse" } : {}}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── PhoneMockup (hero) ── */
|
||||
function PhoneMockup({ reduced }: { reduced: boolean }) {
|
||||
return (
|
||||
<div className="hero-phone-stack">
|
||||
<motion.img
|
||||
src={inboxImg} alt="Inbox preview"
|
||||
initial={reduced ? {} : { opacity: 0, x: 60, rotate: -14 }}
|
||||
whileInView={{ opacity: 0.72, x: 0, rotate: -8 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.85, delay: 0.2, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||
/>
|
||||
<motion.img
|
||||
src={issuesListImg} alt="Issues list preview"
|
||||
initial={reduced ? {} : { opacity: 0, x: 80, rotate: 8 }}
|
||||
whileInView={{ opacity: 1, x: 0, rotate: 4 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.85, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||
/>
|
||||
<motion.img
|
||||
src={issueDetailImg} alt="Issue detail preview"
|
||||
initial={reduced ? {} : { opacity: 0, x: 60, rotate: 14 }}
|
||||
whileInView={{ opacity: 0.86, x: 0, rotate: 10 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.85, delay: 0.4, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Main App ── */
|
||||
export default function App() {
|
||||
const reduced = useReducedMotion() ?? false;
|
||||
const [lang, setLang] = useState<Locale>("en");
|
||||
const [step, setStep] = useState(0);
|
||||
const [activeWs, setActiveWs] = useState(0);
|
||||
const [hasInteracted, setHasInteracted] = useState(false);
|
||||
const completedRef = useRef<Set<number>>(new Set());
|
||||
const journeyRef = useRef<HTMLDivElement>(null);
|
||||
const hasAutoPlayedRef = useRef(false);
|
||||
const t = copy[lang];
|
||||
|
||||
// Auto-play journey on first scroll into view
|
||||
useEffect(() => {
|
||||
if (hasInteracted || hasAutoPlayedRef.current) return;
|
||||
const el = journeyRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && entry.intersectionRatio > 0.3 && !hasAutoPlayedRef.current) {
|
||||
hasAutoPlayedRef.current = true;
|
||||
let currentStep = 0;
|
||||
const advance = () => {
|
||||
currentStep++;
|
||||
if (currentStep < JOURNEY_STEPS) {
|
||||
setStep(currentStep);
|
||||
setTimeout(advance, 2200);
|
||||
}
|
||||
};
|
||||
setTimeout(advance, 2200);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [hasInteracted]);
|
||||
|
||||
const handleStepClick = useCallback((index: number) => {
|
||||
setHasInteracted(true);
|
||||
setStep(index);
|
||||
}, []);
|
||||
|
||||
const handleWorkspaceClick = useCallback((index: number) => {
|
||||
setActiveWs(index);
|
||||
}, []);
|
||||
|
||||
// Phone screen source based on journey step
|
||||
const phoneScreenSrc = useMemo(() => {
|
||||
if (step <= 1) return step === 0 ? inboxImg : issuesListImg;
|
||||
if (step === 2) return issuesBoardImg;
|
||||
if (step <= 5) return step === 3 ? issueDetailImg : issueSubImg;
|
||||
if (step === 6) return commentImg;
|
||||
if (step === 7) return mentionImg;
|
||||
return projectsImg;
|
||||
}, [step]);
|
||||
|
||||
// Focus zoom transform for step 4 (context explore)
|
||||
const focusZones = [
|
||||
{ scale: 1.6, x: 0, y: -120 }, // title/status
|
||||
{ scale: 1.45, x: 0, y: -40 }, // body
|
||||
{ scale: 1.5, x: 0, y: 80 }, // subtasks/comments
|
||||
];
|
||||
const focusIdx = step === 4 ? 0 : -1; // simple: show first zone; cycle handled by auto-play
|
||||
const zoom = step === 4 ? focusZones[focusIdx] || focusZones[0] : { scale: 1, x: 0, y: 0 };
|
||||
|
||||
return (
|
||||
<main lang={lang === "zh" ? "zh-CN" : "en"}>
|
||||
{/* Hero */}
|
||||
<section className="hero">
|
||||
<div className="hero-bg" />
|
||||
<motion.nav className="topbar" {...fade(reduced, 0)}>
|
||||
<div className="brand">
|
||||
<img src={logoImg} alt="Multica Xiaomi" className="brand-mark" />
|
||||
<span>{t.nav.brand}</span>
|
||||
</div>
|
||||
<div className="nav-links">
|
||||
<a href="#download">{t.nav.download}</a>
|
||||
<a href="#trust">{t.nav.trust}</a>
|
||||
<button className="language-toggle" onClick={() => setLang((l) => (l === "en" ? "zh" : "en"))}>
|
||||
{t.nav.language}
|
||||
</button>
|
||||
</div>
|
||||
</motion.nav>
|
||||
<div className="hero-grid">
|
||||
<div>
|
||||
<motion.p className="eyebrow" {...fade(reduced, 0.04)}>{t.hero.eyebrow}</motion.p>
|
||||
<motion.h1 {...fade(reduced, 0.14)}>{t.hero.title}</motion.h1>
|
||||
<motion.p className="hero-subtitle" {...fade(reduced, 0.24)}>{t.hero.subtitle}</motion.p>
|
||||
<motion.div className="cta-row" {...fade(reduced, 0.34)}>
|
||||
<motion.a href="#download" className="pill pill-android" whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}><span className="android-icon"></span>{t.hero.download}</motion.a>
|
||||
<motion.a className="button secondary" href={release.gitlabUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style={{verticalAlign: "middle", marginRight: 6}}><path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/></svg>{t.hero.gitlab}</motion.a>
|
||||
<motion.a className="button ghost" href={release.multicaUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>{t.hero.multica}</motion.a>
|
||||
<motion.a href={release.iosUrl} className="pill pill-ios" whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}><span className="apple-icon"></span>{t.hero.ios}</motion.a>
|
||||
</motion.div>
|
||||
</div>
|
||||
<PhoneMockup reduced={reduced} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* (Relationship section removed for the internal build.) */}
|
||||
|
||||
{/* Interactive Product Journey */}
|
||||
<section ref={journeyRef} className="journey section-pad">
|
||||
<motion.div className="journey-header" {...reveal(reduced)}>
|
||||
<div className="section-kicker">{t.journey.kicker}</div>
|
||||
<h2>{t.journey.title}</h2>
|
||||
<p>{t.journey.subtitle}</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="journey-layout">
|
||||
<div className="journey-steps">
|
||||
{t.journey.steps.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className={`journey-step ${step === i ? "journey-step-active" : ""} ${i < step ? "journey-step-completed" : ""}`}
|
||||
onClick={() => handleStepClick(i)}
|
||||
initial={reduced ? {} : { opacity: 0, x: -16 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: i * 0.06 }}
|
||||
>
|
||||
<div className="journey-step-number">{i < step ? "✓" : i + 1}</div>
|
||||
<div className="journey-step-content">
|
||||
<h3>{s.title}</h3>
|
||||
<p>{s.subtitle}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="journey-phone-area">
|
||||
<div className="journey-phone-container">
|
||||
<PhoneDemo
|
||||
src={phoneScreenSrc}
|
||||
reduced={reduced}
|
||||
zoomScale={step === 4 ? zoom.scale : 1}
|
||||
zoomX={step === 4 ? zoom.x : 0}
|
||||
zoomY={step === 4 ? zoom.y : 0}
|
||||
>
|
||||
{/* Hand + tap ripple for interactive steps */}
|
||||
{step >= 1 && step <= 3 && (
|
||||
<>
|
||||
<motion.div
|
||||
className="journey-hand"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
style={{ top: step === 1 ? "16%" : step === 2 ? "8%" : "35%", right: step === 2 ? "18%" : "22%" }}
|
||||
>👆</motion.div>
|
||||
<motion.div
|
||||
className="journey-tap-ripple"
|
||||
animate={{ scale: [1, 1.3, 1], opacity: [0.8, 0.3, 0.8] }}
|
||||
transition={{ duration: 1.2, repeat: Infinity }}
|
||||
style={{ top: step === 1 ? "18%" : step === 2 ? "10%" : "37%", right: step === 2 ? "14%" : "18%" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Typing effect for step 6 */}
|
||||
{step === 6 && <TypewriterEffect active text={TYPING_TEXT} reduced={reduced} />}
|
||||
|
||||
{/* Mention picker overlay for step 7 */}
|
||||
<AnimatePresence>
|
||||
{step === 7 && (
|
||||
<motion.div
|
||||
className="mention-overlay"
|
||||
initial={{ y: 60, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 60, opacity: 0 }}
|
||||
transition={{ duration: 0.35, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||
>
|
||||
<div className="mention-search">
|
||||
<span>@</span> cod
|
||||
</div>
|
||||
<div className="mention-list">
|
||||
{MENTION_ITEMS.map((m, i) => (
|
||||
<motion.div
|
||||
key={m.name}
|
||||
className={`mention-item ${i === 0 ? "mention-item-active" : ""}`}
|
||||
initial={{ opacity: 0, x: -12 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.15 + i * 0.1 }}
|
||||
>
|
||||
<div className="mention-avatar" style={{ background: m.color }}>{m.name[0].toUpperCase()}</div>
|
||||
<div>
|
||||
<div className="mention-name">{m.name}</div>
|
||||
<div className="mention-role">{i === 0 ? "agent" : i === 1 ? "agent" : "squad"}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Workspace chips overlay for step 8 */}
|
||||
{step === 8 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
style={{ position: "absolute", bottom: 8, left: 8, right: 8, zIndex: 6, display: "flex", gap: 4, justifyContent: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
{["Projects", "Agents", "Runtimes", "Skills", "Settings"].map((label, i) => (
|
||||
<motion.div
|
||||
key={label}
|
||||
className="workspace-chip"
|
||||
style={{ fontSize: 10, padding: "4px 8px", borderRadius: 999 }}
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.1 + i * 0.08 }}
|
||||
>
|
||||
{label}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</PhoneDemo>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Workspace Assets Grid */}
|
||||
<motion.section className="workspace-section section-pad" {...reveal(reduced)}>
|
||||
<div className="workspace-header">
|
||||
<div className="section-kicker">{t.workspace.kicker}</div>
|
||||
<h2>{t.workspace.title}</h2>
|
||||
<p>{t.workspace.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="workspace-chips">
|
||||
{t.workspace.items.map((item, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`workspace-chip ${activeWs === i ? "workspace-chip-active" : ""}`}
|
||||
onClick={() => handleWorkspaceClick(i)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="workspace-phone-wrap">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeWs}
|
||||
initial={reduced ? {} : { opacity: 0, scale: 0.96 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={reduced ? {} : { opacity: 0, scale: 0.96 }}
|
||||
transition={{ duration: 0.35 }}
|
||||
>
|
||||
<PhoneDemo src={WORKSPACE_SCREENSHOTS[activeWs]} reduced={reduced} />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<motion.div className="workspace-card-grid" {...reveal(reduced)}>
|
||||
{t.workspace.items.map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className={`workspace-card ${activeWs === i ? "workspace-card-active" : ""}`}
|
||||
onClick={() => handleWorkspaceClick(i)}
|
||||
initial={reduced ? {} : { opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: i * 0.06 }}
|
||||
>
|
||||
<img src={WORKSPACE_SCREENSHOTS[i]} alt={item.title} className="workspace-card-thumb" />
|
||||
<div className="workspace-card-label">
|
||||
<strong>{item.title}</strong>
|
||||
<span>{item.subtitle}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
|
||||
{/* Video Demo */}
|
||||
<motion.section className="video-demo section-pad" {...reveal(reduced)}>
|
||||
<div className="section-kicker">{t.video.kicker}</div>
|
||||
<div className="video-copy">
|
||||
<h2>{t.video.title}</h2>
|
||||
<p>{t.video.body}</p>
|
||||
</div>
|
||||
<video className="promo-video" src={promoVideo} controls playsInline preload="metadata" />
|
||||
</motion.section>
|
||||
|
||||
{/* Download */}
|
||||
<section id="download" className="download section-pad">
|
||||
<motion.div className="release-card" {...reveal(reduced)}>
|
||||
<div className="section-kicker">{t.download.kicker}</div>
|
||||
<h2>{t.download.title}</h2>
|
||||
<div className="release-grid">
|
||||
<div>
|
||||
<span>{t.download.package}</span>
|
||||
<code>{release.packageName}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t.download.api}</span>
|
||||
<code>{release.apiUrl}</code>
|
||||
</div>
|
||||
<div className="hash-cell">
|
||||
<span>{t.download.sha}</span>
|
||||
<code>{release.sha256}</code>
|
||||
</div>
|
||||
</div>
|
||||
<pre><code>{`adb install -r Multica-Xiaomi-v0.1.5.apk`}</code></pre>
|
||||
<div className="cta-row compact">
|
||||
<a href={release.releasesUrl} className="pill pill-android"><span className="android-icon"></span>{t.download.latest}</a>
|
||||
<a className="button secondary" href={release.gitlabUrl}>{t.download.source}</a>
|
||||
</div>
|
||||
<p className="star-hint">
|
||||
{lang === "en"
|
||||
? <>APK downloads are served by a separate GitHub release artifact repository; source lives on the company <a href={release.gitlabUrl} target="_blank" rel="noopener">GitLab</a>.</>
|
||||
: <>APK 由单独的 GitHub Release 产物仓库分发;源码保存在公司 <a href={release.gitlabUrl} target="_blank" rel="noopener">GitLab</a>。</>
|
||||
}
|
||||
</p>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Trust */}
|
||||
<section id="trust" className="trust section-pad">
|
||||
<div className="section-kicker">{t.trust.kicker}</div>
|
||||
<div className="trust-grid">
|
||||
{t.trust.items.map((item) => (
|
||||
<motion.div key={item} className="trust-item" {...reveal(reduced)}>
|
||||
<span className="check">✓</span>
|
||||
<p>{item}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="footer section-pad">
|
||||
<div>
|
||||
<strong>Multica Xiaomi</strong>
|
||||
<p>{t.footer.tagline}</p>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<a href={release.multicaUrl}>{t.footer.multica}</a>
|
||||
<a href={release.gitlabUrl}>{t.footer.gitlab}</a>
|
||||
<a href={release.releasesUrl} className="pill pill-android"><span className="android-icon"></span>{t.footer.download}</a>
|
||||
<a href={release.iosUrl} className="pill pill-ios"><span className="apple-icon"></span>{t.footer.ios}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,971 @@
|
||||
:root {
|
||||
--blue: #1647ff;
|
||||
--blue-2: #0f33c8;
|
||||
--yellow: #ffe45c;
|
||||
--ink: #101218;
|
||||
--muted: #687080;
|
||||
--paper: #f6f8fc;
|
||||
--card: #ffffff;
|
||||
--line: rgba(16, 18, 24, 0.1);
|
||||
--shadow: 0 24px 70px rgba(16, 18, 24, 0.14);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 82% 5%, rgba(255, 228, 92, 0.4), transparent 31%),
|
||||
radial-gradient(circle at 10% 30%, rgba(22, 71, 255, 0.16), transparent 30%),
|
||||
linear-gradient(180deg, #fbfcff 0%, #e8eeff 55%, #d4dff9 85%, #c4d4f5 100%);
|
||||
}
|
||||
|
||||
body, button, input { font: inherit; }
|
||||
|
||||
button { cursor: pointer; }
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
.section-pad { padding: 80px clamp(22px, 5vw, 76px); }
|
||||
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-top: 26px;
|
||||
padding-left: clamp(16px, 5vw, 76px);
|
||||
padding-right: clamp(16px, 5vw, 76px);
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(16, 18, 24, 0.045) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(16, 18, 24, 0.045) 1px, transparent 1px);
|
||||
background-size: 56px 56px;
|
||||
mask-image: linear-gradient(to bottom, black 0%, transparent 88%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
width: min(1160px, 100%);
|
||||
margin: 0 auto 70px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(18px);
|
||||
box-shadow: 0 12px 36px rgba(16, 18, 24, 0.06);
|
||||
}
|
||||
|
||||
.brand, .nav-links { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
.brand { font-weight: 850; letter-spacing: -0.02em; }
|
||||
|
||||
.brand-mark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.nav-links a:hover { color: var(--ink); }
|
||||
|
||||
.language-toggle {
|
||||
color: var(--blue);
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(22, 71, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 71, 255, 0.07);
|
||||
}
|
||||
|
||||
.language-toggle:hover { background: rgba(22, 71, 255, 0.12); }
|
||||
|
||||
.hero-grid {
|
||||
width: min(1160px, 100%);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.06fr) minmax(320px, 0.72fr);
|
||||
gap: clamp(28px, 7vw, 96px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eyebrow, .section-kicker {
|
||||
margin: 0 0 16px;
|
||||
color: var(--blue);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1, h2, h3, p { margin-top: 0; }
|
||||
|
||||
h1 {
|
||||
max-width: 780px;
|
||||
margin-bottom: 24px;
|
||||
font-size: clamp(54px, 8.2vw, 112px);
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.075em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(36px, 5vw, 72px);
|
||||
line-height: 0.94;
|
||||
letter-spacing: -0.065em;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
main[lang="zh-CN"] h1 {
|
||||
max-width: 700px;
|
||||
font-size: clamp(42px, 6.8vw, 86px);
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.07em;
|
||||
}
|
||||
|
||||
main[lang="zh-CN"] h2 {
|
||||
font-size: clamp(34px, 4.6vw, 62px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
|
||||
.hero-subtitle, .relationship p, .demo p, .journey-desc {
|
||||
max-width: 650px;
|
||||
color: #343b4a;
|
||||
font-size: clamp(18px, 2vw, 23px);
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
.cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
|
||||
.cta-row.compact { margin-top: 22px; }
|
||||
|
||||
.button {
|
||||
font-size: 15px;
|
||||
display: inline-flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
padding: 0 20px;
|
||||
font-weight: 850;
|
||||
border: 1px solid transparent;
|
||||
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.button.primary { color: white; background: var(--blue); box-shadow: 0 14px 34px rgba(22, 71, 255, 0.32); }
|
||||
.button.secondary { background: white; border-color: var(--line); }
|
||||
.button.ghost { color: var(--muted); background: rgba(255, 255, 255, 0.54); border-color: var(--line); }
|
||||
|
||||
/* Phone mockup */
|
||||
.phone-wrap {
|
||||
width: min(390px, 82vw);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.hero-phone-stack {
|
||||
min-height: 720px;
|
||||
position: relative;
|
||||
perspective: 1200px;
|
||||
}
|
||||
|
||||
.hero-phone-stack img {
|
||||
position: absolute;
|
||||
width: min(310px, 54vw);
|
||||
height: 676px;
|
||||
border-radius: 46px;
|
||||
border: 12px solid #11141d;
|
||||
background: white;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero-phone-stack img:nth-child(1) { left: 2%; top: 48px; transform: rotate(-8deg) translateZ(-20px); }
|
||||
.hero-phone-stack img:nth-child(2) { left: 28%; top: 0; transform: rotate(4deg) translateZ(40px); }
|
||||
.hero-phone-stack img:nth-child(3) { right: 0; top: 132px; transform: rotate(10deg) translateZ(10px); }
|
||||
|
||||
.phone-shell {
|
||||
position: relative;
|
||||
padding: 14px;
|
||||
border-radius: 54px;
|
||||
background: #11141d;
|
||||
box-shadow: var(--shadow), inset 0 0 0 2px rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.phone-shell-compact {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
border-radius: 48px;
|
||||
background: #11141d;
|
||||
box-shadow: 0 20px 56px rgba(16, 18, 24, 0.22);
|
||||
}
|
||||
|
||||
.speaker {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
width: 72px;
|
||||
height: 8px;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.18);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.phone-screen {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 40px;
|
||||
background: white;
|
||||
aspect-ratio: 9 / 19.7;
|
||||
}
|
||||
|
||||
.phone-screen-compact {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 36px;
|
||||
background: white;
|
||||
aspect-ratio: 9 / 19.7;
|
||||
}
|
||||
|
||||
.phone-screen img,
|
||||
.phone-screen-compact img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.phone-screen-shot {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tap-ring {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
bottom: 158px;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--blue);
|
||||
background: rgba(255, 228, 92, 0.7);
|
||||
box-shadow: 0 0 0 16px rgba(255, 228, 92, 0.22);
|
||||
}
|
||||
|
||||
/* Shared content sections */
|
||||
.relationship, .features, .demo, .video-demo, .download, .trust, .footer {
|
||||
width: min(1160px, calc(100% - 44px));
|
||||
margin: 0 auto;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.two-col, .demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.85fr) minmax(320px, 1fr);
|
||||
gap: clamp(24px, 5vw, 76px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Relationship section */
|
||||
.relationship-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 30px;
|
||||
background: rgba(255,255,255,0.76);
|
||||
box-shadow: 0 18px 48px rgba(16,18,24,0.08);
|
||||
padding: clamp(28px, 5vw, 48px);
|
||||
}
|
||||
|
||||
.relationship-card p {
|
||||
max-width: 800px;
|
||||
margin: 0 0 16px;
|
||||
font-size: clamp(16px, 1.8vw, 20px);
|
||||
line-height: 1.65;
|
||||
color: #343b4a;
|
||||
}
|
||||
|
||||
.relationship-card p:last-child { margin-bottom: 0; }
|
||||
|
||||
.relationship-card a {
|
||||
color: var(--blue);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(22, 71, 255, 0.25);
|
||||
}
|
||||
|
||||
.relationship-card a:hover { text-decoration-color: var(--blue); }
|
||||
|
||||
/* Interactive product journey */
|
||||
.journey {
|
||||
width: min(1160px, calc(100% - 44px));
|
||||
margin: 0 auto;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.journey-header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.journey-header h2 {
|
||||
max-width: 800px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.journey-header p {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.journey-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 440px);
|
||||
gap: clamp(24px, 5vw, 64px);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.journey-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.journey-step {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
padding: 20px 24px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.68);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.journey-step:hover {
|
||||
background: rgba(255,255,255,0.92);
|
||||
border-color: rgba(22, 71, 255, 0.18);
|
||||
}
|
||||
|
||||
.journey-step-active {
|
||||
background: white;
|
||||
border-color: var(--blue);
|
||||
box-shadow: 0 12px 36px rgba(22, 71, 255, 0.12);
|
||||
}
|
||||
|
||||
.journey-step-completed {
|
||||
opacity: 0.65;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.journey-step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 71, 255, 0.08);
|
||||
color: var(--blue);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.journey-step-active .journey-step-number {
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.journey-step-completed .journey-step-number {
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.journey-step-content h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.journey-step-content p {
|
||||
font-size: 18px;
|
||||
line-height: 1.55;
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.journey-phone-area {
|
||||
position: sticky;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.journey-phone-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Journey phone overlays */
|
||||
.journey-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 6;
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.journey-hand {
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
font-size: 28px;
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.journey-tap-ripple {
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--blue);
|
||||
background: rgba(255, 228, 92, 0.55);
|
||||
box-shadow: 0 0 0 12px rgba(255, 228, 92, 0.18);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Focus zoom */
|
||||
.focus-zoom {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
border-radius: 20px;
|
||||
border: 3px solid var(--blue);
|
||||
background: rgba(22, 71, 255, 0.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Workspace assets grid */
|
||||
.workspace-section {
|
||||
width: min(1160px, calc(100% - 44px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.workspace-header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.workspace-header h2 {
|
||||
max-width: 700px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.workspace-header p {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.workspace-chips {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.workspace-chip {
|
||||
padding: 10px 18px;
|
||||
border-radius: 999px;
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.72);
|
||||
color: var(--muted);
|
||||
transition: background 0.18s, color 0.18s, border-color 0.18s, box-shadow 0.18s;
|
||||
}
|
||||
|
||||
.workspace-chip:hover {
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.workspace-chip-active {
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
border-color: var(--blue);
|
||||
box-shadow: 0 10px 24px rgba(22, 71, 255, 0.28);
|
||||
}
|
||||
|
||||
.workspace-phone-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.workspace-phone-wrap .phone-shell-compact {
|
||||
width: min(260px, 60vw);
|
||||
}
|
||||
|
||||
/* Mention picker overlay */
|
||||
.mention-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: rgba(255,255,255,0.97);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1px solid var(--line);
|
||||
border-radius: 24px 24px 36px 36px;
|
||||
padding: 16px 14px;
|
||||
box-shadow: 0 -12px 36px rgba(16, 18, 24, 0.1);
|
||||
}
|
||||
|
||||
.mention-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
background: #f4f6fa;
|
||||
margin-bottom: 14px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mention-search span {
|
||||
color: var(--blue);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mention-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mention-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.mention-item-active {
|
||||
background: rgba(22, 71, 255, 0.07);
|
||||
}
|
||||
|
||||
.mention-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: white;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.mention-name {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mention-role {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mention-highlight {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--blue);
|
||||
background: rgba(22, 71, 255, 0.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Typing demo */
|
||||
.typing-line {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1;
|
||||
white-space: pre;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.typing-cursor {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 18px;
|
||||
background: var(--blue);
|
||||
border-radius: 999px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
/* Features grid (legacy, kept for reference) */
|
||||
.feature-grid, .trust-grid, .release-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.feature-card, .trust-item, .release-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 30px;
|
||||
background: rgba(255,255,255,0.76);
|
||||
box-shadow: 0 18px 48px rgba(16,18,24,0.08);
|
||||
}
|
||||
|
||||
.feature-card { padding: 28px; min-height: 250px; }
|
||||
.feature-card span { color: var(--blue); font-weight: 950; font-size: 13px; letter-spacing: 0.12em; }
|
||||
.feature-card p, .trust-item p { color: var(--muted); line-height: 1.65; }
|
||||
|
||||
.demo { overflow: hidden; }
|
||||
.screenshot-stack { min-height: 560px; position: relative; overflow: hidden; border-radius: 42px; }
|
||||
.shot {
|
||||
position: absolute;
|
||||
width: min(310px, 52vw);
|
||||
height: 620px;
|
||||
border-radius: 34px;
|
||||
border: 10px solid #11141d;
|
||||
box-shadow: var(--shadow);
|
||||
background: white;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
}
|
||||
.shot.back { right: 210px; top: 40px; transform: rotate(-7deg); opacity: 0.78; }
|
||||
.shot.front { right: 12px; top: 0; transform: rotate(5deg); }
|
||||
|
||||
.video-demo {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.video-copy {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.95fr) minmax(320px, 1fr);
|
||||
gap: clamp(24px, 5vw, 76px);
|
||||
align-items: end;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.video-copy p {
|
||||
color: #343b4a;
|
||||
font-size: clamp(17px, 1.7vw, 21px);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.promo-video, .video-placeholder {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 34px;
|
||||
background: #0e1320;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.promo-video { object-fit: cover; z-index: 1; position: relative; }
|
||||
|
||||
.video-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
background:
|
||||
radial-gradient(circle at 70% 24%, rgba(255, 228, 92, 0.22), transparent 34%),
|
||||
linear-gradient(135deg, #0e1320, #172453);
|
||||
}
|
||||
|
||||
.video-placeholder span {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 18px;
|
||||
border-radius: 999px;
|
||||
color: var(--blue);
|
||||
background: var(--yellow);
|
||||
font-size: 44px;
|
||||
font-weight: 950;
|
||||
box-shadow: 0 18px 42px rgba(255, 228, 92, 0.24);
|
||||
}
|
||||
|
||||
.video-placeholder strong {
|
||||
display: block;
|
||||
color: white;
|
||||
font-size: clamp(22px, 3vw, 42px);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.download { padding-top: 40px; }
|
||||
.release-card { padding: clamp(28px, 5vw, 54px); }
|
||||
.release-grid { margin: 30px 0 22px; }
|
||||
.release-grid div {
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
background: #f7f9ff;
|
||||
border: 1px solid rgba(22, 71, 255, 0.13);
|
||||
}
|
||||
.release-grid span { display: block; color: var(--muted); font-size: 12px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 8px; }
|
||||
.release-grid code, pre code { font-family: "SFMono-Regular", ui-monospace, Menlo, Consolas, "PingFang SC", monospace; }
|
||||
.release-grid code { display: block; overflow-wrap: anywhere; color: var(--ink); font-size: 14px; }
|
||||
.hash-cell { grid-column: span 3; }
|
||||
pre {
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 18px 20px;
|
||||
border-radius: 20px;
|
||||
color: #eef4ff;
|
||||
background: #0e1320;
|
||||
}
|
||||
|
||||
.trust-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.trust-item { display: flex; gap: 14px; padding: 22px; align-items: flex-start; }
|
||||
.check {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
background: var(--blue);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.footer strong { color: var(--ink); }
|
||||
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; font-weight: 800; color: var(--ink); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
.topbar { border-radius: 24px; align-items: flex-start; flex-direction: column; }
|
||||
.nav-links { flex-wrap: wrap; }
|
||||
.hero-grid, .two-col, .demo-grid, .video-copy, .feature-grid, .trust-grid, .release-grid { grid-template-columns: 1fr; }
|
||||
.hero { padding-left: 16px; padding-right: 16px; padding-top: 20px; min-height: auto; }
|
||||
.hero-grid { width: 100%; }
|
||||
.hero-grid > div:first-child { padding-right: 0; }
|
||||
.hero-phone-stack { min-height: 400px; overflow: visible; }
|
||||
.hero-phone-stack img { height: 480px; width: min(240px, 42vw); border-radius: 36px; border-width: 10px; }
|
||||
.hero-phone-stack img:nth-child(1) { left: -5%; top: 36px; transform: rotate(-8deg) translateZ(-20px); }
|
||||
.hero-phone-stack img:nth-child(2) { left: 22%; top: 0; transform: rotate(4deg) translateZ(40px); }
|
||||
.hero-phone-stack img:nth-child(3) { right: -5%; top: 100px; transform: rotate(10deg) translateZ(10px); }
|
||||
.eyebrow { font-size: 11px; letter-spacing: 0.1em; }
|
||||
h1 { font-size: clamp(32px, 8vw, 52px) !important; }
|
||||
.hero-subtitle { font-size: 15px; }
|
||||
.journey-layout { grid-template-columns: 1fr; }
|
||||
.journey-phone-area { position: relative; top: 0; order: -1; }
|
||||
.journey-phone-container { justify-content: center; }
|
||||
.section-pad { padding: 48px 16px; }
|
||||
.hash-cell { grid-column: auto; }
|
||||
.screenshot-stack { min-height: 680px; }
|
||||
.shot { width: min(300px, 74vw); }
|
||||
.shot.back { right: auto; left: 4%; }
|
||||
.shot.front { right: 4%; top: 160px; }
|
||||
.footer { flex-direction: column; }
|
||||
.workspace-chips { justify-content: flex-start; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Workspace card grid */
|
||||
.workspace-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.workspace-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
|
||||
}
|
||||
|
||||
.workspace-card:hover {
|
||||
background: white;
|
||||
border-color: rgba(22, 71, 255, 0.18);
|
||||
}
|
||||
|
||||
.workspace-card-active {
|
||||
border-color: var(--blue);
|
||||
box-shadow: 0 10px 28px rgba(22, 71, 255, 0.12);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.workspace-card-thumb {
|
||||
width: 100%;
|
||||
aspect-ratio: 9 / 19.7;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.workspace-card-label {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.workspace-card-label strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.workspace-card-label span {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.workspace-card-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.star-hint {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.star-hint a {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ── Footer pills ── */
|
||||
.pill {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 10px 22px; border-radius: 999px;
|
||||
font-weight: 850; font-size: 15px; letter-spacing: -0.01em;
|
||||
text-decoration: none; transition: all 0.22s;
|
||||
}
|
||||
.pill-android {
|
||||
background: var(--blue); color: #fff;
|
||||
box-shadow: 0 12px 30px rgba(22,71,255,0.30);
|
||||
}
|
||||
.pill-android:hover { background: var(--blue-2); transform: translateY(-1px); }
|
||||
.pill-android .android-icon {
|
||||
width: 15px; height: 15px; display: inline-block; background: #fff;
|
||||
-webkit-mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYgMThjMCAuNTUuNDUgMSAxIDFoMXYzLjVjMCAuODMuNjcgMS41IDEuNSAxLjVzMS41LS42NyAxLjUtMS41VjE5aDJ2My41YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNVYxOWgxYy41NSAwIDEtLjQ1IDEtMVY4SDZ2MTB6TTMuNSA4QzIuNjcgOCAyIDguNjcgMiA5LjV2N2MwIC44My42NyAxLjUgMS41IDEuNVM1IDE3LjMzIDUgMTYuNXYtN0M1IDguNjcgNC4zMyA4IDMuNSA4em0xNyAwYy0uODMgMC0xLjUuNjctMS41IDEuNXY3YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNXYtN2MwLS44My0uNjctMS41LTEuNS0xLjV6bS00Ljk3LTUuODRsMS4zLTEuM2MuMi0uMi4yLS41MSAwLS43MS0uMi0uMi0uNTEtLjItLjcxIDBsLTEuNDggMS40OEE1Ljg0IDUuODQgMCAwMDEyIDFjLS45NiAwLTEuODYuMjMtMi42Ni42M0w3Ljg1LjE1Yy0uMi0uMi0uNTEtLjItLjcxIDAtLjIuMi0uMi41MSAwIC43MWwxLjMxIDEuMzFBNS45ODMgNS45ODMgMCAwMDYgN2gxMmMwLTIuMTItMS4xLTMuOTgtMi40Ny00Ljg0ek0xMCA1SDlWNGgxdjF6bTUgMGgtMVY0aDF2MXoiLz48L3N2Zz4=) center/contain no-repeat;
|
||||
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYgMThjMCAuNTUuNDUgMSAxIDFoMXYzLjVjMCAuODMuNjcgMS41IDEuNSAxLjVzMS41LS42NyAxLjUtMS41VjE5aDJ2My41YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNVYxOWgxYy41NSAwIDEtLjQ1IDEtMVY4SDZ2MTB6TTMuNSA4QzIuNjcgOCAyIDguNjcgMiA5LjV2N2MwIC44My42NyAxLjUgMS41IDEuNVM1IDE3LjMzIDUgMTYuNXYtN0M1IDguNjcgNC4zMyA4IDMuNSA4em0xNyAwYy0uODMgMC0xLjUuNjctMS41IDEuNXY3YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNXYtN2MwLS44My0uNjctMS41LTEuNS0xLjV6bS00Ljk3LTUuODRsMS4zLTEuM2MuMi0uMi4yLS41MSAwLS43MS0uMi0uMi0uNTEtLjItLjcxIDBsLTEuNDggMS40OEE1Ljg0IDUuODQgMCAwMDEyIDFjLS45NiAwLTEuODYuMjMtMi42Ni42M0w3Ljg1LjE1Yy0uMi0uMi0uNTEtLjItLjcxIDAtLjIuMi0uMi41MSAwIC43MWwxLjMxIDEuMzFBNS45ODMgNS45ODMgMCAwMDYgN2gxMmMwLTIuMTItMS4xLTMuOTgtMi40Ny00Ljg0ek0xMCA1SDlWNGgxdjF6bTUgMGgtMVY0aDF2MXoiLz48L3N2Zz4=) center/contain no-repeat;
|
||||
}
|
||||
|
||||
.pill-ios {
|
||||
position: relative; overflow: hidden;
|
||||
background: rgba(255,255,255,0.22);
|
||||
backdrop-filter: blur(20px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
||||
color: #1a1a2e; font-weight: 850;
|
||||
border: 1px solid rgba(255,255,255,0.55);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08),
|
||||
inset 0 1px 0 rgba(255,255,255,0.9),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.04);
|
||||
}
|
||||
.pill-ios:hover {
|
||||
background: rgba(255,255,255,0.38);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,0.12),
|
||||
inset 0 1px 0 rgba(255,255,255,1),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.04);
|
||||
}
|
||||
.pill-ios::before {
|
||||
content: "";
|
||||
position: absolute; top: 0; left: -40%; width: 180%; height: 50%;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 100%);
|
||||
border-radius: 999px; pointer-events: none;
|
||||
}
|
||||
.pill-ios::after {
|
||||
content: "";
|
||||
position: absolute; bottom: -2px; left: 10%; width: 80%; height: 6px;
|
||||
background: linear-gradient(90deg, #ff6b9d, #c44dff, #6b9dff, #45d4a8);
|
||||
filter: blur(5px); opacity: 0.4; border-radius: 999px;
|
||||
pointer-events: none; z-index: -1;
|
||||
}
|
||||
.pill-ios .apple-icon {
|
||||
width: 14px; height: 14px; display: inline-block; background: #1a1a2e;
|
||||
-webkit-mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE4LjcxIDE5LjVjLS44MyAxLjI0LTEuNzEgMi40NS0zLjA1IDIuNDctMS4zNC4wMy0xLjc3LS43OS0zLjI5LS43OS0xLjUzIDAtMiAuNzctMy4yNy44Mi0xLjMxLjA1LTIuMy0xLjMyLTMuMTQtMi41M0M0LjI1IDE3IDIuOTQgMTIuNDUgNC43IDkuMzljLjg3LTEuNTIgMi40My0yLjQ4IDQuMTItMi41MSAxLjI4LS4wMiAyLjUuODcgMy4yOS44Ny43OCAwIDIuMjYtMS4wNyAzLjgxLS45MS42NS4wMyAyLjQ3LjI2IDMuNjQgMS45OC0uMDkuMDYtMi4xNyAxLjI4LTIuMTUgMy44MS4wMyAzLjAyIDIuNjUgNC4wMyAyLjY4IDQuMDQtLjAzLjA3LS40MiAxLjQ0LTEuMzggMi44M00xMyAzLjVjLjczLS44MyAxLjk0LTEuNDYgMi45NC0xLjUuMTMgMS4xNy0uMzQgMi4zNS0xLjA0IDMuMTktLjY5Ljg1LTEuODMgMS41MS0yLjk1IDEuNDItLjE1LTEuMTUuNDEtMi4zNSAxLjA1LTMuMTF6Ii8+PC9zdmc+) center/contain no-repeat;
|
||||
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE4LjcxIDE5LjVjLS44MyAxLjI0LTEuNzEgMi40NS0zLjA1IDIuNDctMS4zNC4wMy0xLjc3LS43OS0zLjI5LS43OS0xLjUzIDAtMiAuNzctMy4yNy44Mi0xLjMxLjA1LTIuMy0xLjMyLTMuMTQtMi41M0M0LjI1IDE3IDIuOTQgMTIuNDUgNC43IDkuMzljLjg3LTEuNTIgMi40My0yLjQ4IDQuMTItMi41MSAxLjI4LS4wMiAyLjUuODcgMy4yOS44Ny43OCAwIDIuMjYtMS4wNyAzLjgxLS45MS42NS4wMyAyLjQ3LjI2IDMuNjQgMS45OC0uMDkuMDYtMi4xNyAxLjI4LTIuMTUgMy44MS4wMyAzLjAyIDIuNjUgNC4wMyAyLjY4IDQuMDQtLjAzLjA3LS40MiAxLjQ0LTEuMzggMi44M00xMyAzLjVjLjczLS44MyAxLjk0LTEuNDYgMi45NC0xLjUuMTMgMS4xNy0uMzQgMi4zNS0xLjA0IDMuMTktLjY5Ljg1LTEuODMgMS41MS0yLjk1IDEuNDItLjE1LTEuMTUuNDEtMi4zNSAxLjA1LTMuMTF6Ii8+PC9zdmc+) center/contain no-repeat;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pill { padding: 8px 16px; font-size: 14px; }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
plugins: [react()],
|
||||
base: process.env.VITE_BASE || '/',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
},
|
||||
}));
|
||||