diff --git a/src/components/NeonMotion.astro b/src/components/NeonMotion.astro new file mode 100644 index 0000000..42060a3 --- /dev/null +++ b/src/components/NeonMotion.astro @@ -0,0 +1,48 @@ +--- +// Page motion for the neon design language (see "Neon design language" in +// src/styles/global.css): blocks marked .nx-in move into place as they come +// into view, .nx-tile surfaces light up under the pointer, and figures marked +// data-count tick up while the page is visible. Everything reads the same +// without it; with reduced motion only the pointer light stays. +--- + + diff --git a/src/components/home/CompareEditions.astro b/src/components/home/CompareEditions.astro index 891d780..04f90d6 100644 --- a/src/components/home/CompareEditions.astro +++ b/src/components/home/CompareEditions.astro @@ -12,19 +12,20 @@ const editions = [ const rows = ["runsAs", "builtFor", "status", "license"] as const; --- -
-
{c.eyebrow}
+
+
+

{c.eyebrow}

-
diff --git a/src/components/home/CoreStrip.astro b/src/components/home/CoreStrip.astro index d973f5b..62c3715 100644 --- a/src/components/home/CoreStrip.astro +++ b/src/components/home/CoreStrip.astro @@ -12,20 +12,31 @@ const installCommand = "curl -fsSL https://raw.githubusercontent.com/ThinkWatchProject/ThinkWatch-Core/main/scripts/install.sh | sudo sh"; --- -
-
-

- {c.a}{c.name}{c.b} +

+
+

+ {c.a}{c.name}{c.b}

-
-

{c.commandLabel}

- - - {c.docs} - +
+

{c.commandLabel}

+ + {c.docs}
-
+
+ + diff --git a/src/components/home/HomeHero.astro b/src/components/home/HomeHero.astro index 8b232b8..a665785 100644 --- a/src/components/home/HomeHero.astro +++ b/src/components/home/HomeHero.astro @@ -1,62 +1,118 @@ --- -import HeroVisual from "~/components/hero/HeroVisual.tsx"; +// Home hero: both products at a glance. Copy on the left with a door into each +// product, a request typed through the gateway on the right. import Terminal from "~/components/ui/Terminal.tsx"; import { getLang, localePath } from "~/i18n"; import { homeCopy } from "~/i18n/pages/home"; +import { getLatestLiteRelease, getLatestRelease } from "~/lib/github"; const lang = getLang(Astro); const c = homeCopy[lang]; const zh = lang === "zh-CN"; +const lite = (await getLatestLiteRelease())?.tag.replace(/^v/, "") ?? null; +const ent = (await getLatestRelease())?.replace(/^v/, "") ?? null; --- -
- - -
-
-
{c.eyebrow}
-

- {c.h1a} {c.h1b} +
+ + +
diff --git a/src/components/home/LiteBand.astro b/src/components/home/LiteBand.astro index 054f800..c8547f0 100644 --- a/src/components/home/LiteBand.astro +++ b/src/components/home/LiteBand.astro @@ -1,53 +1,114 @@ --- -// The Lite band on the home page. Each page shows the interface in its own -// language; see LitePage.astro. +// The Lite band on the home page: the local gateway, its four points, where it +// runs, and a download, beside its overview screen. Each page shows the +// interface in its own language; see LitePage.astro. import { getLang, localePath } from "~/i18n"; import { homeCopy } from "~/i18n/pages/home"; import LiteDownload from "~/components/LiteDownload.astro"; +import TwMark from "~/components/TwMark.astro"; const lang = getLang(Astro); const c = homeCopy[lang].lite; const zh = lang === "zh-CN"; --- -
-
- {c.shotAlt} -
-
-
{c.eyebrow}
-

{c.title}

-
- {c.points.map((p) => ( -
-

{p.t}

-

{p.b}

-
- ))} -
- {/* 第一项是状态,单独做成徽标;其余是平台与许可证 */} -
- {c.pills[0]} - {c.pills.slice(1).map((pill) => ( - {pill} - ))} +
+ +
+
+

{c.eyebrow}

+

{c.title}

