/);
- assert.match(component, /Reasonix v\{release\.version\}<\/h1>/);
- assert.match(component, / /);
- assert.doesNotMatch(css, /\.release-hero--compact|\.release-hero__actions|\.release-hero blockquote/);
-});
-
-test("changelog has one official navigation and marks archives noindex", () => {
- assert.doesNotMatch(component, /release-channel-tabs/);
- assert.match(component, /Historical archive/);
- assert.match(component, /noindex=\{isPreview\}/);
-});
-
-test("reviewed exact-version routes redirect safely until their publication marker exists", () => {
- assert.match(versionPage, /publishedReleases/);
- assert.match(versionPage, /publishedVersions\.has\(release\.version\)/);
- assert.match(versionPage, /if \(!published\)/);
- assert.match(versionPage, /return Astro\.redirect/);
- assert.match(versionPage, /Astro\.redirect\('\/changelog\/'\)/);
-});
diff --git a/site/src/scripts/changelog.js b/site/src/scripts/changelog.js
deleted file mode 100644
index 52526b61a8..0000000000
--- a/site/src/scripts/changelog.js
+++ /dev/null
@@ -1,31 +0,0 @@
-(function () {
- const versionSelect = document.querySelector('[data-release-version]');
- versionSelect?.addEventListener('change', () => {
- const version = String(versionSelect.value || '').replace(/^v/, '');
- if (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
- window.location.href = `/changelog/v${version}/`;
- }
- });
-
- const links = Array.from(document.querySelectorAll('.release-toc a[href^="#"]'));
- const sections = links.map((link) => document.querySelector(link.getAttribute('href'))).filter(Boolean);
- if (!links.length || !sections.length) return;
-
- let ticking = false;
- const update = () => {
- ticking = false;
- let current = sections[0];
- for (const section of sections) {
- if (section.getBoundingClientRect().top <= 150) current = section;
- }
- links.forEach((link) => link.classList.toggle('active', link.hash === `#${current.id}`));
- };
- const queue = () => {
- if (ticking) return;
- ticking = true;
- requestAnimationFrame(update);
- };
- window.addEventListener('scroll', queue, { passive: true });
- window.addEventListener('resize', queue, { passive: true });
- update();
-})();
diff --git a/site/src/scripts/community.js b/site/src/scripts/community.js
deleted file mode 100644
index 320d4780b0..0000000000
--- a/site/src/scripts/community.js
+++ /dev/null
@@ -1,336 +0,0 @@
-// Reasonix Community client. Renders the forum from the forum.reasonix.io API and
-// gates posting on the shared id.reasonix.io session (cookie sent cross-subdomain).
-// Bilingual like the rest of the site: static labels via .l-en/.l-zh spans that the
-// shared `reasonix-lang` choice toggles; plain-text strings pick the current lang.
-import { initTheme } from "./theme.js";
-import { initMobileNav } from "./mobile-nav.js";
-
-const FORUM = (import.meta.env.PUBLIC_FORUM_API || "https://forum.reasonix.io").replace(/\/$/, "");
-const ACCOUNTS = (import.meta.env.PUBLIC_ACCOUNTS_API || "https://id.reasonix.io").replace(/\/$/, "");
-
-const el = (id) => document.getElementById(id);
-const qp = new URLSearchParams(location.search);
-
-let lang = "en";
-const L = (en, zh) => `${en} ${zh} `;
-const t = (en, zh) => (lang === "zh" ? zh : en);
-
-function applyLangText() {
- const attr = lang === "zh" ? "zh" : "en";
- document.querySelectorAll("[data-ph-en]").forEach((n) => { n.placeholder = n.getAttribute(`data-ph-${attr}`); });
- document.querySelectorAll("[data-l-en]").forEach((n) => { n.textContent = n.getAttribute(`data-l-${attr}`); });
-}
-function setLang(l) {
- lang = l;
- document.body.dataset.lang = l;
- document.documentElement.lang = l === "zh" ? "zh-CN" : "en";
- document.querySelectorAll(".lang-switch button").forEach((b) => b.classList.toggle("active", b.dataset.lang === l));
- try { localStorage.setItem("reasonix-lang", l); } catch {}
- applyLangText();
-}
-function initLang() {
- let saved = "";
- try { saved = localStorage.getItem("reasonix-lang") || ""; } catch {}
- setLang(saved || ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en"));
- document.querySelectorAll(".lang-switch button").forEach((b) => b.addEventListener("click", () => setLang(b.dataset.lang)));
-}
-
-async function api(base, path, opts = {}) {
- const res = await fetch(base + path, {
- method: opts.method || "GET",
- credentials: "include",
- headers: opts.body ? { "content-type": "application/json" } : undefined,
- body: opts.body ? JSON.stringify(opts.body) : undefined,
- });
- let data = null;
- try { data = await res.json(); } catch {}
- if (!res.ok) {
- const err = new Error(data?.error?.message || "Something went wrong.");
- err.code = data?.error?.code;
- err.status = res.status;
- throw err;
- }
- return data;
-}
-const forum = (p, o) => api(FORUM, p, o);
-
-// Anti-spam / gate errors are localized by code; unknown codes fall back to the
-// server message.
-const ERR = {
- email_unverified: ["Confirm your email address before posting.", "发帖前请先验证你的邮箱。"],
- links_restricted: ["New members can't post links yet — participate a little to unlock.", "新成员暂时不能发链接——参与一段时间后解锁。"],
- insufficient_trust: ["You don't have access to post in this category yet.", "你还没有在此分区发帖的权限。"],
- silenced: ["Your account is temporarily restricted from posting.", "你的账号暂时被限制发帖。"],
- rate_limited: ["You're posting too fast — take a short break.", "发帖太快了——稍微歇一会儿。"],
- daily_limit: ["You've hit today's posting limit for your trust level.", "你已达到当前信任等级的每日发帖上限。"],
- closed: ["This topic is closed to new replies.", "该话题已关闭,不能再回复。"],
- self_flag: ["You can't report your own post.", "不能举报自己的帖子。"],
- unauthorized: ["Sign in to continue.", "请先登录。"],
-};
-const errText = (err) => (ERR[err.code] ? t(ERR[err.code][0], ERR[err.code][1]) : err.message);
-
-const CATS = {
- announcements: ["Announcements", "公告"],
- help: ["Help & Support", "帮助与支持"],
- skills: ["Skills & Plugins", "技能与插件"],
- show: ["Show & Tell", "作品展示"],
- feedback: ["Feedback & Ideas", "反馈与建议"],
-};
-const CATDESC = {
- announcements: ["Releases, roadmap, and community news.", "版本发布、路线图与社区动态。"],
- help: ["Stuck on setup, config, or cache behavior? Ask here.", "安装、配置或缓存问题?在这里提问。"],
- skills: ["Share, request, and review community skills and MCP servers.", "分享、求助、评审社区技能与 MCP 服务。"],
- show: ["Built something with Reasonix? Show the community.", "用 Reasonix 做了东西?来给社区看看。"],
- feedback: ["Feature requests and product feedback.", "功能建议与产品反馈。"],
-};
-const catName = (slug, apiName) => (CATS[slug] ? L(CATS[slug][0], CATS[slug][1]) : esc(apiName));
-const catText = (slug, apiName) => (CATS[slug] ? t(CATS[slug][0], CATS[slug][1]) : apiName);
-const ROLES = { admin: ["admin", "管理员"], moderator: ["moderator", "版主"] };
-
-function esc(s) {
- return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
-}
-const AV_GRAD = [
- "var(--accent),var(--violet)",
- "var(--ok),oklch(0.62 0.15 200)",
- "var(--warm),var(--rose)",
- "var(--violet),var(--rose)",
- "oklch(0.6 0.15 200),var(--accent)",
-];
-function avatar(handle, size = "") {
- const h = handle || "?";
- let n = 0;
- for (const ch of h) n = (n + ch.charCodeAt(0)) % AV_GRAD.length;
- const initials = h.replace(/[^a-zA-Z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
- return `${esc(initials)} `;
-}
-function ago(iso) {
- if (!iso) return "";
- const s = Math.max(1, (Date.now() - new Date(iso).getTime()) / 1000);
- const u = [[86400, "d", "天"], [3600, "h", "小时"], [60, "m", "分钟"]];
- for (const [sec, en, zh] of u) if (s >= sec) { const v = Math.floor(s / sec); return L(`${v}${en} ago`, `${v}${zh}前`); }
- return L("just now", "刚刚");
-}
-function md(body) {
- const parts = esc(body).split(/```/);
- let out = "";
- parts.forEach((chunk, i) => {
- if (i % 2 === 1) { out += `
${chunk.replace(/^\n/, "")} `; return; }
- const paras = chunk.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
- out += paras.map((p) => `${p.replace(/`([^`]+)`/g, "$1").replace(/\n/g, " ")}
`).join("");
- });
- return out || "
";
-}
-
-const CAT_ICONS = { announcements: "📣", help: "🛟", skills: "🧩", show: "✨", feedback: "💡" };
-const loginUrl = () => `/login/?next=${encodeURIComponent(location.pathname + location.search)}`;
-
-let account = null;
-async function loadAccount() {
- try { account = (await api(ACCOUNTS, "/me")).user; } catch { account = null; }
- document.querySelectorAll("#nav-account, #mobile-nav-account").forEach((slot) => {
- slot.innerHTML = account
- ? `${avatar(account.handle)} `
- : `${L("Sign in", "登录")} `;
- });
-}
-
-/* ── home ─────────────────────────────────────────── */
-function renderHome() {
- const catBox = el("cat-list");
- const topicBox = el("topic-list");
- const category = qp.get("category") || "";
-
- forum("/categories").then((d) => {
- const cats = d.categories;
- if (el("s-cats")) el("s-cats").textContent = cats.length;
- if (el("s-topics")) el("s-topics").textContent = cats.reduce((a, c) => a + (c.topicCount || 0), 0);
- catBox.innerHTML = cats.map((c) => `
-
- ${CAT_ICONS[c.slug] || "💬"}
- ${catName(c.slug, c.name)} ${CATDESC[c.slug] ? L(CATDESC[c.slug][0], CATDESC[c.slug][1]) : esc(c.description)}
-
${c.topicCount || 0} ${L("topics", "话题")}${c.lastActivity ? " · " + ago(c.lastActivity) : ""}
- `).join("");
- }).catch(() => { catBox.innerHTML = `${L("Couldn't load categories.", "无法加载分区。")}
`; });
-
- const loadTopics = (sort) => {
- topicBox.innerHTML = ``.repeat(3);
- const q = new URLSearchParams();
- if (category) q.set("category", category);
- if (sort) q.set("sort", sort);
- forum("/topics?" + q).then((d) => {
- if (!d.topics.length) { topicBox.innerHTML = ``; return; }
- topicBox.innerHTML = d.topics.map((tp) => `
-
- ${avatar(tp.author)}
-
-
- ${tp.pinned ? `
📌 ${L("Pinned", "置顶")} ` : ""}
- ${tp.status === "solved" ? `
✓ ${L("Solved", "已解决")} ` : ""}
-
${esc(tp.title)}
-
-
${catName(tp.category, tp.categoryName)} ${esc(tp.author)} · ${ago(tp.createdAt)}
-
-
${tp.replyCount}
${L("replies", "回复")}
-
${ago(tp.lastPostAt)}
-
`).join("");
- }).catch(() => { topicBox.innerHTML = `${L("Couldn't load discussions.", "无法加载讨论。")}
`; });
- };
- loadTopics("latest");
-
- el("sort-tabs")?.addEventListener("click", (e) => {
- const b = e.target.closest("button[data-sort]");
- if (!b) return;
- el("sort-tabs").querySelectorAll("button").forEach((x) => x.classList.toggle("on", x === b));
- loadTopics(b.dataset.sort);
- });
-}
-
-/* ── thread ───────────────────────────────────────── */
-let firstPostId = 0;
-function postHtml(p, topic) {
- const answer = topic.acceptedPostId && topic.acceptedPostId === p.id;
- const cls = answer ? "post answer" : p.id === firstPostId ? "post op" : "post";
- const roleWord = ROLES[p.role] ? L(ROLES[p.role][0], ROLES[p.role][1]) : "";
- const role = roleWord ? `${roleWord} ` : "";
- return `
- ${avatar(p.handle || p.author, "lg")}
-
- ${answer ? `
✓ ${L("Accepted answer", "已采纳回答")}
` : ""}
-
${esc(p.handle || p.author)} ${role}${ago(p.createdAt)}
-
${md(p.body)}
-
- 👍 ${p.likeCount || 0}
- ${L("Report", "举报")}
-
-
- `;
-}
-
-async function renderThread() {
- const id = Number(qp.get("id"));
- if (!id) { location.href = "/community/"; return; }
- let data;
- try { data = await forum(`/topics/${id}`); }
- catch { el("posts").innerHTML = `${L("That discussion doesn't exist or was removed.", "该讨论不存在或已被移除。")}
`; return; }
- const { topic, posts } = data;
- firstPostId = posts[0]?.id || 0;
-
- document.title = `${topic.title} — ${t("Reasonix Community", "Reasonix 社区")}`;
- el("crumb-cat").textContent = catText(topic.category, topic.category);
- el("crumb-title").textContent = topic.title;
- el("t-title").textContent = topic.title;
- el("t-meta").innerHTML =
- `${topic.status === "solved" ? `✓ ${L("Solved", "已解决")} ` : ""}
- ${topic.replyCount} ${L("replies", "回复")} · ${topic.viewCount} ${L("views", "浏览")} · ${L("started", "发起于")} ${ago(topic.createdAt)} `;
- el("posts").innerHTML = posts.map((p) => postHtml(p, topic)).join("");
-
- const seen = new Set();
- el("parti").innerHTML = posts.filter((p) => !seen.has(p.author) && seen.add(p.author)).slice(0, 8).map((p) => avatar(p.handle || p.author)).join("");
-
- el("posts").addEventListener("click", async (e) => {
- const like = e.target.closest("[data-like]");
- if (like && account) {
- const wasLiked = like.dataset.liked === "1";
- like.disabled = true;
- try {
- const out = await forum(`/posts/${like.dataset.like}/likes`, { method: wasLiked ? "DELETE" : "POST" });
- like.dataset.liked = out.liked ? "1" : "0";
- like.setAttribute("aria-pressed", out.liked ? "true" : "false");
- like.classList.toggle("on", out.liked);
- like.querySelector("span").textContent = out.likeCount;
- } catch (err) { alert(errText(err)); }
- finally { like.disabled = false; }
- return;
- } else if (like) {
- location.href = loginUrl();
- return;
- }
- const flag = e.target.closest("[data-flag]");
- if (flag && account) {
- if (!confirm(t("Report this post as spam or abuse?", "举报该帖为垃圾/滥用内容?"))) return;
- try { await forum(`/posts/${flag.dataset.flag}/flags`, { method: "POST", body: { reason: "spam" } }); flag.innerHTML = L("Reported ✓", "已举报 ✓"); flag.disabled = true; }
- catch (err) { alert(errText(err)); }
- } else if (flag) { location.href = loginUrl(); }
- });
-
- const zone = el("reply-zone");
- if (!account) {
- zone.innerHTML = ``;
- return;
- }
- zone.innerHTML = `
-
-
-
-
-
`;
- applyLangText();
- el("reply-submit").addEventListener("click", async () => {
- const body = el("reply-body").value.trim();
- const msg = el("reply-msg");
- msg.hidden = true;
- if (body.length < 2) return;
- el("reply-submit").disabled = true;
- try {
- await forum(`/topics/${id}/posts`, { method: "POST", body: { body } });
- location.reload();
- } catch (err) {
- msg.textContent = errText(err); msg.hidden = false;
- el("reply-submit").disabled = false;
- }
- });
-}
-
-/* ── new topic ────────────────────────────────────── */
-async function renderNew() {
- if (!account) {
- el("new-gate").hidden = false;
- el("gate-login").href = loginUrl();
- return;
- }
- el("new-form").hidden = false;
- const sel = el("f-category");
- try {
- const { categories } = await forum("/categories");
- for (const c of categories) {
- const o = document.createElement("option");
- o.value = c.id;
- o.setAttribute("data-l-en", CATS[c.slug] ? CATS[c.slug][0] : c.name);
- o.setAttribute("data-l-zh", CATS[c.slug] ? CATS[c.slug][1] : c.name);
- o.textContent = catText(c.slug, c.name);
- sel.appendChild(o);
- }
- const pre = qp.get("category");
- if (pre) { const m = categories.find((c) => c.slug === pre); if (m) sel.value = m.id; }
- } catch {}
- applyLangText();
-
- el("f-submit").addEventListener("click", async () => {
- const msg = el("new-msg");
- msg.hidden = true;
- const categoryId = Number(sel.value);
- const title = el("f-title").value.trim();
- const body = el("f-body").value.trim();
- if (!categoryId) { msg.textContent = t("Choose a category.", "请选择一个分区。"); msg.hidden = false; return; }
- if (title.length < 6 || body.length < 10) { msg.textContent = t("Add a title (6+ chars) and a bit more detail (10+ chars).", "标题至少 6 个字符,正文至少 10 个字符。"); msg.hidden = false; return; }
- el("f-submit").disabled = true;
- try {
- const { topic } = await forum("/topics", { method: "POST", body: { categoryId, title, body } });
- location.href = `/community/topic/?id=${topic.id}`;
- } catch (err) {
- msg.textContent = errText(err); msg.hidden = false;
- el("f-submit").disabled = false;
- }
- });
-}
-
-(async function () {
- initTheme();
- initMobileNav();
- initLang();
- await loadAccount();
- if (el("topic-list")) renderHome();
- else if (el("posts")) renderThread();
- else if (el("new-form")) renderNew();
-})();
diff --git a/site/src/scripts/docs-codeblock-contract.test.mjs b/site/src/scripts/docs-codeblock-contract.test.mjs
deleted file mode 100644
index 29f1c62856..0000000000
--- a/site/src/scripts/docs-codeblock-contract.test.mjs
+++ /dev/null
@@ -1,67 +0,0 @@
-import assert from "node:assert/strict";
-import { readFile } from "node:fs/promises";
-import test from "node:test";
-
-const configSource = () => readFile(new URL("../../astro.config.mjs", import.meta.url), "utf8");
-const docsSource = () => readFile(new URL("../pages/docs.astro", import.meta.url), "utf8");
-const stylesSource = () => readFile(new URL("../styles/global.css", import.meta.url), "utf8");
-
-test("Astro preserves documentation code sample newlines", async () => {
- const config = await configSource();
-
- assert.match(config, /compressHTML:\s*false/);
-});
-
-test("copyable documentation code blocks reserve space for their buttons", async () => {
- const [page, styles] = await Promise.all([docsSource(), stylesSource()]);
- const codeblocks = [...page.matchAll(/([\s\S]*?)<\/div>/g)];
- const copyable = codeblocks.filter(([, , body]) => body.includes("data-copy="));
-
- assert.ok(copyable.length > 0);
- for (const [, classes] of copyable) {
- assert.match(classes, /(?:^|\s)codeblock--copy(?:\s|$)/);
- }
- assert.match(styles, /\.codeblock--copy\s*\{\s*padding-top:\s*52px;\s*\}/);
-});
-
-test("the macOS quarantine comment and command remain separate source lines", async () => {
- const page = await docsSource();
-
- assert.match(
- page,
- /# Quit Reasonix first, then run in Terminal\.<\/span>\nsudo xattr -rd com\.apple\.quarantine \/Applications\/Reasonix\.app/,
- );
-});
-
-test("website documents the version-matched built-in docs command", async () => {
- const page = await docsSource();
-
- assert.match(page, /id="embedded-docs"/);
- assert.match(page, /\/docs 1\.19\.5 changelog/);
- assert.match(page, /AI configured for the current session/);
- assert.match(page, /normally \/reasonix:docs<\/code>/);
- assert.match(page, /Reasonix never overwrites the existing command/);
- assert.match(page, /Release CI rejects a build when its embedded corpus does not match/);
-});
-
-test("website exposes the Extension Protocol developer path", async () => {
- const page = await docsSource();
-
- assert.match(page, /href="#extensions"/);
- assert.match(page, /id="extensions"/);
- assert.match(page, /MCP or Extension Protocol\?/);
- assert.match(page, /sdk\/go\/examples\/starterextension/);
- assert.match(page, /plugin_root="\$\(pwd -P\)"/);
- assert.match(page, /reasonix plugin install "\$plugin_root" --dry-run/);
- assert.match(page, /reasonix plugin install "\$plugin_root" --link --replace --yes/);
- assert.match(page, /sdk\/go\/v1\.0\.0/);
- assert.match(page, /Full trust:<\/strong>/);
- assert.match(page, /Native Manifest v2 is an explicit capability boundary/);
- assert.match(page, /原生 Manifest v2 是显式能力边界/);
- assert.match(page, /docs\/EXTENSIONS\.md/);
- assert.match(page, /docs\/PLUGIN_PACKAGES\.md#manifest-v2-extensions/);
- assert.match(page, /docs\/PLUGIN_PACKAGES\.zh-CN\.md#manifest-v2扩展/);
- assert.match(page, /sdk\/go\/README\.md/);
- assert.match(page, /docs\/EXTENSION_PROTOCOL\.md/);
- assert.doesNotMatch(page, /Manifest v1/);
-});
diff --git a/site/src/scripts/download-link.js b/site/src/scripts/download-link.js
deleted file mode 100644
index 39d1b90487..0000000000
--- a/site/src/scripts/download-link.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const DOWNLOAD_PANES = new Set(["npm", "brew", "desktop", "cli", "vscode"]);
-
-// Return the requested install pane only for the homepage download section.
-// Plain #start links keep the rendered default pane; query links opt into one.
-export function downloadPaneFromURL(input, base = "https://reasonix.io/") {
- let url;
- try {
- url = new URL(input, base);
- } catch {
- return "";
- }
- if (url.hash !== "#start") return "";
- const pane = url.searchParams.get("download") || "";
- return DOWNLOAD_PANES.has(pane) ? pane : "";
-}
-
-export function releaseChannelFromURL(input, base = "https://reasonix.io/") {
- let url;
- try {
- url = new URL(input, base);
- } catch {
- return "";
- }
- if (url.hash !== "#start") return "";
- const pane = url.searchParams.get("download") || "";
- if (pane !== "desktop" && pane !== "cli") return "";
- return "";
-}
-
-export function downloadURLForPane(input, pane, channel, base = "https://reasonix.io/") {
- if (!DOWNLOAD_PANES.has(pane)) return "";
- let url;
- try {
- url = new URL(input, base);
- } catch {
- return "";
- }
- url.searchParams.set("download", pane);
- void channel;
- url.searchParams.delete("channel");
- url.hash = "start";
- return url.href;
-}
diff --git a/site/src/scripts/download-link.test.mjs b/site/src/scripts/download-link.test.mjs
deleted file mode 100644
index e8fa9c1cd5..0000000000
--- a/site/src/scripts/download-link.test.mjs
+++ /dev/null
@@ -1,42 +0,0 @@
-import { test } from "node:test";
-import assert from "node:assert/strict";
-import { downloadPaneFromURL, downloadURLForPane, releaseChannelFromURL } from "./download-link.js";
-
-test("desktop updater deep link selects the desktop pane", () => {
- assert.equal(downloadPaneFromURL("https://reasonix.io/?download=desktop#start"), "desktop");
-});
-
-test("plain install links preserve the default pane", () => {
- assert.equal(downloadPaneFromURL("https://reasonix.io/#start"), "");
- assert.equal(downloadPaneFromURL("https://reasonix.io/?download=desktop"), "");
-});
-
-test("recognized download panes are strict", () => {
- assert.equal(downloadPaneFromURL("/?download=brew#start"), "brew");
- assert.equal(downloadPaneFromURL("/?download=npm#start"), "npm");
- assert.equal(downloadPaneFromURL("/?download=vscode#start"), "vscode");
- assert.equal(downloadPaneFromURL("/?download=cli#start"), "cli");
- assert.equal(downloadPaneFromURL("/?download=DESKTOP#start"), "");
- assert.equal(downloadPaneFromURL("/?download=unknown#start"), "");
- assert.equal(downloadPaneFromURL("not a url", "not a base"), "");
-});
-
-test("legacy release channel deep links never select a public channel", () => {
- assert.equal(releaseChannelFromURL("/?download=desktop&channel=preview#start"), "");
- assert.equal(releaseChannelFromURL("/?download=cli&channel=stable#start"), "");
- assert.equal(releaseChannelFromURL("/?download=desktop&channel=canary#start"), "");
-});
-
-test("download tab navigation keeps the pane and removes legacy channels", () => {
- assert.equal(
- downloadURLForPane("https://reasonix.io/?download=desktop&channel=preview#start", "cli", "stable"),
- "https://reasonix.io/?download=cli#start",
- );
- assert.equal(
- downloadURLForPane("https://reasonix.io/?download=desktop&channel=preview#start", "npm", "preview"),
- "https://reasonix.io/?download=npm#start",
- );
- assert.equal(downloadURLForPane("https://reasonix.io/", "desktop", "preview"),
- "https://reasonix.io/?download=desktop#start");
- assert.equal(downloadURLForPane("https://reasonix.io/", "unknown", "stable"), "");
-});
diff --git a/site/src/scripts/fx.js b/site/src/scripts/fx.js
deleted file mode 100644
index a98d555d99..0000000000
--- a/site/src/scripts/fx.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Reasonix — motion & FX layer (all effects respect data-motion + prefers-reduced-motion)
-(function () {
- const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
- const rich = () => document.body.dataset.motion === "rich" && !reduced;
-
- /* spotlight border on cards */
- document.querySelectorAll(".feat, .os-card, .doc-card").forEach((card) => {
- card.addEventListener("mousemove", (e) => {
- const r = card.getBoundingClientRect();
- card.style.setProperty("--mx", e.clientX - r.left + "px");
- card.style.setProperty("--my", e.clientY - r.top + "px");
- });
- });
-
- /* stat counters (run when terminal playback finishes) */
- const counters = Array.from(document.querySelectorAll("[data-cnt]"));
- const fmt = (el, v) => {
- if (el.dataset.fmt === "hm") {
- const m = Math.round(v);
- return Math.floor(m / 60) + "h " + (m % 60) + "m";
- }
- const dec = +(el.dataset.dec || 0);
- return (el.dataset.pre || "") + v.toFixed(dec) + (el.dataset.suf || "");
- };
- const runCounters = () => {
- counters.forEach((el) => {
- const target = parseFloat(el.dataset.cnt);
- if (!rich()) { el.textContent = fmt(el, target); return; }
- const t0 = performance.now(), dur = 1500;
- const frame = (t) => {
- const p = Math.min(1, (t - t0) / dur);
- const e = 1 - Math.pow(1 - p, 3);
- el.textContent = fmt(el, target * e);
- if (p < 1) requestAnimationFrame(frame);
- };
- requestAnimationFrame(frame);
- });
- };
- document.addEventListener("rx:term-played", runCounters);
-})();
diff --git a/site/src/scripts/header-contract.test.mjs b/site/src/scripts/header-contract.test.mjs
deleted file mode 100644
index 566ed4f467..0000000000
--- a/site/src/scripts/header-contract.test.mjs
+++ /dev/null
@@ -1,39 +0,0 @@
-import assert from "node:assert/strict";
-import { readFile } from "node:fs/promises";
-import test from "node:test";
-
-const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
-
-test("primary site pages share the same header component", async () => {
- const pages = await Promise.all([
- source("../pages/index.astro"),
- source("../pages/docs.astro"),
- source("../pages/skills.astro"),
- source("../components/ChangelogPage.astro"),
- source("../pages/404.astro"),
- ]);
-
- for (const page of pages) {
- assert.match(page, / {
- const header = await source("../components/SiteHeader.astro");
-
- for (const id of ["features", "surfaces", "skills", "community", "docs", "changelog"]) {
- assert.match(header, new RegExp(`id: '${id}'`));
- }
-
- assert.doesNotMatch(header, /id: 'start'/);
- assert.equal((header.match(/#start/g) ?? []).length, 2);
- assert.match(header, /nav-sign-in/);
- assert.match(header, /nav-install/);
- assert.match(header, /class="nav-github"/);
- assert.match(header, /https:\/\/github[.]com\/esengine\/DeepSeek-Reasonix/);
- assert.match(header, /target="_blank"/);
- assert.match(header, /aria-label="Reasonix GitHub repository"/);
- assert.match(header, /data-mobile-nav-toggle/);
- assert.match(header, /id="site-mobile-nav"/);
-});
diff --git a/site/src/scripts/mobile-nav-contract.test.mjs b/site/src/scripts/mobile-nav-contract.test.mjs
deleted file mode 100644
index b6fad46dee..0000000000
--- a/site/src/scripts/mobile-nav-contract.test.mjs
+++ /dev/null
@@ -1,104 +0,0 @@
-import assert from "node:assert/strict";
-import { readFile } from "node:fs/promises";
-import test from "node:test";
-
-const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
-
-/* Extract the body of a `@media (max-width: Npx)` block (up to the next
- at-rule or EOF) so assertions stay scoped to small viewports. */
-const mediaBlock = (css, px) => {
- const marker = `@media (max-width: ${px}px)`;
- const start = css.indexOf(marker);
- assert.notEqual(start, -1, `missing ${marker}`);
- const rest = css.slice(start + marker.length);
- const next = rest.search(/@media|@keyframes/);
- return next === -1 ? rest : rest.slice(0, next);
-};
-
-/* The header must fit narrow viewports while keeping a clear menu trigger. */
-test("≤640px: marketing nav contracts to fit 390px viewports", async () => {
- const [css, header] = await Promise.all([
- source("../styles/global.css"),
- source("../components/SiteHeader.astro"),
- ]);
- const block = mediaBlock(css, 640);
- assert.match(block, /\.nav-sign-in \{ display: none/);
- assert.match(block, /\.nav \.brand span \{ display: none/);
- assert.match(block, /\.nav \.brand img \{ width: 64px; \}/);
- assert.match(header, / /);
- assert.match(block, /\.theme-switch button \{ padding: 6px 9px/);
-});
-
-test("≤1100px: marketing navigation collapses before controls overlap", async () => {
- const css = await source("../styles/global.css");
- const tabletBlock = mediaBlock(css, 1100);
- const mobileBlock = mediaBlock(css, 900);
-
- assert.match(tabletBlock, /\.nav-links \{ display: none/);
- assert.match(tabletBlock, /\.nav-github \{ margin-left: auto; padding-inline: 8px/);
- assert.match(tabletBlock, /\.nav-github-label \{ display: none/);
- assert.match(tabletBlock, /\.nav-menu-toggle \{ display: inline-flex/);
- assert.match(mobileBlock, /\.nav-github \{ display: none/);
-});
-
-test("≤360px: marketing nav keeps the menu trigger within 320px", async () => {
- const css = await source("../styles/global.css");
- const block = mediaBlock(css, 360);
- assert.match(block, /\.nav-inner \{ padding: 0 12px; gap: 6px/);
- assert.match(block, /\.lang-switch button \{ padding: 6px 8px/);
- assert.match(block, /\.theme-switch button \{ padding: 6px 7px/);
- assert.match(block, /\.nav-install \{ display: none/);
- assert.match(block, /\.nav-menu-toggle \{ width: 36px; height: 36px/);
-});
-
-test("≤400px: marketing nav uses the compact brand mark", async () => {
- const css = await source("../styles/global.css");
- const block = mediaBlock(css, 400);
- assert.match(block, /\.nav \.brand img \{ width: 28px; height: 28px; border-radius: 8px; \}/);
-});
-
-test("≤360px: hero install command keeps its copy control inside the bar", async () => {
- const [css, page] = await Promise.all([
- source("../styles/global.css"),
- source("../pages/index.astro"),
- ]);
- const block = mediaBlock(css, 360);
- assert.match(page, /class=\"install-command\">npm i -g reasonix<\/span>/);
- assert.match(css, /\.hero-install \.install-command\s*\{[\s\S]*?min-width:\s*0;/);
- assert.match(block, /\.hero-install \.install \{ gap: 6px; padding-inline: 6px; \}/);
- assert.match(block, /\.hero-install \.install button \{ padding-inline: 10px; \}/);
-});
-
-test("≤440px: community nav budgets for the async account control", async () => {
- const css = await source("../styles/community.css");
- const layout = await source("../layouts/Community.astro");
- const block = mediaBlock(css, 440);
- assert.match(layout, /class="brand-name">Reasonix/);
- assert.match(layout, /id="nav-account"/);
- assert.match(block, /\.nav \.brand-name \{ display: none/);
- assert.match(block, /\.nav-right \{ gap: 8px/);
- assert.match(block, /\.lang-switch button \{ padding: 6px 8px/);
- assert.match(block, /\.theme-switch button \{ padding: 6px 7px/);
- assert.match(block, /#nav-account \.btn \{ padding: 7px 10px/);
-});
-
-test("shared headers expose an accessible mobile navigation", async () => {
- const [header, community] = await Promise.all([
- source("../components/SiteHeader.astro"),
- source("../layouts/Community.astro"),
- ]);
- for (const sourceText of [header, community]) {
- assert.match(sourceText, /data-mobile-nav-toggle/);
- assert.match(sourceText, /data-mobile-nav/);
- assert.match(sourceText, /data-mobile-nav-close/);
- assert.match(sourceText, /aria-expanded="false"/);
- }
-});
-
-test("mobile navigation behavior supports close, Escape, and focus cycling", async () => {
- const js = await source("./mobile-nav.js");
- assert.match(js, /event\.key === "Escape"/);
- assert.match(js, /event\.key !== "Tab"/);
- assert.match(js, /document\.body\.classList\.add\("mobile-nav-open"\)/);
- assert.match(js, /document\.body\.classList\.remove\("mobile-nav-open"\)/);
-});
diff --git a/site/src/scripts/mobile-nav.js b/site/src/scripts/mobile-nav.js
deleted file mode 100644
index 7287314503..0000000000
--- a/site/src/scripts/mobile-nav.js
+++ /dev/null
@@ -1,72 +0,0 @@
-// Shared responsive navigation behavior for the marketing and community headers.
-export function initMobileNav() {
- document.querySelectorAll("[data-mobile-nav]").forEach((menu) => {
- const header = menu.closest(".nav");
- const toggle = header?.querySelector("[data-mobile-nav-toggle]");
- const panel = menu.querySelector(".mobile-nav-panel");
- if (!toggle || !panel) return;
-
- let closeTimer;
- let stateToken = 0;
- let returnFocus = toggle;
- const focusables = () => Array.from(panel.querySelectorAll(
- 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
- )).filter((element) => !element.closest("[hidden]"));
-
- const close = (restore = true) => {
- stateToken += 1;
- clearTimeout(closeTimer);
- toggle.setAttribute("aria-expanded", "false");
- menu.setAttribute("aria-hidden", "true");
- menu.classList.remove("is-open");
- document.body.classList.remove("mobile-nav-open");
- closeTimer = window.setTimeout(() => { menu.hidden = true; }, 220);
- if (restore) returnFocus.focus();
- };
-
- const open = () => {
- const token = ++stateToken;
- clearTimeout(closeTimer);
- returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : toggle;
- menu.hidden = false;
- menu.setAttribute("aria-hidden", "false");
- toggle.setAttribute("aria-expanded", "true");
- document.body.classList.add("mobile-nav-open");
- requestAnimationFrame(() => {
- if (token === stateToken && !menu.hidden) menu.classList.add("is-open");
- });
- requestAnimationFrame(() => {
- if (token === stateToken && !menu.hidden) focusables()[0]?.focus();
- });
- };
-
- toggle.addEventListener("click", () => (menu.hidden ? open() : close()));
- menu.querySelectorAll("[data-mobile-nav-close]").forEach((button) => button.addEventListener("click", () => close()));
- menu.querySelectorAll("a[href]").forEach((link) => link.addEventListener("click", () => close(false)));
- menu.addEventListener("keydown", (event) => {
- if (event.key === "Escape") {
- event.preventDefault();
- close();
- return;
- }
- if (event.key !== "Tab") return;
- const items = focusables();
- if (!items.length) return;
- const first = items[0];
- const last = items[items.length - 1];
- if (event.shiftKey && document.activeElement === first) {
- event.preventDefault();
- last.focus();
- } else if (!event.shiftKey && document.activeElement === last) {
- event.preventDefault();
- first.focus();
- }
- });
- menu.addEventListener("transitionend", (event) => {
- if (event.propertyName === "opacity" && !menu.classList.contains("is-open")) menu.hidden = true;
- });
- window.addEventListener("resize", () => {
- if (window.matchMedia("(min-width: 1101px)").matches && !menu.hidden) close(false);
- }, { passive: true });
- });
-}
diff --git a/site/src/scripts/release-channels.js b/site/src/scripts/release-channels.js
deleted file mode 100644
index f4f36136e8..0000000000
--- a/site/src/scripts/release-channels.js
+++ /dev/null
@@ -1,294 +0,0 @@
-const STABLE_TAG = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
-const DESKTOP_DOWNLOAD_PAGE = "https://reasonix.io/?download=desktop#start";
-const DESKTOP_ASSETS = [
- ["platforms", "darwin-arm64", "Reasonix-darwin-arm64.zip"],
- ["platforms", "darwin-amd64", "Reasonix-darwin-amd64.zip"],
- ["platforms", "windows-amd64", "Reasonix-windows-amd64-installer.exe"],
- ["platforms", "windows-arm64", "Reasonix-windows-arm64-installer.exe"],
- ["platforms", "linux-amd64", "Reasonix-linux-amd64.tar.gz"],
- ["native_packages", "linux-amd64", "Reasonix-linux-amd64.deb"],
- ["downloads", "Reasonix-darwin-universal.dmg", "Reasonix-darwin-universal.dmg"],
- ["downloads", "Reasonix-windows-amd64.zip", "Reasonix-windows-amd64.zip"],
-];
-const DESKTOP_ASSET_NAMES = new Set(DESKTOP_ASSETS.map(([, , name]) => name));
-const OFFICIAL_DESKTOP_RELEASE_TAG = /^(?:desktop-)?(v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))$/;
-const SHA256 = /^[0-9a-f]{64}$/;
-const MAX_RELEASE_ASSET_SIZE = 1 << 30;
-
-// Keep in lockstep with workers/crash-report CLI asset gate.
-export const CLI_RELEASE_ASSETS = [
- "reasonix-darwin-amd64.tar.gz",
- "reasonix-darwin-arm64.tar.gz",
- "reasonix-linux-amd64.tar.gz",
- "reasonix-linux-arm64.tar.gz",
- "reasonix-windows-amd64.zip",
- "reasonix-windows-arm64.zip",
- "SHA256SUMS",
-];
-const CLI_RELEASE_ASSET_NAMES = new Set(CLI_RELEASE_ASSETS);
-
-export const publicReleaseChannels = new Set(["stable"]);
-
-export function normalizePublicReleaseChannel(_value) {
- return "stable";
-}
-
-export function cliUpgradeCommand(_value) {
- return "reasonix upgrade";
-}
-
-export function releaseVersionLabel(model) {
- return String(model?.version || "").trim() || "latest";
-}
-
-function parsePublicTag(tag) {
- const value = typeof tag === "string" ? tag : "";
- let match = value.match(STABLE_TAG);
- if (match) {
- return { tag: value, channel: "stable", order: match.slice(1) };
- }
- return null;
-}
-
-function compareDecimal(left, right) {
- if (left.length !== right.length) return left.length - right.length;
- return left === right ? 0 : left > right ? 1 : -1;
-}
-
-function compareOrder(left, right) {
- for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
- const delta = compareDecimal(left[i] || "0", right[i] || "0");
- if (delta !== 0) return delta;
- }
- return 0;
-}
-
-function safeHTTPSURL(value) {
- if (typeof value !== "string") return null;
- try {
- const url = new URL(value);
- return url.protocol === "https:" &&
- Boolean(url.hostname) &&
- !url.username &&
- !url.password
- ? url
- : null;
- } catch {
- return null;
- }
-}
-
-function expectedCLIAssetURL(value, tag, name) {
- if (typeof value !== "string") return null;
- const url = safeHTTPSURL(value);
- const path = `/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`;
- return url &&
- url.href === value &&
- url.hostname.toLowerCase() === "github.com" &&
- !url.port &&
- url.pathname === path &&
- !url.search &&
- !url.hash
- ? url
- : null;
-}
-
-// Returns the 7 required CLI asset URLs, or null when any required asset is
-// missing. Never synthesizes download URLs — incomplete releases must be
-// rejected so the site never advertises 404 links.
-export function releaseAssetMap(release) {
- const found = {};
- const seen = new Set();
- for (const asset of Array.isArray(release?.assets) ? release.assets : []) {
- const name = String(asset?.name || "");
- if (!CLI_RELEASE_ASSET_NAMES.has(name)) continue;
- if (seen.has(name)) return null;
- seen.add(name);
- const url = expectedCLIAssetURL(asset?.browser_download_url, String(release?.tag_name || ""), name);
- if (
- name &&
- url &&
- Number.isSafeInteger(asset?.size) &&
- asset.size > 0 &&
- asset.size <= MAX_RELEASE_ASSET_SIZE
- ) {
- found[name] = url.href;
- }
- }
- const assets = {};
- for (const name of CLI_RELEASE_ASSETS) {
- if (!found[name]) return null;
- assets[name] = found[name];
- }
- return assets;
-}
-
-export function selectCLIRelease(releases, requestedChannel) {
- const channel = normalizePublicReleaseChannel(requestedChannel);
- let selected = null;
- let selectedTag = null;
- for (const release of Array.isArray(releases) ? releases : []) {
- const parsed = parsePublicTag(release?.tag_name);
- if (!parsed || parsed.channel !== channel) continue;
- if (Boolean(release?.prerelease)) continue;
- if (!releaseAssetMap(release)) continue;
- if (!selectedTag || compareOrder(parsed.order, selectedTag.order) > 0) {
- selected = release;
- selectedTag = parsed;
- }
- }
- return selected;
-}
-
-export function cliReleaseModel(releases, requestedChannel) {
- const channel = normalizePublicReleaseChannel(requestedChannel);
- const release = selectCLIRelease(releases, channel);
- if (!release) return null;
- const parsed = parsePublicTag(release.tag_name);
- if (!parsed) return null;
- const assets = releaseAssetMap(release);
- if (!assets) return null;
- const releaseURL = `https://github.com/esengine/DeepSeek-Reasonix/releases/tag/${parsed.tag}`;
- const exactChangelogURL = `https://reasonix.io/changelog/${parsed.tag}/`;
- const changelogURL = release.release_notes_url === exactChangelogURL
- ? exactChangelogURL
- : "https://reasonix.io/changelog/";
- return {
- channel,
- version: parsed.tag,
- displayVersion: parsed.tag.slice(1),
- assets,
- releaseURL,
- changelogURL,
- };
-}
-
-function desktopAssetBases(parsed) {
- const tag = `desktop-${parsed.tag}`;
- return [
- `https://dl.reasonix.io/${tag}/`,
- `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/`,
- `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${parsed.tag}/`,
- ];
-}
-
-function normalizeDesktopManifest(manifest, requestedChannel) {
- const channel = normalizePublicReleaseChannel(requestedChannel);
- const parsed = parsePublicTag(manifest?.version);
- if (
- !parsed ||
- parsed.channel !== channel ||
- manifest?.download_page !== DESKTOP_DOWNLOAD_PAGE
- ) {
- return null;
- }
-
- const allowedBases = desktopAssetBases(parsed);
- let selectedBase = "";
- for (const [group, key, name] of DESKTOP_ASSETS) {
- const asset = manifest?.[group]?.[key];
- if (
- !asset ||
- typeof asset !== "object" ||
- Array.isArray(asset) ||
- !Number.isSafeInteger(asset.size) ||
- asset.size <= 0 ||
- asset.size > MAX_RELEASE_ASSET_SIZE ||
- typeof asset.sha256 !== "string" ||
- !SHA256.test(asset.sha256)
- ) {
- return null;
- }
-
- const rawURL = typeof asset.url === "string" ? asset.url : "";
- const url = safeHTTPSURL(rawURL);
- const base = allowedBases.find((candidate) => rawURL === candidate + name);
- if (
- !url ||
- !base ||
- url.href !== rawURL ||
- asset.sig !== `${rawURL}.minisig` ||
- (selectedBase && selectedBase !== base)
- ) {
- return null;
- }
- selectedBase = base;
- }
- return selectedBase ? { parsed } : null;
-}
-
-export function desktopReleaseModel(manifest, requestedChannel) {
- const normalized = normalizeDesktopManifest(manifest, requestedChannel);
- if (!normalized) return null;
- const { parsed } = normalized;
- const assets = Object.fromEntries(DESKTOP_ASSETS.map(([group, key, name]) => [
- name,
- manifest[group][key].url,
- ]));
- return {
- channel: parsed.channel,
- version: parsed.tag,
- displayVersion: parsed.tag.slice(1),
- assets,
- changelogURL: manifest.release_notes_url === `https://reasonix.io/changelog/${parsed.tag}/`
- ? manifest.release_notes_url
- : "https://reasonix.io/changelog/",
- };
-}
-
-// Accept both historical desktop-v* releases and the combined v* release.
-export function desktopGitHubReleaseModel(release) {
- const match = typeof release?.tag_name === "string"
- ? release.tag_name.match(OFFICIAL_DESKTOP_RELEASE_TAG)
- : null;
- if (!match || release?.draft !== false || release?.prerelease !== false) return null;
-
- const tag = release.tag_name;
- const found = {};
- const seen = new Set();
- for (const asset of Array.isArray(release.assets) ? release.assets : []) {
- const name = String(asset?.name || "");
- if (!DESKTOP_ASSET_NAMES.has(name)) continue;
- if (seen.has(name)) return null;
- seen.add(name);
- const rawURL = typeof asset?.browser_download_url === "string" ? asset.browser_download_url : "";
- const url = safeHTTPSURL(rawURL);
- const expected = `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`;
- if (
- !url ||
- url.href !== rawURL ||
- rawURL !== expected ||
- !Number.isSafeInteger(asset?.size) ||
- asset.size <= 0 ||
- asset.size > MAX_RELEASE_ASSET_SIZE
- ) {
- return null;
- }
- found[name] = rawURL;
- }
- if (DESKTOP_ASSETS.some(([, , name]) => !found[name])) return null;
-
- return {
- channel: "stable",
- version: match[1],
- displayVersion: match[1].slice(1),
- assets: Object.fromEntries(DESKTOP_ASSETS.map(([, , name]) => [name, found[name]])),
- changelogURL: "https://reasonix.io/changelog/",
- };
-}
-
-export async function fetchFirstJSON(urls, fetchImpl = fetch, accept = () => true) {
- const failures = [];
- for (const url of urls) {
- try {
- const response = await fetchImpl(url, { cache: "no-cache" });
- if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
- const payload = await response.json();
- if (!accept(payload)) throw new Error("invalid release data");
- return payload;
- } catch (error) {
- failures.push(`${url}: ${String(error?.message || error)}`);
- }
- }
- throw new Error(`release data unavailable (${failures.join("; ")})`);
-}
diff --git a/site/src/scripts/release-channels.test.mjs b/site/src/scripts/release-channels.test.mjs
deleted file mode 100644
index 1eada8b951..0000000000
--- a/site/src/scripts/release-channels.test.mjs
+++ /dev/null
@@ -1,378 +0,0 @@
-import { test } from "node:test";
-import assert from "node:assert/strict";
-import { readFile } from "node:fs/promises";
-import {
- CLI_RELEASE_ASSETS,
- cliUpgradeCommand,
- cliReleaseModel,
- desktopGitHubReleaseModel,
- desktopReleaseModel,
- fetchFirstJSON,
- releaseAssetMap,
- releaseVersionLabel,
- selectCLIRelease,
-} from "./release-channels.js";
-
-function cliAssets(tag, missing = []) {
- const skip = new Set(missing);
- return CLI_RELEASE_ASSETS.filter((name) => !skip.has(name)).map((name) => ({
- name,
- browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
- size: 42,
- }));
-}
-
-const desktopSHA256 = "a".repeat(64);
-
-function desktopManifest(version, base) {
- const releaseBase = base || `https://dl.reasonix.io/desktop-${version}/`;
- const asset = (name) => {
- const url = releaseBase + name;
- return { url, sig: `${url}.minisig`, size: 42, sha256: desktopSHA256 };
- };
- return {
- version,
- download_page: "https://reasonix.io/?download=desktop#start",
- platforms: {
- "darwin-arm64": asset("Reasonix-darwin-arm64.zip"),
- "darwin-amd64": asset("Reasonix-darwin-amd64.zip"),
- "windows-amd64": asset("Reasonix-windows-amd64-installer.exe"),
- "windows-arm64": asset("Reasonix-windows-arm64-installer.exe"),
- "linux-amd64": asset("Reasonix-linux-amd64.tar.gz"),
- },
- native_packages: {
- "linux-amd64": asset("Reasonix-linux-amd64.deb"),
- },
- downloads: {
- "Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"),
- "Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip"),
- },
- };
-}
-
-function desktopGitHubRelease(version = "v1.17.21") {
- const tag = `desktop-${version}`;
- const names = [
- "Reasonix-darwin-arm64.zip",
- "Reasonix-darwin-amd64.zip",
- "Reasonix-windows-amd64-installer.exe",
- "Reasonix-windows-arm64-installer.exe",
- "Reasonix-linux-amd64.tar.gz",
- "Reasonix-linux-amd64.deb",
- "Reasonix-darwin-universal.dmg",
- "Reasonix-windows-amd64.zip",
- ];
- return {
- tag_name: tag,
- draft: false,
- prerelease: false,
- assets: names.map((name) => ({
- name,
- browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`,
- size: 42,
- })),
- };
-}
-
-function copy(value) {
- return structuredClone(value);
-}
-
-test("CLI always emits the official upgrade command", () => {
- assert.equal(cliUpgradeCommand("stable"), "reasonix upgrade");
- assert.equal(cliUpgradeCommand("preview"), "reasonix upgrade");
- assert.equal(cliUpgradeCommand("canary"), "reasonix upgrade");
-});
-
-test("release labels use a complete version or a readable fallback", () => {
- assert.equal(releaseVersionLabel({ version: "v1.17.21" }), "v1.17.21");
- assert.equal(releaseVersionLabel({ version: "v1.18.0-preview.62" }), "v1.18.0-preview.62");
- assert.equal(releaseVersionLabel(null), "latest");
-});
-
-test("site placeholders do not render the synthetic version vlatest", async () => {
- const [home, docs, siteScript] = await Promise.all([
- readFile(new URL("../pages/index.astro", import.meta.url), "utf8"),
- readFile(new URL("../pages/docs.astro", import.meta.url), "utf8"),
- readFile(new URL("./site.js", import.meta.url), "utf8"),
- ]);
- assert.doesNotMatch(home, /v]*data-cli-asset=[^>]*\sdownload(?:[ >])/);
- assert.match(home, /id="download-tab-desktop"[^>]+aria-controls="download-pane-desktop"/);
- assert.match(home, /id="download-pane-cli"[^>]+role="tabpanel"[^>]+hidden/);
- assert.doesNotMatch(home, /releases\/latest\/download/);
- assert.doesNotMatch(siteScript, /releases\/latest\/download/);
- assert.doesNotMatch(siteScript, /desktopPreviewBase/);
-});
-
-test("CLI strictly excludes foreign and all prereleases", () => {
- const releases = [
- { tag_name: "v1.19.0-rc.1", prerelease: true, assets: cliAssets("v1.19.0-rc.1") },
- { tag_name: "v1.18.0-preview.2", prerelease: true, assets: cliAssets("v1.18.0-preview.2") },
- { tag_name: "desktop-v1.18.0", prerelease: false, assets: cliAssets("desktop-v1.18.0") },
- { tag_name: "v1.17.21", prerelease: false, assets: cliAssets("v1.17.21") },
- { tag_name: "v1.18.0-preview.12", prerelease: true, assets: cliAssets("v1.18.0-preview.12") },
- { tag_name: "v1.18.0-preview.13", prerelease: false, assets: cliAssets("v1.18.0-preview.13") },
- ];
- assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.17.21");
- assert.equal(selectCLIRelease(releases, "preview")?.tag_name, "v1.17.21");
-});
-
-test("CLI selection rejects incomplete releases instead of synthesizing asset URLs", () => {
- const releases = [
- {
- tag_name: "v1.20.0",
- prerelease: false,
- assets: cliAssets("v1.20.0", ["reasonix-windows-arm64.zip", "SHA256SUMS"]),
- },
- {
- tag_name: "v1.19.5",
- prerelease: false,
- assets: cliAssets("v1.19.5"),
- },
- ];
- assert.equal(releaseAssetMap(releases[0]), null);
- assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v1.19.5");
- const model = cliReleaseModel(releases, "stable");
- assert.equal(model?.displayVersion, "1.19.5");
- assert.equal(
- model?.assets["reasonix-windows-arm64.zip"],
- "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.5/reasonix-windows-arm64.zip",
- );
- assert.equal(cliReleaseModel([releases[0]], "stable"), null);
-});
-
-test("CLI selection compares arbitrarily large numeric version components exactly", () => {
- const releases = [
- {
- tag_name: "v99999999999999999999.999.999",
- prerelease: false,
- assets: cliAssets("v99999999999999999999.999.999"),
- },
- {
- tag_name: "v100000000000000000000.0.0",
- prerelease: false,
- assets: cliAssets("v100000000000000000000.0.0"),
- },
- ];
- assert.equal(selectCLIRelease(releases, "stable")?.tag_name, "v100000000000000000000.0.0");
-});
-
-test("CLI release links are derived from the validated canonical tag", () => {
- const release = {
- tag_name: "v1.18.0",
- prerelease: false,
- html_url: "https://evil.invalid/phishing",
- assets: cliAssets("v1.18.0"),
- };
- assert.equal(
- cliReleaseModel([release], "stable")?.releaseURL,
- "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.18.0",
- );
- assert.equal(
- cliReleaseModel([release], "stable")?.changelogURL,
- "https://reasonix.io/changelog/",
- );
- release.release_notes_url = "https://reasonix.io/changelog/v1.18.0/";
- assert.equal(cliReleaseModel([release], "stable")?.changelogURL, release.release_notes_url);
-});
-
-test("CLI assets reject spoofed hosts and cross-tag URLs", () => {
- const valid = {
- tag_name: "v1.18.0",
- prerelease: false,
- assets: cliAssets("v1.18.0"),
- };
- const invalidURLs = [
- "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
- "https://github.com@evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
- "http://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz",
- "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.0/reasonix-darwin-amd64.tar.gz",
- "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-arm64.tar.gz",
- ];
- const invalid = invalidURLs.map((browser_download_url) => {
- const release = {
- tag_name: "v1.20.0",
- prerelease: false,
- assets: cliAssets("v1.20.0"),
- };
- release.assets[0].browser_download_url = browser_download_url;
- return release;
- });
- assert.equal(selectCLIRelease([...invalid, valid], "stable")?.tag_name, "v1.18.0");
-});
-
-test("CLI assets require a bounded positive safe integer size", () => {
- for (const size of [0, -1, "42", 1.5, 1073741825, Number.MAX_SAFE_INTEGER + 1, NaN, undefined]) {
- const release = {
- tag_name: "v1.20.0",
- prerelease: false,
- assets: cliAssets("v1.20.0"),
- };
- release.assets[0].size = size;
- assert.equal(releaseAssetMap(release), null, `size ${String(size)} must be rejected`);
- }
-});
-
-test("CLI releases reject duplicate required asset names", () => {
- const release = {
- tag_name: "v1.20.0",
- prerelease: false,
- assets: cliAssets("v1.20.0"),
- };
- release.assets.push({ ...release.assets[0] });
- assert.equal(releaseAssetMap(release), null);
-});
-
-test("Desktop manifests accept only official versions and old or unified asset bases", () => {
- const preview = desktopManifest("v1.18.0-preview.62");
- assert.equal(desktopReleaseModel(preview, "stable"), null);
- assert.equal(desktopReleaseModel(preview, "preview"), null);
-
- const stableR2 = desktopManifest("v1.18.0");
- assert.equal(desktopReleaseModel(stableR2, "stable")?.version, "v1.18.0");
- const githubBase =
- "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/";
- const stableGitHub = desktopManifest("v1.18.0", githubBase);
- assert.equal(
- desktopReleaseModel(stableGitHub, "stable")?.assets["Reasonix-linux-amd64.deb"],
- `${githubBase}Reasonix-linux-amd64.deb`,
- );
- const unifiedBase = "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.18.0/";
- assert.equal(desktopReleaseModel(desktopManifest("v1.18.0", unifiedBase))?.assets["Reasonix-linux-amd64.deb"], `${unifiedBase}Reasonix-linux-amd64.deb`);
-});
-
-test("Desktop manifests reject hostile URLs and incomplete integrity metadata", () => {
- const cases = [
- ["malicious host", (manifest) => {
- const url = "https://evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
- Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
- }],
- ["userinfo", (manifest) => {
- const url = "https://dl.reasonix.io@evil.invalid/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
- Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
- }],
- ["http", (manifest) => {
- const url = "http://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-arm64.zip";
- Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
- }],
- ["wrong channel path", (manifest) => {
- const url = "https://dl.reasonix.io/preview/Reasonix-darwin-arm64.zip";
- Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
- }],
- ["wrong filename", (manifest) => {
- const url = "https://dl.reasonix.io/desktop-v1.18.0-preview.62/Reasonix-darwin-amd64.zip";
- Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` });
- }],
- ["missing platform", (manifest) => {
- delete manifest.platforms["windows-arm64"];
- }],
- ["missing website download", (manifest) => {
- delete manifest.downloads["Reasonix-darwin-universal.dmg"];
- }],
- ["invalid website download", (manifest) => {
- manifest.downloads["Reasonix-windows-amd64.zip"].size = 0;
- }],
- ["bad SHA", (manifest) => {
- manifest.platforms["darwin-arm64"].sha256 = "A".repeat(64);
- }],
- ["zero size", (manifest) => {
- manifest.platforms["darwin-arm64"].size = 0;
- }],
- ["size above release maximum", (manifest) => {
- manifest.platforms["darwin-arm64"].size = 1073741825;
- }],
- ["string size", (manifest) => {
- manifest.platforms["darwin-arm64"].size = "42";
- }],
- ["missing size", (manifest) => {
- delete manifest.platforms["darwin-arm64"].size;
- }],
- ["bad signature URL", (manifest) => {
- manifest.platforms["darwin-arm64"].sig += "?mirror=1";
- }],
- ["wrong download page", (manifest) => {
- manifest.download_page = "https://evil.invalid/download";
- }],
- ];
-
- for (const [name, mutate] of cases) {
- const manifest = copy(desktopManifest("v1.18.0-preview.62"));
- mutate(manifest);
- assert.equal(desktopReleaseModel(manifest, "preview"), null, name);
- }
-
- const crossVersion = desktopManifest(
- "v1.18.0",
- "https://dl.reasonix.io/desktop-v1.17.9/",
- );
- assert.equal(desktopReleaseModel(crossVersion, "stable"), null);
-});
-
-test("Desktop Stable falls back to a complete exact GitHub Latest release", () => {
- const release = desktopGitHubRelease();
- const model = desktopGitHubReleaseModel(release);
- assert.equal(model?.version, "v1.17.21");
- assert.equal(
- model?.assets["Reasonix-darwin-universal.dmg"],
- "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-universal.dmg",
- );
-
- const invalid = [
- { ...release, prerelease: true },
- { ...release, tag_name: "v1.17.21" },
- { ...release, assets: release.assets.slice(1) },
- { ...release, assets: [...release.assets, { ...release.assets[0] }] },
- ];
- const spoofed = copy(release);
- spoofed.assets[0].browser_download_url =
- "https://github.com.evil.invalid/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.21/Reasonix-darwin-arm64.zip";
- invalid.push(spoofed);
- for (const candidate of invalid) assert.equal(desktopGitHubReleaseModel(candidate), null);
-});
-
-test("release JSON fetch falls back in order", async () => {
- const calls = [];
- const result = await fetchFirstJSON(["https://one.invalid", "https://two.invalid"], async (url) => {
- calls.push(url);
- if (url.includes("one")) return { ok: false, status: 503, statusText: "Unavailable" };
- return { ok: true, json: async () => ({ version: "v1.2.3" }) };
- });
- assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
- assert.deepEqual(result, { version: "v1.2.3" });
-});
-
-test("release JSON fetch skips a successful response that fails channel validation", async () => {
- const calls = [];
- const result = await fetchFirstJSON(
- ["https://one.invalid", "https://two.invalid"],
- async (url) => {
- calls.push(url);
- return {
- ok: true,
- json: async () => ({ version: url.includes("one") ? "v1.2.3-canary.1" : "v1.2.3-preview.1" }),
- };
- },
- (payload) => payload.version === "v1.2.3-preview.1",
- );
- assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
- assert.deepEqual(result, { version: "v1.2.3-preview.1" });
-});
-
-test("Desktop JSON fetch continues after an invalid 200 response", async () => {
- const invalid = desktopManifest("v1.18.0");
- invalid.platforms["darwin-arm64"].size = 0;
- const valid = desktopManifest("v1.17.21");
- const calls = [];
- const result = await fetchFirstJSON(
- ["https://one.invalid", "https://two.invalid"],
- async (url) => {
- calls.push(url);
- return { ok: true, json: async () => url.includes("one") ? invalid : valid };
- },
- (payload) => Boolean(desktopReleaseModel(payload)),
- );
- assert.deepEqual(calls, ["https://one.invalid", "https://two.invalid"]);
- assert.equal(result.version, "v1.17.21");
-});
diff --git a/site/src/scripts/safe-next.js b/site/src/scripts/safe-next.js
deleted file mode 100644
index d9b5b4399f..0000000000
--- a/site/src/scripts/safe-next.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// A post-login `next` redirect target is honoured only if it resolves — via
-// full URL parsing against the real page origin, so control characters and
-// protocol-relative tricks get the same normalization a browser applies
-// before navigating — to a same-origin path, or to an absolute https URL
-// under reasonix.io. That lets a subdomain (e.g. crash.reasonix.io) return
-// here after sign-in without opening a redirect to an arbitrary host.
-//
-// Validating a raw string prefix (e.g. checking it starts with "/") is not
-// enough: URLSearchParams decodes percent-encoding, so a value like
-// "/%09/evil.example" arrives as "/\t/evil.example", which passes a naive
-// prefix check but the URL parser strips the tab during navigation, leaving
-// "//evil.example" — a protocol-relative redirect off-site. Parsing with
-// `new URL()` first and checking the *parsed* origin/host closes that gap
-// because it uses the same normalization the browser itself applies.
-//
-// Both branches return the fully-serialized `u.href`, never a relative
-// fragment. Returning `u.pathname` would reopen the hole: dot-segment inputs
-// such as "/.//evil.example" or "/a/..//evil.example" resolve to a
-// same-origin URL whose *pathname* is "//evil.example", and handing that
-// bare pathname back to `location.href` re-parses it as a protocol-relative
-// URL to evil.example. `u.href` keeps the origin attached, so assigning it
-// can never leave reasonix.io.
-export function safeNext(next, origin) {
- if (!next) return null;
- let u;
- try {
- u = new URL(next, origin);
- } catch {
- return null;
- }
- if (u.origin === origin) return u.href;
- if (u.protocol === "https:" && (u.host === "reasonix.io" || u.host.endsWith(".reasonix.io"))) return u.href;
- return null;
-}
diff --git a/site/src/scripts/safe-next.test.mjs b/site/src/scripts/safe-next.test.mjs
deleted file mode 100644
index d10304d5b4..0000000000
--- a/site/src/scripts/safe-next.test.mjs
+++ /dev/null
@@ -1,71 +0,0 @@
-import { test } from "node:test";
-import assert from "node:assert/strict";
-import { safeNext } from "./safe-next.js";
-
-const ORIGIN = "https://reasonix.io";
-
-// The security invariant: whatever safeNext returns, re-resolving it the way a
-// browser does when it assigns `location.href` must stay on our origin or an
-// allowed reasonix.io host. Asserting this (rather than a fixed string) catches
-// any value that looks same-origin but re-parses off-site.
-function assertLandsSomewhereSafe(returned) {
- if (returned === null) return;
- const landed = new URL(returned, ORIGIN);
- const ok = landed.origin === ORIGIN || landed.host === "reasonix.io" || landed.host.endsWith(".reasonix.io");
- assert.ok(ok, `returned ${JSON.stringify(returned)} re-resolves off-site to ${landed.href}`);
-}
-
-test("same-origin path passes through as an absolute same-origin URL", () => {
- const got = safeNext("/account/settings?x=1#y", ORIGIN);
- assert.equal(got, "https://reasonix.io/account/settings?x=1#y");
- assertLandsSomewhereSafe(got);
-});
-
-test("allowed reasonix.io subdomain passes through", () => {
- const got = safeNext("https://crash.reasonix.io/x", ORIGIN);
- assert.equal(got, "https://crash.reasonix.io/x");
- assertLandsSomewhereSafe(got);
-});
-
-test("empty/missing next", () => {
- assert.equal(safeNext("", ORIGIN), null);
- assert.equal(safeNext(null, ORIGIN), null);
-});
-
-test("rejects a plain protocol-relative redirect", () => {
- assert.equal(safeNext("//evil.example", ORIGIN), null);
-});
-
-test("rejects a backslash-prefixed redirect", () => {
- assert.equal(safeNext("/\\evil.example", ORIGIN), null);
- assert.equal(safeNext("/\\/evil.example", ORIGIN), null);
-});
-
-test("rejects control characters (as decoded by URLSearchParams) smuggling a protocol-relative redirect", () => {
- // URLSearchParams.get() decodes %09/%0A/%0D before this function ever sees
- // the value, e.g. "?next=/%09/evil.example" arrives as "/\t/evil.example".
- for (const c of ["\t", "\n", "\r"]) {
- assert.equal(safeNext(`/${c}/evil.example`, ORIGIN), null, JSON.stringify(c));
- }
-});
-
-test("dot-segment inputs that normalize to a //host pathname do not escape", () => {
- // These resolve to a same-origin URL whose pathname is "//evil.example".
- // Returning the bare pathname would re-parse as a protocol-relative redirect;
- // returning the absolute href keeps them on reasonix.io. Assert end to end.
- for (const p of ["/.//evil.example", "/a/..//evil.example", "/..//evil.example", "/%2e//evil.example"]) {
- const got = safeNext(decodeURIComponent(p), ORIGIN);
- assert.notEqual(got, null, p);
- assert.equal(new URL(got, ORIGIN).origin, ORIGIN, `${p} -> ${got}`);
- assertLandsSomewhereSafe(got);
- }
-});
-
-test("rejects an unrelated https host and non-https schemes", () => {
- assert.equal(safeNext("https://evil.example/", ORIGIN), null);
- assert.equal(safeNext("javascript:alert(1)", ORIGIN), null);
-});
-
-test("rejects a reasonix.io-lookalike host", () => {
- assert.equal(safeNext("https://reasonix.io.evil.example/", ORIGIN), null);
-});
diff --git a/site/src/scripts/site.js b/site/src/scripts/site.js
deleted file mode 100644
index 08a3015ed0..0000000000
--- a/site/src/scripts/site.js
+++ /dev/null
@@ -1,334 +0,0 @@
-import { downloadPaneFromURL, downloadURLForPane } from "./download-link.js";
-import {
- cliReleaseModel,
- cliUpgradeCommand,
- desktopGitHubReleaseModel,
- desktopReleaseModel,
- fetchFirstJSON,
- releaseVersionLabel,
-} from "./release-channels.js";
-import { initTheme } from "./theme.js";
-import { initMobileNav } from "./mobile-nav.js";
-
-// Reasonix site — vanilla interactions
-(function () {
- initTheme();
- initMobileNav();
- const motionOK = () =>
- document.body.dataset.motion === "rich" &&
- !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
-
- const nav = document.querySelector(".nav");
- if (nav) {
- const onScroll = () => nav.classList.toggle("scrolled", window.scrollY > 12);
- window.addEventListener("scroll", onScroll, { passive: true });
- onScroll();
- }
-
- const revealEls = Array.from(document.querySelectorAll(".reveal"));
- const inView = (el, factor) =>
- el.getBoundingClientRect().top < window.innerHeight * (factor || 0.95);
-
- const term = document.querySelector(".term");
- const lines = Array.from(document.querySelectorAll(".term-body .tl"));
- let played = false;
- const playTerm = () => {
- if (played) return;
- played = true;
- const fire = () => document.dispatchEvent(new CustomEvent("rx:term-played"));
- if (!motionOK()) {
- lines.forEach((l) => l.classList.add("on"));
- fire();
- return;
- }
- lines.forEach((l, i) => setTimeout(() => l.classList.add("on"), 350 + i * 520));
- setTimeout(fire, 350 + Math.max(0, lines.length - 2) * 520);
- };
-
- let sweepQueued = false;
- const sweep = () => {
- sweepQueued = false;
- revealEls.forEach((el) => {
- if (!el.classList.contains("in") && inView(el, 0.95)) el.classList.add("in");
- });
- if (term && !played && inView(term, 0.85)) playTerm();
- };
- const queueSweep = () => {
- if (sweepQueued) return;
- sweepQueued = true;
- requestAnimationFrame(sweep);
- };
- window.addEventListener("scroll", queueSweep, { passive: true });
- window.addEventListener("resize", queueSweep, { passive: true });
- window.addEventListener("load", queueSweep);
- sweep();
- setTimeout(sweep, 400);
-
- /* contributors marquee — duplicate the server-rendered set for a seamless loop */
- document.querySelectorAll(".crew-row").forEach((row) => {
- const set = row.querySelector(".crew-set");
- if (set) row.appendChild(set.cloneNode(true));
- });
-
- /* download / channel tabs */
- const tabs = Array.from(document.querySelectorAll(".dl-tab"));
- const panes = Array.from(document.querySelectorAll(".dl-pane"));
- const activatePane = (name) => {
- tabs.forEach((b) => {
- const active = b.dataset.pane === name;
- b.classList.toggle("active", active);
- b.setAttribute("aria-selected", active ? "true" : "false");
- b.tabIndex = active ? 0 : -1;
- });
- panes.forEach((p) => {
- const active = p.dataset.pane === name;
- p.classList.toggle("active", active);
- p.hidden = !active;
- });
- };
- tabs.forEach((tab) => {
- tab.addEventListener("click", () => {
- activatePane(tab.dataset.pane);
- reflectPaneURL(tab.dataset.pane);
- });
- tab.addEventListener("keydown", (event) => {
- const current = tabs.indexOf(tab);
- let next = -1;
- if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (current + 1) % tabs.length;
- else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (current - 1 + tabs.length) % tabs.length;
- else if (event.key === "Home") next = 0;
- else if (event.key === "End") next = tabs.length - 1;
- if (next < 0) return;
- event.preventDefault();
- const nextTab = tabs[next];
- activatePane(nextTab.dataset.pane);
- reflectPaneURL(nextTab.dataset.pane);
- nextTab.focus();
- });
- });
-
- /* OS detection — hero download button + card badge + highlight */
- const ua = navigator.userAgent;
- const os = /Windows/i.test(ua) ? "win" : /Mac|iPhone|iPad/i.test(ua) ? "mac" : /Linux|X11/i.test(ua) ? "linux" : "mac";
- const osNames = { mac: "macOS", win: "Windows", linux: "Linux" };
- document.querySelectorAll("[data-os-dl] .os-name").forEach((s) => (s.textContent = osNames[os]));
- const osCard = document.querySelector('.os-card[data-os="' + os + '"]');
- if (osCard) {
- osCard.classList.add("detected");
- const chip = document.createElement("span");
- chip.className = "os-chip";
- chip.innerHTML = 'your OS 当前系统 ';
- osCard.appendChild(chip);
- }
-
- const flashOSCard = () => {
- if (!osCard) return;
- osCard.classList.remove("flash");
- void osCard.offsetWidth;
- setTimeout(() => osCard.classList.add("flash"), 450);
- setTimeout(() => osCard.classList.remove("flash"), 2600);
- };
-
- const requestedPane = downloadPaneFromURL(window.location.href);
- const legacyChannelURL = new URL(window.location.href);
- if (legacyChannelURL.searchParams.has("channel")) {
- legacyChannelURL.searchParams.delete("channel");
- window.history.replaceState(null, "", legacyChannelURL.href);
- }
- if (requestedPane) {
- activatePane(requestedPane);
- if (requestedPane === "desktop") flashOSCard();
- requestAnimationFrame(() => {
- document.getElementById("start")?.scrollIntoView({ block: "start" });
- queueSweep();
- });
- }
-
- /* links that deep-link into a specific download tab */
- document.querySelectorAll("[data-goto]").forEach((a) => {
- a.addEventListener("click", (event) => {
- event.preventDefault();
- activatePane(a.dataset.goto);
- reflectPaneURL(a.dataset.goto);
- if (a.hasAttribute("data-os-dl")) flashOSCard();
- document.getElementById("start")?.scrollIntoView({ block: "start" });
- setTimeout(queueSweep, 500);
- });
- });
-
- /* language switch */
- const LANG_KEY = "reasonix-lang";
- const langBtns = Array.from(document.querySelectorAll(".lang-switch button"));
- const setLang = (l, alignHash) => {
- document.body.dataset.lang = l;
- document.documentElement.lang = l === "zh" ? "zh-CN" : "en";
- const t = document.body.dataset[l === "zh" ? "titleZh" : "titleEn"];
- if (t) document.title = t;
- langBtns.forEach((b) => b.classList.toggle("active", b.dataset.lang === l));
- try { localStorage.setItem(LANG_KEY, l); } catch (e) {}
- if (alignHash && window.location.hash) {
- const target = document.getElementById(window.location.hash.slice(1));
- if (target) requestAnimationFrame(() => target.scrollIntoView({ block: "start" }));
- }
- };
- langBtns.forEach((b) => b.addEventListener("click", () => setLang(b.dataset.lang)));
- let savedLang = "";
- try { savedLang = localStorage.getItem(LANG_KEY) || ""; } catch (e) {}
- const requestedLang = new URLSearchParams(window.location.search).get("lang");
- const initialLang = requestedLang === "zh" || requestedLang === "en"
- ? requestedLang
- : savedLang || ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en");
- setLang(initialLang, true);
-
- /* docs scrollspy */
- const sideLinks = Array.from(document.querySelectorAll(".docs-side a[href^='#']"));
- if (sideLinks.length) {
- const targets = sideLinks
- .map((a) => document.getElementById(a.getAttribute("href").slice(1)))
- .filter(Boolean)
- // Sidebar links are grouped editorially, so their order differs from the
- // page order. The spy below picks the last section past the 140px line,
- // which is only correct when targets are sorted in document order.
- .sort((a, b) =>
- a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
- const setActive = (id) =>
- sideLinks.forEach((a) => {
- const on = a.getAttribute("href") === "#" + id;
- a.classList.toggle("active", on);
- if (on) a.setAttribute("aria-current", "true");
- else a.removeAttribute("aria-current");
- });
- // While a click smooth-scrolls to a section, pin the highlight to it so it
- // doesn't sweep through every section scrolled past on the way. scrollend
- // releases the pin when the scroll settles — on arrival or when the user
- // takes over (wheel, touch, keyboard, scrollbar). Browsers without
- // scrollend just skip pinning: correct destination, no sweep suppression.
- let pinned = null;
- const spy = () => {
- if (pinned) return;
- let current = targets[0];
- for (const t of targets) if (t.getBoundingClientRect().top < 140) current = t;
- if (current) setActive(current.id);
- };
- if ("onscrollend" in window) {
- const ids = new Set(targets.map((t) => t.id));
- document.querySelectorAll("a[href^='#']").forEach((a) => {
- const id = a.getAttribute("href").slice(1);
- if (ids.has(id)) a.addEventListener("click", () => { pinned = id; setActive(id); });
- });
- window.addEventListener("scrollend", () => { pinned = null; spy(); }, { passive: true });
- }
- window.addEventListener("scroll", spy, { passive: true });
- spy();
- }
-
- /* copy-to-clipboard */
- document.querySelectorAll("[data-copy]").forEach((btn) => {
- btn.addEventListener("click", () => {
- const text = btn.getAttribute("data-copy");
- const done = () => {
- btn.classList.add("copied");
- const prev = btn.textContent;
- btn.textContent = "Copied";
- setTimeout(() => { btn.classList.remove("copied"); btn.textContent = prev; }, 1600);
- };
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(text).then(done).catch(done);
- } else done();
- });
- });
-
- /* public official releases */
- const releaseModels = { desktop: null, cli: null };
- const releasesPage = "https://github.com/esengine/DeepSeek-Reasonix/releases";
- const reflectPaneURL = (surface) => {
- const nextURL = downloadURLForPane(window.location.href, surface, "");
- if (nextURL) window.history.replaceState(null, "", nextURL);
- };
-
- // npm / Homebrew / generic product chips track the CLI stable line only.
- // Desktop and CLI download panes render their own versions via
- // [data-release-version=""]; never let Desktop and CLI race on .rxv.
- const updateCLIPackageVersion = (model) => {
- if (!model) return;
- document.querySelectorAll(".rxv").forEach((element) => { element.textContent = releaseVersionLabel(model); });
- document.querySelectorAll("a.rxnotes").forEach((link) => {
- link.href = new URL("changelog/v" + model.displayVersion + "/", window.location.origin + "/").href;
- });
- };
-
- // Never synthesize public artifact URLs. If every required asset is not
- // attested by live release data, fall back to the release list instead of a
- // plausible-looking URL that may 404.
- const fallbackReleaseURL = () => releasesPage;
-
- const renderReleaseSurface = (surface) => {
- const model = releaseModels[surface];
- document.querySelectorAll('[data-release-version="' + surface + '"]').forEach((element) => {
- element.textContent = releaseVersionLabel(model);
- });
- document.querySelectorAll('[data-release-notes="' + surface + '"]').forEach((link) => {
- const path = model?.changelogURL ? new URL(model.changelogURL).pathname : "changelog/";
- link.href = new URL(path, window.location.origin + "/").href;
- });
-
- const assetAttribute = "data-" + surface + "-asset";
- document.querySelectorAll("[" + assetAttribute + "]").forEach((link) => {
- const asset = link.getAttribute(assetAttribute);
- const target = model?.assets?.[asset] || fallbackReleaseURL();
- link.href = target;
- if (target === releasesPage) link.removeAttribute("download");
- else link.setAttribute("download", "");
- });
-
- if (surface === "cli") {
- const command = cliUpgradeCommand();
- document.querySelectorAll('[data-release-command="cli"]').forEach((element) => { element.textContent = command; });
- document.querySelectorAll('.release-upgrade-command [data-copy]').forEach((button) => { button.dataset.copy = command; });
- }
- };
-
- renderReleaseSurface("desktop");
- renderReleaseSurface("cli");
- if (requestedPane) reflectPaneURL(requestedPane);
-
- fetchFirstJSON([
- "https://dl.reasonix.io/latest/latest.json",
- "https://crash.reasonix.io/v1/desktop/releases/stable/latest.json",
- ], fetch, (manifest) => Boolean(desktopReleaseModel(manifest)))
- .then((manifest) => desktopReleaseModel(manifest))
- .catch(() => fetchFirstJSON(
- ["https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases/latest"],
- fetch,
- (release) => Boolean(desktopGitHubReleaseModel(release)),
- ).then(desktopGitHubReleaseModel))
- .then((model) => {
- if (!model) return;
- releaseModels.desktop = model;
- renderReleaseSurface("desktop");
- })
- .catch(() => {});
-
- let githubCLIReleases;
- const fallbackCLIReleases = () => {
- githubCLIReleases ??= fetchFirstJSON([
- "https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases?per_page=100",
- ]).catch(() => null);
- return githubCLIReleases;
- };
- fetchFirstJSON(
- ["https://crash.reasonix.io/v1/cli/releases/stable/latest.json"],
- fetch,
- (payload) => Boolean(cliReleaseModel(Array.isArray(payload) ? payload : [payload])),
- )
- .catch(() => fallbackCLIReleases())
- .then((payload) => {
- const releases = Array.isArray(payload) ? payload : payload ? [payload] : [];
- const model = cliReleaseModel(releases);
- if (!model) return;
- releaseModels.cli = model;
- updateCLIPackageVersion(model);
- renderReleaseSurface("cli");
- })
- .catch(() => {});
-})();
diff --git a/site/src/scripts/skills-layout-contract.test.mjs b/site/src/scripts/skills-layout-contract.test.mjs
deleted file mode 100644
index 5b32ad8978..0000000000
--- a/site/src/scripts/skills-layout-contract.test.mjs
+++ /dev/null
@@ -1,48 +0,0 @@
-import assert from "node:assert/strict";
-import { readFile } from "node:fs/promises";
-import test from "node:test";
-
-const source = () => readFile(new URL("../pages/skills.astro", import.meta.url), "utf8");
-
-test("the registry publish CTA does not inherit the publish panel layout", async () => {
- const page = await source();
-
- assert.match(page, /class="btn btn-dark reg-publish-cta" id="publish-open"/);
- assert.match(page, //);
- assert.doesNotMatch(page, /class="btn btn-dark reg-publish" id="publish-open"/);
-});
-
-test("the registry exposes plugins as a first-class package kind", async () => {
- const page = await source();
-
- assert.match(page, /Plugins<\/button>/);
- assert.match(page, /plugin<\/option>/);
- assert.match(page, /installKind: f\.get\('kind'\)/);
- assert.match(page, /Plugins must point to a GitHub repository or path containing/);
- assert.match(page, /reasonix-plugin\.json/);
- assert.match(page, /\.codex-plugin\/plugin\.json/);
- assert.match(page, /\.claude-plugin\/plugin\.json/);
- assert.match(page, /\.claude-plugin\/marketplace\.json/);
- assert.match(page, /pattern="\(https\?:\/\/\[\^ \]\+\|git:github\[\.\]com\/\[\^ \]\+/);
-});
-
-test("registry copy requests preserve the reviewed package kind", async () => {
- const page = await source();
-
- assert.match(page, /Install this Reasonix \$\{p\.kind\} package from \$\{p\.source\}/);
- assert.match(page, /Use install_source with kind="\$\{p\.kind\}"/);
- assert.match(page, /data-copy="\$\{esc\(installRequest\(p\)\)\}"/);
- assert.doesNotMatch(page, /data-copy="\$\{esc\(p\.source\)\}"/);
-});
-
-test("admin approval is bound to the package revision shown in the review queue", async () => {
- const page = await source();
-
- assert.match(page, /data-version="\$\{esc\(p\.latestVersion\)\}"/);
- assert.match(page, /data-updated-at="\$\{esc\(p\.updatedAt\)\}"/);
- assert.match(page, /data-status="\$\{esc\(p\.status\)\}"/);
- assert.match(page, /expectedVersion: row\.dataset\.version/);
- assert.match(page, /expectedUpdatedAt: row\.dataset\.updatedAt/);
- assert.match(page, /expectedStatus: row\.dataset\.status/);
- assert.match(page, /if \(r\.status === 409\) loadReview\(\)/);
-});
diff --git a/site/src/scripts/theme.js b/site/src/scripts/theme.js
deleted file mode 100644
index 6e0b48bd8c..0000000000
--- a/site/src/scripts/theme.js
+++ /dev/null
@@ -1,54 +0,0 @@
-// Reasonix — shared three-state theme switch (system / light / dark).
-// The pre-paint inline script in each layout sets documentElement.dataset.theme
-// before first paint; this module wires the toggle buttons and keeps the
-// resolved theme in sync with OS changes while the preference is "system".
-const THEME_KEY = "reasonix-theme";
-const META_LIGHT = "#ffffff";
-const META_DARK = "#1f232b";
-
-export function currentThemePref() {
- try {
- const p = localStorage.getItem(THEME_KEY);
- return p === "light" || p === "dark" ? p : "system";
- } catch (e) {
- return "system";
- }
-}
-
-export function resolveTheme(pref) {
- return pref === "dark" ||
- (pref === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches)
- ? "dark"
- : "light";
-}
-
-export function applyTheme(pref) {
- const resolved = resolveTheme(pref);
- document.documentElement.dataset.theme = resolved;
- const meta = document.querySelector('meta[name="theme-color"]');
- if (meta) meta.content = resolved === "dark" ? META_DARK : META_LIGHT;
- document.querySelectorAll(".theme-switch button").forEach((b) => {
- const on = b.dataset.theme === pref;
- b.classList.toggle("active", on);
- if (on) b.setAttribute("aria-pressed", "true");
- else b.setAttribute("aria-pressed", "false");
- });
-}
-
-export function initTheme() {
- applyTheme(currentThemePref());
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
- const onOsChange = () => {
- const pref = currentThemePref();
- if (pref === "system") applyTheme(pref);
- };
- if (mq.addEventListener) mq.addEventListener("change", onOsChange);
- document.querySelectorAll(".theme-switch button").forEach((b) => {
- b.addEventListener("click", () => {
- try {
- localStorage.setItem(THEME_KEY, b.dataset.theme);
- } catch (e) {}
- applyTheme(b.dataset.theme);
- });
- });
-}
diff --git a/site/src/scripts/vscode-extension-contract.test.mjs b/site/src/scripts/vscode-extension-contract.test.mjs
deleted file mode 100644
index 0e1d052c36..0000000000
--- a/site/src/scripts/vscode-extension-contract.test.mjs
+++ /dev/null
@@ -1,28 +0,0 @@
-import assert from "node:assert/strict";
-import { readFile } from "node:fs/promises";
-import test from "node:test";
-
-const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
-
-test("homepage presents one VS Code extension through both registries", async () => {
- const page = await source("../pages/index.astro");
-
- assert.match(page, /data-pane="vscode"/);
- assert.match(page, /data-pane="desktop"[\s\S]*data-pane="npm"[\s\S]*data-pane="brew"[\s\S]*data-pane="vscode"/);
- assert.match(page, /Editor extension[\s\S]*编辑器扩展/);
- assert.match(page, /More ways to use Reasonix:[\s\S]*更多使用方式:/);
- assert.match(page, /Local Web UI:[\s\S]*reasonix serve[\s\S]*本地 Web UI:/);
- assert.match(page, /ACP editor integration[\s\S]*ACP 编辑器接入/);
- assert.match(page, /SivanLiu\.reasonix-agent/);
- assert.match(page, /marketplace\.visualstudio\.com\/items\?itemName=SivanLiu\.reasonix-agent/);
- assert.match(page, /open-vsx\.org\/extension\/SivanLiu\/reasonix-agent/);
- assert.match(page, /does not bundle the CLI/);
- assert.match(page, /data-goto="vscode"/);
-});
-
-test("mobile download channels use a two-column tab grid", async () => {
- const css = await source("../styles/global.css");
- const mobile = css.slice(css.indexOf("@media (max-width: 640px)"), css.indexOf("@media (max-width: 360px)"));
-
- assert.match(mobile, /\.dl-tabs \{\s*display: grid; grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
-});
diff --git a/site/src/styles/changelog.css b/site/src/styles/changelog.css
deleted file mode 100644
index 6e9eec003c..0000000000
--- a/site/src/styles/changelog.css
+++ /dev/null
@@ -1,443 +0,0 @@
-/* Palette unified with the global.css base tokens; --release-paper keeps the
- ledger identity. Dark flips for base tokens live in global.css. */
-:root {
- --release-backdrop: var(--bg-soft);
- --release-paper: #fbfaf7;
- --release-ink: var(--ink);
- --release-blue: var(--accent);
- --release-blue-soft: var(--accent-soft);
- --release-orange: #d97732;
- --release-green: #178b67;
-}
-
-:root[data-theme="dark"] {
- --release-paper: var(--card);
- --release-orange: oklch(0.76 0.13 60);
- --release-green: oklch(0.76 0.13 160);
-}
-
-body {
- background:
- linear-gradient(90deg, transparent 0 49.94%, color-mix(in oklch, var(--accent) 2.5%, transparent) 50%, transparent 50.06%),
- var(--release-backdrop);
-}
-
-.release-stage {
- min-height: 100vh;
- padding: 108px 34px 80px;
-}
-
-.release-grid {
- width: min(1440px, 100%);
- margin: 0 auto;
- display: grid;
- grid-template-columns: 220px minmax(0, 920px) 190px;
- gap: 30px;
- align-items: start;
- justify-content: center;
-}
-
-.release-rail,
-.release-toc {
- position: sticky;
- top: 96px;
- max-height: calc(100vh - 120px);
- overflow-y: auto;
- scrollbar-width: thin;
-}
-
-.release-rail__eyebrow {
- display: flex;
- align-items: center;
- gap: 9px;
- margin: 4px 0 18px 10px;
- color: var(--ink-2);
- font-size: 12px;
- font-weight: 700;
- letter-spacing: .08em;
- text-transform: uppercase;
-}
-
-.release-rail__mark {
- width: 10px;
- height: 10px;
- border: 2px solid var(--release-blue);
- border-radius: 3px 3px 3px 0;
- transform: rotate(-12deg);
-}
-
-.release-rail nav { display: grid; gap: 18px; }
-.release-rail__group { display: grid; gap: 7px; }
-.release-rail__minor {
- margin: 0 0 2px 10px;
- color: var(--ink-3);
- font-family: var(--font-mono);
- font-size: 10.5px;
- font-weight: 700;
- letter-spacing: .08em;
- text-transform: uppercase;
-}
-.release-rail__more summary {
- margin: 2px 10px 6px;
- color: var(--ink-3);
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
- list-style: none;
-}
-.release-rail__more summary::-webkit-details-marker { display: none; }
-.release-rail__more summary:hover { color: var(--release-blue); }
-.release-rail__more .when-open { display: none; }
-.release-rail__more[open] .when-open { display: inline; }
-.release-rail__more[open] .when-closed { display: none; }
-
-.release-version-link {
- display: grid;
- gap: 4px;
- padding: 12px 13px;
- border: 1px solid transparent;
- border-radius: 11px;
- color: var(--ink-2);
- text-decoration: none;
- transition: background .18s, border-color .18s, transform .18s;
-}
-
-.release-version-link:hover {
- transform: translateX(2px);
- border-color: var(--line);
- background: color-mix(in oklch, var(--card) 68%, transparent);
-}
-
-.release-version-link.active {
- border-color: color-mix(in srgb, var(--release-blue) 22%, transparent);
- background: var(--card);
- color: var(--release-blue);
- box-shadow: 0 8px 24px rgba(35, 55, 92, .08);
-}
-
-.release-version-link__line { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
-.release-version-link strong { font-family: var(--font-mono); font-size: 13px; }
-.release-version-link small { color: var(--ink-3); font-size: 11px; }
-.release-version-link em {
- padding: 2px 6px;
- border-radius: 999px;
- background: var(--release-blue-soft);
- color: var(--release-blue);
- font-size: 9px;
- font-style: normal;
- font-weight: 700;
- letter-spacing: .04em;
- text-transform: uppercase;
-}
-
-.release-rail__all {
- display: inline-block;
- margin: 18px 10px;
- color: var(--ink-3);
- font-size: 12px;
- font-weight: 600;
- text-decoration: none;
-}
-.release-rail__all:hover { color: var(--release-blue); }
-
-.release-paper {
- min-width: 0;
- overflow: hidden;
- border: 1px solid var(--line);
- border-radius: 20px;
- background:
- linear-gradient(color-mix(in oklch, var(--accent) 3.5%, transparent) 1px, transparent 1px) 0 0 / 100% 34px,
- var(--release-paper);
- box-shadow: 0 28px 80px -42px rgba(24, 35, 60, .38);
-}
-
-.release-hero {
- position: relative;
- padding: 48px 62px 44px;
- background:
- radial-gradient(circle at 90% 5%, color-mix(in oklch, var(--accent) 14%, transparent), transparent 28%),
- linear-gradient(180deg, color-mix(in oklch, var(--card) 88%, transparent), color-mix(in oklch, var(--release-paper) 92%, transparent));
-}
-
-.release-hero::after {
- content: "";
- position: absolute;
- left: 62px;
- right: 62px;
- bottom: 0;
- height: 1px;
- background: linear-gradient(90deg, var(--release-blue), var(--line) 36%, transparent);
-}
-
-.release-hero__meta { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
-.release-hero__meta time { font-family: var(--font-mono); color: var(--ink-3); font-size: 12px; }
-.release-channel {
- display: inline-flex;
- align-items: center;
- gap: 7px;
- color: var(--release-green);
- font-size: 11px;
- font-weight: 700;
- letter-spacing: .08em;
- text-transform: uppercase;
-}
-.release-channel__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 14%, transparent); }
-.release-hero h1 {
- max-width: 15em;
- margin: 28px 0 0;
- color: var(--release-ink);
- font-size: clamp(32px, 4vw, 46px);
- font-weight: 600;
- letter-spacing: -.032em;
- line-height: 1.08;
- text-wrap: balance;
-}
-
-.release-surfaces { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 24px; }
-.release-surfaces > span {
- padding: 5px 9px;
- border: 1px solid color-mix(in srgb, var(--release-blue) 18%, var(--line));
- border-radius: 999px;
- background: color-mix(in oklch, var(--card) 72%, transparent);
- color: var(--ink-2);
- font-size: 11px;
- font-weight: 600;
-}
-
-.release-lede { margin: 28px 0 0; max-width: 720px; color: var(--ink-2); font-size: 16.5px; line-height: 1.75; }
-
-.release-section { padding: 52px 62px 0; scroll-margin-top: 100px; }
-.release-section__head { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; }
-.release-section__head > span {
- display: grid;
- place-items: center;
- width: 30px;
- height: 30px;
- border: 1px solid color-mix(in srgb, var(--release-blue) 24%, var(--line));
- border-radius: 8px;
- color: var(--release-blue);
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 700;
-}
-.release-section h2 { color: var(--release-ink); font-size: 27px; font-weight: 650; letter-spacing: -.02em; }
-.release-section h3 { color: var(--release-ink); font-size: 17px; font-weight: 650; line-height: 1.35; }
-.release-section p { color: var(--ink-2); font-size: 15px; line-height: 1.68; }
-
-.release-guide-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
-.release-guide {
- position: relative;
- display: grid;
- align-content: start;
- min-height: 168px;
- padding: 22px;
- border: 1px solid var(--line);
- border-radius: 14px;
- background: color-mix(in oklch, var(--card) 72%, transparent);
- color: inherit;
- text-decoration: none;
- transition: transform .2s, border-color .2s, box-shadow .2s;
-}
-.release-guide:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--release-blue) 35%, var(--line)); box-shadow: 0 18px 35px -24px rgba(35, 65, 120, .4); }
-.release-guide__arrow { position: absolute; top: 18px; right: 18px; color: var(--release-blue); font-size: 18px; }
-.release-guide h3 { padding-right: 24px; }
-.release-guide p { margin-top: 12px; font-size: 13.5px; }
-
-.release-highlight-list { display: grid; gap: 12px; }
-.release-highlight {
- display: grid;
- grid-template-columns: 46px minmax(0, 1fr);
- gap: 18px;
- padding: 24px;
- border: 1px solid var(--line);
- border-radius: 14px;
- background: color-mix(in oklch, var(--card) 68%, transparent);
-}
-.release-highlight__index { color: color-mix(in srgb, var(--release-blue) 48%, var(--ink-3)); font-family: var(--font-mono); font-size: 12px; padding-top: 4px; }
-.release-highlight__kind { margin-bottom: 7px; color: var(--release-blue); font-size: 10px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; }
-.release-highlight[data-kind="security"] .release-highlight__kind { color: var(--release-orange); }
-.release-highlight[data-kind="fixed"] .release-highlight__kind { color: var(--release-green); }
-.release-highlight p { margin-top: 8px; }
-.release-refs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 13px; }
-.release-refs a {
- padding: 3px 7px;
- border-radius: 6px;
- background: var(--release-blue-soft);
- color: var(--release-blue);
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 600;
- text-decoration: none;
-}
-
-.release-change-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
-.release-change-list article {
- padding: 20px 22px;
- border-top: 2px solid color-mix(in srgb, var(--release-blue) 42%, var(--line));
- background: color-mix(in oklch, var(--card) 46%, transparent);
-}
-.release-change-list p { margin-top: 8px; font-size: 14px; }
-
-.release-notice-list { display: grid; gap: 10px; }
-.release-notice {
- display: grid;
- grid-template-columns: 30px minmax(0, 1fr);
- gap: 14px;
- padding: 18px 20px;
- border: 1px solid color-mix(in srgb, var(--release-blue) 22%, var(--line));
- border-radius: 12px;
- background: color-mix(in srgb, var(--release-blue-soft) 56%, var(--card));
-}
-.release-notice--warning { border-color: color-mix(in srgb, var(--release-orange) 32%, var(--line)); background: color-mix(in srgb, var(--release-orange) 12%, var(--card)); }
-.release-notice__icon { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: var(--release-blue); color: white; font-family: var(--font-mono); font-size: 12px; font-weight: 700; }
-.release-notice--warning .release-notice__icon { background: var(--release-orange); }
-.release-notice p { margin-top: 6px; font-size: 14px; }
-
-.release-thanks > p { margin-bottom: 14px; }
-.release-thanks > div { display: flex; flex-wrap: wrap; gap: 8px; }
-.release-thanks a { padding: 6px 10px; border: 1px solid var(--line); border-radius: 999px; background: color-mix(in oklch, var(--card) 70%, transparent); color: var(--ink-2); font-family: var(--font-mono); font-size: 11px; text-decoration: none; }
-.release-thanks a:hover { color: var(--release-blue); border-color: color-mix(in srgb, var(--release-blue) 34%, var(--line)); }
-
-.release-download {
- margin: 56px 62px 62px;
- padding: 30px 32px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 26px;
- border-radius: 16px;
- background: #20242d;
-}
-.release-download h2 { max-width: 480px; color: #fff; font-size: 28px; }
-.release-download__kicker { margin-bottom: 7px; color: #91b8ff !important; font-size: 11px !important; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
-.release-download__actions { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
-.release-compare { color: #c9d4e9; font-size: 13px; font-weight: 600; text-decoration: none; }
-.release-compare:hover { color: #fff; }
-
-.release-toc { padding: 6px 0 20px; }
-.release-toc > p { margin: 0 0 16px 12px; color: var(--release-ink); font-size: 13px; font-weight: 700; }
-.release-toc nav { display: grid; border-left: 1px solid var(--line); }
-.release-toc a {
- position: relative;
- padding: 7px 0 7px 13px;
- color: var(--ink-3);
- font-size: 12px;
- font-weight: 550;
- text-decoration: none;
- transition: color .16s;
-}
-.release-toc a::before { content: ""; position: absolute; left: -1px; top: 5px; bottom: 5px; width: 2px; border-radius: 4px; background: transparent; }
-.release-toc a:hover { color: var(--release-ink); }
-.release-toc a.active { color: var(--release-blue); }
-.release-toc a.active::before { background: var(--release-blue); }
-
-.release-mobile-switcher { display: none; }
-.release-footer {
- width: min(1180px, calc(100% - 64px));
- margin: 0 auto;
- padding: 28px 0 38px;
- display: flex;
- align-items: center;
- gap: 20px;
- border-top: 1px solid var(--line);
- color: var(--ink-3);
- font-size: 12px;
-}
-.release-footer > span { margin-left: auto; color: var(--ink-3); }
-.release-footer > a:last-child { color: var(--ink-2); text-decoration: none; }
-
-@media (max-width: 1180px) {
- .release-grid { grid-template-columns: 200px minmax(0, 850px); }
- .release-toc { display: none; }
-}
-
-@media (max-width: 840px) {
- .release-stage { padding: 92px 16px 48px; }
- .release-grid { display: block; }
- .release-rail { display: none; }
- .release-mobile-switcher {
- width: min(720px, 100%);
- margin: 0 auto 12px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- padding: 10px 12px;
- border: 1px solid var(--line);
- border-radius: 12px;
- background: var(--card);
- color: var(--ink-2);
- font-size: 12px;
- font-weight: 700;
- }
- .release-mobile-switcher select { min-width: 130px; padding: 7px 28px 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg-soft); color: var(--ink); font: inherit; }
- .release-paper { width: min(720px, 100%); margin: 0 auto; border-radius: 15px; }
- .release-hero { padding: 38px 28px 34px; }
- .release-hero::after { left: 28px; right: 28px; }
- .release-section { padding: 40px 28px 0; }
- .release-guide-grid, .release-change-list { grid-template-columns: 1fr; }
- .release-download { margin: 44px 28px 28px; padding: 26px; flex-direction: column; align-items: flex-start; }
-}
-
-@media (max-width: 560px) {
- .release-hero h1 { font-size: 34px; }
- .release-highlight { grid-template-columns: 1fr; gap: 8px; padding: 20px; }
- .release-highlight__index { display: none; }
- .release-section h2 { font-size: 23px; }
- .release-download__actions { align-items: flex-start; flex-direction: column; }
- .release-footer { width: calc(100% - 32px); flex-wrap: wrap; }
- .release-footer > span { order: 3; width: 100%; margin: 0; }
-}
-
-.release-channel-tabs {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 6px;
- margin: 14px 0 18px;
-}
-
-.release-channel-tabs a {
- border: 1px solid var(--line);
- border-radius: 999px;
- color: var(--ink-2);
- padding: 7px 10px;
- text-align: center;
- text-decoration: none;
- transition: background 0.2s, border-color 0.2s, color 0.2s;
-}
-
-.release-channel-tabs a:hover {
- border-color: color-mix(in oklch, var(--accent) 40%, var(--line));
- color: var(--accent-deep);
-}
-
-.release-channel-tabs a.active {
- background: var(--accent-soft);
- border-color: color-mix(in oklch, var(--accent) 46%, var(--line));
- color: var(--accent-deep);
-}
-
-.release-channel-tabs a:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
-}
-
-.release-builds {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 14px;
-}
-
-.release-builds span {
- border: 1px solid var(--line);
- border-radius: 999px;
- color: var(--ink-2);
- font-family: var(--mono);
- font-size: 0.78rem;
- padding: 6px 10px;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .release-version-link, .release-guide, .release-channel-tabs a { transition: none; }
-}
diff --git a/site/src/styles/community.css b/site/src/styles/community.css
deleted file mode 100644
index ac9f4c19c0..0000000000
--- a/site/src/styles/community.css
+++ /dev/null
@@ -1,271 +0,0 @@
-/* Reasonix Community — self-hosted fonts + the reasonix.io token values, so the
- forum shares the site's visual language without depending on its marketing CSS. */
-@import "@fontsource-variable/inter";
-@import "@fontsource-variable/jetbrains-mono";
-
-:root {
- color-scheme: light;
- --accent: oklch(0.55 0.17 257);
- --accent-deep: oklch(0.48 0.17 257);
- --accent-soft: color-mix(in oklch, var(--accent) 8%, var(--bg));
- --accent-line: color-mix(in oklch, var(--accent) 26%, transparent);
- --bg: #ffffff;
- --bg-soft: oklch(0.977 0.0025 247);
- --card: #ffffff;
- --ink: oklch(0.21 0.006 260);
- --ink-2: oklch(0.45 0.008 260);
- --ink-3: oklch(0.6 0.008 260);
- --line: oklch(0.905 0.004 247);
- --ok: oklch(0.6 0.14 155);
- --ok-soft: color-mix(in oklch, var(--ok) 12%, var(--bg));
- --err: oklch(0.5 0.19 25);
- --warm: oklch(0.72 0.13 66);
- --violet: oklch(0.58 0.17 300);
- --rose: oklch(0.62 0.19 15);
- --radius: 13px;
- --radius-sm: 9px;
- --shadow-1: 0 1px 2px oklch(0.4 0.02 260 / 0.08);
- --shadow-2: 0 1px 2px oklch(0.4 0.02 260 / 0.06), 0 12px 32px -16px oklch(0.4 0.02 260 / 0.24);
- --font-sans: "Inter Variable", "Inter", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
- --font-mono: "JetBrains Mono Variable", "SF Mono", Menlo, monospace;
- --ease: cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-:root[data-theme="dark"] {
- color-scheme: dark;
- --accent: oklch(0.66 0.16 257);
- --accent-deep: oklch(0.73 0.14 257);
- --accent-soft: color-mix(in oklch, var(--accent) 16%, var(--bg));
- --bg: oklch(0.19 0.006 260);
- --bg-soft: oklch(0.225 0.008 260);
- --card: oklch(0.222 0.008 260);
- --ink: oklch(0.93 0.004 260);
- --ink-2: oklch(0.78 0.008 260);
- --ink-3: oklch(0.63 0.008 260);
- --line: oklch(0.31 0.008 260);
- --ok-soft: color-mix(in oklch, var(--ok) 18%, var(--bg));
- --shadow-1: 0 1px 2px oklch(0 0 0 / 0.3);
- --shadow-2: 0 1px 2px oklch(0 0 0 / 0.28), 0 12px 32px -16px oklch(0 0 0 / 0.6);
-}
-
-* { margin: 0; padding: 0; box-sizing: border-box; }
-html { scroll-behavior: smooth; }
-body { font-family: var(--font-sans); background: var(--bg); color: var(--ink); -webkit-font-smoothing: antialiased; line-height: 1.5; }
-a { color: inherit; text-decoration: none; }
-::selection { background: oklch(0.88 0.05 257); }
-.wrap { max-width: 1180px; margin: 0 auto; padding: 0 32px; }
-[hidden] { display: none !important; }
-
-/* bilingual toggle — mirrors the main site (shared reasonix-lang choice) */
-body[data-lang="en"] .l-zh { display: none !important; }
-body[data-lang="zh"] .l-en { display: none !important; }
-.lang-switch, .theme-switch { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg-soft); border-radius: 9px; }
-.lang-switch button, .theme-switch button { font-family: var(--font-sans); font-size: 12.5px; font-weight: 500; color: var(--ink-2); background: transparent; border: none; cursor: pointer; padding: 6px 12px; border-radius: 6px; transition: background 0.2s, color 0.2s, box-shadow 0.2s; }
-.lang-switch button:hover, .theme-switch button:hover { color: var(--ink); }
-.lang-switch button.active, .theme-switch button.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
-.theme-switch button { display: inline-flex; align-items: center; justify-content: center; }
-.theme-switch svg { display: block; }
-
-.nav { position: sticky; top: 0; z-index: 50; background: color-mix(in oklch, var(--bg) 86%, transparent); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid var(--line); }
-.nav-inner { display: flex; align-items: center; gap: 20px; height: 64px; max-width: 1180px; margin: 0 auto; padding: 0 32px; }
-.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 17px; letter-spacing: -0.01em; }
-.brand .mark { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: linear-gradient(140deg, var(--accent), var(--violet)); color: #fff; font-weight: 700; font-size: 15px; }
-.brand .ctag { font-size: 12px; font-weight: 600; color: var(--accent); background: var(--accent-soft); padding: 3px 9px; border-radius: 999px; }
-.nav-links { display: flex; gap: 4px; margin-left: 12px; }
-.nav-links a { color: var(--ink-2); font-size: 14.5px; font-weight: 500; padding: 8px 14px; border-radius: 7px; transition: color 0.2s, background 0.2s; }
-.nav-links a:hover { color: var(--ink); background: var(--bg-soft); }
-.nav-links a.here { color: var(--accent); background: var(--accent-soft); }
-.nav-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
-.nav-menu-toggle {
- display: none; flex: none; align-items: center; justify-content: center;
- width: 38px; height: 38px; padding: 0; color: var(--ink);
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px; cursor: pointer;
- transition: color 0.2s, background 0.2s, border-color 0.2s;
-}
-.nav-menu-toggle:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
-.nav-menu-toggle:focus-visible, .mobile-nav-close:focus-visible, .mobile-nav-backdrop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-.nav-menu-toggle svg, .mobile-nav-close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
-body.mobile-nav-open { overflow: hidden; }
-.mobile-nav { position: fixed; inset: 0; z-index: 100; height: 100vh; min-height: 100dvh; opacity: 0; transition: opacity 0.22s ease; }
-.mobile-nav.is-open { opacity: 1; }
-.mobile-nav-backdrop { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: oklch(0.12 0.01 260 / 0.42); cursor: pointer; }
-.mobile-nav-panel { position: absolute; top: 0; right: 0; display: flex; flex-direction: column; width: min(360px, calc(100% - 24px)); height: 100%; padding: 24px; color: var(--ink); background: var(--bg); border-left: 1px solid var(--line); box-shadow: -18px 0 40px oklch(0.12 0.01 260 / 0.16); transform: translateX(100%); transition: transform 0.28s var(--ease); overflow-y: auto; }
-.mobile-nav.is-open .mobile-nav-panel { transform: translateX(0); }
-.mobile-nav-head { display: flex; align-items: center; justify-content: space-between; min-height: 38px; font-size: 15px; font-weight: 600; }
-.mobile-nav-close { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; color: var(--ink-2); background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px; cursor: pointer; }
-.mobile-nav-close:hover { color: var(--accent); border-color: var(--accent-line); }
-.mobile-nav-links { display: flex; flex-direction: column; gap: 4px; margin-top: 26px; }
-.mobile-nav-links a { display: flex; align-items: center; min-height: 44px; padding: 10px 12px; color: var(--ink-2); font-size: 16px; font-weight: 500; border-radius: 8px; }
-.mobile-nav-links a:hover, .mobile-nav-links a.here { color: var(--accent); background: var(--accent-soft); }
-.mobile-nav-actions { display: flex; flex-direction: column; gap: 10px; margin-top: auto; padding-top: 28px; }
-.mobile-nav-actions > a, .mobile-nav-actions > span { width: 100%; }
-.mobile-nav-actions > span .btn { width: 100%; }
-.mobile-nav-actions .btn { min-height: 44px; }
-.mobile-nav-github { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; color: var(--ink-2); font-size: 14px; font-weight: 500; border: 1px solid var(--line); border-radius: 9px; }
-.mobile-nav-github:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
-.mobile-nav-github svg { width: 18px; height: 18px; }
-@media (prefers-reduced-motion: reduce) { .mobile-nav, .mobile-nav-panel { transition: none; } }
-.nav-search { display: flex; align-items: center; gap: 8px; background: var(--bg-soft); border: 1px solid transparent; border-radius: 9px; padding: 8px 14px; width: 220px; transition: border-color 0.2s, background 0.2s, box-shadow 0.2s; }
-.nav-search:focus-within { background: var(--card); border-color: var(--accent-line); box-shadow: 0 0 0 4px var(--accent-soft); }
-.nav-search input { border: none; outline: none; background: none; font-family: var(--font-sans); font-size: 14px; color: var(--ink); width: 100%; }
-.nav-search .ico { color: var(--ink-3); }
-
-.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-family: var(--font-sans); font-size: 14.5px; font-weight: 600; white-space: nowrap; padding: 10px 20px; border-radius: 9px; border: none; cursor: pointer; transition: background 0.2s, box-shadow 0.25s, color 0.2s, border-color 0.2s, transform 0.15s var(--ease); }
-.btn:active { transform: translateY(1px); }
-.btn-primary { background: var(--accent); color: #fff; }
-.btn-primary:hover { background: var(--accent-deep); box-shadow: var(--shadow-1); }
-.btn-ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }
-.btn-ghost:hover { background: var(--accent-soft); border-color: transparent; }
-.btn.sm { padding: 7px 14px; font-size: 13px; }
-.btn[disabled] { opacity: 0.55; pointer-events: none; }
-.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-
-.av { flex: none; display: grid; place-items: center; border-radius: 50%; width: 40px; height: 40px; font-size: 14px; font-weight: 600; color: #fff; border: 2px solid var(--bg); box-shadow: var(--shadow-1); overflow: hidden; }
-.av.sm { width: 28px; height: 28px; font-size: 11px; border-width: 1.5px; }
-.av.lg { width: 52px; height: 52px; font-size: 18px; }
-
-.badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 3px 9px; border-radius: 999px; }
-.badge.solved { color: var(--ok); background: var(--ok-soft); }
-.badge.pinned { color: var(--accent); background: var(--accent-soft); }
-.badge.role { color: var(--ink-3); background: var(--bg-soft); box-shadow: inset 0 0 0 1px var(--line); }
-.badge.role.staff, .badge.role.admin, .badge.role.moderator { color: var(--accent); background: var(--accent-soft); box-shadow: none; }
-.tag { font-size: 12px; font-weight: 500; color: var(--ink-2); background: var(--bg-soft); padding: 4px 11px; border-radius: 999px; transition: color 0.2s, background 0.2s; }
-a.tag:hover { color: var(--accent); background: var(--accent-soft); }
-.cat-tag { font-size: 11.5px; font-weight: 600; padding: 2px 9px; border-radius: 999px; color: var(--accent); background: var(--accent-soft); }
-
-.head { padding: 40px 0 28px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, var(--accent-soft), var(--bg) 76%); }
-.head .row { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
-.head .eyebrow { font-size: 13px; font-weight: 600; color: var(--accent); }
-.head h1 { font-size: clamp(30px, 4vw, 42px); font-weight: 600; letter-spacing: -0.02em; margin-top: 8px; }
-.head p { margin-top: 10px; font-size: 16px; color: var(--ink-2); max-width: 46ch; }
-
-.layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 32px; padding: 32px 0 80px; align-items: start; }
-.side { position: sticky; top: 88px; display: flex; flex-direction: column; gap: 18px; }
-.sec-title { display: flex; align-items: center; justify-content: space-between; margin: 4px 2px 14px; }
-.sec-title h2 { font-size: 15px; font-weight: 600; }
-.tabs { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg-soft); border-radius: 9px; }
-.tabs button { font-family: var(--font-sans); font-size: 13.5px; font-weight: 500; color: var(--ink-2); background: none; border: none; cursor: pointer; padding: 7px 15px; border-radius: 6px; transition: background 0.2s, color 0.2s, box-shadow 0.2s; }
-.tabs button.on { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
-.tabs button:hover:not(.on) { color: var(--ink); }
-
-.cats { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 34px; }
-.cat { display: flex; gap: 14px; align-items: flex-start; background: var(--bg-soft); border-radius: var(--radius-sm); padding: 18px; transition: box-shadow 0.3s, transform 0.3s var(--ease), background 0.3s; }
-.cat:hover { background: var(--card); box-shadow: var(--shadow-2); transform: translateY(-2px); }
-.cat .ico { width: 42px; height: 42px; border-radius: 12px; flex: none; display: grid; place-items: center; font-size: 20px; background: var(--accent-soft); }
-.cat h3 { font-size: 15.5px; font-weight: 600; }
-.cat p { font-size: 13px; color: var(--ink-2); margin-top: 3px; }
-.cat .meta { font-size: 12px; color: var(--ink-3); font-family: var(--font-mono); margin-top: 9px; }
-
-.topics { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--card); }
-.topic { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 16px; padding: 16px 20px; border-bottom: 1px solid var(--line); transition: background 0.18s; }
-.topic:last-child { border-bottom: none; }
-.topic:hover { background: var(--bg-soft); }
-.topic .main { min-width: 0; }
-.topic .title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
-.topic .title a { font-size: 15.5px; font-weight: 600; color: var(--ink); }
-.topic:hover .title a { color: var(--accent); }
-.topic .sub { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12.5px; color: var(--ink-3); flex-wrap: wrap; }
-.topic .sub .who { color: var(--ink-2); font-weight: 500; }
-.topic .stat { text-align: center; min-width: 52px; }
-.topic .stat .n { font-size: 15px; font-weight: 600; font-family: var(--font-mono); }
-.topic .stat .l { font-size: 11px; color: var(--ink-3); }
-.topic .last { min-width: 96px; text-align: right; font-size: 12px; color: var(--ink-3); }
-
-.card { background: var(--bg-soft); border-radius: var(--radius-sm); padding: 20px; }
-.card.cta { background: linear-gradient(160deg, var(--accent-soft), var(--bg) 82%); border: 1px solid var(--accent-line); }
-.card h4 { font-size: 14.5px; font-weight: 600; margin-bottom: 6px; }
-.card .lead { font-size: 13px; color: var(--ink-2); line-height: 1.55; margin-bottom: 14px; }
-.card .btn { width: 100%; }
-.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
-.stat-box .n { font-size: 22px; font-weight: 700; font-family: var(--font-mono); letter-spacing: -0.02em; }
-.stat-box .n .dot { color: var(--ok); }
-.stat-box .l { font-size: 12px; color: var(--ink-3); }
-.tagcloud { display: flex; flex-wrap: wrap; gap: 8px; }
-
-.empty { padding: 40px 20px; text-align: center; color: var(--ink-3); font-size: 14px; }
-.skeleton { padding: 16px 20px; border-bottom: 1px solid var(--line); }
-.skeleton .bar { height: 12px; border-radius: 6px; background: linear-gradient(90deg, var(--bg-soft), var(--line), var(--bg-soft)); background-size: 200% 100%; animation: sk 1.3s linear infinite; }
-.skeleton .bar.short { width: 40%; margin-top: 8px; }
-@keyframes sk { to { background-position: -200% 0; } }
-.msg { margin: 16px 0; font-size: 13.5px; border-radius: var(--radius-sm); padding: 11px 14px; }
-.msg.error { color: var(--err); background: color-mix(in oklch, var(--err) 9%, var(--bg)); }
-.msg.ok { color: var(--ok); background: var(--ok-soft); }
-
-/* thread */
-.crumb { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-3); margin-bottom: 14px; }
-.crumb a:hover { color: var(--accent); }
-.crumb .sep { opacity: 0.5; }
-.thread-head h1 { font-size: clamp(24px, 3vw, 32px); font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; }
-.thread-head .meta { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; font-size: 13px; color: var(--ink-3); }
-.thread-layout { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 32px; padding: 24px 0 80px; align-items: start; }
-.post { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 16px; padding: 24px 0; border-top: 1px solid var(--line); }
-.post:first-child { border-top: none; }
-.post.op { background: linear-gradient(180deg, var(--accent-soft), transparent 40%); border-radius: var(--radius); padding: 24px; margin: 0 -24px 4px; }
-.post.answer { background: var(--ok-soft); border: 1px solid color-mix(in oklch, var(--ok) 28%, transparent); border-radius: var(--radius); padding: 22px 24px; margin: 8px -24px; }
-.post .who { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
-.post .who .name { font-size: 14.5px; font-weight: 600; }
-.post .who .when { font-size: 12px; color: var(--ink-3); }
-.post .answer-flag { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; color: var(--ok); margin-bottom: 10px; }
-.post .body { font-size: 15px; color: var(--ink); line-height: 1.7; overflow-wrap: anywhere; }
-.post .body p { margin: 0 0 12px; }
-.post .body p:last-child { margin-bottom: 0; }
-.post .body code { font-family: var(--font-mono); font-size: 13px; background: var(--bg-soft); padding: 2px 7px; border-radius: 6px; }
-.post .body pre { background: oklch(0.25 0.006 260); color: oklch(0.9 0.004 260); border-radius: var(--radius-sm); padding: 16px 18px; overflow-x: auto; font-family: var(--font-mono); font-size: 13px; line-height: 1.7; margin: 4px 0 12px; }
-.post .actions { display: flex; align-items: center; gap: 6px; margin-top: 16px; }
-.react { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-sans); font-size: 13px; font-weight: 500; color: var(--ink-2); background: var(--bg-soft); border: 1px solid transparent; border-radius: 7px; padding: 6px 13px; cursor: pointer; transition: background 0.2s, border-color 0.2s, color 0.2s; }
-.react:hover { border-color: var(--accent-line); color: var(--accent); }
-.react.on { background: var(--accent-soft); color: var(--accent); }
-.link-act { font-size: 13px; font-weight: 500; color: var(--ink-3); padding: 6px 10px; border-radius: 7px; cursor: pointer; background: none; border: none; font-family: var(--font-sans); }
-.link-act:hover { color: var(--accent); background: var(--accent-soft); }
-
-.composer { border: 1px solid var(--line); border-radius: var(--radius); padding: 8px; margin-top: 24px; background: var(--card); transition: border-color 0.2s, box-shadow 0.2s; }
-.composer:focus-within { border-color: var(--accent-line); box-shadow: 0 0 0 4px var(--accent-soft); }
-.composer input.title { width: 100%; border: none; outline: none; padding: 12px 14px 4px; font-family: var(--font-sans); font-size: 18px; font-weight: 600; color: var(--ink); background: none; }
-.composer textarea { width: 100%; border: none; outline: none; resize: vertical; min-height: 96px; padding: 12px 14px; font-family: var(--font-sans); font-size: 15px; color: var(--ink); background: none; line-height: 1.6; }
-.composer .foot { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px 6px; gap: 12px; flex-wrap: wrap; }
-.composer .foot .hint { font-size: 12px; color: var(--ink-3); }
-.composer select { font-family: var(--font-sans); font-size: 13px; color: var(--ink-2); background: var(--bg-soft); border: none; border-radius: 7px; padding: 8px 12px; cursor: pointer; }
-.gate { text-align: center; padding: 22px; }
-.gate p { font-size: 14px; color: var(--ink-2); margin-bottom: 14px; }
-
-.side-card { background: var(--bg-soft); border-radius: var(--radius-sm); padding: 18px 20px; }
-.side-card h4 { font-size: 13px; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 12px; }
-.parti { display: flex; }
-.parti .av { margin-left: -8px; }
-.parti .av:first-child { margin-left: 0; }
-
-.foot { border-top: 1px solid var(--line); }
-.foot-inner { max-width: 1180px; margin: 0 auto; padding: 28px 32px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; font-size: 13.5px; color: var(--ink-3); }
-.foot-inner .links { display: flex; gap: 4px; margin-left: auto; flex-wrap: wrap; }
-.foot-inner .links a { padding: 6px 12px; border-radius: 7px; color: var(--ink-2); font-weight: 500; transition: background 0.2s, color 0.2s; }
-.foot-inner .links a:hover { background: var(--bg-soft); color: var(--ink); }
-
-@media (max-width: 940px) {
- .layout, .thread-layout { grid-template-columns: 1fr; }
- .side { position: static; }
- .cats { grid-template-columns: 1fr; }
- .nav-links, .nav-search { display: none; }
- .nav-menu-toggle { display: inline-flex; }
- .topic { grid-template-columns: auto minmax(0, 1fr); }
- .topic .stat, .topic .last { display: none; }
- .wrap { padding: 0 20px; }
-}
-
-@media (max-width: 640px) {
- /* drop the Community tag and tighten the shared controls */
- .nav-inner { padding: 0 16px; gap: 10px; }
- .brand .ctag { display: none; }
- #nav-account { display: none; }
- .theme-switch button { padding: 6px 9px; }
-}
-
-@media (max-width: 440px) {
- /* Include the async account control in the narrow-header budget. */
- .nav-inner { padding: 0 12px; gap: 8px; }
- .nav .brand { gap: 0; }
- .nav .brand-name { display: none; }
- .nav-right { gap: 8px; }
- .lang-switch button { padding: 6px 8px; }
- .theme-switch button { padding: 6px 7px; }
- #nav-account .btn { padding: 7px 10px; }
-}
diff --git a/site/src/styles/global.css b/site/src/styles/global.css
deleted file mode 100644
index 9c5d8925e5..0000000000
--- a/site/src/styles/global.css
+++ /dev/null
@@ -1,1422 +0,0 @@
-
-@import '@fontsource-variable/inter';
-@import '@fontsource-variable/jetbrains-mono';
-
-:root {
- color-scheme: light;
- --acc-1: #4f9dff;
- --acc-2: #7a6bff;
- --acc-3: #c46bff;
- --accent: oklch(0.55 0.17 257);
- --accent-deep: oklch(0.48 0.17 257);
- --accent-line: color-mix(in oklch, var(--accent) 30%, var(--line));
- --accent-soft: color-mix(in oklch, var(--accent) 8%, var(--bg));
- --bg: #ffffff;
- --bg-soft: oklch(0.976 0.0025 247);
- --ink: oklch(0.21 0.006 260);
- --ink-2: oklch(0.45 0.008 260);
- --ink-3: oklch(0.6 0.008 260);
- --line: oklch(0.905 0.004 247);
- --card: #ffffff;
- --term-bg: oklch(0.21 0.006 260);
- --term-edge: oklch(0.31 0.008 260);
- --radius: 13px;
- --radius-sm: 9px;
- --shadow-1: 0 1px 2px oklch(0.35 0.01 260 / 0.08);
- --shadow-2: 0 1px 2px oklch(0.35 0.01 260 / 0.06), 0 12px 32px -16px oklch(0.35 0.01 260 / 0.24);
- --font-sans: "Inter Variable", "Inter", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
- --font-mono: "JetBrains Mono Variable", "JetBrains Mono", "SF Mono", Menlo, monospace;
- --ease: cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-:root[data-theme="dark"] {
- color-scheme: dark;
- --accent: oklch(0.66 0.16 257);
- --accent-deep: oklch(0.73 0.14 257);
- --accent-soft: color-mix(in oklch, var(--accent) 16%, var(--bg));
- --bg: oklch(0.19 0.006 260);
- --bg-soft: oklch(0.225 0.008 260);
- --ink: oklch(0.93 0.004 260);
- --ink-2: oklch(0.78 0.008 260);
- --ink-3: oklch(0.63 0.008 260);
- --line: oklch(0.31 0.008 260);
- --card: oklch(0.222 0.008 260);
- --term-bg: oklch(0.155 0.005 260);
- --term-edge: oklch(0.28 0.008 260);
- --shadow-1: 0 1px 2px oklch(0 0 0 / 0.3);
- --shadow-2: 0 1px 2px oklch(0 0 0 / 0.28), 0 12px 32px -16px oklch(0 0 0 / 0.6);
-}
-
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-[hidden] { display: none !important; }
-
-html { scroll-behavior: smooth; background: var(--bg); }
-
-body {
- font-family: var(--font-sans);
- font-optical-sizing: auto;
- font-feature-settings: "cv11", "ss01";
- background: var(--bg);
- color: var(--ink);
- -webkit-font-smoothing: antialiased;
- overflow-x: hidden;
-}
-
-::selection { background: oklch(0.88 0.05 257); }
-
-.wrap { max-width: 1180px; margin: 0 auto; padding: 0 40px; }
-
-
-body[data-lang="en"] .l-zh { display: none !important; }
-body[data-lang="zh"] .l-en { display: none !important; }
-body[data-lang="zh"] h1,
-body[data-lang="zh"] h2,
-body[data-lang="zh"] h3 { letter-spacing: 0; }
-body[data-lang="zh"] .hero h1 { font-size: clamp(38px, 4.6vw, 62px); line-height: 1.24; }
-
-.lang-switch, .theme-switch {
- display: inline-flex; gap: 2px; padding: 3px;
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px;
-}
-.lang-switch button, .theme-switch button {
- font-family: var(--font-sans); font-size: 12.5px; font-weight: 500;
- color: var(--ink-2); background: transparent; border: none; cursor: pointer;
- padding: 6px 12px; border-radius: 6px;
- transition: background 0.2s, color 0.2s, box-shadow 0.2s;
-}
-.theme-switch button { display: inline-flex; align-items: center; justify-content: center; }
-.theme-switch svg { display: block; }
-.lang-switch button:hover, .theme-switch button:hover { color: var(--ink); }
-.lang-switch button.active, .theme-switch button.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
-
-
-.nav {
- position: fixed; top: 0; left: 0; right: 0; z-index: 50;
- transition: background 0.4s, box-shadow 0.4s;
-}
-.nav.scrolled {
- background: color-mix(in oklch, var(--bg) 92%, transparent);
- backdrop-filter: blur(16px);
- -webkit-backdrop-filter: blur(16px);
- box-shadow: 0 1px 0 var(--line);
-}
-.nav-inner {
- display: flex; align-items: center; gap: 18px;
- height: 68px; max-width: 1480px; margin: 0 auto; padding: 0 40px;
-}
-.brand { display: flex; flex: none; align-items: center; gap: 10px; text-decoration: none; color: var(--ink); }
-.brand-picture { display: block; flex: none; }
-.brand img { width: 28px; height: 28px; border-radius: 8px; display: block; }
-.nav .brand img { width: 90px; height: auto; border-radius: 0; }
-.brand span { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
-.nav-links { display: flex; min-width: 0; gap: 6px; margin-right: auto; margin-left: 18px; }
-.nav-links a {
- flex: none; white-space: nowrap;
- color: var(--ink-2); text-decoration: none; font-size: 14px; font-weight: 500;
- padding: 8px 12px; border-radius: 7px;
- transition: color 0.2s, background 0.2s;
-}
-.nav-links a:hover { color: var(--ink); background: var(--bg-soft); }
-.nav-links a.here { color: var(--accent); background: var(--accent-soft); }
-.nav-github {
- display: inline-flex; flex: none; align-items: center; gap: 8px;
- color: var(--ink-2); text-decoration: none; font-size: 14px; font-weight: 500;
- padding: 8px 10px; border-radius: 7px;
- transition: color 0.2s, background 0.2s;
-}
-.nav-github svg { width: 18px; height: 18px; display: block; }
-.nav-github:hover { color: var(--ink); background: var(--bg-soft); }
-.nav-github:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-
-.btn {
- display: inline-flex; align-items: center; justify-content: center; gap: 8px;
- font-family: var(--font-sans); font-size: 14.5px; font-weight: 550; letter-spacing: -0.005em;
- padding: 11px 20px; border-radius: 9px; text-decoration: none; cursor: pointer;
- border: none; transition: background 0.2s, box-shadow 0.25s, color 0.2s, border-color 0.2s;
-}
-.btn-dark { background: var(--accent); color: #fff; }
-.btn-dark:hover { background: var(--accent-deep); }
-.btn-ghost { background: transparent; color: var(--ink); border: 1px solid var(--line); }
-.btn-ghost:hover { border-color: color-mix(in oklch, var(--accent) 45%, var(--line)); color: var(--accent); }
-.nav .btn { padding: 9px 20px; font-size: 14px; }
-.nav .btn, .nav .lang-switch, .nav .theme-switch, .nav-acct { flex: none; }
-
-.nav-menu-toggle {
- display: none; flex: none; align-items: center; justify-content: center;
- width: 38px; height: 38px; padding: 0; color: var(--ink);
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px;
- cursor: pointer; transition: color 0.2s, background 0.2s, border-color 0.2s;
-}
-.nav-menu-toggle:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
-.nav-menu-toggle:focus-visible, .mobile-nav-close:focus-visible, .mobile-nav-backdrop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-.nav-menu-toggle svg, .mobile-nav-close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
-body.mobile-nav-open { overflow: hidden; }
-.mobile-nav { position: fixed; inset: 0; z-index: 100; height: 100vh; min-height: 100dvh; opacity: 0; transition: opacity 0.22s ease; }
-.mobile-nav.is-open { opacity: 1; }
-.mobile-nav-backdrop { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: oklch(0.12 0.01 260 / 0.42); cursor: pointer; }
-.mobile-nav-panel {
- position: absolute; top: 0; right: 0; display: flex; flex-direction: column;
- width: min(360px, calc(100% - 24px)); height: 100%; padding: 24px;
- color: var(--ink); background: var(--bg); border-left: 1px solid var(--line);
- box-shadow: -18px 0 40px oklch(0.12 0.01 260 / 0.16);
- transform: translateX(100%); transition: transform 0.28s var(--ease); overflow-y: auto;
-}
-.mobile-nav.is-open .mobile-nav-panel { transform: translateX(0); }
-.mobile-nav-head { display: flex; align-items: center; justify-content: space-between; min-height: 38px; font-size: 15px; font-weight: 600; }
-.mobile-nav-close { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; color: var(--ink-2); background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px; cursor: pointer; }
-.mobile-nav-close:hover { color: var(--accent); border-color: var(--accent-line); }
-.mobile-nav-links { display: flex; flex-direction: column; gap: 4px; margin-top: 26px; }
-.mobile-nav-links a { display: flex; align-items: center; min-height: 44px; padding: 10px 12px; color: var(--ink-2); text-decoration: none; font-size: 16px; font-weight: 500; border-radius: 8px; }
-.mobile-nav-links a:hover, .mobile-nav-links a.here { color: var(--accent); background: var(--accent-soft); }
-.mobile-nav-actions { display: flex; flex-direction: column; gap: 10px; margin-top: auto; padding-top: 28px; }
-.mobile-nav-actions > a, .mobile-nav-actions > .nav-acct, .mobile-nav-actions > span { width: 100%; }
-.mobile-nav-actions > span .btn, .mobile-nav-actions > .nav-acct .btn { width: 100%; }
-.mobile-nav-actions .btn { min-height: 44px; }
-.mobile-nav-github { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; color: var(--ink-2); font-size: 14px; font-weight: 500; border: 1px solid var(--line); border-radius: 9px; }
-.mobile-nav-github:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
-.mobile-nav-github svg { width: 18px; height: 18px; }
-
-@media (prefers-reduced-motion: reduce) {
- .mobile-nav, .mobile-nav-panel { transition: none; }
-}
-
-@media (max-width: 1260px) {
- .nav-link--secondary { display: none; }
-}
-
-@media (max-width: 1100px) {
- .nav-links { display: none; }
- .nav-github { margin-left: auto; padding-inline: 8px; }
- .nav-github-label { display: none; }
- .nav-menu-toggle { display: inline-flex; }
-}
-
-
-.hero { position: relative; padding: 150px 0 0; text-align: center; }
-
-.eyebrow {
- display: inline-flex; align-items: center; gap: 9px;
- font-family: var(--font-mono); font-size: 12.5px; font-weight: 500;
- color: var(--accent); margin-bottom: 26px; letter-spacing: 0.01em;
-}
-.eyebrow .dot { width: 7px; height: 7px; border-radius: 50%; background: oklch(0.7 0.16 150); box-shadow: 0 0 0 3px oklch(0.7 0.16 150 / 0.18); }
-
-.hero h1 {
- max-width: 13em; margin: 0 auto;
- font-size: clamp(44px, 5.4vw, 76px);
- line-height: 1.06; letter-spacing: -0.035em; font-weight: 600;
- text-wrap: balance;
-}
-.hero h1 .grad { color: var(--accent); }
-.hero h1 em { font-style: italic; color: var(--accent); }
-.hero h1 .tt {
- color: var(--accent);
- box-shadow: inset 0 -0.06em 0 color-mix(in oklch, var(--accent) 30%, transparent);
-}
-.hero .sub {
- margin: 26px auto 0; max-width: 640px;
- font-size: 17.5px; line-height: 1.62; color: var(--ink-2); font-weight: 400;
- text-wrap: pretty;
-}
-.hero .sub b { color: var(--ink); font-weight: 600; }
-
-
-.hero-install {
- width: min(960px, 100%);
- margin: 56px auto 0;
- display: grid;
- grid-template-columns: 1fr 1px 1fr;
- gap: 0;
- background: #f6f7f9;
- border: none;
- border-radius: var(--radius);
- overflow: hidden;
-}
-
-
-:root[data-theme="dark"] .hero-install {
- background: var(--bg-soft);
-}
-
-
-.hero-install::before {
- content: "";
- grid-column: 2;
- grid-row: 1;
- background: var(--line);
- align-self: stretch;
- margin: 32px 0;
- opacity: 0.7;
-}
-
-.install-option {
- position: relative;
- display: flex;
- flex-direction: column;
- gap: 10px;
- min-height: 200px;
- padding: 36px 44px;
- background: transparent;
- border: none;
- border-radius: 0;
- overflow: hidden;
- cursor: default;
-}
-
-
-.install-option--desktop {
- background: transparent;
- border-color: transparent;
-}
-
-
-.install-option canvas.particle-canvas {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- z-index: 0;
-}
-
-.install-option > * {
- position: relative;
- z-index: 1;
-}
-
-.install-kicker {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 12.5px;
- font-weight: 500;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--ink-2);
-}
-
-.install-kicker::before {
- content: "";
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: var(--accent);
-}
-
-.install-option strong {
- font-size: 15px;
- line-height: 1.5;
- font-weight: 600;
- color: var(--ink);
- max-width: 340px;
-}
-
-
-.install-option .btn,
-.install-option .install {
- margin-top: auto;
- padding-top: 20px;
- align-self: stretch;
- width: 100%;
- height: 40px;
- box-sizing: border-box;
-}
-
-
-.hero-install .btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 0 20px;
- font-size: 14px;
- font-weight: 600;
- color: #fff;
- background: var(--accent);
- border: none;
- border-radius: 8px;
- text-decoration: none;
- transition: background 0.2s;
-}
-
-.hero-install .btn:hover {
- background: var(--accent-deep);
-}
-
-
-.hero-install .install {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 0 6px 0 14px;
- background: var(--card);
- border: 1px solid var(--line);
- border-radius: 8px;
- font-family: var(--font-mono);
- font-size: 13px;
- color: var(--ink);
- white-space: nowrap;
-}
-
-.hero-install .install-command {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.hero-install .install .ps1 {
- color: var(--accent);
- user-select: none;
- flex: none;
-}
-
-
-.hero-install .install button {
- flex: none;
- height: 28px;
- margin-left: auto;
- padding: 0 14px;
- font-family: var(--font-sans);
- font-size: 12px;
- font-weight: 600;
- color: #fff;
- background: var(--accent);
- border: none;
- border-radius: 6px;
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.hero-install .install button:hover {
- background: var(--accent-deep);
-}
-
-.hero-install .install button.copied {
- background: oklch(0.55 0.14 155);
-}
-
-
-.install-option__more,
-.install-option a[href*="原生"],
-.install-option a[href*="安装包"] {
- display: none !important;
-}
-
-.install {
- display: inline-flex; align-items: center; gap: 14px; min-width: 0;
- font-family: var(--font-mono); font-size: 13.5px; color: var(--ink);
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: var(--radius-sm);
- padding: 10px 8px 10px 16px;
- white-space: nowrap;
-}
-.install .ps1 { color: var(--accent); user-select: none; }
-.install button {
- flex: none;
- font-family: var(--font-sans); font-size: 12.5px; font-weight: 600;
- background: var(--card); color: var(--ink-2); border: 1px solid var(--line); cursor: pointer;
- padding: 6px 13px; border-radius: 6px;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
-}
-.install button:hover { color: var(--accent); border-color: color-mix(in oklch, var(--accent) 45%, var(--line)); }
-
-
-.also {
- max-width: 960px;
- margin: 18px auto 0;
- font-size: 13.5px;
- line-height: 1.7;
- color: var(--ink-3);
-}
-
-.also-row {
- display: flex;
- align-items: baseline;
- justify-content: center;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.also-label {
- flex: none;
- font-weight: 600;
- color: var(--ink-2);
-}
-
-.also-links {
- display: flex;
- align-items: baseline;
- justify-content: center;
- gap: 0 10px;
- flex-wrap: wrap;
-}
-
-.also a {
- color: var(--accent);
- font-weight: 500;
- text-decoration: none;
- transition: opacity 0.2s;
-}
-
-.also a:hover {
- opacity: 0.75;
- text-decoration: underline;
- text-underline-offset: 3px;
-}
-
-.also a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }
-
-.also-sep {
- color: var(--ink-3);
-}
-
-.also code {
- font-family: var(--font-mono);
- font-size: 12px;
- color: var(--ink-2);
- background: var(--bg-soft);
- padding: 2px 6px;
- border-radius: 5px;
-}
-
-
-.hero-cta { margin-top: 38px; display: flex; justify-content: center; gap: 14px; flex-wrap: wrap; }
-.cold-blocks { display: flex; justify-content: center; gap: 8px; margin-top: 46px; }
-.cold-blocks i {
- width: 26px; height: 26px; border-radius: 7px;
- background: var(--accent-soft);
- box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--accent) 26%, transparent);
-}
-.cold-blocks i.new { background: var(--accent); box-shadow: none; }
-
-
-.term-stage { position: relative; margin: 58px auto 0; max-width: 880px; }
-.fig {
- font-family: var(--font-mono); font-size: 12px; color: var(--ink-3);
- margin-bottom: 12px; text-align: left;
-}
-.term {
- position: relative; text-align: left;
- background: var(--term-bg);
- border: 1px solid var(--term-edge);
- border-radius: var(--radius); overflow: hidden;
- box-shadow: var(--shadow-2);
-}
-.term-bar {
- display: flex; align-items: center; gap: 8px;
- padding: 14px 20px; border-bottom: 1px solid var(--term-edge);
-}
-.term-bar i { width: 10px; height: 10px; border-radius: 50%; background: oklch(0.42 0.008 260); }
-.term-bar .title {
- margin: 0 auto; transform: translateX(-20px);
- font-family: var(--font-mono); font-size: 12px; color: oklch(0.62 0.008 260);
-}
-.term-body {
- padding: 26px 30px 28px; min-height: 312px;
- font-family: var(--font-mono); font-size: 13.5px; line-height: 2.05;
- color: oklch(0.9 0.004 260); white-space: pre-wrap;
-}
-.term-body .tl { display: block; opacity: 0; transform: translateY(4px); }
-.term-body .tl.on { opacity: 1; transform: none; transition: opacity 0.4s, transform 0.4s var(--ease); }
-.t-dim { color: oklch(0.6 0.008 260); }
-.t-acc { color: var(--acc-1); }
-.t-vio { color: var(--acc-2); }
-.t-ok { color: oklch(0.78 0.13 160); }
-.t-warm{ color: oklch(0.82 0.11 80); }
-.t-add { color: oklch(0.78 0.13 160); }
-.caret {
- display: inline-block; width: 8px; height: 16px; vertical-align: -3px;
- background: var(--acc-1); animation: blink 1.1s steps(1) infinite;
-}
-@keyframes blink { 50% { opacity: 0; } }
-
-.term-stat {
- display: flex; gap: 26px; justify-content: space-between; flex-wrap: wrap;
- border-top: 1px solid var(--term-edge); padding: 14px 30px;
- font-family: var(--font-mono); font-size: 12px; color: oklch(0.6 0.008 260);
-}
-.term-stat b { color: oklch(0.86 0.02 260); font-weight: 500; }
-
-
-section { padding: 118px 0 0; }
-.sec-head { max-width: 680px; margin: 0 auto 56px; text-align: center; }
-.sec-head.center { margin-left: auto; margin-right: auto; text-align: center; }
-.kicker {
- font-family: var(--font-mono); font-size: 13.5px; font-weight: 500; letter-spacing: 0.02em;
- color: var(--accent); display: block; margin-bottom: 18px;
-}
-.kicker::before { content: "$ "; color: var(--ink-3); }
-.sec-head h2 {
- font-size: clamp(32px, 3.8vw, 48px); line-height: 1.12;
- letter-spacing: -0.02em; font-weight: 600; text-wrap: balance;
-}
-.sec-head h2 .em { color: var(--accent); }
-.sec-head p { margin-top: 18px; font-size: 17px; line-height: 1.65; color: var(--ink-2); text-wrap: pretty; }
-
-.sec-head.split {
- max-width: none; margin: 0 0 56px; text-align: left;
- display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
- gap: 20px 90px; align-items: end;
-}
-.sec-head.split .kicker { grid-column: 1 / -1; margin-bottom: 4px; }
-.sec-head.split h2 { margin: 0; }
-.sec-head.split p { margin: 0 0 6px; }
-
-
-.feat-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 20px; }
-.feat { grid-column: span 6; }
-.feat:nth-child(1) { grid-column: span 7; }
-.feat:nth-child(2) { grid-column: span 5; }
-.feat:nth-child(3) { grid-column: span 5; }
-.feat:nth-child(4) { grid-column: span 7; }
-.feat:nth-child(5) { grid-column: span 6; }
-.feat:nth-child(6) { grid-column: span 6; }
-.feat-viz { display: flex; gap: 6px; margin-top: 30px; }
-.feat-viz i {
- width: 20px; height: 20px; border-radius: 6px;
- background: var(--accent-soft);
- box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--accent) 26%, transparent);
-}
-.feat-viz i.n { background: var(--accent); box-shadow: none; }
-.feat-chip {
- display: inline-block; margin-top: 26px;
- font-family: var(--font-mono); font-size: 12px;
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: 7px;
- padding: 8px 12px; color: var(--ink-2);
-}
-.feat {
- position: relative; overflow: hidden;
- background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
- padding: 30px 30px 32px;
- transition: border-color 0.25s;
-}
-.feat:hover { border-color: color-mix(in oklch, var(--accent) 38%, var(--line)); }
-.feat .tag {
- font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3);
- letter-spacing: 0.04em; display: block; margin-bottom: 18px;
-}
-.feat:hover .tag { color: var(--accent); }
-.feat h3 { font-size: 20px; letter-spacing: -0.018em; font-weight: 600; margin-bottom: 10px; }
-.feat p { font-size: 15px; line-height: 1.65; color: var(--ink-2); text-wrap: pretty; }
-.feat code { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); }
-
-
-.surf-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; }
-.surf {
- display: flex; flex-direction: column; gap: 10px;
- padding: 20px 20px 24px;
- border: 1px solid var(--line); border-radius: var(--radius);
- background: var(--card);
- transition: border-color 0.25s, transform 0.25s var(--ease);
-}
-.surf:hover { border-color: color-mix(in oklch, var(--accent) 38%, var(--line)); transform: translateY(-2px); }
-.surf h3 { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
-.surf p { flex: 1; font-size: 14px; line-height: 1.6; color: var(--ink-2); text-wrap: pretty; }
-.surf code {
- align-self: flex-start;
- font-family: var(--font-mono); font-size: 12px; color: var(--ink-2);
- background: var(--bg-soft); border: 1px solid var(--line);
- border-radius: 7px; padding: 6px 10px;
-}
-.surf-art {
- display: grid; place-items: center;
- height: 132px; margin-bottom: 4px;
- border-radius: var(--radius-sm);
- background: var(--bg-soft); overflow: hidden;
-}
-.surf-win {
- display: grid; grid-template-rows: auto 1fr;
- width: 78%; height: 88px;
- border: 1px solid var(--line); border-radius: 8px;
- background: var(--card); overflow: hidden;
-}
-.surf-win .bar { height: 12px; border-bottom: 1px solid var(--line); background: var(--bg-soft); }
-.surf-win .bar.url { position: relative; }
-.surf-win .bar.url::after {
- content: ""; position: absolute; left: 8px; top: 3px;
- width: 58%; height: 6px; border-radius: 999px;
- background: color-mix(in oklch, var(--ink) 12%, transparent);
-}
-.surf-win .body { display: flex; flex-direction: column; gap: 6px; padding: 9px 10px; }
-.surf-win .l { height: 5px; width: var(--w, 60%); border-radius: 999px; background: color-mix(in oklch, var(--ink) 14%, transparent); }
-.surf-win .l.ok { background: color-mix(in oklch, var(--accent) 55%, transparent); }
-.surf-win .l.add { background: color-mix(in oklch, oklch(0.65 0.15 155) 62%, transparent); }
-.surf-win .cur { width: 5px; height: 8px; background: var(--accent); }
-.surf-win.app .body, .surf-win.ide .body { flex-direction: row; gap: 8px; }
-.surf-win .side, .surf-win .tree {
- display: flex; flex-direction: column; gap: 5px;
- width: 24px; flex: none; padding-right: 7px;
- border-right: 1px solid var(--line);
-}
-.surf-win .side i, .surf-win .tree i { height: 5px; width: 100%; border-radius: 999px; background: color-mix(in oklch, var(--ink) 12%, transparent); }
-.surf-win .side i:first-child { background: color-mix(in oklch, var(--accent) 50%, transparent); }
-.surf-win .tree i:nth-child(2) { width: 68%; }
-.surf-win .tree i:nth-child(3) { width: 86%; }
-.surf-win .main { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
-.surf-win.cli { background: var(--term-bg); border-color: var(--term-edge); }
-.surf-win.cli .bar { background: color-mix(in oklch, #fff 6%, var(--term-bg)); border-bottom-color: var(--term-edge); }
-.surf-win.cli .l { background: color-mix(in oklch, #fff 22%, transparent); }
-.surf-win.cli .l.ok { background: oklch(0.72 0.13 160); }
-
-
-.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; }
-.step { padding: 6px 40px 6px 0; }
-.step + .step { padding-left: 40px; border-left: 1px solid var(--line); }
-.step .num {
- display: inline-flex; align-items: center; justify-content: center;
- width: 30px; height: 30px; border-radius: 7px;
- background: var(--accent-soft);
- font-family: var(--font-mono); font-size: 12.5px; font-weight: 500; color: var(--accent);
- margin-bottom: 18px;
-}
-.step h3 { font-size: 18px; font-weight: 600; letter-spacing: -0.018em; margin-bottom: 10px; }
-.step p { font-size: 14.5px; line-height: 1.62; color: var(--ink-2); }
-.step code {
- display: inline-block; margin-top: 16px; font-family: var(--font-mono); font-size: 12.5px;
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: 7px;
- padding: 7px 12px; color: var(--ink);
-}
-
-
-.crew {
- margin-top: 8px; display: grid; gap: 18px; overflow: hidden; padding: 24px 0 10px;
- -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
- mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
-}
-.crew-row { display: flex; width: max-content; }
-.crew-set { display: flex; align-items: center; gap: 16px; padding-right: 16px; }
-@media (prefers-reduced-motion: no-preference) {
- body[data-motion="rich"] .crew-row { animation: crew-slide 60s linear infinite; }
- body[data-motion="rich"] .crew-row.rev { animation-duration: 74s; animation-direction: reverse; }
- .crew-row:hover { animation-play-state: paused !important; }
-}
-@keyframes crew-slide { to { transform: translateX(-50%); } }
-
-.av {
- position: relative; flex: none; display: flex; align-items: center; justify-content: center;
- width: 54px; height: 54px; border-radius: 50%;
- color: #fff; font-size: 14px; font-weight: 600; letter-spacing: 0.02em;
- border: 2px solid var(--bg); box-shadow: var(--shadow-1);
- text-decoration: none; overflow: hidden;
- transition: transform 0.3s var(--ease), box-shadow 0.3s;
-}
-.av img { width: 100%; height: 100%; object-fit: cover; }
-.av.sm { width: 44px; height: 44px; font-size: 12px; }
-.av.lg { width: 64px; height: 64px; font-size: 16px; }
-.av:hover { transform: translateY(-6px) scale(1.1); box-shadow: var(--shadow-2); z-index: 2; }
-.av::after {
- content: attr(data-h);
- position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%) translateY(4px);
- font-size: 12px; font-weight: 500; white-space: nowrap;
- background: var(--ink); color: var(--bg); padding: 5px 12px; border-radius: 8px;
- opacity: 0; pointer-events: none;
- transition: opacity 0.2s, transform 0.2s var(--ease);
-}
-.av:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); }
-.av.you {
- background: transparent; border: 1.5px dashed var(--ink-3); color: var(--ink-2);
- font-size: 18px; box-shadow: none;
-}
-.av.you:hover { border-color: var(--accent); color: var(--accent); }
-
-.crew-cta { display: flex; justify-content: center; gap: 14px; margin-top: 38px; flex-wrap: wrap; }
-.crew-note {
- margin-top: 26px; text-align: center;
- font-size: 13.5px; color: var(--ink-3);
-}
-
-
-@media (prefers-reduced-motion: no-preference) {
- body[data-motion="rich"] .reveal {
- opacity: 0; transform: translateY(10px);
- transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
- transition-delay: var(--d, 0s);
- }
- body[data-motion="rich"] .reveal.in { opacity: 1; transform: none; }
-}
-
-
-.os-card::after, .doc-card::after {
- content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px;
- background: radial-gradient(220px circle at var(--mx, 50%) var(--my, 50%),
- color-mix(in oklch, var(--accent) 45%, transparent), transparent 70%);
- -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
- -webkit-mask-composite: xor;
- mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
- mask-composite: exclude;
- opacity: 0; transition: opacity 0.35s; pointer-events: none;
-}
-.os-card:hover::after, .doc-card:hover::after { opacity: 1; }
-
-
-.divider { position: relative; height: 24px; margin-top: 84px; pointer-events: none; }
-.divider + section { padding-top: 52px; }
-.divider .line {
- position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
- width: min(1100px, calc(100% - 80px)); height: 1px;
- background: var(--line);
- -webkit-mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
- mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
-}
-
-
-.cta {
- margin-top: 150px; position: relative;
- background: var(--bg-soft); color: var(--ink); text-align: center;
- padding: 110px 40px 120px;
-}
-.cta h2 { font-size: clamp(34px, 4.2vw, 52px); letter-spacing: -0.02em; font-weight: 600; text-wrap: balance; }
-.cta h2 .em { color: var(--accent); }
-.cta p { margin: 20px auto 0; max-width: 560px; font-size: 17px; line-height: 1.65; color: var(--ink-2); }
-.cta .install { background: var(--card); box-shadow: var(--shadow-1); }
-.cta .install button { background: var(--accent-soft); box-shadow: none; }
-.cta .install button:hover { background: color-mix(in oklch, var(--accent) 16%, var(--bg)); }
-.cta .install button.copied { background: oklch(0.55 0.14 155); color: #fff; }
-
-
-.dl { margin-top: 44px; }
-.dl-tabs {
- display: inline-flex; gap: 2px; padding: 3px;
- background: var(--bg-soft); border: 1px solid var(--line); border-radius: 9px;
-}
-.dl-tab {
- font-family: var(--font-sans); font-size: 13.5px; font-weight: 500;
- color: var(--ink-2); background: transparent; border: none; cursor: pointer;
- padding: 8px 18px; border-radius: 6px;
- transition: background 0.25s, color 0.25s, box-shadow 0.25s;
-}
-.dl-tab:hover { color: var(--ink); }
-.dl-tab.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow-1); }
-
-.dl-pane { display: none; margin-top: 36px; }
-.dl-pane.active { display: block; animation: pane-in 0.45s var(--ease); }
-@keyframes pane-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
-
-.release-channel-switch {
- display: inline-grid; grid-template-columns: repeat(2, minmax(112px, 1fr)); gap: 3px;
- padding: 4px; margin: 0 auto 10px;
- background: color-mix(in oklch, var(--ink) 6%, var(--bg));
- border: 1px solid var(--line); border-radius: var(--radius-sm);
-}
-.release-channel-switch button {
- min-height: 36px; padding: 7px 18px;
- border: 0; border-radius: 7px; cursor: pointer;
- background: transparent; color: var(--ink-2);
- font-family: var(--font-sans); font-size: 13px; font-weight: 600;
- transition: background 0.2s, color 0.2s, box-shadow 0.2s;
-}
-.release-channel-switch button:hover { color: var(--ink); }
-.release-channel-switch button.active {
- background: var(--card); color: var(--accent); box-shadow: var(--shadow-1);
-}
-.release-channel-switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-.release-channel-summary {
- min-height: 24px; margin: 0 auto 22px;
- color: var(--ink-3); font-size: 13px; line-height: 1.55;
-}
-.release-upgrade-command { margin: 12px auto 24px; }
-.cli-os-grid { margin-top: 0; }
-
-.dl-note { margin-top: 22px; font-size: 13px; color: var(--ink-3); }
-.dl-note .mono { font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); }
-.dl-note a { color: var(--accent); text-decoration: none; }
-.dl-note a:hover { text-decoration: underline; }
-
-
-.chan {
- max-width: 660px; margin: 26px auto 0; text-align: left;
- background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden;
-}
-.chan-row {
- display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
- padding: 16px 22px; font-size: 13.5px;
- color: var(--ink-3);
-}
-.chan-row + .chan-row { border-top: 1px solid var(--line); }
-.chan-row code { font-family: var(--font-mono); font-size: 13px; color: var(--ink); width: 185px; flex: none; }
-.chan-row .v { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink-2); width: 92px; flex: none; }
-.chan-row .s { flex: 1; min-width: 200px; }
-.chan-row.rec { background: var(--accent-soft); }
-.chan-row.rec code { color: var(--accent); font-weight: 500; }
-.chan-row.rec .s { color: var(--ink-2); }
-.chan-badge {
- font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
- color: #fff; background: var(--accent);
- padding: 4px 9px; border-radius: 5px; flex: none;
-}
-.chan-badge.pre { color: var(--ink-3); background: var(--bg-soft); box-shadow: inset 0 0 0 1px var(--line); }
-.chan-row:not(.rec) code, .chan-row:not(.rec) .v { text-decoration: line-through; text-decoration-color: var(--ink-3); }
-
-.os-grid {
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px;
- max-width: 900px; margin: 0 auto; text-align: left;
-}
-.os-card {
- position: relative; display: flex; flex-direction: column; gap: 10px;
- background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
- padding: 26px 24px 24px;
- transition: border-color 0.25s;
-}
-.os-card:hover { border-color: color-mix(in oklch, var(--accent) 38%, var(--line)); }
-.os-card h4 { font-size: 19px; font-weight: 600; letter-spacing: -0.018em; color: var(--ink); }
-.os-card .meta { font-family: var(--font-mono); font-size: 11.5px; line-height: 1.7; color: var(--ink-3); }
-.os-card .btn { margin-top: 16px; }
-.os-options {
- display: flex; flex-wrap: wrap; gap: 8px;
- margin-top: 4px;
-}
-.os-options a {
- display: inline-flex; align-items: center; min-height: 28px;
- padding: 5px 9px; border-radius: 7px;
- background: var(--bg-soft); border: 1px solid var(--line);
- color: var(--ink-2); text-decoration: none;
- font-size: 12px; font-weight: 600; line-height: 1.2;
-}
-.os-options a:hover {
- border-color: color-mix(in oklch, var(--accent) 40%, var(--line));
- color: var(--accent); text-decoration: none;
-}
-.os-card .alt {
- font-size: 12.5px; color: var(--ink-3); text-align: center; margin-top: 2px;
-}
-.os-card .alt a { color: var(--accent); text-decoration: none; font-weight: 500; }
-.os-card .alt a:hover { text-decoration: underline; }
-.editor-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 760px; }
-.editor-card { min-height: 190px; }
-.editor-card--primary {
- background: linear-gradient(180deg, color-mix(in oklch, var(--accent) 10%, var(--card)), var(--card) 82%);
- border: 1px solid color-mix(in oklch, var(--accent) 24%, var(--line));
-}
-.editor-card .btn { margin-top: auto; align-self: flex-start; }
-.editor-note { max-width: 820px; margin-inline: auto; line-height: 1.65; }
-.editor-note code { font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); }
-.btn-light { background: var(--accent-soft); color: var(--accent); }
-.btn-light:hover { background: color-mix(in oklch, var(--accent) 16%, var(--bg)); }
-.dl-help {
- position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
- gap: 8px 12px; align-items: center;
- max-width: 900px; margin: 18px auto 0; padding: 16px 18px;
- background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-sm);
- box-shadow: var(--shadow-1); text-align: left;
- color: var(--ink-2); font-size: 13.5px; line-height: 1.5;
-}
-.dl-help strong { grid-column: 1 / -1; color: var(--ink); font-size: 14.5px; }
-.dl-help code {
- grid-column: 1 / 2; min-width: 0; overflow-x: auto;
- font-family: var(--font-mono); font-size: 12.5px;
- background: var(--bg-soft); border-radius: 8px; padding: 8px 10px;
- color: var(--ink); white-space: nowrap;
-}
-.dl-help button {
- font-family: var(--font-sans); font-size: 12.5px; font-weight: 600;
- color: var(--accent); background: var(--accent-soft);
- border: none; border-radius: 7px; padding: 7px 13px; cursor: pointer;
-}
-.dl-help button.copied { background: oklch(0.55 0.14 155); color: #fff; }
-.dl-help a { color: var(--accent); text-decoration: none; font-weight: 600; }
-.dl-help a:hover { text-decoration: underline; }
-.os-card.detected { box-shadow: 0 0 0 2px var(--accent), var(--shadow-1); }
-@keyframes cardFlash {
- 0%, 100% { box-shadow: 0 0 0 2px var(--accent), var(--shadow-1); }
- 25%, 65% { box-shadow: 0 0 0 3px var(--accent), 0 18px 44px -18px color-mix(in oklch, var(--accent) 55%, transparent); transform: translateY(-3px); }
-}
-.os-card.flash { animation: cardFlash 2s var(--ease); }
-.os-chip {
- position: absolute; top: -11px; right: 16px;
- font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
- color: #fff; background: var(--accent);
- padding: 4px 10px; border-radius: 5px;
-}
-
-
-.docs-layout {
- display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 56px;
- max-width: 1240px; margin: 0 auto; padding: 124px 40px 110px;
-}
-.docs-side {
- position: sticky; top: 110px; align-self: start;
- display: flex; flex-direction: column; gap: 16px;
- max-height: calc(100dvh - 132px); overflow-y: auto; padding-right: 4px;
-}
-.docs-side-links {
- border: 1px solid var(--line);
- border-radius: var(--radius-sm);
- background: var(--card);
- padding: 16px;
-}
-.side-card-kicker {
- display: block; margin-bottom: 8px;
- font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em;
- text-transform: uppercase; color: var(--accent);
-}
-.side-group a small {
- display: block; margin-top: 3px;
- font-size: 12px; font-weight: 500; line-height: 1.35;
- color: var(--ink-3);
-}
-.docs-main { min-width: 0; }
-.side-nav {
- display: flex; flex-direction: column; gap: 18px;
- padding: 2px 0;
-}
-.side-group .gh {
- font-size: 12px; font-weight: 600; letter-spacing: 0.06em;
- text-transform: uppercase; color: var(--ink-3); display: block; margin: 0 0 8px; padding-left: 16px;
-}
-.side-group a {
- display: block; padding: 9px 16px; border-radius: 14px;
- color: var(--ink-2); text-decoration: none; font-size: 14px; font-weight: 500;
- transition: color 0.2s, background 0.2s;
-}
-.side-group a strong { display: block; color: inherit; font-size: 14px; line-height: 1.25; }
-.side-group a code {
- font-family: var(--font-mono); font-size: 11px;
- background: transparent; color: inherit; padding: 0;
-}
-.side-group a:hover { color: var(--ink); background: var(--bg-soft); }
-.side-group a.active { color: var(--accent); background: var(--accent-soft); }
-.side-group a.active small { color: color-mix(in oklch, var(--accent) 72%, var(--ink-3)); }
-.docs-side-links {
- display: grid; gap: 8px; margin-top: 2px;
-}
-.docs-side-links a {
- display: flex; align-items: center; justify-content: space-between; gap: 10px;
- color: var(--ink-2); text-decoration: none; font-size: 13.5px; font-weight: 600;
-}
-.docs-side-links a::after { content: "->"; color: var(--ink-3); font-size: 12px; }
-.docs-side-links a:hover { color: var(--accent); }
-.docs-hero {
- padding: 8px 0 36px; margin-bottom: 10px;
- border-bottom: 1px solid var(--line);
-}
-.docs-kicker {
- display: inline-flex; align-items: center; margin-bottom: 12px;
- font-family: var(--font-mono); font-size: 12px; font-weight: 700; letter-spacing: 0.07em;
- text-transform: uppercase; color: var(--accent);
-}
-.docs-kicker::before { content: "$ "; color: var(--ink-3); }
-.docs-main h1 { font-size: 42px; line-height: 1.12; letter-spacing: -0.02em; font-weight: 600; margin: 0 0 14px; max-width: 760px; }
-.docs-main .lede { font-size: 17px; color: var(--ink-2); line-height: 1.65; margin: 0 0 26px; max-width: 760px; text-wrap: pretty; }
-.docs-path-grid {
- display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 12px; margin-top: 24px;
-}
-.docs-path-grid a {
- display: flex; flex-direction: column; gap: 7px;
- min-height: 148px; padding: 17px;
- border: 1px solid var(--line); border-radius: var(--radius-sm);
- background: var(--card);
- color: var(--ink-2); text-decoration: none;
- transition: border-color 0.2s, background 0.2s, transform 0.2s var(--ease);
-}
-.docs-path-grid a:hover {
- border-color: color-mix(in oklch, var(--accent) 38%, var(--line));
- background: var(--card); transform: translateY(-1px); text-decoration: none;
-}
-.docs-path-grid strong {
- color: var(--ink); font-size: 14.5px; line-height: 1.25;
-}
-.docs-path-grid span {
- font-size: 13px; line-height: 1.55;
-}
-.docs-main h2 {
- font-size: 25px; letter-spacing: -0.012em; font-weight: 600;
- margin: 56px 0 8px; padding-top: 28px; border-top: 1px solid var(--line);
- scroll-margin-top: 88px;
-}
-.docs-main h2:first-of-type { border-top: none; padding-top: 0; margin-top: 0; }
-.docs-main h3 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; margin: 28px 0 6px; }
-.docs-main p { font-size: 15.5px; line-height: 1.75; color: var(--ink-2); margin: 12px 0; text-wrap: pretty; }
-.docs-main a { color: var(--accent); text-decoration: none; }
-.docs-main a:hover { text-decoration: underline; }
-.docs-main ul { margin: 12px 0 12px 22px; color: var(--ink-2); font-size: 15.5px; line-height: 1.85; }
-.docs-main p code, .docs-main li code, .docs-main td code {
- font-family: var(--font-mono); font-size: 13px;
- background: var(--bg-soft); border-radius: 6px;
- padding: 2.5px 8px; color: var(--ink);
-}
-.codeblock {
- position: relative; margin: 18px 0; padding: 20px 24px;
- background: var(--term-bg); border-radius: var(--radius-sm);
- font-family: var(--font-mono); font-size: 13.5px; line-height: 1.85;
- color: oklch(0.9 0.004 260); overflow-x: auto; white-space: pre;
-}
-.codeblock--copy { padding-top: 52px; }
-.codeblock .c { color: oklch(0.58 0.008 260); }
-.codeblock .a { color: var(--acc-1); }
-.codeblock button {
- position: absolute; top: 12px; right: 12px;
- font-family: var(--font-sans); font-size: 12.5px; font-weight: 600;
- background: oklch(0.34 0.008 260); color: oklch(0.85 0.005 260);
- border: none; border-radius: 7px; padding: 6px 12px; cursor: pointer;
- transition: background 0.2s, color 0.2s;
-}
-.codeblock button:hover { background: oklch(0.4 0.01 260); color: #fff; }
-.codeblock button.copied { background: oklch(0.55 0.14 155); color: #fff; }
-.callout {
- display: flex; gap: 12px; align-items: flex-start;
- background: var(--accent-soft);
- border-radius: var(--radius-sm); padding: 18px 22px; margin: 22px 0;
- font-size: 14.5px; line-height: 1.65; color: var(--ink-2);
-}
-.callout .ico {
- flex: none; display: flex; align-items: center; justify-content: center;
- width: 22px; height: 22px; border-radius: 50%;
- background: var(--accent); color: #fff; font-size: 13px; font-weight: 700;
-}
-.tui-panel {
- margin: 18px 0 20px; padding: 20px 22px;
- border: 1px solid var(--line); border-radius: var(--radius-sm);
- background: var(--bg-soft);
-}
-.tui-panel .desktop-feature-list { margin-top: 10px !important; }
-.doc-choice-grid,
-.doc-note-grid {
- display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px; margin: 18px 0;
-}
-.doc-choice-grid > div,
-.doc-note-grid > div {
- border: 1px solid var(--line);
- border-radius: var(--radius-sm);
- background: var(--card);
- padding: 17px 18px;
-}
-.doc-choice-grid strong,
-.doc-note-grid strong {
- display: block; color: var(--ink);
- font-size: 14px; line-height: 1.3; margin-bottom: 5px;
-}
-.doc-choice-grid p {
- margin: 0; font-size: 13.5px; line-height: 1.6;
-}
-.doc-note-grid {
- grid-template-columns: repeat(3, minmax(0, 1fr));
-}
-.doc-note-grid > div {
- display: flex; flex-direction: column; gap: 4px;
-}
-.doc-note-grid span { font-size: 13px; color: var(--ink-2); overflow-wrap: anywhere; }
-.desktop-panel,
-.web-panel {
- margin: 20px 0 8px; padding: 24px;
- border: 1px solid color-mix(in oklch, var(--accent) 18%, var(--line));
- border-radius: var(--radius-sm);
- background: linear-gradient(180deg, color-mix(in oklch, var(--accent) 9%, var(--bg)), var(--bg) 72%);
-}
-.desktop-panel-head { max-width: 680px; }
-.desktop-kicker {
- display: inline-flex; align-items: center; margin-bottom: 10px;
- font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
- color: var(--accent);
-}
-.docs-main .desktop-panel h3,
-.docs-main .web-panel h3 {
- margin: 0 0 8px; font-size: 21px; line-height: 1.25;
- letter-spacing: -0.012em; color: var(--ink);
-}
-.docs-main .desktop-panel p { margin: 0; max-width: 720px; }
-.desktop-downloads {
- display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 10px; margin-top: 20px;
-}
-.desktop-downloads--secondary { margin-top: 10px; }
-.desktop-downloads--secondary .desktop-download {
- box-shadow: none; background: var(--bg-soft);
-}
-.desktop-download {
- display: flex; flex-direction: column; gap: 4px;
- min-width: 0; padding: 15px 16px; border-radius: 10px;
- background: var(--card); border: 1px solid var(--line);
- color: var(--ink); text-decoration: none;
- box-shadow: var(--shadow-1);
- transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s var(--ease);
-}
-.desktop-download:hover {
- border-color: color-mix(in oklch, var(--accent) 42%, var(--line));
- box-shadow: var(--shadow-2); transform: translateY(-1px); text-decoration: none;
-}
-.desktop-download strong { font-size: 15px; font-weight: 700; color: var(--ink); }
-.desktop-download span { font-size: 13px; color: var(--ink-2); line-height: 1.35; }
-.desktop-feature-list {
- display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 10px 16px; margin: 18px 0 0 !important; padding: 0; list-style: none;
-}
-.desktop-feature-list li {
- position: relative; padding-left: 16px;
- font-size: 13.5px; line-height: 1.55; color: var(--ink-2);
-}
-.desktop-feature-list li::before {
- content: ""; position: absolute; left: 0; top: 0.62em;
- width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
-}
-.doc-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 22px; }
-.doc-card {
- position: relative; display: block; text-decoration: none;
- background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-sm);
- padding: 22px 24px; transition: border-color 0.25s;
-}
-.doc-card:hover { border-color: color-mix(in oklch, var(--accent) 38%, var(--line)); text-decoration: none; }
-.doc-card h3 { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); margin: 0 0 6px; }
-.doc-card p { font-size: 13.5px; line-height: 1.6; color: var(--ink-2); margin: 0; }
-
-
-footer { background: var(--bg); border-top: 1px solid var(--line); margin-top: 0; }
-.foot-inner {
- max-width: 1180px; margin: 0 auto; padding: 36px 40px 28px;
- display: flex; align-items: center; gap: 28px; flex-wrap: wrap;
- font-size: 14px; color: var(--ink-3);
-}
-.foot-sub {
- max-width: 1180px; margin: 0 auto; padding: 16px 40px 26px;
- display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
- border-top: 1px solid var(--line);
- font-size: 13px; color: var(--ink-3);
-}
-.foot-sub .ver { font-family: var(--font-mono); font-size: 12px; }
-.foot-inner .brand {
- display: flex;
- align-items: center;
- gap: 0;
- text-decoration: none;
- color: var(--ink);
-}
-.foot-inner .brand img { width: 90px; height: auto; border-radius: 0; display: block; }
-.foot-inner .brand span { display: none; }
-.foot-links { display: flex; gap: 8px; margin-left: auto; flex-wrap: wrap; }
-.foot-links a {
- color: var(--ink-2); text-decoration: none; font-weight: 500;
- padding: 7px 11px; border-radius: 7px;
- transition: color 0.2s, background 0.2s;
-}
-.foot-links a:hover { color: var(--ink); background: var(--bg-soft); }
-
-@media (max-width: 900px) {
- .wrap { padding: 0 24px; }
- .nav-github { display: none; }
- .lang-switch { margin-left: auto; }
- .docs-layout { grid-template-columns: 1fr; gap: 36px; padding: 110px 24px 80px; }
- .docs-side { position: static; max-height: none; overflow: visible; padding-right: 0; }
- .docs-path-grid { grid-template-columns: 1fr 1fr; }
- .doc-choice-grid, .doc-note-grid { grid-template-columns: 1fr; }
- .desktop-downloads, .desktop-feature-list { grid-template-columns: 1fr; }
- .doc-cards { grid-template-columns: 1fr; }
- .divider { display: none; }
- .divider + section { padding-top: 100px; }
- .feat-grid, .os-grid, .steps { grid-template-columns: 1fr; }
- .surf-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
- .feat, .feat:nth-child(1), .feat:nth-child(2), .feat:nth-child(3),
- .feat:nth-child(4), .feat:nth-child(5), .feat:nth-child(6) { grid-column: auto; }
- .sec-head.split { grid-template-columns: 1fr; align-items: start; }
- .steps { gap: 28px; }
- .step { padding: 0; }
- .step + .step { padding-left: 0; border-left: none; border-top: 1px solid var(--line); padding-top: 28px; }
- .dl-tab { padding: 9px 14px; font-size: 13px; }
- .dl-help { grid-template-columns: 1fr; }
- .dl-help code { grid-column: auto; white-space: normal; overflow-wrap: anywhere; }
- .dl-help button { justify-self: start; }
- .hero { padding-top: 130px; }
- section { padding-top: 100px; }
-}
-
-@media (max-width: 640px) {
- .nav-inner { padding: 0 16px; gap: 10px; }
- .nav-sign-in { display: none; }
- .nav .brand img { width: 64px; }
- .nav .brand span { display: none; }
- .theme-switch button { padding: 6px 9px; }
- .docs-path-grid { grid-template-columns: 1fr; }
- .surf-grid { grid-template-columns: 1fr; }
- .docs-main h1 { font-size: 34px; }
- .dl-tabs {
- display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
- width: 100%; max-width: 360px; border-radius: var(--radius-sm);
- }
- .release-channel-switch { width: 100%; max-width: 360px; }
- .release-channel-switch button { min-width: 0; padding-inline: 12px; }
- .release-upgrade-command {
- width: 100%; max-width: 520px;
- white-space: normal;
- display: flex;
- flex-wrap: wrap;
- row-gap: 8px;
- }
- .release-upgrade-command [data-release-command="cli"] {
- min-width: 0;
- flex: 1 1 180px;
- overflow-wrap: anywhere;
- }
-}
-
-
-@media (max-width: 700px) {
- .hero-install {
- grid-template-columns: 1fr;
- border-radius: var(--radius-sm);
- margin-top: 40px;
- }
- .hero-install::before {
- display: none;
- }
- .install-option {
- padding: 28px;
- min-height: 0;
- }
- .install-option + .install-option {
- border-top: 1px solid var(--line);
- }
- .also-row {
- flex-direction: column;
- align-items: center;
- gap: 6px;
- }
-}
-
-@media (max-width: 400px) {
- .nav .brand img { width: 28px; height: 28px; border-radius: 8px; }
-}
-
-@media (max-width: 360px) {
- .nav-inner { padding: 0 12px; gap: 6px; }
- .lang-switch button { padding: 6px 8px; }
- .theme-switch button { padding: 6px 7px; }
- .nav .btn { padding: 9px 14px; }
- .hero-install .install { gap: 6px; padding-inline: 6px; }
- .hero-install .install button { padding-inline: 10px; }
- .nav-install { display: none; }
- .nav-menu-toggle { width: 36px; height: 36px; }
-}
-
-
-.auth-nav { background: color-mix(in oklch, var(--bg) 92%, transparent); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); box-shadow: 0 1px 0 var(--line); }
-.auth-main { min-height: 100vh; display: grid; place-items: center; padding: 116px 24px 72px; }
-.auth-card {
- width: 100%; max-width: 420px;
- background: var(--card); border-radius: var(--radius);
- box-shadow: var(--shadow-2); padding: 42px 40px 36px;
-}
-.auth-card.wide { max-width: 560px; }
-.auth-head { text-align: center; }
-.auth-head h1 { font-size: 26px; font-weight: 600; letter-spacing: -0.02em; }
-.auth-head p { margin-top: 10px; font-size: 15px; line-height: 1.6; color: var(--ink-2); }
-.auth-form { display: flex; flex-direction: column; gap: 16px; margin-top: 24px; }
-.field { display: flex; flex-direction: column; gap: 7px; text-align: left; }
-.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
-.field .hint { font-size: 12px; font-weight: 400; color: var(--ink-3); }
-.field input, .field textarea {
- width: 100%; font-family: var(--font-sans); font-size: 15px; color: var(--ink);
- background: var(--bg-soft); border: 1px solid transparent; border-radius: var(--radius-sm);
- padding: 12px 14px; resize: vertical;
- transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
-}
-.field input:focus, .field textarea:focus {
- outline: none; background: var(--card);
- border-color: color-mix(in oklch, var(--accent) 45%, var(--line));
- box-shadow: 0 0 0 4px var(--accent-soft);
-}
-.field input:disabled, .field textarea:disabled { opacity: 0.55; }
-.auth-form .btn { width: 100%; margin-top: 4px; }
-.btn[disabled], .btn.loading { opacity: 0.55; pointer-events: none; }
-.auth-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
-.auth-small { font-size: 12.5px; color: var(--accent); text-decoration: none; font-weight: 500; }
-.auth-small:hover { text-decoration: underline; }
-.auth-msg { margin-top: 20px; font-size: 13.5px; line-height: 1.55; border-radius: var(--radius-sm); padding: 11px 14px; text-align: left; }
-.auth-msg.error { background: color-mix(in oklch, oklch(0.63 0.21 25) 10%, var(--card)); color: oklch(0.5 0.19 25); }
-.auth-msg.ok { background: color-mix(in oklch, oklch(0.65 0.16 155) 13%, var(--card)); color: oklch(0.44 0.12 155); }
-:root[data-theme="dark"] .auth-msg.error { color: oklch(0.75 0.16 25); }
-:root[data-theme="dark"] .auth-msg.ok { color: oklch(0.78 0.14 155); }
-.auth-alt { margin-top: 24px; text-align: center; font-size: 14px; color: var(--ink-3); }
-.auth-alt a { color: var(--accent); text-decoration: none; font-weight: 500; }
-.auth-alt a:hover { text-decoration: underline; }
-
-.acct-id { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 8px; }
-.acct-handle { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
-.acct-email { margin-top: 3px; font-size: 13.5px; color: var(--ink-3); }
-.acct-role {
- font-family: var(--font-mono); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.04em;
- color: var(--accent); background: var(--accent-soft); padding: 5px 10px; border-radius: 6px;
-}
-.acct-h2 {
- font-size: 15px; font-weight: 600; color: var(--ink);
- margin: 30px 0 4px; padding-top: 24px; border-top: 1px solid var(--line);
-}
-.acct-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 34px; padding-top: 22px; border-top: 1px solid var(--line); }
-.acct-foot .btn { width: auto; margin: 0; }
-.acct-danger { font-family: var(--font-sans); font-size: 14px; font-weight: 500; color: oklch(0.55 0.19 25); background: none; border: none; cursor: pointer; padding: 8px 4px; }
-.acct-danger:hover { text-decoration: underline; }
-
-.device-code { font-family: var(--font-mono); font-size: 20px; letter-spacing: 0.16em; text-transform: uppercase; text-align: center; }
-.device-meta { margin-top: 16px; text-align: center; font-size: 13.5px; color: var(--ink-2); }
-.device-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 18px; }
-.device-actions .btn { width: 100%; margin: 0; }
-
-
-
-
-
-body, .btn, .lang-switch button, .theme-switch button,
-.nav-links a, .nav-github, .dl-tab,
-.release-channel-switch button, .field input, .field textarea,
-.brand span, .foot-inner, .foot-sub,
-.hero h1, .hero .sub, .sec-head h2, .sec-head p,
-.feat h3, .feat p, .surf h3, .surf p,
-.step h3, .step p, .cta h2, .cta p,
-.docs-main h1, .docs-main h2, .docs-main h3,
-.docs-main p, .docs-main ul, .callout,
-.auth-head h1, .auth-head p, .auth-form, .auth-alt,
-.acct-handle, .acct-email, .acct-h2 {
- font-family: var(--font-sans);
-}
-
-
-.eyebrow, .kicker, .install-kicker, .feat .tag, .feat-chip,
-.step .num, .docs-kicker, .os-card .meta, .acct-role,
-.chan-badge, .side-group .gh {
- font-family: var(--font-sans);
- font-weight: 500;
-}
-
-
-.install, .term-bar .title, .term-body, .term-stat,
-.codeblock, .chan-row code, .chan-row .v,
-.docs-main p code, .docs-main li code, .docs-main td code,
-.side-group a code, .dl-help code, .editor-note code,
-.device-code, .surf code, .step code, .also code,
-.install .ps1 {
- font-family: var(--font-mono);
-}
-
-
-.eyebrow { font-size: 12px; letter-spacing: 0.02em; }
-.kicker { font-size: 13px; letter-spacing: 0.02em; }
-.kicker::before { font-family: var(--font-mono); }
-.docs-kicker { font-size: 12px; letter-spacing: 0.06em; font-weight: 700; }
-.docs-kicker::before { font-family: var(--font-mono); }
-.feat .tag { font-size: 11.5px; letter-spacing: 0.04em; }
-.feat-chip { font-size: 12px; letter-spacing: 0; }
-.step .num { font-size: 12.5px; font-weight: 500; }
-.os-card .meta { font-size: 11.5px; line-height: 1.7; }
-.acct-role { font-size: 11.5px; letter-spacing: 0.04em; text-transform: uppercase; }
-
-@media (max-width: 640px) {
- .foot-inner .brand img {
- width: 72px;
- }
-}
-
-
-
-.surf-grid + .divider {
- margin-top: 48px;
- height: 16px;
-}
-
-.surf-grid + .divider + section {
- padding-top: 36px;
-}
-
-
-.surf-grid + section {
- padding-top: 48px;
-}
diff --git a/site/tsconfig.json b/site/tsconfig.json
deleted file mode 100644
index bcbf8b5090..0000000000
--- a/site/tsconfig.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "extends": "astro/tsconfigs/strict"
-}
diff --git a/workers/crash-report/src/shell.ts b/workers/crash-report/src/shell.ts
index 8d10b0a066..136ed2c859 100644
--- a/workers/crash-report/src/shell.ts
+++ b/workers/crash-report/src/shell.ts
@@ -1,5 +1,5 @@
-// Shared page shell + UI kit. Visual language mirrors site/src/styles/global.css
-// — white, blue accent, large radii; crash stacks use the site's dark terminal.
+// Shared page shell + UI kit. Visual language mirrors the reasonix.io website:
+// white, blue accent, large radii; crash stacks use its dark terminal style.
export function esc(s: unknown): string {
return String(s).replace(/[&<>"']/g, (c) => `${c.charCodeAt(0)};`);