@@ -1,5 +1,44 @@
const { useState, useEffect } = React;
+// --- Global hooks ---
+
+function useScrollReveal() {
+ useEffect(() => {
+ const els = document.querySelectorAll(".reveal");
+ if (!els.length) return;
+ if (!("IntersectionObserver" in window)) {
+ els.forEach((el) => el.classList.add("visible"));
+ return;
+ }
+ const obs = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((e) => {
+ if (e.isIntersecting) {
+ e.target.classList.add("visible");
+ obs.unobserve(e.target);
+ }
+ });
+ },
+ { threshold: 0.08, rootMargin: "0px 0px -40px 0px" },
+ );
+ els.forEach((el) => obs.observe(el));
+ return () => obs.disconnect();
+ }, []);
+}
+
+function useMouseGlow() {
+ useEffect(() => {
+ const move = (e) => {
+ document.documentElement.style.setProperty("--mx", e.clientX + "px");
+ document.documentElement.style.setProperty("--my", e.clientY + "px");
+ };
+ window.addEventListener("mousemove", move, { passive: true });
+ return () => window.removeEventListener("mousemove", move);
+ }, []);
+}
+
+// --- Components ---
+
function FloatpaneMark({ size = 20 }) {
return (
<img
@@ -15,7 +54,6 @@ function FloatpaneMark({ size = 20 }) {
function MatchaWordmark() {
const [version, setVersion] = useState("v0.8.2");
useEffect(() => {
- // floatpane/matcha repo โ fetch latest release tag
fetch("https://api.github.com/repos/floatpane/matcha/releases/latest")
.then((r) => (r.ok ? r.json() : null))
.then((d) => {
@@ -61,7 +99,6 @@ function TopNav() {
</a>
<nav className="nav-links">
<a href="#features">Features</a>
- <a href="#keys">Keybinds</a>
<a href="#install">Install</a>
<a href="https://docs.matcha.email">Docs โ</a>
<a href="https://github.com/floatpane/matcha" className="nav-github">
@@ -78,9 +115,64 @@ function TopNav() {
);
}
-function Hero({ datasetKey, setDatasetKey }) {
- const [pressed, setPressed] = useState(false);
- const TUI = window.MatchaTUI;
+function QuickStart() {
+ const CMD1 = "brew install floatpane/matcha/matcha";
+ const CMD2 = "matcha";
+ const [l1, setL1] = useState("");
+ const [l2, setL2] = useState("");
+ const [phase, setPhase] = useState("pre"); // pre โ t1 โ pause โ t2 โ done
+
+ useEffect(() => {
+ let t;
+ if (phase === "pre") {
+ t = setTimeout(() => setPhase("t1"), 1400);
+ } else if (phase === "t1") {
+ if (l1.length < CMD1.length) {
+ t = setTimeout(() => setL1(CMD1.slice(0, l1.length + 1)), 36);
+ } else {
+ t = setTimeout(() => setPhase("pause"), 420);
+ }
+ } else if (phase === "pause") {
+ t = setTimeout(() => setPhase("t2"), 320);
+ } else if (phase === "t2") {
+ if (l2.length < CMD2.length) {
+ t = setTimeout(() => setL2(CMD2.slice(0, l2.length + 1)), 90);
+ } else {
+ setPhase("done");
+ }
+ }
+ return () => clearTimeout(t);
+ }, [phase, l1, l2]);
+
+ const caret1 = phase === "pre" || phase === "t1" || phase === "pause";
+ const showL2 = phase === "t2" || phase === "done";
+
+ return (
+ <div className="quickstart">
+ <div className="qs-bar">
+ <span className="qs-dot qs-r" />
+ <span className="qs-dot qs-y" />
+ <span className="qs-dot qs-g" />
+ <span className="qs-bar-label">terminal</span>
+ </div>
+ <pre className="qs-code">
+ <span className="qs-prompt">$ </span>
+ {l1}
+ {caret1 && <span className="qs-caret" />}
+ {showL2 && (
+ <>
+ {"\n"}
+ <span className="qs-prompt">$ </span>
+ {l2}
+ <span className="qs-caret" />
+ </>
+ )}
+ </pre>
+ </div>
+ );
+}
+
+function Hero() {
return (
<section className="hero" id="top">
<div className="hero-copy">
@@ -89,25 +181,24 @@ function Hero({ datasetKey, setDatasetKey }) {
<span>by floatpane ยท local-first ยท secure ยท no telemetry</span>
</div>
<h1 className="hero-h1">
- A powerful, feature-rich
+ Email for people who
<br />
- email client <em>for your terminal.</em>
+ live in the <em>terminal.</em>
</h1>
<p className="hero-sub">
- Matcha is a modern TUI email client for people who live in the shell.
- Vim keybindings, PGP, IMAP multi-account, markdown composing,
- visual-mode batch ops, and a CLI that speaks your language.
+ Matcha is a keyboard-native email client built for the shell.
+ Multi-account IMAP, PGP encryption, markdown composing, and a CLI
+ that pipes. One static binary. No cloud. No trackers.
</p>
<div className="hero-cta">
- <a href="#install" className="btn btn-primary">
- <span>Install</span>
- <span className="btn-k">โต</span>
+ <a href="#install" className="btn btn-primary btn-lg">
+ Install now
</a>
- <a href="https://docs.matcha.email" className="btn btn-ghost">
- <span>Read the docs</span>
- <span className="btn-k">โ</span>
+ <a href="https://docs.matcha.email" className="btn btn-ghost btn-lg">
+ Read the docs <span className="btn-k">โ</span>
</a>
</div>
+ <QuickStart />
<div className="hero-meta">
<div>
<span className="dim">license</span> MIT
@@ -120,45 +211,6 @@ function Hero({ datasetKey, setDatasetKey }) {
</div>
</div>
</div>
-
- <div className="hero-demo">
- <div className="hero-demo-chrome">
- <div className="hero-demo-label">
- <span className="dim">demo ยท</span>
- <button
- className={"demo-swap " + (datasetKey === "default" ? "on" : "")}
- onClick={() => setDatasetKey("default")}
- >
- drew's inbox
- </button>
- <button
- className={"demo-swap " + (datasetKey === "dev" ? "on" : "")}
- onClick={() => setDatasetKey("dev")}
- >
- floatpane dev
- </button>
- <button
- className={"demo-swap " + (datasetKey === "personal" ? "on" : "")}
- onClick={() => setDatasetKey("personal")}
- >
- personal
- </button>
- </div>
- <div className="hero-demo-hint">
- {pressed ? (
- <span>โ keyboard live</span>
- ) : (
- <span className="dim">
- click ยท <kbd>j</kbd>
- <kbd>k</kbd> ยท <kbd>tab</kbd> ยท <kbd>โต</kbd>
- </span>
- )}
- </div>
- </div>
- {TUI && (
- <TUI datasetKey={datasetKey} onKeyPressed={() => setPressed(true)} />
- )}
- </div>
</section>
);
}
@@ -166,14 +218,14 @@ function Hero({ datasetKey, setDatasetKey }) {
const FEATURES = [
{
k: "01",
- title: "Email, the way you move",
- body: "Read, reply, delete, archive โ all from the home row. j/k to move, h/l between accounts, tab between folders. No mouse, no modals.",
- mono: "j k h l r d a โต",
+ title: "Keyboard-native",
+ body: "Read, reply, delete, archive โ all from the keyboard. Navigate messages, switch accounts, and jump between folders without touching the mouse.",
+ mono: "j k r d a โต esc",
},
{
k: "02",
- title: "Visual mode, for real",
- body: "Press v to enter Vim-style multi-select. Expand with j/k, then d, a, or m to run batch ops as a single IMAP command.",
+ title: "Visual mode batch ops",
+ body: "Enter visual mode to select a range of messages, then delete, archive, or move them all as a single IMAP command.",
mono: "v j j j d\nโ deleted 4 messages",
},
{
@@ -185,19 +237,19 @@ const FEATURES = [
{
k: "04",
title: "Multi-account, tabbed",
- body: "IMAP, Gmail, Fastmail, Proton Bridge โ all tabbed in one window. h and l switch between them so you never reply from the wrong address.",
+ body: "IMAP, Gmail, Fastmail, Proton Bridge โ all in one window. Switch between them instantly so you never reply from the wrong address.",
mono: "โ me@andrinoff\nโ drew@floatpane",
},
{
k: "05",
title: "Fuzzy filter",
- body: "Press / to fuzzy-filter across senders, subjects, and bodies in the active view. Results stream in as you type.",
+ body: "Filter across senders, subjects, and bodies in the active view. Results stream in as you type.",
mono: "/lena โ 3 hits",
},
{
k: "06",
title: "Local-first drafts",
- body: "Every keystroke hits disk before it hits the wire. Close the laptop, open it anywhere, pick up mid-sentence. Esc saves.",
+ body: "Every keystroke hits disk before it hits the wire. Close the laptop, open it anywhere, pick up mid-sentence.",
mono: "~/.cache/matcha/drafts",
},
{
@@ -208,22 +260,22 @@ const FEATURES = [
},
{
k: "08",
- title: "AI, on your terms",
- body: "Rewrite drafts with the model of your choice. Let agents send on your behalf โ with strict scopes and an audit log.",
- mono: "alt + r: make it more formal",
+ title: "Inline image rendering",
+ body: "Images render inline via iTerm2 or kitty graphics where supported. Toggle with a key. Off by default, always.",
+ mono: "โ โง images on",
},
{
k: "09",
- title: "Smart image rendering",
- body: "Images render inline via iterm2 or kitty-graphics where supported. Toggle with i. Off by default, always.",
- mono: "i โ โง images on",
+ title: "Full-disk encryption",
+ body: "Encrypt all local data with a password that is never stored โ not on disk, not in the keyring. Matcha shows a lock screen on startup. Forget the password and there is no reset.",
+ mono: "matcha is locked\n> โขโขโขโขโขโขโขโข\nenter: unlock",
},
];
function Features() {
return (
<section className="features" id="features">
- <div className="section-head">
+ <div className="section-head reveal">
<div className="section-head-l">
<div className="section-eyebrow">ยง features</div>
<h2 className="section-h2">
@@ -234,13 +286,17 @@ function Features() {
</div>
<p className="section-head-r">
Matcha is opinionated. It won't follow you around the web, won't
- upsell you on AI credits, and won't sync your signatures to a SaaS. It
+ upsell you on credits, and won't sync your signatures to a SaaS. It
reads mail. It writes mail. It stays out of the way.
</p>
</div>
<div className="feature-grid">
- {FEATURES.map((f) => (
- <article key={f.k} className="feature">
+ {FEATURES.map((f, i) => (
+ <article
+ key={f.k}
+ className="feature reveal"
+ style={{ transitionDelay: `${i * 55}ms` }}
+ >
<div className="feature-head">
<span className="feature-k">{f.k}</span>
<span className="feature-dash">โโ</span>
@@ -255,113 +311,6 @@ function Features() {
);
}
-function Keybinds() {
- const rows = [
- {
- g: "motion",
- items: [
- ["j / k", "next / prev message"],
- ["โ / โ", "next / prev message"],
- ["h / l", "prev / next account"],
- ["โ / โ", "prev / next account"],
- ["tab", "next folder"],
- ["shift-tab", "prev folder"],
- ],
- },
- {
- g: "inbox",
- items: [
- ["โต", "open email"],
- ["r", "refresh"],
- ["d", "delete"],
- ["a", "archive"],
- ["/", "filter"],
- ["v", "visual mode"],
- ["esc", "back / main menu"],
- ],
- },
- {
- g: "visual mode",
- items: [
- ["v", "enter visual mode"],
- ["j / k", "expand selection"],
- ["d", "delete all selected"],
- ["a", "archive all selected"],
- ["m", "move to folder"],
- ["v / esc", "exit visual mode"],
- ],
- },
- {
- g: "email view",
- items: [
- ["j / k", "scroll body"],
- ["r", "reply"],
- ["d", "delete"],
- ["a", "archive"],
- ["tab", "focus attachments"],
- ["i", "toggle images"],
- ["esc", "back to inbox"],
- ],
- },
- {
- g: "attachments",
- items: [
- ["j / k", "navigate"],
- ["โต", "download & open"],
- ["tab / esc", "back to body"],
- ],
- },
- {
- g: "composer",
- items: [
- ["tab / shift-tab", "navigate fields"],
- ["โต on From", "select account"],
- ["โต on Attachment", "open file picker"],
- ["โต on Send", "send email"],
- ["โ / โ", "contact suggestions"],
- ["esc", "save draft & exit"],
- ],
- },
- ];
- return (
- <section className="keybinds" id="keys">
- <div className="section-head">
- <div className="section-head-l">
- <div className="section-eyebrow">ยง keybinds</div>
- <h2 className="section-h2">
- Vim-native.
- <br />
- <span className="dim">Home row to inbox zero.</span>
- </h2>
- </div>
- <p className="section-head-r">
- Every binding is documented at{" "}
- <a href="https://docs.matcha.email" className="underline-link">
- docs.matcha.email
- </a>
- . Muscle-memory for vimmers, learnable for everyone else.
- </p>
- </div>
- <div className="keybinds-grid">
- {rows.map((row) => (
- <div key={row.g} className="keybinds-col">
- <div className="keybinds-col-head">โโ {row.g} โโ</div>
- {row.items.map(([k, label]) => (
- <div key={k} className="keybind-row">
- <span className="keybind-k">
- <kbd>{k}</kbd>
- </span>
- <span className="keybind-dots">{"ยท".repeat(26)}</span>
- <span className="keybind-label">{label}</span>
- </div>
- ))}
- </div>
- ))}
- </div>
- </section>
- );
-}
-
const INSTALL_TABS = {
brew: {
plat: "macOS ยท Linux",
@@ -371,6 +320,10 @@ const INSTALL_TABS = {
plat: "Windows 10 / 11",
cmd: "$ winget install --id=floatpane.matcha\n$ matcha",
},
+ scoop: {
+ plat: "Windows",
+ cmd: "$ scoop install matcha\n$ matcha",
+ },
snap: { plat: "Ubuntu ยท Linux", cmd: "$ sudo snap install matcha\n$ matcha" },
flatpak: {
plat: "Linux",
@@ -379,7 +332,11 @@ const INSTALL_TABS = {
aur: { plat: "Arch Linux", cmd: "$ yay -S matcha-client-bin\n$ matcha" },
nix: {
plat: "NixOS ยท any Nix",
- cmd: "$ nix profile install github:floatpane/matcha\n$ matcha",
+ cmd: "$ nix profile install github:floatpane/nix-matcha\n$ matcha",
+ },
+ nixpkgs: {
+ plat: "NixOS ยท nixpkgs",
+ cmd: "$ nix profile install nixpkgs#matcha\n$ matcha",
},
};
@@ -416,7 +373,7 @@ function Install() {
const t = INSTALL_TABS[tab];
return (
<section className="install" id="install">
- <div className="section-head">
+ <div className="section-head reveal">
<div className="section-head-l">
<div className="section-eyebrow">ยง install</div>
<h2 className="section-h2">
@@ -437,7 +394,7 @@ function Install() {
.
</p>
</div>
- <div className="install-card">
+ <div className="install-card reveal" style={{ transitionDelay: "0.15s" }}>
<div className="install-tabs">
{Object.keys(INSTALL_TABS).map((k) => (
<button
@@ -469,15 +426,21 @@ function CTA() {
return (
<section className="cta">
<div className="cta-inner">
- <div className="cta-pre">$ _</div>
- <h2 className="cta-h2">
+ <div className="cta-pre reveal">$ _</div>
+ <h2
+ className="cta-h2 reveal"
+ style={{ transitionDelay: "0.12s" }}
+ >
Your inbox is waiting
<br />
in the terminal.
</h2>
- <div className="cta-row">
+ <div
+ className="cta-row reveal"
+ style={{ transitionDelay: "0.24s" }}
+ >
<a href="#install" className="btn btn-primary btn-lg">
- make your emails secure
+ install matcha
</a>
<a href="https://docs.matcha.email" className="btn btn-ghost btn-lg">
read the docs โ
@@ -495,7 +458,7 @@ function Footer() {
<div className="footer-brand">
<MatchaWordmark />
<p className="footer-tag">
- a modern TUI email client.
+ a keyboard-native email client.
<br />
made with care by floatpane.
</p>
@@ -504,7 +467,6 @@ function Footer() {
<div>
<div className="footer-h">product</div>
<a href="#features">features</a>
- <a href="#keys">keybinds</a>
<a href="#install">install</a>
<a href="https://github.com/floatpane/matcha/releases">releases</a>
</div>
@@ -544,79 +506,17 @@ function Footer() {
);
}
-// ---------- Tweaks ----------
-function Tweaks({ datasetKey, setDatasetKey, visible }) {
- if (!visible) return null;
- return (
- <div className="tweaks">
- <div className="tweaks-head">Tweaks</div>
- <div className="tweaks-sub">Demo content</div>
- {Object.entries({
- default: "drew's inbox",
- dev: "floatpane dev",
- personal: "personal",
- }).map(([k, label]) => (
- <button
- key={k}
- onClick={() => setDatasetKey(k)}
- className={"tweaks-opt " + (datasetKey === k ? "on" : "")}
- >
- <span className="tweaks-dot">{datasetKey === k ? "โ" : "โ"}</span>
- <span>{label}</span>
- </button>
- ))}
- </div>
- );
-}
-
function App() {
- const [datasetKey, setDatasetKey] = useState(() => {
- try {
- return localStorage.getItem("matcha-dataset") || "default";
- } catch {
- return "default";
- }
- });
- const [tweaksVisible, setTweaksVisible] = useState(false);
-
- useEffect(() => {
- try {
- localStorage.setItem("matcha-dataset", datasetKey);
- } catch {}
- }, [datasetKey]);
-
- useEffect(() => {
- const onMsg = (e) => {
- const d = e.data || {};
- if (d.type === "__activate_edit_mode") setTweaksVisible(true);
- if (d.type === "__deactivate_edit_mode") setTweaksVisible(false);
- };
- window.addEventListener("message", onMsg);
- window.parent.postMessage({ type: "__edit_mode_available" }, "*");
- return () => window.removeEventListener("message", onMsg);
- }, []);
-
- useEffect(() => {
- window.parent.postMessage(
- { type: "__edit_mode_set_keys", edits: { datasetKey } },
- "*",
- );
- }, [datasetKey]);
-
+ useScrollReveal();
+ useMouseGlow();
return (
<div className="site">
<TopNav />
- <Hero datasetKey={datasetKey} setDatasetKey={setDatasetKey} />
+ <Hero />
<Features />
- <Keybinds />
<Install />
<CTA />
<Footer />
- <Tweaks
- datasetKey={datasetKey}
- setDatasetKey={setDatasetKey}
- visible={tweaksVisible}
- />
</div>
);
}
@@ -1,6 +1,5 @@
/* ============ Matcha site styles ============ */
:root {
- /* Dark blue-green terminal, like the real Matcha screenshots */
--bg: #05080a;
--bg-2: #060a0d;
--panel: #0a1013;
@@ -11,7 +10,6 @@
--ink-dim: #7d9099;
--ink-dim2: #52666e;
- /* green accent โ slightly darker, per request */
--accent: #6aa84f;
--accent-2: #85c26a;
--accent-deep: #3b6b28;
@@ -24,11 +22,20 @@
--sans:
"IBM Plex Sans", ui-sans-serif, -apple-system, system-ui, sans-serif;
--serif: "IBM Plex Serif", ui-serif, Georgia, serif;
+
+ /* mouse glow position โ updated by JS */
+ --mx: 50%;
+ --my: 30%;
}
* {
box-sizing: border-box;
}
+
+html {
+ scroll-behavior: smooth;
+}
+
html,
body {
margin: 0;
@@ -41,6 +48,8 @@ body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
+
+/* Static ambient gradient */
body::before {
content: "";
position: fixed;
@@ -59,28 +68,48 @@ body::before {
transparent 60%
);
}
+
+/* Mouse-follow glow overlay */
+body::after {
+ content: "";
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ background: radial-gradient(
+ 560px circle at var(--mx) var(--my),
+ rgba(106, 168, 79, 0.055),
+ transparent 70%
+ );
+}
+
a {
color: inherit;
text-decoration: none;
}
+
code,
kbd,
pre {
font-family: var(--mono);
}
+
em {
font-style: italic;
color: var(--accent);
font-family: var(--serif);
font-weight: 500;
}
+
.dim {
color: var(--ink-dim);
}
+
.underline-link {
color: var(--accent);
border-bottom: 1px dashed var(--accent-deep);
padding-bottom: 1px;
+ transition: color 0.15s;
}
.underline-link:hover {
color: var(--accent-2);
@@ -107,10 +136,12 @@ em {
backdrop-filter: blur(12px);
z-index: 50;
}
+
.nav-brand {
display: flex;
align-items: center;
}
+
.wordmark {
display: inline-flex;
align-items: center;
@@ -119,6 +150,7 @@ em {
font-size: 15px;
letter-spacing: -0.01em;
}
+
.wm-bracket {
color: var(--accent);
font-size: 12px;
@@ -132,7 +164,6 @@ em {
color: var(--ink-dim2);
font-size: 12px;
}
-
.wm-logo {
display: flex;
align-items: center;
@@ -148,11 +179,26 @@ em {
}
.nav-links a {
color: var(--ink-dim);
- transition: color 0.12s;
+ transition: color 0.15s;
+ position: relative;
+}
+.nav-links a::after {
+ content: "";
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 0;
+ height: 1px;
+ background: var(--accent);
+ transition: width 0.25s ease;
}
.nav-links a:hover {
color: var(--ink);
}
+.nav-links a:hover::after {
+ width: 100%;
+}
+
.nav-github {
display: inline-flex;
align-items: center;
@@ -180,7 +226,10 @@ em {
color: var(--ink);
background: transparent;
cursor: pointer;
- transition: all 0.12s;
+ transition:
+ border-color 0.15s,
+ color 0.15s,
+ box-shadow 0.25s;
}
.btn:hover {
border-color: var(--accent);
@@ -189,6 +238,7 @@ em {
.btn-k {
font-size: 11px;
color: var(--ink-dim);
+ transition: color 0.15s;
}
.btn:hover .btn-k {
color: var(--accent);
@@ -203,6 +253,7 @@ em {
background: var(--accent-2);
border-color: var(--accent-2);
color: #05080a;
+ box-shadow: 0 0 28px rgba(106, 168, 79, 0.35);
}
.btn-primary .btn-k {
color: #05080a;
@@ -216,16 +267,45 @@ em {
font-size: 14px;
}
+/* ---------- Hero load animations ---------- */
+@keyframes heroFadeUp {
+ from {
+ opacity: 0;
+ transform: translateY(18px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hero-eyebrow {
+ animation: heroFadeUp 0.55s cubic-bezier(0.22, 1, 0.36, 1) both 0.05s;
+}
+.hero-h1 {
+ animation: heroFadeUp 0.65s cubic-bezier(0.22, 1, 0.36, 1) both 0.18s;
+}
+.hero-sub {
+ animation: heroFadeUp 0.65s cubic-bezier(0.22, 1, 0.36, 1) both 0.32s;
+}
+.hero-cta {
+ animation: heroFadeUp 0.65s cubic-bezier(0.22, 1, 0.36, 1) both 0.46s;
+}
+.quickstart {
+ animation: heroFadeUp 0.65s cubic-bezier(0.22, 1, 0.36, 1) both 0.58s;
+}
+.hero-meta {
+ animation: heroFadeUp 0.65s cubic-bezier(0.22, 1, 0.36, 1) both 0.72s;
+}
+
/* ---------- Hero ---------- */
.hero {
- display: grid;
- grid-template-columns: 1fr 1.35fr;
- gap: 56px;
- padding: 72px 0 80px;
- align-items: start;
+ padding: 96px 0 72px;
+ text-align: center;
}
.hero-copy {
- padding-top: 24px;
+ max-width: 720px;
+ margin: 0 auto;
}
.hero-eyebrow {
display: inline-flex;
@@ -253,37 +333,44 @@ em {
opacity: 1;
}
50% {
- opacity: 0.4;
+ opacity: 0.35;
}
}
.hero-h1 {
font-family: var(--sans);
- font-size: 56px;
+ font-size: 62px;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1.04;
margin: 0 0 24px;
text-wrap: pretty;
}
+.hero-h1 em {
+ text-shadow: 0 0 56px rgba(106, 168, 79, 0.4);
+}
.hero-sub {
font-size: 17px;
color: var(--ink-dim);
- max-width: 46ch;
- margin: 0 0 36px;
- line-height: 1.55;
+ max-width: 50ch;
+ margin: 0 auto 36px;
+ line-height: 1.6;
}
.hero-cta {
display: flex;
gap: 12px;
- margin-bottom: 44px;
+ justify-content: center;
+ margin-bottom: 36px;
+ flex-wrap: wrap;
}
.hero-meta {
- display: grid;
- grid-template-columns: repeat(3, auto);
+ display: flex;
gap: 32px;
+ justify-content: center;
+ flex-wrap: wrap;
font-family: var(--mono);
font-size: 12px;
color: var(--ink);
+ margin-top: 32px;
}
.hero-meta .dim {
display: block;
@@ -294,406 +381,75 @@ em {
letter-spacing: 0.08em;
}
-.hero-demo {
- position: relative;
-}
-.hero-demo-chrome {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 0 10px;
- font-family: var(--mono);
- font-size: 12px;
-}
-.hero-demo-label {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--ink-dim);
-}
-.demo-swap {
- background: transparent;
- border: 1px solid var(--line);
- color: var(--ink-dim);
- padding: 3px 8px;
- font-family: var(--mono);
- font-size: 11px;
- border-radius: 2px;
- cursor: pointer;
- transition: all 0.12s;
-}
-.demo-swap:hover {
- color: var(--ink);
- border-color: var(--line-2);
-}
-.demo-swap.on {
- color: var(--accent);
- border-color: var(--accent);
-}
-.hero-demo-hint {
- color: var(--ink-dim);
-}
-.hero-demo-hint kbd {
- display: inline-block;
- margin: 0 2px;
- padding: 1px 5px;
- border: 1px solid var(--line-2);
- border-radius: 2px;
- font-size: 10px;
- color: var(--ink);
-}
-
-@media (max-width: 960px) {
- .hero {
- grid-template-columns: 1fr;
- }
- .hero-h1 {
- font-size: 40px;
- }
-}
-
-/* ---------- TUI โ matches real Matcha layout ---------- */
-.tui-root {
- position: relative;
- background: var(--bg);
+/* ---------- QuickStart ---------- */
+.quickstart {
+ background: var(--panel-2);
border: 1px solid var(--line-2);
border-radius: 6px;
overflow: hidden;
- font-family: var(--mono);
- font-size: 12px;
- line-height: 1.5;
- color: var(--ink);
- outline: none;
+ margin: 0 auto 8px;
+ max-width: 480px;
+ text-align: left;
box-shadow:
- 0 1px 0 rgba(255, 255, 255, 0.02) inset,
- 0 30px 80px -20px rgba(0, 0, 0, 0.7),
- 0 10px 30px -10px rgba(106, 168, 79, 0.08);
- user-select: none;
+ 0 20px 60px -20px rgba(0, 0, 0, 0.5),
+ 0 8px 20px -8px rgba(106, 168, 79, 0.06);
transition:
- box-shadow 0.2s,
- border-color 0.2s;
- min-height: 520px;
+ border-color 0.25s,
+ box-shadow 0.25s;
}
-.tui-root.is-focused {
- border-color: var(--accent);
+.quickstart:hover {
+ border-color: var(--accent-deep);
box-shadow:
- 0 0 0 1px var(--accent),
- 0 30px 80px -20px rgba(0, 0, 0, 0.7),
- 0 10px 30px -10px rgba(106, 168, 79, 0.2);
-}
-
-.tui-titlebar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 12px;
- background: #02050a;
- border-bottom: 1px solid var(--line);
- font-size: 11px;
- color: var(--ink-dim);
+ 0 24px 70px -20px rgba(0, 0, 0, 0.55),
+ 0 8px 28px -8px rgba(106, 168, 79, 0.16);
}
-.tui-titlebar-left {
+.qs-bar {
display: flex;
align-items: center;
- gap: 10px;
-}
-.tui-traffic {
- display: inline-flex;
gap: 6px;
+ padding: 9px 14px;
+ background: var(--bg-2);
+ border-bottom: 1px solid var(--line);
}
-.tl {
- width: 11px;
- height: 11px;
+.qs-dot {
+ width: 10px;
+ height: 10px;
border-radius: 50%;
- display: inline-block;
}
-.tl-r {
+.qs-r {
background: #2a1414;
}
-.tl-y {
+.qs-y {
background: #2a2514;
}
-.tl-g {
+.qs-g {
background: #0f2a16;
}
-.tui-title-text {
- color: var(--ink-dim);
-}
-.tui-titlebar-right {
- display: flex;
- gap: 8px;
-}
-.tui-titlebar-right .sep {
- color: var(--ink-dim2);
-}
-
-.tui-shell {
- display: grid;
- grid-template-columns: 160px 1fr;
- min-height: 460px;
-}
-
-/* Sidebar โ folders */
-.tui-sidebar {
- background: var(--bg-2);
- padding: 10px 0;
- border-right: 1px solid var(--line);
- font-size: 12px;
-}
-.tui-sidebar-head {
- padding: 2px 14px 10px;
- color: var(--accent);
- font-weight: 500;
-}
-.tui-folder {
- padding: 2px 14px;
- color: var(--ink-dim);
- cursor: pointer;
- white-space: nowrap;
-}
-.tui-folder:hover {
- color: var(--ink);
-}
-.tui-folder.active {
- background: var(--accent);
- color: #05080a;
- font-weight: 500;
-}
-
-/* Main pane */
-.tui-main {
- position: relative;
- padding: 0;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-/* Account tabs row */
-.tui-acct-row {
- display: flex;
- gap: 22px;
- padding: 8px 16px;
- border-bottom: 1px solid var(--line);
- font-size: 12px;
-}
-.tui-acct {
- color: var(--ink-dim);
- cursor: pointer;
- padding: 2px 0;
-}
-.tui-acct:hover {
- color: var(--ink);
-}
-.tui-acct.active {
- color: var(--accent);
- border-bottom: 1px solid var(--accent);
- padding-bottom: 1px;
-}
-
-.tui-heading {
- padding: 14px 16px 2px;
- color: var(--accent);
- font-weight: 500;
- font-size: 12.5px;
-}
-.tui-subhead {
- padding: 0 16px 8px;
- color: var(--ink-dim);
- font-size: 11.5px;
-}
-.tui-filter-chip {
- color: var(--yellow);
- margin-left: 10px;
-}
-
-.tui-list {
- flex: 1;
- padding: 2px 16px 40px;
- position: relative;
- z-index: 2;
-}
-
-.tui-row {
- display: grid;
- grid-template-columns: 18px 26px auto 14px auto 14px 1fr auto;
- gap: 6px;
- align-items: baseline;
- padding: 1px 0;
- cursor: pointer;
- color: var(--ink-dim);
- white-space: nowrap;
- font-size: 12px;
-}
-.tui-row:hover {
- color: var(--ink);
-}
-.tui-row.cur {
- color: var(--accent);
- background: rgba(106, 168, 79, 0.04);
-}
-.tui-row.visual.cur {
- color: var(--yellow);
-}
-.tui-row-cursor {
- color: var(--accent);
- white-space: pre;
- font-weight: 600;
-}
-.tui-row.visual .tui-row-cursor {
- color: var(--yellow);
-}
-.tui-row-num {
- color: var(--ink-dim2);
-}
-.tui-row-acct {
- color: var(--blue);
-}
-.tui-row.cur .tui-row-acct {
- color: var(--blue);
-}
-.tui-row-sep {
- color: var(--ink-dim2);
-}
-.tui-row-from {
- color: var(--ink);
-}
-.tui-row.cur .tui-row-from {
- color: var(--accent);
-}
-.tui-row-subj {
- overflow: hidden;
- text-overflow: ellipsis;
- color: var(--ink-dim);
-}
-.tui-row.cur .tui-row-subj {
- color: var(--accent-2);
-}
-.tui-row-date {
- color: var(--ink-dim2);
- padding-left: 12px;
-}
-.tui-row.cur .tui-row-date {
- color: var(--green-soft);
-}
-.tui-row-dots {
- color: var(--ink-dim2);
- padding-top: 12px;
- letter-spacing: 4px;
-}
-.tui-row-empty {
- color: var(--ink-dim2);
- padding: 32px 0;
- text-align: center;
-}
-
-/* Email view */
-.tui-email {
- position: relative;
- flex: 1;
- padding: 0;
- display: flex;
- flex-direction: column;
-}
-.tui-email-head {
- padding: 10px 16px;
- color: var(--ink);
- border-bottom: 1px solid var(--line);
- font-size: 12px;
-}
-.tui-email-body {
- padding: 16px;
- flex: 1;
- color: var(--ink);
- position: relative;
- z-index: 2;
-}
-.tui-email-line {
- white-space: pre-wrap;
-}
-.tui-link {
- color: var(--blue);
- border-bottom: 1px solid var(--blue);
- cursor: pointer;
-}
-
-/* Watermark */
-.tui-watermark {
- position: absolute;
- left: 0;
- right: 0;
- bottom: 30px;
- width: 100%;
- height: 60%;
- color: var(--ink-dim2);
- pointer-events: none;
- z-index: 1;
- opacity: 0.55;
-}
-
-/* Flash */
-.tui-flash {
- position: absolute;
- top: 52px;
- right: 16px;
- background: rgba(5, 8, 10, 0.94);
- border: 1px solid var(--accent);
- color: var(--accent);
- padding: 4px 10px;
+.qs-bar-label {
+ font-family: var(--mono);
font-size: 11px;
- border-radius: 2px;
- z-index: 6;
-}
-
-/* Status / legend line โ dim like the real TUI */
-.tui-status {
- display: flex;
- flex-wrap: wrap;
- gap: 0 14px;
- padding: 6px 16px;
- border-top: 1px solid var(--line);
- background: var(--bg-2);
color: var(--ink-dim2);
- font-size: 11px;
- min-height: 26px;
-}
-.tui-status .seg {
- color: var(--ink-dim);
- white-space: nowrap;
-}
-.tui-status .seg.hi {
- color: var(--yellow);
-}
-
-/* Filter command line */
-.tui-cmdline {
- position: absolute;
- bottom: 26px;
- left: 0;
- right: 0;
- padding: 5px 12px;
- background: #02050a;
- color: var(--accent);
- border-top: 1px solid var(--accent);
- font-size: 12px;
- display: flex;
- align-items: center;
- gap: 4px;
- z-index: 5;
+ margin-left: 6px;
}
-.tui-cmdline-prompt {
+.qs-code {
+ margin: 0;
+ padding: 20px 24px;
+ font-family: var(--mono);
+ font-size: 14px;
+ line-height: 2;
color: var(--accent);
+ min-height: 80px;
}
-.tui-cmdline-buf {
- color: var(--ink);
+.qs-prompt {
+ color: var(--ink-dim2);
}
-.tui-caret {
+.qs-caret {
display: inline-block;
- width: 7px;
- height: 13px;
+ width: 8px;
+ height: 15px;
background: var(--accent);
- margin-left: 2px;
- transform: translateY(2px);
+ vertical-align: text-bottom;
+ margin-left: 1px;
animation: blink 1s step-end infinite;
}
@keyframes blink {
@@ -702,27 +458,17 @@ em {
}
}
-.tui-focus-hint {
- position: absolute;
- top: 48px;
- right: 16px;
- padding: 5px 10px;
- background: rgba(5, 8, 10, 0.9);
- border: 1px solid var(--line-2);
- border-radius: 2px;
- font-size: 11px;
- color: var(--ink-dim);
- pointer-events: none;
- z-index: 3;
+/* ---------- Scroll reveal ---------- */
+.reveal {
+ opacity: 0;
+ transform: translateY(16px);
+ transition:
+ opacity 0.7s ease,
+ transform 0.7s ease;
}
-.tui-focus-hint kbd {
- display: inline-block;
- margin: 0 1px;
- padding: 0 4px;
- border: 1px solid var(--line-2);
- background: var(--panel);
- border-radius: 2px;
- color: var(--ink);
+.reveal.visible {
+ opacity: 1;
+ transform: translateY(0);
}
/* ---------- Sections ---------- */
@@ -776,12 +522,32 @@ em {
margin-top: 24px;
}
.feature {
+ position: relative;
+ overflow: hidden;
background: var(--bg);
padding: 32px 28px;
min-height: 240px;
display: flex;
flex-direction: column;
- transition: background 0.2s;
+ transition:
+ background 0.2s,
+ opacity 0.7s ease,
+ transform 0.7s ease;
+}
+.feature::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, var(--accent), var(--accent-2));
+ transform: scaleX(0);
+ transform-origin: left;
+ transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
+}
+.feature:hover::before {
+ transform: scaleX(1);
}
.feature:hover {
background: var(--bg-2);
@@ -822,66 +588,10 @@ em {
color: var(--accent);
border-radius: 2px;
white-space: pre-wrap;
+ transition: border-color 0.2s;
}
-@media (max-width: 960px) {
- .feature-grid {
- grid-template-columns: 1fr;
- }
- .section-head {
- grid-template-columns: 1fr;
- gap: 20px;
- }
- .section-h2 {
- font-size: 32px;
- }
-}
-
-/* Keybinds */
-.keybinds-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 40px 32px;
- margin-top: 24px;
- padding: 32px 0 80px;
-}
-.keybinds-col-head {
- font-family: var(--mono);
- font-size: 12px;
- color: var(--accent);
- margin-bottom: 16px;
- letter-spacing: 0.04em;
-}
-.keybind-row {
- display: flex;
- align-items: baseline;
- gap: 8px;
- margin-bottom: 8px;
- font-family: var(--mono);
- font-size: 12.5px;
-}
-.keybind-k kbd {
- display: inline-block;
- padding: 1px 8px;
- border: 1px solid var(--line-2);
- background: var(--panel);
- color: var(--ink);
- border-radius: 2px;
- font-size: 11.5px;
- white-space: nowrap;
-}
-.keybind-dots {
- color: var(--line-2);
- flex: 1;
- overflow: hidden;
- white-space: nowrap;
-}
-.keybind-label {
- color: var(--ink-dim);
-}
-@media (max-width: 960px) {
- .keybinds-grid {
- grid-template-columns: repeat(2, 1fr);
- }
+.feature:hover .feature-mono {
+ border-color: var(--accent-deep);
}
/* Install */
@@ -891,6 +601,10 @@ em {
border-radius: 4px;
overflow: hidden;
margin-bottom: 80px;
+ transition: border-color 0.25s;
+}
+.install-card:hover {
+ border-color: var(--line-2);
}
.install-tabs {
display: flex;
@@ -909,7 +623,9 @@ em {
font-size: 13px;
cursor: pointer;
border-bottom: 2px solid transparent;
- transition: all 0.12s;
+ transition:
+ color 0.15s,
+ border-color 0.15s;
white-space: nowrap;
}
.install-tab:hover {
@@ -990,11 +706,6 @@ em {
justify-content: center;
flex-wrap: wrap;
}
-@media (max-width: 960px) {
- .cta-h2 {
- font-size: 36px;
- }
-}
/* Footer */
.footer {
@@ -1031,6 +742,7 @@ em {
font-size: 13px;
padding: 4px 0;
cursor: pointer;
+ transition: color 0.15s;
}
.footer-cols a:hover {
color: var(--accent);
@@ -1057,70 +769,111 @@ em {
.footer-meta .sep {
color: var(--line-2);
}
+
+/* ---------- Responsive ---------- */
@media (max-width: 960px) {
+ .hero-h1 {
+ font-size: 46px;
+ }
+ .feature-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ .section-head {
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }
+ .section-h2 {
+ font-size: 34px;
+ }
.footer-top {
grid-template-columns: 1fr;
}
.footer-cols {
grid-template-columns: repeat(2, 1fr);
}
+ .cta-h2 {
+ font-size: 40px;
+ }
}
-/* Tweaks */
-.tweaks {
- position: fixed;
- bottom: 20px;
- right: 20px;
- background: var(--bg-2);
- border: 1px solid var(--accent);
- padding: 14px;
- border-radius: 4px;
- min-width: 220px;
- font-family: var(--mono);
- box-shadow:
- 0 20px 40px -10px rgba(0, 0, 0, 0.6),
- 0 0 0 4px rgba(106, 168, 79, 0.06);
- z-index: 100;
-}
-.tweaks-head {
- font-size: 11px;
- color: var(--accent);
- text-transform: uppercase;
- letter-spacing: 0.12em;
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px dashed var(--line-2);
-}
-.tweaks-sub {
- font-size: 10px;
- color: var(--ink-dim2);
- text-transform: uppercase;
- letter-spacing: 0.08em;
- margin-bottom: 8px;
-}
-.tweaks-opt {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 100%;
- padding: 6px 8px;
- background: transparent;
- border: none;
- color: var(--ink-dim);
- font-family: var(--mono);
- font-size: 12px;
- text-align: left;
- cursor: pointer;
- border-radius: 2px;
-}
-.tweaks-opt:hover {
- background: var(--panel);
- color: var(--ink);
-}
-.tweaks-opt.on {
- color: var(--accent);
+@media (max-width: 640px) {
+ .site {
+ padding: 0 20px;
+ }
+ .nav-links {
+ display: none;
+ }
+ .nav-right {
+ display: none;
+ }
+ .hero {
+ padding: 64px 0 48px;
+ }
+ .hero-h1 {
+ font-size: 36px;
+ }
+ .hero-sub {
+ font-size: 15px;
+ }
+ .hero-cta .btn-lg {
+ padding: 12px 18px;
+ font-size: 13px;
+ }
+ .quickstart {
+ max-width: 100%;
+ }
+ .qs-code {
+ font-size: 12px;
+ padding: 16px 18px;
+ }
+ .hero-meta {
+ flex-direction: column;
+ gap: 12px;
+ align-items: center;
+ }
+ .feature-grid {
+ grid-template-columns: 1fr;
+ }
+ .section-h2 {
+ font-size: 28px;
+ }
+ .cta-h2 {
+ font-size: 30px;
+ }
+ .install-code {
+ font-size: 13px;
+ padding: 24px 18px;
+ }
+ .footer-cols {
+ grid-template-columns: repeat(2, 1fr);
+ }
}
-.tweaks-dot {
- color: var(--accent);
- width: 10px;
+
+/* ---------- Reduced motion ---------- */
+@media (prefers-reduced-motion: reduce) {
+ .hero-eyebrow,
+ .hero-h1,
+ .hero-sub,
+ .hero-cta,
+ .quickstart,
+ .hero-meta {
+ animation: none;
+ }
+ .reveal {
+ opacity: 1;
+ transform: none;
+ transition: none;
+ }
+ .feature {
+ opacity: 1;
+ transform: none;
+ }
+ .dot-live,
+ .qs-caret {
+ animation: none;
+ opacity: 1;
+ }
+ .feature::before {
+ transition: none;
+ }
}