+
+ {c.points.map((p) => ( +
+

{p.t}

+

{p.b}

+
+ ))} +
+
+ {c.pills[0]} + {c.pills.slice(1).map((pill) => {pill})} +
+
+ + {c.cta} +
-
- - {c.cta} +
+ {c.shotAlt}
+ + diff --git a/src/components/home/Marquee.astro b/src/components/home/Marquee.astro index ab1742b..ab3d58e 100644 --- a/src/components/home/Marquee.astro +++ b/src/components/home/Marquee.astro @@ -3,28 +3,26 @@ import { getLang } from "~/i18n"; import { homeCopy } from "~/i18n/pages/home"; const c = homeCopy[getLang(Astro)]; -const clients = ["Claude Code", "Codex", "Cursor", "Continue", "Cline", "OpenAI SDK", "Anthropic SDK"]; +const clients = ["Claude Code", "Codex", "Cursor", "Continue", "Cline", "OpenAI SDK", "Anthropic SDK", "Gemini SDK", "opencode", "Zed"]; --- -
- {c.worksWith} -
-
- {clients.map((name) => {name})} - {clients.map((name) => )} -
+
+

{c.wallLabel}

+
+
    + {[...clients, ...clients].map((name, i) =>
  • = clients.length ? "true" : undefined}>{name}
  • )} +
-
+
diff --git a/src/components/home/Questions.astro b/src/components/home/Questions.astro index 2035cb7..4beefe0 100644 --- a/src/components/home/Questions.astro +++ b/src/components/home/Questions.astro @@ -1,64 +1,180 @@ --- +// Visibility into every request: three things the gateway records, in hairline +// columns with line art, and a sample trace that plays row by row. import { getLang } from "~/i18n"; import { homeCopy } from "~/i18n/pages/home"; const lang = getLang(Astro); const c = homeCopy[lang].questions; const t = homeCopy[lang].trace; +const rows: { k: string; v: string; tone?: "warn" | "ok" | "flag" }[] = [ + { k: t.client[0], v: t.client[1] }, + { k: t.rule[0], v: t.rule[1] }, + { k: t.hop1[0], v: `${t.hop1[1]} · ${t.hop1[2]} · ${t.hop1[3]}`, tone: "warn" }, + { k: t.hop2[0], v: `${t.hop2[1]} · ${t.hop2[2]} · ${t.hop2[3]}`, tone: "ok" }, + { k: t.cost[0], v: t.cost[1] }, + { k: t.redaction[0], v: t.redaction[1] }, + { k: t.tool[0], v: t.tool[1], tone: "flag" }, +]; --- -
-
-
-
{c.eyebrow}
-
- {c.items.map((item) => ( -
-
{item.tag}
-

{item.title}

-

{item.body}

-
- ))} -
+
+
+

{c.eyebrow}

+
+ {c.items.map((item, i) => ( +
+

{item.tag}

+ +

{item.title}

+

{item.body}

+
+ ))}
-
-
- - - POST /v1/messages - - {t.tag} -
-
-
{t.client[0]}{t.client[1]}
-
{t.rule[0]}{t.rule[1]}
-
{t.hop1[0]}{t.hop1[1]} · {t.hop1[2]} · {t.hop1[3]}
-
{t.hop2[0]}{t.hop2[1]} · {t.hop2[2]} · {t.hop2[3]}
-
{t.cost[0]}{t.cost[1]}
-
{t.redaction[0]}{t.redaction[1]}
-
{t.tool[0]}{t.tool[1]}
+
+
+ + POST /v1/messages + {t.tag}
-
{t.note}
+
    + {rows.map((r, i) => ( +
  1. + {r.k}{r.v} +
  2. + ))} +
+

{t.note}

diff --git a/src/components/home/ThinkWatchBand.astro b/src/components/home/ThinkWatchBand.astro index 94851c7..d7233a3 100644 --- a/src/components/home/ThinkWatchBand.astro +++ b/src/components/home/ThinkWatchBand.astro @@ -1,4 +1,7 @@ --- +// ThinkWatch Enterprise on the home page: what it is, its four pillars as +// tiles, the six stages of a request as a pipeline a light runs along, the +// console preview, and per-user identity for MCP. import DashboardMock from "~/components/mocks/DashboardMock.tsx"; import LogExplorerMock from "~/components/mocks/LogExplorerMock.tsx"; import ProviderHealthMock from "~/components/mocks/ProviderHealthMock.tsx"; @@ -8,51 +11,56 @@ import { homeCopy } from "~/i18n/pages/home"; const lang = getLang(Astro); const c = homeCopy[lang].tw; +/** Line icons for the four pillars, in the order of the copy */ +const icons = [ + '', + '', + '', + '', +]; --- -
-
-
-
-
{c.eyebrow}
-

{c.title}

-

{c.sub}

- {c.cta} -
-
- {c.features.map((f) => ( -
-
{f.t}
-
{f.b}
-
- ))} +
+
+
+
+

{c.eyebrow}

+

{c.title}

+

{c.sub}

