Files
OSGKeyboard/docs/index.html
T
Rocky cee5c312a7 docs(landing): interactive pillars, section bands, and copy polish
Replace the duplicate BYOK block with hover/tap detail panels under
the differentiators, drop the hero title period, center the matrix
footnote, and alternate section washes so vs / steps read as separate bands.
2026-07-12 17:03:55 +08:00

1297 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-Hans" data-theme="system">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OSGKeyboard — 开口即文字 · iOS &amp; Mac 语音输入</title>
<meta name="description" content="免费开源的跨端语音输入。本地识别、BYOK 润色、个性词库。iPhone、iPad、Mac — 开口即文字">
<meta name="keywords" content="voice keyboard, dictation, BYOK, open source, privacy, 语音输入, 听写, OSGKeyboard">
<meta name="theme-color" content="#0a0a0b" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#f7f7f5" media="(prefers-color-scheme: light)">
<link rel="canonical" href="https://hkgood.github.io/OSGKeyboard/">
<link rel="icon" href="assets/app-icon.png" type="image/png">
<link rel="apple-touch-icon" href="assets/app-icon.png">
<meta property="og:type" content="website">
<meta property="og:title" content="OSGKeyboard — 开口即文字">
<meta property="og:description" content="免费 · 跨端 · 开源 · 隐私优先 · BYOK。语音留在本机,AI 用你自己的 Key。">
<meta property="og:url" content="https://hkgood.github.io/OSGKeyboard/">
<meta property="og:image" content="https://hkgood.github.io/OSGKeyboard/assets/screenshots/zh/dark/mac-dashboard.png">
<meta name="twitter:card" content="summary_large_image">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "OSGKeyboard",
"operatingSystem": "iOS, iPadOS, macOS",
"applicationCategory": "UtilitiesApplication",
"url": "https://hkgood.github.io/OSGKeyboard/",
"image": "https://hkgood.github.io/OSGKeyboard/assets/app-icon.png",
"description": "Free open-source voice input for iPhone, iPad, and Mac. On-device recognition, BYOK AI polish, personal dictionary.",
"softwareVersion": "0.5.4",
"isAccessibleForFree": true,
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"author": { "@type": "Organization", "name": "OSGKeyboard" }
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,300,0,0&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f7f7f5;
--bg-elevated: #ffffff;
--bg-soft: #efefec;
--hero-wash: #e7f3eb;
--text: #121214;
--text-2: #5a5a62;
--text-3: #8e8e96;
--line: rgba(18, 18, 20, 0.10);
--line-strong: rgba(18, 18, 20, 0.16);
--accent: #2f9a52;
--accent-soft: rgba(47, 154, 82, 0.12);
--accent-glow: rgba(47, 154, 82, 0.28);
--nav: rgba(247, 247, 245, 0.78);
--shadow: 0 28px 80px rgba(18, 18, 20, 0.12);
--radius: 20px;
--max: 1120px;
--font: "Outfit", -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", sans-serif;
}
html[data-theme="dark"] {
--bg: #0a0a0b;
--bg-elevated: #131316;
--bg-soft: #18181b;
--hero-wash: #102018;
--text: #f4f4f2;
--text-2: #a8a8b0;
--text-3: #6f6f78;
--line: rgba(255, 255, 255, 0.09);
--line-strong: rgba(255, 255, 255, 0.14);
--accent: #4cc46e;
--accent-soft: rgba(76, 196, 110, 0.14);
--accent-glow: rgba(76, 196, 110, 0.32);
--nav: rgba(10, 10, 11, 0.78);
--shadow: 0 28px 80px rgba(0, 0, 0, 0.45);
}
@media (prefers-color-scheme: dark) {
html[data-theme="system"] {
--bg: #0a0a0b;
--bg-elevated: #131316;
--bg-soft: #18181b;
--hero-wash: #102018;
--text: #f4f4f2;
--text-2: #a8a8b0;
--text-3: #6f6f78;
--line: rgba(255, 255, 255, 0.09);
--line-strong: rgba(255, 255, 255, 0.14);
--accent: #4cc46e;
--accent-soft: rgba(76, 196, 110, 0.14);
--accent-glow: rgba(76, 196, 110, 0.32);
--nav: rgba(10, 10, 11, 0.78);
--shadow: 0 28px 80px rgba(0, 0, 0, 0.45);
}
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font);
background: var(--bg);
color: var(--text);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
button { font: inherit; color: inherit; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
line-height: 1;
user-select: none;
}
.wrap {
width: min(100% - 2.5rem, var(--max));
margin-inline: auto;
}
/* —— Nav —— */
.nav {
position: sticky;
top: 0;
z-index: 40;
background: var(--nav);
backdrop-filter: blur(18px) saturate(1.4);
-webkit-backdrop-filter: blur(18px) saturate(1.4);
border-bottom: 1px solid var(--line);
}
.nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
gap: 1rem;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.7rem;
font-weight: 600;
letter-spacing: -0.02em;
}
.brand-mark {
width: 72px;
height: 22px;
color: var(--accent);
}
.brand-name { font-size: 1rem; }
.nav-actions { display: flex; align-items: center; gap: 0.35rem; }
.icon-btn, .pill-toggle button {
border: 1px solid var(--line);
background: transparent;
border-radius: 999px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.icon-btn {
width: 36px;
height: 36px;
display: grid;
place-items: center;
color: var(--text-2);
}
.icon-btn:hover { color: var(--text); border-color: var(--line-strong); }
.icon-btn .material-symbols-outlined { font-size: 20px; }
.pill-toggle {
display: inline-flex;
padding: 2px;
border-radius: 999px;
border: 1px solid var(--line);
}
.pill-toggle button {
border: 0;
padding: 0.28rem 0.65rem;
font-size: 0.78rem;
font-weight: 600;
color: var(--text-3);
background: transparent;
}
.pill-toggle button.active {
background: var(--text);
color: var(--bg);
}
.nav-link {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.7rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
color: var(--text-2);
}
.nav-link:hover { color: var(--text); }
.nav-link .material-symbols-outlined { font-size: 18px; }
/* —— Buttons —— */
.btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.85rem 1.35rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.95rem;
border: 1px solid transparent;
transition: transform 0.18s ease, background 0.18s, border-color 0.18s;
}
.btn:hover { transform: translateY(-1px); }
.btn .material-symbols-outlined { font-size: 20px; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-ghost {
background: transparent;
border-color: var(--line-strong);
color: var(--text);
}
.btn-ghost:hover { background: var(--bg-soft); }
/* —— Hero —— */
.hero {
position: relative;
/* Full-bleed solid wash (no side gaps from inset gradients). */
background: var(--hero-wash);
padding: 4.5rem 0 2.5rem;
overflow: hidden;
}
.hero .wrap { position: relative; z-index: 1; }
.hero-copy {
max-width: 42rem;
margin: 0 auto 2.5rem;
text-align: center;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-bottom: 1.1rem;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
}
.eyebrow .material-symbols-outlined { font-size: 16px; }
h1 {
margin: 0 0 1rem;
font-size: clamp(2.6rem, 7vw, 4.4rem);
line-height: 1.02;
letter-spacing: -0.045em;
font-weight: 700;
}
.lead {
margin: 0 auto 1.75rem;
max-width: 34rem;
font-size: 1.12rem;
color: var(--text-2);
}
.hero-cta {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.7rem;
margin-bottom: 1.4rem;
}
.hero-meta {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem 1.2rem;
font-size: 0.86rem;
color: var(--text-3);
}
.hero-meta span {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.hero-meta .material-symbols-outlined { font-size: 16px; color: var(--accent); }
/* Marketing device-family composite (replaces CSS mockup frames). */
.hero-stage {
position: relative;
margin: 2.25rem auto 0;
max-width: 1080px;
transform: translateY(16px);
opacity: 0;
animation: rise-in 1s cubic-bezier(0.22, 1, 0.36, 1) 0.12s forwards;
}
.hero-stage img {
width: 100%;
height: auto;
display: block;
}
@keyframes rise-in {
to { transform: translateY(0); opacity: 1; }
}
/* —— Reveal —— */
.reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal.in { opacity: 1; transform: none; }
.reveal-delay-1 { transition-delay: 0.08s; }
.reveal-delay-2 { transition-delay: 0.16s; }
.reveal-delay-3 { transition-delay: 0.24s; }
.reveal-delay-4 { transition-delay: 0.32s; }
/* —— Sections —— */
section.block { padding: 5rem 0; }
/* Full-bleed bands to separate adjacent marketing blocks */
section.block.block-band {
background: var(--bg-soft);
border-block: 1px solid var(--line);
padding: 4.25rem 0;
}
/* Alternate wash so consecutive bands dont read as one slab */
section.block.block-band-wash {
background: var(--hero-wash);
border-block: 1px solid var(--line);
padding: 4.25rem 0;
}
.block-head {
max-width: 38rem;
margin: 0 auto 2.75rem;
text-align: center;
}
.block-head h2 {
margin: 0 0 0.65rem;
font-size: clamp(1.7rem, 3.5vw, 2.35rem);
letter-spacing: -0.03em;
font-weight: 700;
}
.block-head p {
margin: 0;
color: var(--text-2);
font-size: 1.05rem;
}
/* —— Diff pillars (hover / tap → detail stage) —— */
.pillars {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.75rem;
}
@media (max-width: 960px) {
.pillars { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 620px) {
.pillars { grid-template-columns: 1fr 1fr; }
}
.pillar {
appearance: none;
width: 100%;
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
padding: 1.15rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--bg-elevated);
transition: border-color 0.22s, transform 0.22s, background 0.22s, box-shadow 0.22s;
}
.pillar:hover {
transform: translateY(-2px);
border-color: var(--line-strong);
}
.pillar.is-active,
.pillar:focus-visible {
border-color: var(--accent);
background: var(--accent-soft);
box-shadow: 0 0 0 1px var(--accent);
outline: none;
transform: translateY(-2px);
}
.pillar .material-symbols-outlined {
font-size: 24px;
color: var(--accent);
margin-bottom: 0.65rem;
display: block;
}
.pillar h3 {
margin: 0 0 0.3rem;
font-size: 1rem;
letter-spacing: -0.015em;
}
.pillar .pillar-lead {
margin: 0;
font-size: 0.82rem;
color: var(--text-2);
line-height: 1.4;
}
.pillar-hint {
display: inline-block;
margin-top: 0.7rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--accent);
opacity: 0.75;
}
.pillar.is-active .pillar-hint { opacity: 1; }
.pillar-stage {
margin-top: 1rem;
border-radius: calc(var(--radius) + 4px);
border: 1px solid var(--line);
background: var(--hero-wash);
padding: 1.5rem 1.35rem;
min-height: 9.5rem;
}
.pillar-panel[hidden] { display: none !important; }
.pillar-panel-kicker {
margin: 0 0 1rem;
font-size: 0.9rem;
color: var(--text-2);
max-width: 42rem;
}
.pillar-points {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.1rem;
}
@media (max-width: 900px) {
.pillar-points { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
.pillar-points { grid-template-columns: 1fr; }
}
.pillar-point .material-symbols-outlined {
font-size: 22px;
color: var(--accent);
margin-bottom: 0.45rem;
display: block;
}
.pillar-point h4 {
margin: 0 0 0.28rem;
font-size: 0.95rem;
letter-spacing: -0.015em;
}
.pillar-point p {
margin: 0;
font-size: 0.86rem;
color: var(--text-2);
line-height: 1.45;
}
/* —— Competitor matrix —— */
.vs-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid var(--line);
border-radius: calc(var(--radius) + 4px);
background: var(--bg-elevated);
}
.vs-table {
width: 100%;
min-width: 720px;
border-collapse: collapse;
font-size: 0.88rem;
}
.vs-table th,
.vs-table td {
padding: 0.85rem 1rem;
text-align: left;
vertical-align: top;
border-bottom: 1px solid var(--line);
line-height: 1.45;
}
.vs-table thead th {
font-size: 0.82rem;
font-weight: 600;
letter-spacing: -0.01em;
background: var(--bg-soft);
color: var(--text);
white-space: nowrap;
}
.vs-table tbody th {
font-weight: 600;
color: var(--text-2);
white-space: nowrap;
width: 7.5rem;
background: var(--bg-elevated);
position: sticky;
left: 0;
z-index: 1;
box-shadow: 4px 0 12px rgba(18, 18, 20, 0.04);
}
.vs-table tbody tr:last-child th,
.vs-table tbody tr:last-child td {
border-bottom: 0;
}
.vs-table .col-osg {
background: var(--accent-soft);
color: var(--text);
font-weight: 500;
}
.vs-table thead .col-osg {
color: var(--accent);
font-weight: 700;
}
.vs-table .cell-mute { color: var(--text-3); }
.vs-table .cell-price {
white-space: pre-line;
color: var(--text-2);
}
.vs-table .cell-price-warn {
color: var(--text);
font-weight: 500;
}
.vs-table .col-osg.cell-price {
color: var(--text);
font-weight: 600;
}
.vs-foot {
margin: 0;
padding: 0.85rem 1rem 1rem;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--text-3);
text-align: center;
}
/* —— Steps —— */
.steps {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
counter-reset: step;
}
@media (max-width: 720px) {
.steps { grid-template-columns: 1fr; }
}
.step {
position: relative;
padding: 1.5rem 1.25rem 1.5rem 1.5rem;
border-top: 1px solid var(--line-strong);
}
.step::before {
counter-increment: step;
content: "0" counter(step);
display: block;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--accent);
margin-bottom: 0.85rem;
}
.step h3 {
margin: 0 0 0.4rem;
font-size: 1.05rem;
}
.step p {
margin: 0;
color: var(--text-2);
font-size: 0.93rem;
}
/* —— Footer —— */
footer {
border-top: 1px solid var(--line);
padding: 2rem 0 2.75rem;
font-size: 0.85rem;
color: var(--text-3);
}
.footer-inner {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.footer-links {
display: flex;
flex-wrap: wrap;
gap: 1.1rem;
}
.footer-links a { color: var(--text-2); }
.footer-links a:hover { color: var(--text); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
.reveal { opacity: 1; transform: none; }
.hero-stage { opacity: 1; transform: none; }
}
</style>
</head>
<body>
<header class="nav">
<div class="wrap nav-inner">
<a class="brand" href="#top" aria-label="OSGKeyboard">
<svg class="brand-mark" viewBox="36 367 952 291" aria-hidden="true">
<g fill="currentColor">
<path d="M627.511 512.419C627.511 448.783 576.127 397.197 512.741 397.197C449.355 397.197 397.971 448.783 397.971 512.419C397.971 576.054 449.355 627.641 512.741 627.641V637.837C443.746 637.837 387.814 581.686 387.814 512.419C387.814 443.152 443.746 387 512.741 387C581.736 387 637.668 443.152 637.668 512.419C637.668 581.686 581.736 637.837 512.741 637.837V627.641C576.127 627.641 627.511 576.054 627.511 512.419Z"/>
<path d="M56.2632 512.419C56.2632 581.686 112.195 637.837 181.19 637.837C250.185 637.837 306.117 581.686 306.117 512.419C306.117 443.152 250.185 387 181.19 387C112.195 387 56.2632 443.152 56.2632 512.419Z"/>
<path d="M399.618 562.094L399.618 552.257L512.74 552.257L512.74 562.094L399.618 562.094Z"/>
<path d="M512.741 483.398V473.561H625.864V483.398H512.741Z"/>
<path d="M843.308 387.981C910.987 387.981 966.093 441.8 968.171 508.975H843.308V518.812H968.096C965.014 585.066 910.324 637.836 843.308 637.836C774.313 637.836 718.381 581.904 718.381 512.909C718.381 443.914 774.312 387.981 843.308 387.981ZM968.234 518.812H968.096C968.187 516.855 968.234 514.888 968.234 512.909C968.234 511.593 968.211 510.281 968.171 508.975H968.234V518.812Z"/>
</g>
</svg>
<span class="brand-name">OSGKeyboard</span>
</a>
<div class="nav-actions">
<div class="pill-toggle" role="group" aria-label="Language">
<button type="button" data-lang="en">EN</button>
<button type="button" class="active" data-lang="zh">中文</button>
</div>
<button type="button" class="icon-btn" id="themeToggle" aria-label="Toggle theme">
<span class="material-symbols-outlined" data-theme-icon>contrast</span>
</button>
<a class="nav-link" href="https://github.com/hkgood/OSGKeyboard" rel="noopener">
<span class="material-symbols-outlined" aria-hidden="true">code</span>
<span data-i18n="nav.github">GitHub</span>
</a>
</div>
</div>
</header>
<main id="top">
<!-- Hero -->
<section class="hero">
<div class="wrap">
<div class="hero-copy">
<div class="eyebrow">
<span class="material-symbols-outlined" aria-hidden="true">graphic_eq</span>
<span data-i18n="hero.eyebrow">Free · Open · Cross-platform</span>
</div>
<h1 data-i18n="hero.title">开口即文字</h1>
<p class="lead" data-i18n="hero.lead">免费开源的跨端语音输入。本地识别,自带 API Key 润色,避免重复订阅。iPhone、iPad、Mac — 说完即落字。</p>
<div class="hero-cta">
<a class="btn btn-primary" href="https://github.com/hkgood/OSGKeyboard/releases" rel="noopener">
<span class="material-symbols-outlined" aria-hidden="true">download</span>
<span data-i18n="hero.cta.primary">立即获取</span>
</a>
<a class="btn btn-ghost" href="https://github.com/hkgood/OSGKeyboard" rel="noopener">
<span class="material-symbols-outlined" aria-hidden="true">terminal</span>
<span data-i18n="hero.cta.secondary">查看源码</span>
</a>
</div>
<div class="hero-meta">
<span><span class="material-symbols-outlined" aria-hidden="true">lock</span><span data-i18n="hero.meta1">默认本地识别</span></span>
<span><span class="material-symbols-outlined" aria-hidden="true">key</span><span data-i18n="hero.meta2">BYOK 不重复付费</span></span>
<span><span class="material-symbols-outlined" aria-hidden="true">devices</span><span data-i18n="hero.meta3">iOS · iPad · Mac</span></span>
</div>
</div>
<div class="hero-stage" aria-label="OSGKeyboard on Mac, iPad, and iPhone">
<img src="assets/hero-devices.png" width="3200" height="1432" alt="OSGKeyboard on iPad, Mac, and iPhone" loading="eager">
</div>
</div>
</section>
<!-- Differentiation -->
<section class="block" id="why">
<div class="wrap">
<div class="block-head reveal">
<h2 data-i18n="diff.title">开源,尽是不同</h2>
<p data-i18n="diff.subtitle">悬停或点按下方卡片,展开每一项差异——内容写在产品里,不写在定价页。</p>
</div>
<div class="pillars reveal" role="tablist" aria-label="OSGKeyboard differentiators">
<button type="button" class="pillar is-active" role="tab" id="tab-free" aria-selected="true" aria-controls="panel-free" data-pillar="free">
<span class="material-symbols-outlined" aria-hidden="true">volunteer_activism</span>
<h3 data-i18n="diff.free.t">免费</h3>
<p class="pillar-lead" data-i18n="diff.free.b">核心全开,没有 Pro 墙。</p>
<span class="pillar-hint" data-i18n="diff.hint">展开详情</span>
</button>
<button type="button" class="pillar" role="tab" id="tab-cross" aria-selected="false" aria-controls="panel-cross" data-pillar="cross" tabindex="-1">
<span class="material-symbols-outlined" aria-hidden="true">devices</span>
<h3 data-i18n="diff.cross.t">跨端</h3>
<p class="pillar-lead" data-i18n="diff.cross.b">iPhone · iPad · Mac 同源。</p>
<span class="pillar-hint" data-i18n="diff.hint">展开详情</span>
</button>
<button type="button" class="pillar" role="tab" id="tab-open" aria-selected="false" aria-controls="panel-open" data-pillar="open" tabindex="-1">
<span class="material-symbols-outlined" aria-hidden="true">code</span>
<h3 data-i18n="diff.open.t">开源</h3>
<p class="pillar-lead" data-i18n="diff.open.b">代码可审计,信任可验证。</p>
<span class="pillar-hint" data-i18n="diff.hint">展开详情</span>
</button>
<button type="button" class="pillar" role="tab" id="tab-privacy" aria-selected="false" aria-controls="panel-privacy" data-pillar="privacy" tabindex="-1">
<span class="material-symbols-outlined" aria-hidden="true">shield_lock</span>
<h3 data-i18n="diff.privacy.t">隐私</h3>
<p class="pillar-lead" data-i18n="diff.privacy.b">默认本地,录音不上云。</p>
<span class="pillar-hint" data-i18n="diff.hint">展开详情</span>
</button>
<button type="button" class="pillar" role="tab" id="tab-byok" aria-selected="false" aria-controls="panel-byok" data-pillar="byok" tabindex="-1">
<span class="material-symbols-outlined" aria-hidden="true">vpn_key</span>
<h3 data-i18n="diff.byok.t">BYOK</h3>
<p class="pillar-lead" data-i18n="diff.byok.b">本地免费;云端 Key 自付。</p>
<span class="pillar-hint" data-i18n="diff.hint">展开详情</span>
</button>
</div>
<div class="pillar-stage reveal">
<div class="pillar-panel" id="panel-free" role="tabpanel" aria-labelledby="tab-free" data-panel="free">
<p class="pillar-panel-kicker" data-i18n="diff.free.k">没有「基础版听写、进阶版解锁」——认真用也不用升级。</p>
<div class="pillar-points">
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">mic</span>
<h4 data-i18n="diff.free.1.t">听写与历史</h4>
<p data-i18n="diff.free.1.b">录音转写、历史回看不设订阅门槛。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">menu_book</span>
<h4 data-i18n="diff.free.2.t">个性词库</h4>
<p data-i18n="diff.free.2.b">专有名词、产品名照常加,不为词条付费。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">devices</span>
<h4 data-i18n="diff.free.3.t">跨端核心</h4>
<p data-i18n="diff.free.3.b">iPhone / iPad / Mac 能力同一档,不按设备加价。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">favorite</span>
<h4 data-i18n="diff.free.4.t">打赏可选</h4>
<p data-i18n="diff.free.4.b">自愿支持开发;不解锁任何功能。</p>
</div>
</div>
</div>
<div class="pillar-panel" id="panel-cross" role="tabpanel" aria-labelledby="tab-cross" data-panel="cross" hidden>
<p class="pillar-panel-kicker" data-i18n="diff.cross.k">学会一次交互语言,三端接着用——不是三套互不相干的 App。</p>
<div class="pillar-points">
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">smartphone</span>
<h4 data-i18n="diff.cross.1.t">iPhone</h4>
<p data-i18n="diff.cross.1.b">系统自定义键盘,在任意输入框开口落字。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">tablet_mac</span>
<h4 data-i18n="diff.cross.2.t">iPad</h4>
<p data-i18n="diff.cross.2.b">分栏侧栏 + 首页同源,大屏也是同一产品语言。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">laptop_mac</span>
<h4 data-i18n="diff.cross.3.t">Mac</h4>
<p data-i18n="diff.cross.3.b">按住 Option 全局听写,菜单栏常驻,不抢焦点。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">cloud_sync</span>
<h4 data-i18n="diff.cross.4.t">词库可同步</h4>
<p data-i18n="diff.cross.4.b">可选 iCloud 同步个性词;关掉也完全能离线用。</p>
</div>
</div>
</div>
<div class="pillar-panel" id="panel-open" role="tabpanel" aria-labelledby="tab-open" data-panel="open" hidden>
<p class="pillar-panel-kicker" data-i18n="diff.open.k">闭源工具让你相信隐私页;我们让你直接读仓库。</p>
<div class="pillar-points">
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">visibility</span>
<h4 data-i18n="diff.open.1.t">可审计</h4>
<p data-i18n="diff.open.1.b">识别、润色、存储路径都在 GitHub,可自行核对。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">fork_right</span>
<h4 data-i18n="diff.open.2.t">可 fork</h4>
<p data-i18n="diff.open.2.b">改键位、换引擎、裁功能——仓库在,路就在。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">gavel</span>
<h4 data-i18n="diff.open.3.t">许可清晰</h4>
<p data-i18n="diff.open.3.b">开源许可写在仓库里,不是营销口号。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">build</span>
<h4 data-i18n="diff.open.4.t">可自建</h4>
<p data-i18n="diff.open.4.b">按文档用 Xcode 编译安装,不依赖我们的商店账号。</p>
</div>
</div>
</div>
<div class="pillar-panel" id="panel-privacy" role="tabpanel" aria-labelledby="tab-privacy" data-panel="privacy" hidden>
<p class="pillar-panel-kicker" data-i18n="diff.privacy.k">默认路径就是本地——隐私不是付费升级项。</p>
<div class="pillar-points">
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">phonelink_lock</span>
<h4 data-i18n="diff.privacy.1.t">默认本机识别</h4>
<p data-i18n="diff.privacy.1.b">开箱走本地 ASR;不上我们的识别服务器。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">cloud_off</span>
<h4 data-i18n="diff.privacy.2.t">录音不上云</h4>
<p data-i18n="diff.privacy.2.b">我们不收集你的语音流做训练或分析。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">block</span>
<h4 data-i18n="diff.privacy.3.t">无追踪器</h4>
<p data-i18n="diff.privacy.3.b">不做广告标识符、行为画像那一套。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">person_off</span>
<h4 data-i18n="diff.privacy.4.t">无强制账号</h4>
<p data-i18n="diff.privacy.4.b">不用注册就能听写;数据默认留在本机。</p>
</div>
</div>
</div>
<div class="pillar-panel" id="panel-byok" role="tabpanel" aria-labelledby="tab-byok" data-panel="byok" hidden>
<p class="pillar-panel-kicker" data-i18n="compare.subtitle">订阅常把「听写壳」和「模型用量」捆在一起。本地识别免费起步;AI 润色按你已有的额度计费。</p>
<div class="pillar-points">
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">payments</span>
<h4 data-i18n="compare.1.t">躲开重复付费</h4>
<p data-i18n="compare.1.b">已有 DeepSeek / OpenAI 额度就直接用,不必再开一份「听写订阅」。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">visibility</span>
<h4 data-i18n="compare.2.t">费用看得见</h4>
<p data-i18n="compare.2.b">模型账单只在服务商侧;OSGKeyboard 不经手、不加价。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">swap_horiz</span>
<h4 data-i18n="compare.3.t">模型你说了算</h4>
<p data-i18n="compare.3.b">OpenAI 兼容端点即可接入;不需要 AI 时,本地识别照用不填 Key。</p>
</div>
<div class="pillar-point">
<span class="material-symbols-outlined" aria-hidden="true">encrypted</span>
<h4 data-i18n="compare.4.t">Key 留在本机</h4>
<p data-i18n="compare.4.b">写入系统 Keychain,不经过我们的服务器。</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Competitor matrix -->
<section class="block block-band" id="vs">
<div class="wrap">
<div class="block-head reveal">
<h2 data-i18n="vs.title">和常见听写工具比一比</h2>
<p data-i18n="vs.subtitle">免费额度用着用着就撞墙;订阅不便宜。开源这条路:本地免费,云端 Key 自付。</p>
</div>
<div class="vs-scroll reveal">
<table class="vs-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Typeless</th>
<th scope="col">Superwhisper</th>
<th scope="col">Openless</th>
<th scope="col" class="col-osg">OSGKeyboard</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" data-i18n="vs.row.open">开源</th>
<td class="cell-mute" data-i18n="vs.no"></td>
<td class="cell-mute" data-i18n="vs.no"></td>
<td data-i18n="vs.yes"></td>
<td class="col-osg" data-i18n="vs.yes"></td>
</tr>
<tr>
<th scope="row" data-i18n="vs.row.price">付费</th>
<td class="cell-price cell-price-warn" data-i18n="vs.typeless.price">每周约 8,000 词免费额度;订阅不便宜:Pro 约 $12/月起(年付)/ $30/月(月付)</td>
<td class="cell-price cell-price-warn" data-i18n="vs.superwhisper.price">免费档额度有限;订阅/买断不便宜:Pro 约 $8.5/月,买断约 $250</td>
<td class="cell-price" data-i18n="vs.openless.price">App 免费;识别 / 润色按你的 Key 计费</td>
<td class="col-osg cell-price" data-i18n="vs.osg.price">本地模式完全免费;云端润色 Key 自付(不另收听写订阅)</td>
</tr>
<tr>
<th scope="row" data-i18n="vs.row.local">可本地识别</th>
<td class="cell-mute" data-i18n="vs.no"></td>
<td data-i18n="vs.yes"></td>
<td data-i18n="vs.yes"></td>
<td class="col-osg" data-i18n="vs.yes"></td>
</tr>
<tr>
<th scope="row" data-i18n="vs.row.byok">BYOK</th>
<td class="cell-mute" data-i18n="vs.no"></td>
<td data-i18n="vs.partial">部分(Pro</td>
<td data-i18n="vs.yes"></td>
<td class="col-osg" data-i18n="vs.yes"></td>
</tr>
<tr>
<th scope="row">Mac</th>
<td data-i18n="vs.yes"></td>
<td data-i18n="vs.yes"></td>
<td data-i18n="vs.yes"></td>
<td class="col-osg" data-i18n="vs.yes"></td>
</tr>
<tr>
<th scope="row" data-i18n="vs.row.mobile">iPhone / iPad</th>
<td data-i18n="vs.yes"></td>
<td data-i18n="vs.yes"></td>
<td class="cell-mute" data-i18n="vs.no"></td>
<td class="col-osg" data-i18n="vs.yes"></td>
</tr>
<tr>
<th scope="row">Windows</th>
<td data-i18n="vs.yes"></td>
<td data-i18n="vs.yes"></td>
<td data-i18n="vs.yes"></td>
<td class="col-osg cell-mute" data-i18n="vs.no"></td>
</tr>
</tbody>
</table>
<p class="vs-foot" data-i18n="vs.foot">价格与功能据各产品公开说明整理,可能更新,以官方为准。</p>
</div>
</div>
</section>
<!-- Steps -->
<section class="block block-band-wash" id="start">
<div class="wrap">
<div class="block-head reveal">
<h2 data-i18n="steps.title">开箱可用,只要三步</h2>
<p data-i18n="steps.subtitle">不用订阅、不用注册账号。装好权限,说第一句就行。</p>
</div>
<div class="steps">
<div class="step reveal">
<h3 data-i18n="step1.t">装好并授权</h3>
<p data-i18n="step1.b">iOS:设置 → 键盘 → 开启完全访问。Mac:麦克风 + 辅助功能。完成时键盘列表能看到 OSG,或菜单栏出现图标。</p>
</div>
<div class="step reveal reveal-delay-1">
<h3 data-i18n="step2.t">先本地,Key 可选</h3>
<p data-i18n="step2.b">默认本地识别即可开口;要 AI 润色再填自己的 API Key。不配 Key 也能落字。</p>
</div>
<div class="step reveal reveal-delay-2">
<h3 data-i18n="step3.t">开口即写</h3>
<p data-i18n="step3.b">iOS 切到 OSGKeyboard 点麦克风;Mac 按住 Option。文字出现在当前光标处。</p>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap footer-inner">
<span data-i18n="footer.copy">© OSGKeyboard · v0.5.4 · 源码可见</span>
<span class="footer-links">
<a href="https://github.com/hkgood/OSGKeyboard" rel="noopener" data-i18n="footer.source">Source</a>
<a href="privacy/" data-i18n="footer.privacy">Privacy</a>
<a href="https://github.com/hkgood/OSGKeyboard/blob/main/LICENSE" rel="noopener" data-i18n="footer.license">License</a>
</span>
</div>
</footer>
<script>
const copy = {
en: {
"nav.github": "GitHub",
"hero.eyebrow": "Free · Open · Cross-platform",
"hero.title": "Speak it. It's typed.",
"hero.lead": "Free, open-source voice input across iPhone, iPad, and Mac. On-device recognition. Bring your own API key — no double-billing.",
"hero.cta.primary": "Get it now",
"hero.cta.secondary": "View source",
"hero.meta1": "On-device by default",
"hero.meta2": "BYOK — no double pay",
"hero.meta3": "iOS · iPad · Mac",
"diff.title": "Open source. Built different.",
"diff.subtitle": "Hover or tap a card to expand each difference — designed into the product, not the pricing page.",
"diff.hint": "Show details",
"diff.free.t": "Free",
"diff.free.b": "Core open. No Pro wall.",
"diff.free.k": "No “basic dictation / unlock advanced” split — serious use doesnt require an upgrade.",
"diff.free.1.t": "Dictation & history",
"diff.free.1.b": "Transcribe and revisit history with no subscription gate.",
"diff.free.2.t": "Personal dictionary",
"diff.free.2.b": "Add names and product terms — never pay per entry.",
"diff.free.3.t": "Cross-device core",
"diff.free.3.b": "iPhone / iPad / Mac stay on one tier — no per-device upsell.",
"diff.free.4.t": "Tips optional",
"diff.free.4.b": "Support the project if you want — tips unlock nothing.",
"diff.cross.t": "Cross-platform",
"diff.cross.b": "iPhone · iPad · Mac, one language.",
"diff.cross.k": "Learn the interaction once — three surfaces, not three unrelated apps.",
"diff.cross.1.t": "iPhone",
"diff.cross.1.b": "System keyboard extension — speak into any text field.",
"diff.cross.2.t": "iPad",
"diff.cross.2.b": "Split sidebar + Home share the same product language.",
"diff.cross.3.t": "Mac",
"diff.cross.3.b": "Hold Option to dictate globally; menu bar stays out of your way.",
"diff.cross.4.t": "Dictionary sync",
"diff.cross.4.b": "Optional iCloud for terms — turn it off and stay fully local.",
"diff.open.t": "Open source",
"diff.open.b": "Auditable code. Verifiable trust.",
"diff.open.k": "Closed tools ask you to trust a privacy page. We hand you the repo.",
"diff.open.1.t": "Auditable",
"diff.open.1.b": "ASR, polish, and storage paths live on GitHub — inspect them yourself.",
"diff.open.2.t": "Forkable",
"diff.open.2.b": "Change hotkeys, swap engines, trim features — the code is there.",
"diff.open.3.t": "Clear license",
"diff.open.3.b": "The open-source license is in the repo, not a slogan.",
"diff.open.4.t": "Self-build",
"diff.open.4.b": "Build with Xcode from the docs — no dependency on our store account.",
"diff.privacy.t": "Privacy",
"diff.privacy.b": "On-device by default. Audio stays local.",
"diff.privacy.k": "Local is the default path — privacy isnt a paid upgrade.",
"diff.privacy.1.t": "On-device ASR",
"diff.privacy.1.b": "Ships on local recognition — not our transcription servers.",
"diff.privacy.2.t": "No cloud audio",
"diff.privacy.2.b": "We dont collect your voice stream for training or analytics.",
"diff.privacy.3.t": "No trackers",
"diff.privacy.3.b": "No ad IDs, no behavioral profiling stack.",
"diff.privacy.4.t": "No forced account",
"diff.privacy.4.b": "Dictate without signing up; data stays on-device by default.",
"diff.byok.t": "BYOK",
"diff.byok.b": "Local free; cloud on your key.",
"compare.subtitle": "Subscriptions often bundle the dictation shell with model usage. Local ASR starts free; AI polish bills your existing credits.",
"compare.1.t": "No double billing",
"compare.1.b": "Use DeepSeek / OpenAI credits you already have — skip another dictation subscription.",
"compare.2.t": "Costs you can see",
"compare.2.b": "Model invoices stay with your provider. We never touch or markup them.",
"compare.3.t": "You pick the model",
"compare.3.b": "Any OpenAI-compatible endpoint. Skip the key entirely when local ASR is enough.",
"compare.4.t": "Keys stay on-device",
"compare.4.b": "Stored in Keychain. Never routed through our servers.",
"vs.title": "How we compare",
"vs.subtitle": "Free tiers run out. Subscriptions add up. Open source: local free, cloud on your key.",
"vs.row.open": "Open source",
"vs.row.price": "Pricing",
"vs.row.local": "On-device ASR",
"vs.row.byok": "BYOK",
"vs.row.mobile": "iPhone / iPad",
"vs.yes": "✓",
"vs.no": "—",
"vs.partial": "Partial (Pro)",
"vs.typeless.price": "≈8,000 words/week free.\nSubscription isnt cheap: Pro ~$12/mo (annual) / $30/mo (monthly).",
"vs.superwhisper.price": "Limited free tier.\nSub / lifetime isnt cheap: Pro ~$8.5/mo, lifetime ~$250.",
"vs.openless.price": "Free app; ASR / polish billed to your key.",
"vs.osg.price": "Local mode fully free.\nCloud polish via your key — no dictation subscription.",
"vs.foot": "Prices and features from public product pages; may change — check each vendor.",
"steps.title": "Ready in three steps",
"steps.subtitle": "No subscription. No account. Grant permissions once, then speak.",
"step1.t": "Install & authorize",
"step1.b": "iOS: Settings → Keyboard → Full Access. Mac: Microphone + Accessibility. Done when OSG appears in the keyboard list or the menu bar.",
"step2.t": "Local first, key optional",
"step2.b": "On-device ASR works out of the box. Add your API key only for AI polish — text still lands without one.",
"step3.t": "Speak it. It's typed.",
"step3.b": "iOS: switch to OSGKeyboard and tap the mic. Mac: hold Option. Text appears at the cursor.",
"footer.copy": "© OSGKeyboard · v0.5.4 · source available",
"footer.source": "Source",
"footer.privacy": "Privacy",
"footer.license": "License"
},
zh: {
"nav.github": "GitHub",
"hero.eyebrow": "免费 · 开源 · 跨端",
"hero.title": "开口即文字",
"hero.lead": "免费开源的跨端语音输入。本地识别,自带 API Key 润色,避免重复订阅。iPhone、iPad、Mac — 说完即落字。",
"hero.cta.primary": "立即获取",
"hero.cta.secondary": "查看源码",
"hero.meta1": "默认本地识别",
"hero.meta2": "BYOK 不重复付费",
"hero.meta3": "iOS · iPad · Mac",
"diff.title": "开源,尽是不同",
"diff.subtitle": "悬停或点按下方卡片,展开每一项差异——内容写在产品里,不写在定价页。",
"diff.hint": "展开详情",
"diff.free.t": "免费",
"diff.free.b": "核心全开,没有 Pro 墙。",
"diff.free.k": "没有「基础版听写、进阶版解锁」——认真用也不用升级。",
"diff.free.1.t": "听写与历史",
"diff.free.1.b": "录音转写、历史回看不设订阅门槛。",
"diff.free.2.t": "个性词库",
"diff.free.2.b": "专有名词、产品名照常加,不为词条付费。",
"diff.free.3.t": "跨端核心",
"diff.free.3.b": "iPhone / iPad / Mac 能力同一档,不按设备加价。",
"diff.free.4.t": "打赏可选",
"diff.free.4.b": "自愿支持开发;不解锁任何功能。",
"diff.cross.t": "跨端",
"diff.cross.b": "iPhone · iPad · Mac 同源。",
"diff.cross.k": "学会一次交互语言,三端接着用——不是三套互不相干的 App。",
"diff.cross.1.t": "iPhone",
"diff.cross.1.b": "系统自定义键盘,在任意输入框开口落字。",
"diff.cross.2.t": "iPad",
"diff.cross.2.b": "分栏侧栏 + 首页同源,大屏也是同一产品语言。",
"diff.cross.3.t": "Mac",
"diff.cross.3.b": "按住 Option 全局听写,菜单栏常驻,不抢焦点。",
"diff.cross.4.t": "词库可同步",
"diff.cross.4.b": "可选 iCloud 同步个性词;关掉也完全能离线用。",
"diff.open.t": "开源",
"diff.open.b": "代码可审计,信任可验证。",
"diff.open.k": "闭源工具让你相信隐私页;我们让你直接读仓库。",
"diff.open.1.t": "可审计",
"diff.open.1.b": "识别、润色、存储路径都在 GitHub,可自行核对。",
"diff.open.2.t": "可 fork",
"diff.open.2.b": "改键位、换引擎、裁功能——仓库在,路就在。",
"diff.open.3.t": "许可清晰",
"diff.open.3.b": "开源许可写在仓库里,不是营销口号。",
"diff.open.4.t": "可自建",
"diff.open.4.b": "按文档用 Xcode 编译安装,不依赖我们的商店账号。",
"diff.privacy.t": "隐私",
"diff.privacy.b": "默认本地,录音不上云。",
"diff.privacy.k": "默认路径就是本地——隐私不是付费升级项。",
"diff.privacy.1.t": "默认本机识别",
"diff.privacy.1.b": "开箱走本地 ASR;不上我们的识别服务器。",
"diff.privacy.2.t": "录音不上云",
"diff.privacy.2.b": "我们不收集你的语音流做训练或分析。",
"diff.privacy.3.t": "无追踪器",
"diff.privacy.3.b": "不做广告标识符、行为画像那一套。",
"diff.privacy.4.t": "无强制账号",
"diff.privacy.4.b": "不用注册就能听写;数据默认留在本机。",
"diff.byok.t": "BYOK",
"diff.byok.b": "本地免费;云端 Key 自付。",
"vs.title": "和常见听写工具比一比",
"vs.subtitle": "免费额度用着用着就撞墙;订阅不便宜。开源这条路:本地免费,云端 Key 自付。",
"vs.row.open": "开源",
"vs.row.price": "付费",
"vs.row.local": "可本地识别",
"vs.row.byok": "BYOK",
"vs.row.mobile": "iPhone / iPad",
"vs.yes": "✓",
"vs.no": "—",
"vs.partial": "部分(Pro",
"vs.typeless.price": "每周约 8,000 词免费额度。\n订阅不便宜:Pro 约 $12/月起(年付)/ $30/月(月付)。",
"vs.superwhisper.price": "免费档额度有限。\n订阅/买断不便宜:Pro 约 $8.5/月,买断约 $250。",
"vs.openless.price": "App 免费;识别 / 润色按你的 Key 计费。",
"vs.osg.price": "本地模式完全免费。\n云端润色 Key 自付(不另收听写订阅)。",
"vs.foot": "价格与功能据各产品公开说明整理,可能更新,以官方为准。",
"compare.subtitle": "订阅常把「听写壳」和「模型用量」捆在一起。本地识别免费起步;AI 润色按你已有的额度计费。",
"compare.1.t": "躲开重复付费",
"compare.1.b": "已有 DeepSeek / OpenAI 额度就直接用,不必再开一份「听写订阅」。",
"compare.2.t": "费用看得见",
"compare.2.b": "模型账单只在服务商侧;OSGKeyboard 不经手、不加价。",
"compare.3.t": "模型你说了算",
"compare.3.b": "OpenAI 兼容端点即可接入;不需要 AI 时,本地识别照用不填 Key。",
"compare.4.t": "Key 留在本机",
"compare.4.b": "写入系统 Keychain,不经过我们的服务器。",
"steps.title": "开箱可用,只要三步",
"steps.subtitle": "不用订阅、不用注册账号。装好权限,说第一句就行。",
"step1.t": "装好并授权",
"step1.b": "iOS:设置 → 键盘 → 开启完全访问。Mac:麦克风 + 辅助功能。完成时键盘列表能看到 OSG,或菜单栏出现图标。",
"step2.t": "先本地,Key 可选",
"step2.b": "默认本地识别即可开口;要 AI 润色再填自己的 API Key。不配 Key 也能落字。",
"step3.t": "开口即写",
"step3.b": "iOS 切到 OSGKeyboard 点麦克风;Mac 按住 Option。文字出现在当前光标处。",
"footer.copy": "© OSGKeyboard · v0.5.4 · 源码可见",
"footer.source": "源代码",
"footer.privacy": "隐私",
"footer.license": "许可协议"
}
};
function applyLang(lang) {
const strings = copy[lang] || copy.zh;
document.documentElement.lang = lang === "zh" ? "zh-Hans" : "en";
document.querySelectorAll("[data-i18n]").forEach((el) => {
const value = strings[el.getAttribute("data-i18n")];
if (value != null) el.textContent = value;
});
document.querySelectorAll(".pill-toggle button[data-lang]").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.lang === lang);
});
try { localStorage.setItem("osg-lang", lang); } catch (_) {}
applyShots(lang);
}
function resolvedTheme() {
const theme = document.documentElement.getAttribute("data-theme") || "system";
if (theme === "light" || theme === "dark") return theme;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function shotURL(name, langOverride) {
let lang = langOverride;
if (!lang) {
try { lang = localStorage.getItem("osg-lang"); } catch (_) {}
}
if (!lang) lang = document.documentElement.lang.startsWith("zh") ? "zh" : "en";
const theme = resolvedTheme();
return `assets/screenshots/${lang}/${theme}/${name}.png`;
}
function applyShots(langOverride) {
document.querySelectorAll("img[data-shot]").forEach((img) => {
const name = img.getAttribute("data-shot");
if (!name) return;
const next = shotURL(name, langOverride);
if (img.getAttribute("src") !== next) img.setAttribute("src", next);
});
}
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
const icon = document.querySelector("[data-theme-icon]");
if (icon) {
icon.textContent = theme === "dark" ? "light_mode" : theme === "light" ? "dark_mode" : "contrast";
}
try { localStorage.setItem("osg-theme", theme); } catch (_) {}
applyShots();
}
function cycleTheme() {
const current = document.documentElement.getAttribute("data-theme") || "system";
const next = current === "system" ? "light" : current === "light" ? "dark" : "system";
applyTheme(next);
}
document.querySelectorAll(".pill-toggle button[data-lang]").forEach((btn) => {
btn.addEventListener("click", () => applyLang(btn.dataset.lang));
});
document.getElementById("themeToggle")?.addEventListener("click", cycleTheme);
// Diff pillars → detail stage (hover on pointer devices, click/tap everywhere)
(function initPillarTabs() {
const tabs = Array.from(document.querySelectorAll(".pillar[data-pillar]"));
const panels = Array.from(document.querySelectorAll(".pillar-panel[data-panel]"));
if (!tabs.length || !panels.length) return;
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");
function activate(id, { focusTab = false } = {}) {
tabs.forEach((tab) => {
const on = tab.dataset.pillar === id;
tab.classList.toggle("is-active", on);
tab.setAttribute("aria-selected", on ? "true" : "false");
tab.tabIndex = on ? 0 : -1;
});
panels.forEach((panel) => {
const on = panel.dataset.panel === id;
panel.hidden = !on;
});
if (focusTab) {
const active = tabs.find((t) => t.dataset.pillar === id);
active?.focus();
}
}
tabs.forEach((tab) => {
tab.addEventListener("click", () => activate(tab.dataset.pillar));
tab.addEventListener("keydown", (e) => {
const i = tabs.indexOf(tab);
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault();
activate(tabs[(i + 1) % tabs.length].dataset.pillar, { focusTab: true });
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault();
activate(tabs[(i - 1 + tabs.length) % tabs.length].dataset.pillar, { focusTab: true });
} else if (e.key === "Home") {
e.preventDefault();
activate(tabs[0].dataset.pillar, { focusTab: true });
} else if (e.key === "End") {
e.preventDefault();
activate(tabs[tabs.length - 1].dataset.pillar, { focusTab: true });
}
});
tab.addEventListener("mouseenter", () => {
if (finePointer.matches) activate(tab.dataset.pillar);
});
tab.addEventListener("focus", () => activate(tab.dataset.pillar));
});
})();
const savedLang = (() => { try { return localStorage.getItem("osg-lang"); } catch (_) { return null; } })();
const savedTheme = (() => { try { return localStorage.getItem("osg-theme"); } catch (_) { return null; } })();
applyLang(savedLang || ((navigator.language || "").startsWith("zh") ? "zh" : "en"));
applyTheme(savedTheme || "system");
try {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if ((document.documentElement.getAttribute("data-theme") || "system") === "system") {
applyShots();
}
});
} catch (_) {}
// Scroll reveal
const reveals = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("in");
io.unobserve(entry.target);
}
});
}, { rootMargin: "0px 0px -8% 0px", threshold: 0.12 });
reveals.forEach((el) => io.observe(el));
} else {
reveals.forEach((el) => el.classList.add("in"));
}
// Soft parallax after hero entrance animation settles
const stage = document.querySelector(".hero-stage");
if (stage && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
let ready = false;
setTimeout(() => { ready = true; }, 1100);
window.addEventListener("scroll", () => {
if (!ready) return;
const y = Math.min(window.scrollY * 0.06, 28);
stage.style.transform = `translateY(${y}px)`;
}, { passive: true });
}
</script>
</body>
</html>