+ {c.cta} +
+ +
+ {c.features.map((f, i) => ( +
+ +

{f.t}

+

{f.b}

+
+ ))}
-
{c.stepsLabel}
-
    +

    {c.stepsLabel}

    +
      {c.steps.map((s, i) => ( -
    1. - {String(i + 1).padStart(2, "0")} - {s.t} - {s.b} +
    2. + + {String(i + 1).padStart(2, "0")} + {s.t} + {s.b}
    3. ))}
    -
    {c.consoleLabel}
    - {c.sample} +

    {c.consoleLabel}

    + {c.sample}
    -
    +
    -
    +
    @@ -62,30 +70,162 @@ const c = homeCopy[lang].tw;
    -
    -
    {c.quoteLabel}
    -
    -

    - {c.quoteA}{c.quoteEm}{c.quoteB} -

    -

    {c.quoteBody}

    -
    +
    +

    {c.quoteLabel}

    +

    {c.quoteA}{c.quoteEm}{c.quoteB}

    +

    {c.quoteBody}

diff --git a/src/components/pages/CorePage.astro b/src/components/pages/CorePage.astro index 9ace317..7b4b80f 100644 --- a/src/components/pages/CorePage.astro +++ b/src/components/pages/CorePage.astro @@ -1,11 +1,12 @@ --- // The /core page: ThinkWatch Core, the MIT-licensed Rust crates and the twcore // binary. twcore is the gateway inside ThinkWatch Lite and also runs on its own -// on a Linux server; ThinkWatch Enterprise depends on three of the crates. +// on a Linux server; ThinkWatch Enterprise depends on four of the crates. import Base from "~/layouts/Base.astro"; import SiteHeader from "~/components/SiteHeader.astro"; import SiteFooter from "~/components/SiteFooter.astro"; import CopyCommand from "~/components/CopyCommand.astro"; +import NeonMotion from "~/components/NeonMotion.astro"; import { getLang, localePath } from "~/i18n"; import { coreCopy } from "~/i18n/pages/core"; import { getLatestCoreRelease, getReleases } from "~/lib/github"; @@ -50,7 +51,12 @@ const sourceCommands = [ "cargo build --release -p twcore", ]; -const delay = (i: number) => `animation-delay: ${i * 90}ms;`; +/** What twcore serve prints once it is up: illustrative lines for the hero terminal */ +const serveLog = [ + 'ready gateway 127.0.0.1:8788 · control socket', + 'POST /v1/messages claude-code → anthropic 200 1.2 s', + 'POST /v1/responses codex → chatgpt 429 → relay 200', +]; const eyebrow = "font-mono text-[11px] uppercase tracking-[0.08em] text-[var(--color-muted)] sm:text-[13px]"; const label = "m-0 font-mono text-[13px] uppercase tracking-[0.08em] text-[var(--color-muted)]"; const h2 = zh @@ -64,90 +70,97 @@ const fileLink = "break-all text-[var(--color-brand-1)] transition-colors hover:
-
-
-
-

- {c.hero.eyebrow} -

-

- {zh ? ( - // Kept on one line, so that the break falls before the highlight - // rather than leaving "的" at the start of the second line. - {c.hero.titleA.trimEnd()}{c.hero.titleA.endsWith(" ") && " "} - ) : ( - c.hero.titleA - )}{c.hero.titleHighlight} +
+ + +
+
+ {version && ( + {c.motion.announce(version)}→ + )} +

+ {zh ? {c.hero.titleA} : c.hero.titleA}{c.hero.titleHighlight}

-

- {c.hero.sub} -

- -
-
- - {c.hero.cardTitle} +
+
+ + {c.hero.cardTitle} + {c.motion.sample}
-
-
    - {c.hero.commands.map((row, i) => ( -
  1. - - {row.cmd} - - {row.note} -
  2. - ))} -
-
-
+
    + {c.hero.commands.map((row) => ( + +
  1. $ {row.cmd}
  2. +
  3. {row.note}
  4. +
    + ))} + {serveLog.map((line) => ( +
  5. + ))} +
+
-
+
-

{c.does.eyebrow}

-
- {c.does.items.map((item) => ( -
-

{item.title}

-

{item.body}

+

{c.does.eyebrow}

+

{c.motion.doesTitle}{c.motion.doesTitleDim}

+
+
+ - ))} +

{c.does.items[0].title}

+

{c.does.items[0].body}

+
+
+ +

{c.does.items[1].title}

+

{c.does.items[1].body}

+
+
+

$0.0124

+ +

{c.does.items[2].title}

+

{c.does.items[2].body}

+
+
+ +

{c.does.items[3].title}

+

{c.does.items[3].body}

+
+
+ +

{c.does.items[4].title}

+

{c.does.items[4].body}

+
+
+ +
+

{c.does.items[5].title}

+

{c.does.items[5].body}

+
+
@@ -295,4 +308,367 @@ const fileLink = "break-all text-[var(--color-brand-1)] transition-colors hover:

+ + + + + + diff --git a/src/components/pages/HomePage.astro b/src/components/pages/HomePage.astro index 12efc73..9beaa1f 100644 --- a/src/components/pages/HomePage.astro +++ b/src/components/pages/HomePage.astro @@ -9,6 +9,7 @@ import ThinkWatchBand from "~/components/home/ThinkWatchBand.astro"; import LiteBand from "~/components/home/LiteBand.astro"; import CoreStrip from "~/components/home/CoreStrip.astro"; import CompareEditions from "~/components/home/CompareEditions.astro"; +import NeonMotion from "~/components/NeonMotion.astro"; import { getLang } from "~/i18n"; import { homeCopy } from "~/i18n/pages/home"; import { coreLd, enterpriseLd, liteLd, webPage, website } from "~/lib/structured-data"; @@ -39,4 +40,5 @@ const jsonLd = [ + diff --git a/src/components/pages/ThinkWatchPage.astro b/src/components/pages/ThinkWatchPage.astro index 804dcc2..572b07b 100644 --- a/src/components/pages/ThinkWatchPage.astro +++ b/src/components/pages/ThinkWatchPage.astro @@ -12,6 +12,7 @@ import MCPAdvantage from "~/components/sections/MCPAdvantage.astro"; import LivePanels from "~/components/sections/LivePanels.astro"; import QuickStart from "~/components/sections/QuickStart.astro"; import LogExplorerMock from "~/components/mocks/LogExplorerMock.tsx"; +import NeonMotion from "~/components/NeonMotion.astro"; import { getLang, localePath } from "~/i18n"; import { getLatestRelease } from "~/lib/github"; import { enterpriseLd } from "~/lib/structured-data"; @@ -38,66 +39,34 @@ const delay = (i: number) => `animation-delay: ${i * 90}ms;`;
-
- -
-
-

- {c.hero.eyebrow} -

-

- {c.hero.titleA}{c.hero.titleHighlight} +
+ + +
+
+ {version && ( + {c.hero.announce(version.replace(/^v/, ""))}→ + )} +

+ {c.hero.titleA}{c.hero.titleHighlight}

-

- {c.hero.sub} -

-
- - {c.hero.ctaPrimary} - - - {c.hero.ctaSecondary} - +

{c.hero.sub}

+ -
    +
      {pills.map((p) => ( -
    • - {p.label} -
    • +
    • {p.label}
    • ))}
-
-
- {c.hero.mockTitle} - {c.hero.mockSample} -
-
+
+
-

{c.hero.mockNote}

+
{c.hero.mockSample} · {c.hero.mockNote}
@@ -135,4 +104,39 @@ const delay = (i: number) => `animation-delay: ${i * 90}ms;`;

+ + + diff --git a/src/components/sections/CompatibleWith.astro b/src/components/sections/CompatibleWith.astro index 08dea6f..01f2b50 100644 --- a/src/components/sections/CompatibleWith.astro +++ b/src/components/sections/CompatibleWith.astro @@ -4,46 +4,26 @@ import { getLang, t } from "~/i18n"; const s = t(getLang(Astro)).compatible; // Only clients the ThinkWatch README names as drop-in compatible. -const items = [ - "Claude Code", - "Cursor", - "Continue", - "Cline", - "OpenAI SDK", - "Anthropic SDK", -]; -// Doubled for seamless loop -const doubled = [...items, ...items]; +const items = ["Claude Code", "Cursor", "Continue", "Cline", "OpenAI SDK", "Anthropic SDK"]; --- -
-
-

- {s.label} -

-
-
    - {doubled.map((name) => ( -
  • - {name} -
  • - ))} -
-
+
+

{s.label}

+
+
    + {[...items, ...items, ...items].map((name, i) =>
  • = items.length ? "true" : undefined}>{name}
  • )} +
diff --git a/src/components/sections/Features.tsx b/src/components/sections/Features.tsx index 7d3137f..8c97e49 100644 --- a/src/components/sections/Features.tsx +++ b/src/components/sections/Features.tsx @@ -16,7 +16,7 @@ export default function Features({ modules }: { modules: readonly FeatureModule[
{modules.map((m) => { @@ -27,22 +27,19 @@ export default function Features({ modules }: { modules: readonly FeatureModule[ role="tab" aria-selected={isActive} onClick={() => setActive(m.id)} - className={`relative shrink-0 snap-start px-4 py-2 rounded-lg text-sm font-medium transition-all ${ + className={`relative shrink-0 snap-start px-4 py-2 rounded-[9px] text-sm font-medium transition-all ${ isActive - ? "text-white bg-white/[0.06]" + ? "text-white bg-[var(--color-surface-2)] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.14)]" : "text-[var(--color-muted)] hover:text-white" }`} > {m.label} - {isActive && ( - - )} ); })}
-
+

{current.tagline}

@@ -52,7 +49,7 @@ export default function Features({ modules }: { modules: readonly FeatureModule[ {current.bullets.map((b, i) => (
diff --git a/src/components/sections/FeaturesSection.astro b/src/components/sections/FeaturesSection.astro index e8fdd60..0b8f5f8 100644 --- a/src/components/sections/FeaturesSection.astro +++ b/src/components/sections/FeaturesSection.astro @@ -8,10 +8,10 @@ const s = t(getLang(Astro)).features;
-
+
{s.eyebrow}
-

+

{s.title}{s.titleHighlight}

diff --git a/src/components/sections/HowItWorks.astro b/src/components/sections/HowItWorks.astro index 6c054f4..3951c36 100644 --- a/src/components/sections/HowItWorks.astro +++ b/src/components/sections/HowItWorks.astro @@ -21,10 +21,10 @@ const providers = [

-
+
{s.eyebrow}
-

+

{s.title}{s.titleHighlight}

diff --git a/src/components/sections/LivePanels.astro b/src/components/sections/LivePanels.astro index 2100bec..ac95857 100644 --- a/src/components/sections/LivePanels.astro +++ b/src/components/sections/LivePanels.astro @@ -11,10 +11,10 @@ const s = t(getLang(Astro)).live;

-
+
{s.eyebrow}
-

+

{s.title}{s.titleHighlight}

diff --git a/src/components/sections/MCPAdvantage.astro b/src/components/sections/MCPAdvantage.astro index f682aea..facde55 100644 --- a/src/components/sections/MCPAdvantage.astro +++ b/src/components/sections/MCPAdvantage.astro @@ -43,10 +43,10 @@ const treatmentClass: Record = {

-
+
{s.eyebrow}
-

+

{s.title}{s.titleHighlight}

@@ -60,7 +60,7 @@ const treatmentClass: Record = { {s.tableTitle}

-
+
@@ -123,7 +123,7 @@ const treatmentClass: Record = {
{s.cards.map((card, i) => (
diff --git a/src/components/sections/Problem.astro b/src/components/sections/Problem.astro index 5079042..49d54f7 100644 --- a/src/components/sections/Problem.astro +++ b/src/components/sections/Problem.astro @@ -18,10 +18,10 @@ const icons: Record = {
-
+
{s.eyebrow}
-

+

{s.title}{s.titleHighlight}

@@ -33,7 +33,7 @@ const icons: Record = { {s.pains.map((p, i) => (

-
+
diff --git a/src/components/sections/QuickStart.astro b/src/components/sections/QuickStart.astro index 2554f75..51a0231 100644 --- a/src/components/sections/QuickStart.astro +++ b/src/components/sections/QuickStart.astro @@ -111,10 +111,10 @@ const steps = s.steps.map((step, i) => ({
-
+
{s.eyebrow}
-

+

{s.title}{s.titleHighlight}{s.titleEnd}

@@ -134,7 +134,7 @@ const steps = s.steps.map((step, i) => ({

    {steps.map((step) => ( -
  1. +
  2. diff --git a/src/i18n/pages/core.ts b/src/i18n/pages/core.ts index 4810655..9625e96 100644 --- a/src/i18n/pages/core.ts +++ b/src/i18n/pages/core.ts @@ -31,6 +31,12 @@ export const coreCopy = { { cmd: "twcore serve", note: "# start the gateway and the control plane" }, ], }, + motion: { + announce: (v: string) => `Core ${v} released`, + doesTitle: "One binary, ", + doesTitleDim: "the whole gateway.", + sample: "Sample", + }, does: { eyebrow: "Capabilities", items: [ @@ -149,6 +155,12 @@ export const coreCopy = { { cmd: "twcore serve", note: "# 启动网关与控制面" }, ], }, + motion: { + announce: (v: string) => `Core ${v} 已发布`, + doesTitle: "一个二进制,", + doesTitleDim: "就是完整的网关。", + sample: "示例", + }, does: { eyebrow: "功能", items: [ diff --git a/src/i18n/pages/home.ts b/src/i18n/pages/home.ts index 688d17f..f19b8ed 100644 --- a/src/i18n/pages/home.ts +++ b/src/i18n/pages/home.ts @@ -8,6 +8,9 @@ export const homeCopy = { description: "ThinkWatch provides AI gateways that route, inspect, and meter model requests and MCP tool calls: ThinkWatch Enterprise, a self-hosted server for organizations, and ThinkWatch Lite, a desktop app for individual developers on macOS, Windows and Linux. Both share ThinkWatch Core, the MIT-licensed gateway engine, which also runs on its own on a Linux server.", }, + announce: (lite: string | null, ent: string | null) => + [ent && `Enterprise ${ent}`, lite && `Lite ${lite}`].filter(Boolean).join(" and ") + " released", + wallLabel: "Works with the clients and SDKs teams already use", eyebrow: "AI API and MCP gateways", h1a: "AI gateways for", h1b: "organizations and individual developers", @@ -132,6 +135,9 @@ export const homeCopy = { description: "ThinkWatch 提供对模型请求与 MCP 工具调用进行路由、检查和计量的 AI 网关,包括面向组织的自托管服务端 ThinkWatch 企业版,以及面向个人开发者、支持 macOS、Windows 与 Linux 的桌面应用 ThinkWatch Lite。二者共用采用 MIT 许可证的网关引擎 ThinkWatch Core,它也可以独立运行在 Linux 服务器上。", }, + announce: (lite: string | null, ent: string | null) => + [ent && `企业版 ${ent}`, lite && `Lite ${lite}`].filter(Boolean).join(" 与 ") + " 已发布", + wallLabel: "兼容团队已在使用的客户端与 SDK", eyebrow: "AI API 与 MCP 网关", h1a: "面向组织与个人开发者的", h1b: "AI 网关", diff --git a/src/i18n/pages/thinkwatch.ts b/src/i18n/pages/thinkwatch.ts index a7703a1..f4d421a 100644 --- a/src/i18n/pages/thinkwatch.ts +++ b/src/i18n/pages/thinkwatch.ts @@ -19,6 +19,7 @@ export const thinkwatchCopy = { pills: ["Self-hosted", "Docker Compose or Kubernetes"], license: "BSL 1.1", versionLabel: "Latest release", + announce: (v: string) => `Enterprise ${v} released`, mockTitle: "Log explorer", mockSample: "Sample data", mockNote: "Users, keys, and models in this panel are illustrative.", @@ -45,6 +46,7 @@ export const thinkwatchCopy = { pills: ["自托管", "Docker Compose 或 Kubernetes"], license: "BSL 1.1", versionLabel: "最新版本", + announce: (v: string) => `企业版 ${v} 已发布`, mockTitle: "日志查询", mockSample: "示例数据", mockNote: "面板中的用户、密钥和模型仅作示意。", diff --git a/src/styles/global.css b/src/styles/global.css index 0e32866..41c52de 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -428,3 +428,142 @@ scroll-behavior: auto !important; } } + +/* ---------- Neon design language (shared by the product pages) ---------- + Near-black ground, the mark's cyan→magenta pair, large tight headings, + hairlines rather than boxes, the product shown in motion. Motion moves + position only, so every block reads without script (NeonMotion adds + .nx-motion and data-in); reduced motion stops all of it. */ +@layer components { + .nx-section { padding-block: 104px; } + .nx-section-tight { padding-block: 72px; } + .nx-hairline { border-top: 1px solid rgba(255, 255, 255, 0.08); } + + .nx-hero { position: relative; isolation: isolate; padding: 72px 0 64px; } + .nx-aurora { + position: absolute; inset: -160px 0 auto; height: 820px; z-index: -1; pointer-events: none; + -webkit-mask-image: linear-gradient(#000 40%, transparent 92%); mask-image: linear-gradient(#000 40%, transparent 92%); + filter: blur(40px); + } + .nx-aurora i { position: absolute; border-radius: 50%; opacity: 0.5; } + .nx-aurora i:nth-child(1) { width: 620px; height: 360px; left: 52%; top: 90px; background: radial-gradient(closest-side, rgb(var(--a1) / 0.42), transparent); animation: nx-drift1 16s ease-in-out infinite; } + .nx-aurora i:nth-child(2) { width: 520px; height: 320px; left: 68%; top: 220px; background: radial-gradient(closest-side, rgb(var(--a2) / 0.36), transparent); animation: nx-drift2 19s ease-in-out infinite; } + .nx-aurora i:nth-child(3) { width: 480px; height: 280px; left: 8%; top: 40px; background: radial-gradient(closest-side, rgba(110, 130, 255, 0.16), transparent); animation: nx-drift1 22s ease-in-out infinite reverse; } + .nx-gridbg { + position: absolute; inset: 0 0 auto; height: 720px; z-index: -1; pointer-events: none; + background-image: linear-gradient(to right, rgba(255,255,255,0.035) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.035) 1px, transparent 1px); + background-size: 64px 64px; + -webkit-mask-image: radial-gradient(ellipse 70% 60% at 65% 30%, #000 20%, transparent 75%); mask-image: radial-gradient(ellipse 70% 60% at 65% 30%, #000 20%, transparent 75%); + } + + .nx-announce { + display: inline-flex; align-items: center; gap: 10px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px; + padding: 6px 14px 6px 10px; font-size: 13px; color: var(--color-muted); background: rgba(255,255,255,0.02); transition: color 150ms; + } + .nx-announce:hover { color: var(--color-text); } + .nx-announce .dot { width: 6px; height: 6px; border-radius: 99px; background: rgb(var(--a1)); box-shadow: 0 0 10px rgb(var(--a1)); animation: nx-blink 2s ease-in-out infinite; } + .nx-announce .chev { color: var(--color-dim); } + + .nx-h1 { margin: 26px 0 0; font-size: clamp(38px, 5.2vw, 64px); line-height: 1.08; font-weight: 600; letter-spacing: -0.035em; color: #fff; text-wrap: balance; } + :lang(zh-CN) .nx-h1 { font-size: clamp(34px, 4.4vw, 56px); line-height: 1.2; letter-spacing: -0.01em; } + .nx-h2 { margin: 0; max-width: 860px; font-size: clamp(30px, 4vw, 48px); line-height: 1.14; font-weight: 600; letter-spacing: -0.03em; color: #fff; text-wrap: balance; } + :lang(zh-CN) .nx-h2 { letter-spacing: -0.01em; line-height: 1.25; } + .nx-h3 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; color: #fff; } + .nx-grad { background: linear-gradient(90deg, rgb(var(--a1)), rgb(var(--a2))); -webkit-background-clip: text; background-clip: text; color: transparent; } + .nx-dim { color: var(--color-muted); } + .nx-lede { margin: 24px 0 0; max-width: 600px; font-size: 18px; line-height: 1.65; color: var(--color-muted); } + .nx-sub { margin: 18px 0 0; max-width: 680px; font-size: 17px; line-height: 1.7; color: var(--color-muted); } + .nx-body { margin: 8px 0 0; font-size: 15px; line-height: 1.65; color: var(--color-muted); } + .nx-label { font-size: 13px; color: var(--color-dim); } + .nx-rise { animation: nx-rise 800ms cubic-bezier(0.2, 0.7, 0.2, 1) both; animation-delay: calc(var(--d, 0) * 130ms + 80ms); } + + .nx-actions { margin-top: 32px; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 16px; } + .nx-pill { + display: inline-flex; min-height: 44px; align-items: center; justify-content: center; border-radius: 999px; background: #f7f8f8; + padding: 0 22px; font-size: 15px; font-weight: 600; color: #08090a; transition: background 160ms, transform 160ms, box-shadow 160ms; + } + .nx-pill:hover { background: #fff; transform: translateY(-1px); box-shadow: 0 8px 30px -8px rgb(var(--a1) / 0.5); } + .nx-ghost { + display: inline-flex; min-height: 44px; align-items: center; border-radius: 999px; border: 1px solid rgba(255,255,255,0.15); + padding: 0 20px; font-size: 15px; color: var(--color-text); transition: background 160ms, border-color 160ms; + } + .nx-ghost:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.26); } + .nx-link { display: inline-flex; min-height: 44px; align-items: center; gap: 6px; font-size: 15px; font-weight: 500; color: var(--color-text); transition: color 150ms; } + .nx-link::after { content: "→"; color: var(--color-dim); transition: transform 150ms, color 150ms; } + .nx-link:hover { color: #fff; } + .nx-link:hover::after { transform: translateX(3px); color: rgb(var(--a1)); } + + /* A window: hairline frame, a light line sweeping along its top edge */ + .nx-panel { + position: relative; min-width: 0; overflow: hidden; border: 1px solid rgba(255,255,255,0.14); border-radius: 16px; + background: linear-gradient(180deg, rgba(20,21,24,0.92), rgba(12,13,15,0.92)); + box-shadow: 0 40px 120px -40px rgb(var(--a1) / 0.28), 0 30px 80px -30px rgba(0,0,0,0.8); + } + .nx-panel::before { + content: ""; position: absolute; inset: 0 0 auto; height: 1px; z-index: 1; + background: linear-gradient(90deg, transparent, rgb(var(--a1) / 0.8), rgb(var(--a2) / 0.8), transparent); background-size: 200% 100%; + animation: nx-sweep 6s linear infinite; + } + .nx-panel-top { display: flex; align-items: center; gap: 14px; border-bottom: 1px solid rgba(255,255,255,0.08); padding: 12px 16px; font-size: 12px; color: var(--color-muted); } + .nx-lights { display: inline-flex; gap: 6px; } + .nx-lights i { width: 10px; height: 10px; border-radius: 99px; background: #2a2c31; } + .nx-live { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; color: rgb(var(--a1)); } + .nx-live i { width: 6px; height: 6px; border-radius: 99px; background: rgb(var(--a1)); animation: nx-blink 1.6s ease-in-out infinite; } + + /* Tiles: a quiet surface with a light that follows the pointer */ + .nx-bento { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; } + .nx-tile { + --mx: -200px; --my: -200px; position: relative; min-width: 0; overflow: hidden; border-radius: 16px; + border: 1px solid rgba(255,255,255,0.08); background: linear-gradient(180deg, #141518, #0f1012); padding: 28px; transition: border-color 200ms; + } + .nx-tile::before { content: ""; position: absolute; inset: 0; pointer-events: none; background: radial-gradient(360px circle at var(--mx) var(--my), rgb(var(--a1) / 0.1), transparent 60%); } + .nx-tile:hover { border-color: rgba(255,255,255,0.14); } + .nx-span-2 { grid-column: span 2; } + .nx-span-3 { grid-column: span 3; } + .nx-span-4 { grid-column: span 4; } + .nx-span-6 { grid-column: span 6; } + @media (max-width: 1023px) { + .nx-bento { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .nx-span-2, .nx-span-3 { grid-column: span 1; } + .nx-span-4, .nx-span-6 { grid-column: span 2; } + } + @media (max-width: 700px) { + .nx-bento { grid-template-columns: 1fr; } + .nx-span-2, .nx-span-3, .nx-span-4, .nx-span-6 { grid-column: auto; } + } + .nx-big { margin: 0; font-size: 48px; font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; font-variant-numeric: tabular-nums; } + + /* Hairline columns: sections side by side, split by thin rules */ + .nx-cols { display: grid; grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); border-top: 1px solid rgba(255,255,255,0.08); } + .nx-cols > * { padding: 28px 28px 8px; border-left: 1px solid rgba(255,255,255,0.08); min-width: 0; } + .nx-cols > *:first-child { border-left: 0; padding-left: 0; } + @media (max-width: 860px) { + .nx-cols { grid-template-columns: 1fr; } + .nx-cols > *, .nx-cols > *:first-child { border-left: 0; padding: 28px 0 8px; border-top: 1px solid rgba(255,255,255,0.08); } + .nx-cols > *:first-child { border-top: 0; } + } + .nx-fig { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; color: var(--color-dim); } + + /* Scrolling wall of names */ + .nx-marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); } + .nx-track { display: flex; width: max-content; gap: 56px; margin: 0; padding: 0; list-style: none; font-size: 17px; font-weight: 560; letter-spacing: -0.01em; color: #9ca1a9; animation: nx-marquee 38s linear infinite; } + .nx-marquee:hover .nx-track { animation-play-state: paused; } + + .nx-motion .nx-in { transform: translateY(28px); transition: transform 900ms cubic-bezier(0.2, 0.7, 0.2, 1); } + .nx-motion .nx-in[data-in] { transform: none; } + .nx-tick { animation: nx-tick 600ms ease-out; } +} + +@keyframes nx-rise { from { opacity: 0; transform: translateY(14px); } } +@keyframes nx-blink { 50% { opacity: 0.3; } } +@keyframes nx-sweep { to { background-position: -200% 0; } } +@keyframes nx-drift1 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-60px, 40px) scale(1.15); } } +@keyframes nx-drift2 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(50px, -30px) scale(0.9); } } +@keyframes nx-halo { 0%, 100% { opacity: 0.8; transform: scale(1); } 50% { opacity: 1; transform: scale(1.06); } } +@keyframes nx-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } +@keyframes nx-marquee { to { transform: translateX(calc(-50% - 28px)); } } +@keyframes nx-tick { 0% { color: rgb(var(--a1)); text-shadow: 0 0 12px rgb(var(--a1) / 0.8); } } +@media (prefers-reduced-motion: reduce) { + .nx-aurora i, .nx-rise, .nx-panel::before, .nx-track, .nx-announce .dot, .nx-live i, .nx-tick { animation: none !important; } + .nx-motion .nx-in { transform: none; transition: none; } +}