Detailed changes
@@ -1,3 +1,7 @@
clib/stb_image.h linguist-vendored
+*.jsx linguist-detectable=false
+*.css linguist-detectable=false
+*.js linguist-detectable=false
+*.json linguist-detectable=false
*.html linguist-detectable=false
public/** linguist-documentation
@@ -1,30 +0,0 @@
-# public
-
-Static website files for the Matcha project landing page. This site is deployed via GitHub Pages using the `.github/workflows/static.yml` workflow.
-
-## Architecture
-
-A simple static HTML/CSS website that serves as the public-facing project page with information about features, installation, and contact details.
-
-## Files
-
-| File | Description |
-|------|-------------|
-| `index.html` | Main landing page. |
-| `style.css` | Stylesheet for all pages. |
-| `matcha.flatpakref` | Flatpak package reference for Linux distribution. |
-| `features/index.html` | Features overview page. |
-| `get-started/index.html` | Getting started / installation guide page. |
-| `contact/index.html` | Contact information page. |
-
-### assets/
-
-| File | Description |
-|------|-------------|
-| `demo.gif` | Animated terminal demo. |
-| `logo.png` | Project logo. |
-| `logo-transparent.png` | Logo with transparent background. |
-| `logo.txt` | ASCII text version of the logo. |
-| `favicon.ico` | Browser favicon. |
-| `preview.png` | Social media / link preview image. |
-| `screenshots/` | Directory for generated UI screenshots (populated by CI). |
@@ -0,0 +1,630 @@
+const { useState, useEffect } = React;
+
+function FloatpaneMark({ size = 20 }) {
+ return (
+ <img
+ src="../assets/floatpane.png"
+ alt="Floatpane logo"
+ className="wm-logo"
+ height={size}
+ width={size}
+ />
+ );
+}
+
+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) => {
+ if (d && d.tag_name)
+ setVersion(
+ d.tag_name.startsWith("v") ? d.tag_name : "v" + d.tag_name,
+ );
+ })
+ .catch(() => {});
+ }, []);
+ return (
+ <div className="wordmark">
+ <img
+ src="../assets/logo-transparent.png"
+ alt="Matcha logo"
+ className="wm-logo"
+ height={24}
+ width={24}
+ />
+ <span className="wm-name">matcha</span>
+ <span className="wm-dim">โ {version}</span>
+ </div>
+ );
+}
+
+function TopNav() {
+ const [stars, setStars] = useState(null);
+ useEffect(() => {
+ fetch("https://api.github.com/repos/floatpane/matcha")
+ .then((r) => (r.ok ? r.json() : null))
+ .then((d) => {
+ if (d && typeof d.stargazers_count === "number") {
+ const n = d.stargazers_count;
+ setStars(n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
+ }
+ })
+ .catch(() => {});
+ }, []);
+ return (
+ <header className="nav">
+ <a href="#top" className="nav-brand">
+ <MatchaWordmark />
+ </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.floatpane.com">Docs โ</a>
+ <a href="https://github.com/floatpane/matcha" className="nav-github">
+ <span>GitHub</span>
+ {stars && <span className="nav-star">โ
{stars}</span>}
+ </a>
+ </nav>
+ <div className="nav-right">
+ <a href="#install" className="btn btn-ghost">
+ install
+ </a>
+ </div>
+ </header>
+ );
+}
+
+function Hero({ datasetKey, setDatasetKey }) {
+ const [pressed, setPressed] = useState(false);
+ const TUI = window.MatchaTUI;
+ return (
+ <section className="hero" id="top">
+ <div className="hero-copy">
+ <div className="hero-eyebrow">
+ <span className="dot-live" />
+ <span>by floatpane ยท local-first ยท secure ยท no telemetry</span>
+ </div>
+ <h1 className="hero-h1">
+ A powerful, feature-rich
+ <br />
+ email client <em>for your 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.
+ </p>
+ <div className="hero-cta">
+ <a href="#install" className="btn btn-primary">
+ <span>Install</span>
+ <span className="btn-k">โต</span>
+ </a>
+ <a href="https://docs.matcha.floatpane.com" className="btn btn-ghost">
+ <span>Read the docs</span>
+ <span className="btn-k">โ</span>
+ </a>
+ </div>
+ <div className="hero-meta">
+ <div>
+ <span className="dim">license</span> MIT
+ </div>
+ <div>
+ <span className="dim">runtime</span> single static binary
+ </div>
+ <div>
+ <span className="dim">platforms</span> macOS ยท Linux ยท Windows
+ </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>
+ );
+}
+
+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 โต",
+ },
+ {
+ 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.",
+ mono: "v j j j d\nโ deleted 4 messages",
+ },
+ {
+ k: "03",
+ title: "Compose in markdown",
+ body: "Write in the syntax you already know. Headings, lists, fenced code, and tables render cleanly on the other side.",
+ mono: "# subject\n- bullet\n`inline`",
+ },
+ {
+ 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.",
+ 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.",
+ 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.",
+ mono: "~/.cache/matcha/drafts",
+ },
+ {
+ k: "07",
+ title: "CLI that composes",
+ body: "Pipe errors into apologies. Send from scripts, CI, or cron. `matcha send` does one thing well.",
+ mono: "$ matcha send --to โฆ",
+ },
+ {
+ 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",
+ },
+ {
+ 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",
+ },
+];
+
+function Features() {
+ return (
+ <section className="features" id="features">
+ <div className="section-head">
+ <div className="section-head-l">
+ <div className="section-eyebrow">ยง features</div>
+ <h2 className="section-h2">
+ Everything you'd expect.
+ <br />
+ <span className="dim">And nothing you wouldn't.</span>
+ </h2>
+ </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
+ 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">
+ <div className="feature-head">
+ <span className="feature-k">{f.k}</span>
+ <span className="feature-dash">โโ</span>
+ </div>
+ <h3 className="feature-title">{f.title}</h3>
+ <p className="feature-body">{f.body}</p>
+ <pre className="feature-mono">{f.mono}</pre>
+ </article>
+ ))}
+ </div>
+ </section>
+ );
+}
+
+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.floatpane.com"
+ className="underline-link"
+ >
+ docs.matcha.floatpane.com
+ </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",
+ cmd: "$ brew install floatpane/matcha/matcha\n$ matcha",
+ },
+ winget: {
+ plat: "Windows 10 / 11",
+ cmd: "$ winget install --id=floatpane.matcha\n$ matcha",
+ },
+ snap: { plat: "Ubuntu ยท Linux", cmd: "$ sudo snap install matcha\n$ matcha" },
+ flatpak: {
+ plat: "Linux",
+ cmd: "$ flatpak install https://matcha.floatpane.com/matcha.flatpakref\n$ matcha",
+ },
+ 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",
+ },
+};
+
+function Install() {
+ const [tab, setTab] = useState("brew");
+ const [meta, setMeta] = useState({
+ version: "0.8.2",
+ date: "apr 23, 2026",
+ size: null,
+ });
+ useEffect(() => {
+ fetch("https://api.github.com/repos/floatpane/matcha/releases/latest")
+ .then((r) => (r.ok ? r.json() : null))
+ .then((d) => {
+ if (!d) return;
+ const version = (d.tag_name || "").replace(/^v/, "") || "0.8.2";
+ const date = d.published_at
+ ? new Date(d.published_at)
+ .toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })
+ .toLowerCase()
+ : "apr 23, 2026";
+ const asset = (d.assets || []).find((a) => a.size) || null;
+ const size = asset
+ ? (asset.size / (1024 * 1024)).toFixed(1) + " MB"
+ : null;
+ setMeta({ version, date, size });
+ })
+ .catch(() => {});
+ }, []);
+ const t = INSTALL_TABS[tab];
+ return (
+ <section className="install" id="install">
+ <div className="section-head">
+ <div className="section-head-l">
+ <div className="section-eyebrow">ยง install</div>
+ <h2 className="section-h2">
+ One binary.
+ <br />
+ <span className="dim">Pick your package manager.</span>
+ </h2>
+ </div>
+ <p className="section-head-r">
+ No runtime. Ships natively for macOS, Linux, Windows. Source and
+ issues at{" "}
+ <a
+ href="https://github.com/floatpane/matcha"
+ className="underline-link"
+ >
+ github.com/floatpane/matcha
+ </a>
+ .
+ </p>
+ </div>
+ <div className="install-card">
+ <div className="install-tabs">
+ {Object.keys(INSTALL_TABS).map((k) => (
+ <button
+ key={k}
+ onClick={() => setTab(k)}
+ className={"install-tab " + (tab === k ? "active" : "")}
+ >
+ {k}
+ </button>
+ ))}
+ <div className="install-tabs-spacer" />
+ <span className="install-plat">{t.plat}</span>
+ </div>
+ <pre className="install-code">{t.cmd}</pre>
+ <div className="install-foot">
+ <div>
+ <span className="dim">latest</span> {meta.version} ยท {meta.date}
+ </div>
+ <div>
+ <span className="dim">source</span> github.com/floatpane/matcha
+ </div>
+ </div>
+ </div>
+ </section>
+ );
+}
+
+function CTA() {
+ return (
+ <section className="cta">
+ <div className="cta-inner">
+ <div className="cta-pre">$ _</div>
+ <h2 className="cta-h2">
+ Your inbox is waiting
+ <br />
+ in the terminal.
+ </h2>
+ <div className="cta-row">
+ <a href="#install" className="btn btn-primary btn-lg">
+ make your emails secure
+ </a>
+ <a
+ href="https://docs.matcha.floatpane.com"
+ className="btn btn-ghost btn-lg"
+ >
+ read the docs โ
+ </a>
+ </div>
+ </div>
+ </section>
+ );
+}
+
+function Footer() {
+ return (
+ <footer className="footer">
+ <div className="footer-top">
+ <div className="footer-brand">
+ <MatchaWordmark />
+ <p className="footer-tag">
+ a modern TUI email client.
+ <br />
+ made with care by floatpane.
+ </p>
+ </div>
+ <div className="footer-cols">
+ <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>
+ <div>
+ <div className="footer-h">resources</div>
+ <a href="https://docs.matcha.floatpane.com">docs</a>
+ <a href="https://docs.matcha.floatpane.com/Configuration">config</a>
+ <a href="https://docs.matcha.floatpane.com/Features/CLI">cli</a>
+ <a href="https://github.com/floatpane/matcha/blob/master/SECURITY.md">
+ security
+ </a>
+ </div>
+ <div>
+ <div className="footer-h">community</div>
+ <a href="https://github.com/floatpane/matcha">github</a>
+ <a href="https://discord.gg/RxNrJgfatk">discord</a>
+ <a href="https://fosstodon.org/@floatpane">mastodon</a>
+ </div>
+ <div>
+ <div className="footer-h">floatpane</div>
+ <a href="https://floatpane.com">website</a>
+ <a href="mailto:us@floatpane.com">contact</a>
+ <a href="mailto:support@floatpane.com">support</a>
+ </div>
+ </div>
+ </div>
+ <div className="footer-bot">
+ <div className="footer-copy">
+ <FloatpaneMark size={16} />
+ <span>
+ ยฉ {new Date().getFullYear()} floatpane ยท MIT licensed ยท no trackers
+ on this page
+ </span>
+ </div>
+ </div>
+ </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]);
+
+ return (
+ <div className="site">
+ <TopNav />
+ <Hero datasetKey={datasetKey} setDatasetKey={setDatasetKey} />
+ <Features />
+ <Keybinds />
+ <Install />
+ <CTA />
+ <Footer />
+ <Tweaks
+ datasetKey={datasetKey}
+ setDatasetKey={setDatasetKey}
+ visible={tweaksVisible}
+ />
+ </div>
+ );
+}
+
+window.MatchaApp = App;
@@ -0,0 +1,940 @@
+// Interactive TUI email client demo โ matches real Matcha layout
+const { useState, useEffect, useRef, useCallback, useMemo } = React;
+
+// ---------- Data: folders, accounts, messages ----------
+const FOLDERS_LIST = [
+ "INBOX",
+ "Archive",
+ "Deleted Messages",
+ "Drafts",
+ "Junk",
+ "Notes",
+ "Sent Messages",
+ "Work",
+ "Receipts",
+];
+
+const ACCOUNTS_DEFAULT = [
+ { id: "all", label: "ALL", isAll: true },
+ { id: "home", label: "sam@proton.me" },
+ { id: "work", label: "s.park@kestrel.works" },
+ { id: "side", label: "hello@driftnotes.xyz" },
+ { id: "news", label: "inbox@fastmail.sam" },
+];
+
+const MSGS_DEFAULT = [
+ {
+ acct: "work",
+ from: "GitHub",
+ subject:
+ "[kestrel/hedge] PR #184 ready for review โ refactor auth middleware",
+ date: "2 min ago",
+ },
+ {
+ acct: "home",
+ from: "Linear",
+ subject: 'You were assigned KST-219 ยท "Fix timezone drift in daily digest"',
+ date: "14 min ago",
+ },
+ {
+ acct: "home",
+ from: "Margo Tran",
+ subject: "re: pickup pottery on saturday?",
+ date: "1 hour ago",
+ },
+ {
+ acct: "side",
+ from: "Stripe",
+ subject: "Your payout of $642.11 is on its way",
+ date: "3 hours ago",
+ },
+ {
+ acct: "news",
+ from: "Hacker News Digest",
+ subject: 'Top 10: "Ask HN: what are you running on your homelab in 2026?"',
+ date: "5 hours ago",
+ },
+ {
+ acct: "home",
+ from: "DMV",
+ subject: "Appointment confirmed โ Tue May 12, 10:45 AM",
+ date: "8 hours ago",
+ },
+ {
+ acct: "work",
+ from: "Figma",
+ subject: 'Ines shared a file with you: "Hedge ยท onboarding v3"',
+ date: "Yesterday",
+ },
+ {
+ acct: "home",
+ from: "Alaska Airlines",
+ subject: "Check in for flight AS 1312 โ SEA โ SFO",
+ date: "Yesterday",
+ },
+ {
+ acct: "news",
+ from: "The Browser",
+ subject: "Five long reads for a slow sunday",
+ date: "Yesterday",
+ },
+ {
+ acct: "work",
+ from: "Vercel",
+ subject: "Deploy failed โ hedge-app (main) ยท missing env DATABASE_URL",
+ date: "21/04/2026 17:12",
+ },
+ {
+ acct: "side",
+ from: "Plausible Analytics",
+ subject: "driftnotes.xyz ยท 1,204 visitors this week (+18%)",
+ date: "21/04/2026 09:02",
+ },
+ {
+ acct: "home",
+ from: "Spotify",
+ subject: "Your Discover Weekly is ready",
+ date: "20/04/2026 08:00",
+ },
+ {
+ acct: "news",
+ from: "Metafilter",
+ subject: 'MeFi Digest โ "I finally fixed my kitchen sink (a story)"',
+ date: "19/04/2026 22:44",
+ },
+ {
+ acct: "work",
+ from: "Sentry",
+ subject:
+ "[hedge-prod] New issue: TimeoutError in worker.py:128 (ร42 events)",
+ date: "18/04/2026 13:27",
+ },
+ {
+ acct: "home",
+ from: "Oliver Kim",
+ subject: "we are going to regret this tattoo idea (attached)",
+ date: "17/04/2026 23:10",
+ },
+ {
+ acct: "home",
+ from: "Patagonia",
+ subject: "Your Nano Puff has shipped โ arriving Thu Apr 25",
+ date: "16/04/2026 11:03",
+ },
+ {
+ acct: "side",
+ from: "Cloudflare",
+ subject: "Reminder: driftnotes.xyz renews in 9 days",
+ date: "15/04/2026 04:18",
+ },
+ {
+ acct: "work",
+ from: "Notion",
+ subject: 'Weekly digest: 4 pages edited in "Kestrel / Engineering"',
+ date: "14/04/2026 17:55",
+ },
+ {
+ acct: "news",
+ from: "arXiv daily",
+ subject: "cs.DC โ 6 new submissions (2026-04-13)",
+ date: "13/04/2026 06:00",
+ },
+ {
+ acct: "home",
+ from: "mom",
+ subject: "did the package get there ok??",
+ date: "12/04/2026 19:44",
+ },
+];
+
+const MSGS_DEV = [
+ {
+ acct: "work",
+ from: "GitHub",
+ subject: "[kestrel/hedge] PR #184 approved by ines-w",
+ date: "09:42",
+ },
+ {
+ acct: "work",
+ from: "Vercel",
+ subject: "Deploy succeeded โ hedge-app (main) ยท commit 9f2c31a",
+ date: "09:18",
+ },
+ {
+ acct: "work",
+ from: "Linear",
+ subject: "KST-219 moved to In Review ยท assigned to you",
+ date: "08:55",
+ },
+ {
+ acct: "work",
+ from: "Sentry",
+ subject: "[hedge-prod] Regression: 3 new issues in last 24h",
+ date: "Yesterday",
+ },
+ {
+ acct: "work",
+ from: "Cloudflare",
+ subject: "Worker deployed: hedge-edge@v4.1.0",
+ date: "Yesterday",
+ },
+ {
+ acct: "side",
+ from: "npm",
+ subject: "Your package 'haze-schedule' received 1,043 downloads",
+ date: "Mon",
+ },
+ {
+ acct: "news",
+ from: "Hacker News",
+ subject: "Show HN: A tiny CRDT you can read in one afternoon",
+ date: "Mon",
+ },
+ {
+ acct: "home",
+ from: "Stripe",
+ subject: "New payout: $428.00 USD to Mercury โขโข4411",
+ date: "Mon",
+ },
+ {
+ acct: "work",
+ from: "GitHub",
+ subject: "[kestrel/hedge] Issue #412: wrap long headers in digest",
+ date: "Sun",
+ },
+ {
+ acct: "work",
+ from: "AWS Billing",
+ subject: "April forecast: $387.22 (-12% vs Mar)",
+ date: "Sun",
+ },
+ {
+ acct: "side",
+ from: "Fly.io",
+ subject: "Machine restarted in ord: driftnotes-web (oom)",
+ date: "Sat",
+ },
+ {
+ acct: "news",
+ from: "arXiv daily",
+ subject: "cs.PL โ 4 new submissions (2026-04-20)",
+ date: "Sat",
+ },
+];
+
+const MSGS_PERSONAL = [
+ { acct: "home", from: "mom", subject: "did you eat?", date: "12:14" },
+ { acct: "home", from: "Margo", subject: "pottery saturday??", date: "09:33" },
+ {
+ acct: "home",
+ from: "REI",
+ subject: "Your order has shipped",
+ date: "10:01",
+ },
+ {
+ acct: "home",
+ from: "Strava",
+ subject: "Your week: 38km ยท 3 runs",
+ date: "Yesterday",
+ },
+ {
+ acct: "home",
+ from: "Ava (landlord)",
+ subject: "building โ water shutoff Sat 9-11am",
+ date: "Yesterday",
+ },
+ {
+ acct: "home",
+ from: "Goodreads",
+ subject: "Nadia finished 'The MANIAC'",
+ date: "Mon",
+ },
+ {
+ acct: "home",
+ from: "Calendly",
+ subject: "New booking: Coffee w/ Oliver โ Fri 3pm",
+ date: "Mon",
+ },
+ {
+ acct: "home",
+ from: "your past self",
+ subject: "rotate the sourdough starter today",
+ date: "Sun",
+ },
+ {
+ acct: "home",
+ from: "Spotify",
+ subject: "Your Spring Mix is ready",
+ date: "Sun",
+ },
+ {
+ acct: "news",
+ from: "The New Yorker",
+ subject: "The daily โ a long weekend read",
+ date: "Sat",
+ },
+];
+
+const DATASETS = {
+ default: {
+ accounts: ACCOUNTS_DEFAULT,
+ messages: MSGS_DEFAULT,
+ label: "inbox",
+ },
+ dev: { accounts: ACCOUNTS_DEFAULT, messages: MSGS_DEV, label: "work" },
+ personal: {
+ accounts: ACCOUNTS_DEFAULT,
+ messages: MSGS_PERSONAL,
+ label: "personal",
+ },
+};
+
+const EMAIL_BODIES = {
+ "re: pickup pottery on saturday?": `yes! saturday works โ 11am at the studio?
+they said the bowls finally came out of the kiln.
+
+also margo owes you coffee. i'm bringing cash.
+
+โ m`,
+ "Hello world": `Hello world!
+
+this is a simple greeting from nowhere in particular.
+just confirming the new inbox is working as expected.
+
+cheers,
+m`,
+};
+
+function bodyFor(msg) {
+ if (EMAIL_BODIES[msg.subject]) return EMAIL_BODIES[msg.subject];
+ return `[no plain-text part]
+
+This message has no rendered body in the demo.
+Press esc to return to the inbox.`;
+}
+
+// ---------- Watermark (original โ desert + running figure, NOT copyrighted) ----------
+function Watermark() {
+ return (
+ <svg
+ className="tui-watermark"
+ viewBox="0 0 1000 520"
+ aria-hidden="true"
+ preserveAspectRatio="xMidYMax meet"
+ >
+ {/* dust/star dots */}
+ {Array.from({ length: 60 }).map((_, i) => {
+ const x = (i * 173) % 1000;
+ const y = (i * 97 + 50) % 380;
+ const r = 0.8 + ((i * 13) % 5) * 0.3;
+ return (
+ <circle
+ key={i}
+ cx={x}
+ cy={y}
+ r={r}
+ fill="currentColor"
+ opacity={0.25 + ((i * 7) % 5) * 0.06}
+ />
+ );
+ })}
+ {/* ground */}
+ <path
+ d="M 0 470 L 260 470 Q 310 460 360 470 L 540 470 Q 580 482 620 470 L 1000 470"
+ stroke="currentColor"
+ strokeWidth="1.2"
+ fill="none"
+ opacity="0.35"
+ />
+ {/* original: saguaro cactus */}
+ <g transform="translate(660 300)" opacity="0.35" fill="currentColor">
+ <rect x="54" y="10" width="24" height="160" rx="4" />
+ <rect x="30" y="60" width="24" height="70" rx="4" />
+ <rect x="30" y="60" width="16" height="14" rx="3" />
+ <rect x="78" y="40" width="24" height="50" rx="4" />
+ <rect x="78" y="40" width="24" height="14" rx="3" />
+ <rect x="60" y="170" width="12" height="4" />
+ </g>
+ {/* original: little running figure (stick person) โ not a branded character */}
+ <g transform="translate(190 360)" fill="currentColor" opacity="0.32">
+ <circle cx="22" cy="14" r="11" />
+ <rect x="14" y="26" width="22" height="42" rx="4" />
+ <rect
+ x="4"
+ y="34"
+ width="14"
+ height="5"
+ rx="2"
+ transform="rotate(-18 11 36)"
+ />
+ <rect
+ x="30"
+ y="30"
+ width="14"
+ height="5"
+ rx="2"
+ transform="rotate(20 37 32)"
+ />
+ <rect
+ x="10"
+ y="68"
+ width="8"
+ height="24"
+ rx="3"
+ transform="rotate(-12 14 80)"
+ />
+ <rect
+ x="26"
+ y="68"
+ width="8"
+ height="24"
+ rx="3"
+ transform="rotate(14 30 80)"
+ />
+ </g>
+ </svg>
+ );
+}
+
+// ---------- Selection indicator char ----------
+function cursor(i, cur, visual, vStart) {
+ if (visual && vStart != null) {
+ const lo = Math.min(cur, vStart),
+ hi = Math.max(cur, vStart);
+ const inRange = i >= lo && i <= hi;
+ if (inRange && i === cur) return ">*";
+ if (inRange) return " *";
+ if (i === cur) return "> ";
+ return " ";
+ }
+ return i === cur ? "> " : " ";
+}
+
+// ---------- TUI component ----------
+function TUI({ datasetKey = "default", onKeyPressed }) {
+ const dataset = DATASETS[datasetKey] || DATASETS.default;
+ const [folderIdx, setFolderIdx] = useState(0);
+ const [acctIdx, setAcctIdx] = useState(0);
+ const [cur, setCur] = useState(0);
+ const [mode, setMode] = useState("list"); // "list" | "email" | "filter" | "visual"
+ const [filter, setFilter] = useState("");
+ const [visualStart, setVisualStart] = useState(null);
+ const [flash, setFlash] = useState("");
+ const [deleted, setDeleted] = useState(new Set()); // indices into dataset.messages
+ const containerRef = useRef(null);
+ const [focused, setFocused] = useState(false);
+
+ const messages = dataset.messages;
+ const accounts = dataset.accounts;
+ const activeAcct = accounts[acctIdx];
+
+ // Reset on dataset change
+ useEffect(() => {
+ setFolderIdx(0);
+ setAcctIdx(0);
+ setCur(0);
+ setMode("list");
+ setFilter("");
+ setVisualStart(null);
+ setDeleted(new Set());
+ setFlash(`loaded: ${dataset.label}`);
+ const t = setTimeout(() => setFlash(""), 1400);
+ return () => clearTimeout(t);
+ }, [datasetKey]);
+
+ // Visible messages
+ const visible = useMemo(() => {
+ let list = messages
+ .map((m, origIdx) => ({ ...m, origIdx }))
+ .filter((m) => !deleted.has(m.origIdx));
+ if (folderIdx === 0) {
+ if (!activeAcct.isAll)
+ list = list.filter((m) => m.acct === activeAcct.id);
+ } else {
+ // Other folders are empty in the demo
+ list = [];
+ }
+ if (filter.trim()) {
+ const q = filter.toLowerCase();
+ list = list.filter(
+ (m) =>
+ m.subject.toLowerCase().includes(q) ||
+ m.from.toLowerCase().includes(q),
+ );
+ }
+ return list;
+ }, [messages, deleted, folderIdx, activeAcct, filter]);
+
+ const selected = visible[cur] || null;
+
+ const focusMe = useCallback(() => {
+ if (containerRef.current) containerRef.current.focus();
+ }, []);
+
+ const flashFor = (msg, ms = 1200) => {
+ setFlash(msg);
+ setTimeout(() => setFlash((f) => (f === msg ? "" : f)), ms);
+ };
+
+ const doDelete = (indices) => {
+ setDeleted((prev) => {
+ const next = new Set(prev);
+ indices.forEach((i) => next.add(i));
+ return next;
+ });
+ flashFor(
+ `โ deleted ${indices.length} message${indices.length === 1 ? "" : "s"}`,
+ );
+ };
+
+ // Key handling
+ const handleKey = (e) => {
+ if (onKeyPressed) onKeyPressed();
+ const k = e.key;
+
+ if (mode === "filter") {
+ if (k === "Enter" || k === "Escape") {
+ e.preventDefault();
+ setMode("list");
+ flashFor(filter ? `filter: "${filter}"` : "filter cleared");
+ return;
+ }
+ if (k === "Backspace") {
+ e.preventDefault();
+ setFilter((f) => f.slice(0, -1));
+ return;
+ }
+ if (k.length === 1) {
+ e.preventDefault();
+ setFilter((f) => f + k);
+ return;
+ }
+ return;
+ }
+
+ if (mode === "email") {
+ if (k === "Escape") {
+ e.preventDefault();
+ setMode("list");
+ return;
+ }
+ if (k === "r") {
+ e.preventDefault();
+ flashFor("โข reply (composer)");
+ return;
+ }
+ if (k === "f") {
+ e.preventDefault();
+ flashFor("โข forward");
+ return;
+ }
+ if (k === "d") {
+ e.preventDefault();
+ if (selected) {
+ doDelete([selected.origIdx]);
+ setMode("list");
+ setCur((c) => Math.max(0, Math.min(c, visible.length - 2)));
+ }
+ return;
+ }
+ if (k === "a") {
+ e.preventDefault();
+ flashFor("โค archived");
+ setMode("list");
+ return;
+ }
+ if (k === "i") {
+ e.preventDefault();
+ flashFor("โง images toggled");
+ return;
+ }
+ return;
+ }
+
+ if (mode === "visual") {
+ if (k === "v" || k === "Escape") {
+ e.preventDefault();
+ setMode("list");
+ setVisualStart(null);
+ return;
+ }
+ if (k === "j" || k === "ArrowDown") {
+ e.preventDefault();
+ setCur((c) => Math.min(c + 1, visible.length - 1));
+ return;
+ }
+ if (k === "k" || k === "ArrowUp") {
+ e.preventDefault();
+ setCur((c) => Math.max(c - 1, 0));
+ return;
+ }
+ if (k === "d") {
+ e.preventDefault();
+ const lo = Math.min(cur, visualStart),
+ hi = Math.max(cur, visualStart);
+ const indices = visible.slice(lo, hi + 1).map((m) => m.origIdx);
+ doDelete(indices);
+ setMode("list");
+ setVisualStart(null);
+ setCur((c) =>
+ Math.max(0, Math.min(lo, visible.length - indices.length - 1)),
+ );
+ return;
+ }
+ if (k === "a") {
+ e.preventDefault();
+ flashFor("โค archived batch");
+ setMode("list");
+ setVisualStart(null);
+ return;
+ }
+ if (k === "m") {
+ e.preventDefault();
+ flashFor("โข move to folderโฆ");
+ setMode("list");
+ setVisualStart(null);
+ return;
+ }
+ return;
+ }
+
+ // list mode
+ if (k === "Escape") {
+ e.preventDefault();
+ flashFor("โ main menu");
+ return;
+ }
+ if (k === "/") {
+ e.preventDefault();
+ setMode("filter");
+ setFilter("");
+ return;
+ }
+ if (k === "j" || k === "ArrowDown") {
+ e.preventDefault();
+ setCur((c) => Math.min(c + 1, visible.length - 1));
+ return;
+ }
+ if (k === "k" || k === "ArrowUp") {
+ e.preventDefault();
+ setCur((c) => Math.max(c - 1, 0));
+ return;
+ }
+ if (k === "h" || k === "ArrowLeft") {
+ e.preventDefault();
+ setAcctIdx((i) => (i - 1 + accounts.length) % accounts.length);
+ setCur(0);
+ return;
+ }
+ if (k === "l" || k === "ArrowRight") {
+ e.preventDefault();
+ setAcctIdx((i) => (i + 1) % accounts.length);
+ setCur(0);
+ return;
+ }
+ if (k === "Tab" && !e.shiftKey) {
+ e.preventDefault();
+ setFolderIdx((i) => (i + 1) % FOLDERS_LIST.length);
+ setCur(0);
+ return;
+ }
+ if (k === "Tab" && e.shiftKey) {
+ e.preventDefault();
+ setFolderIdx((i) => (i - 1 + FOLDERS_LIST.length) % FOLDERS_LIST.length);
+ setCur(0);
+ return;
+ }
+ if (k === "Enter") {
+ e.preventDefault();
+ if (selected) setMode("email");
+ return;
+ }
+ if (k === "v") {
+ e.preventDefault();
+ if (visible.length) {
+ setMode("visual");
+ setVisualStart(cur);
+ }
+ return;
+ }
+ if (k === "d") {
+ e.preventDefault();
+ if (selected) {
+ doDelete([selected.origIdx]);
+ setCur((c) => Math.max(0, Math.min(c, visible.length - 2)));
+ }
+ return;
+ }
+ if (k === "a") {
+ e.preventDefault();
+ flashFor("โค archived");
+ return;
+ }
+ if (k === "r") {
+ e.preventDefault();
+ flashFor("โป inbox refreshed");
+ return;
+ }
+ if (k === "q") {
+ e.preventDefault();
+ flashFor("quit");
+ return;
+ }
+ };
+
+ const titleSuffix =
+ mode === "visual"
+ ? ` - VISUAL (${Math.abs(cur - (visualStart ?? cur)) + 1} selected)`
+ : "";
+ const folderLabel = FOLDERS_LIST[folderIdx];
+ const isInbox = folderIdx === 0;
+ const acctLabel = activeAcct.isAll ? "All Accounts" : activeAcct.label;
+
+ return (
+ <div
+ ref={containerRef}
+ className={"tui-root " + (focused ? "is-focused" : "")}
+ tabIndex={0}
+ onKeyDown={handleKey}
+ onFocus={() => setFocused(true)}
+ onBlur={() => setFocused(false)}
+ onClick={focusMe}
+ >
+ {/* Title bar */}
+ <div className="tui-titlebar">
+ <div className="tui-titlebar-left">
+ <span className="tui-traffic">
+ <span className="tl tl-r" />
+ <span className="tl tl-y" />
+ <span className="tl tl-g" />
+ </span>
+ <span className="tui-title-text">matcha โ kai@floatpane</span>
+ </div>
+ <div className="tui-titlebar-right">
+ <span className="sep">ยท</span>
+ <span>80ร24</span>
+ </div>
+ </div>
+
+ {/* Body: sidebar + main pane */}
+ <div className="tui-shell">
+ {/* Sidebar */}
+ <div className="tui-sidebar">
+ <div className="tui-sidebar-head">Drew Smirnoff</div>
+ {FOLDERS_LIST.map((f, i) => (
+ <div
+ key={f}
+ className={"tui-folder " + (i === folderIdx ? "active" : "")}
+ onClick={() => {
+ setFolderIdx(i);
+ setCur(0);
+ focusMe();
+ }}
+ >
+ {f}
+ </div>
+ ))}
+ </div>
+
+ {/* Main */}
+ <div className="tui-main">
+ {mode !== "email" ? (
+ <>
+ {/* Account tabs */}
+ <div className="tui-acct-row">
+ {accounts.map((a, i) => (
+ <div
+ key={a.id}
+ className={"tui-acct " + (i === acctIdx ? "active" : "")}
+ onClick={() => {
+ setAcctIdx(i);
+ setCur(0);
+ focusMe();
+ }}
+ >
+ {a.label}
+ </div>
+ ))}
+ </div>
+
+ {/* Header line */}
+ <div className="tui-heading">
+ {isInbox ? (
+ <>
+ INBOX โ {acctLabel}
+ {titleSuffix}
+ </>
+ ) : (
+ <>
+ {folderLabel}
+ {titleSuffix}
+ </>
+ )}
+ </div>
+ <div className="tui-subhead">
+ {isInbox
+ ? `${visible.length} ${visible.length === 1 ? "email" : "emails"}`
+ : "folder empty"}
+ {filter && <span className="tui-filter-chip"> /{filter}</span>}
+ </div>
+
+ {/* Message rows */}
+ <div className="tui-list">
+ {visible.length === 0 && (
+ <div className="tui-row-empty">โ no messages โ</div>
+ )}
+ {visible.map((m, i) => {
+ const acct = accounts.find((a) => a.id === m.acct);
+ return (
+ <div
+ key={m.origIdx}
+ className={
+ "tui-row " +
+ (i === cur ? "cur " : "") +
+ (mode === "visual" ? "visual " : "")
+ }
+ onClick={() => {
+ setCur(i);
+ focusMe();
+ }}
+ onDoubleClick={() => {
+ setCur(i);
+ setMode("email");
+ focusMe();
+ }}
+ >
+ <span className="tui-row-cursor">
+ {cursor(i, cur, mode === "visual", visualStart)}
+ </span>
+ <span className="tui-row-num">
+ {String(i + 1).padStart(2, " ")}.
+ </span>
+ <span className="tui-row-acct">
+ [{acct ? acct.label : m.acct}]
+ </span>
+ <span className="tui-row-sep">โ</span>
+ <span className="tui-row-from">{m.from}</span>
+ <span className="tui-row-sep">โข</span>
+ <span className="tui-row-subj">{m.subject}</span>
+ <span className="tui-row-date">{m.date}</span>
+ </div>
+ );
+ })}
+ <div className="tui-row-dots">โข โข โข โข</div>
+ </div>
+
+ <Watermark />
+ </>
+ ) : (
+ <EmailView
+ msg={selected}
+ acct={accounts.find((a) => a.id === selected?.acct)}
+ />
+ )}
+ </div>
+ </div>
+
+ {/* Filter input */}
+ {mode === "filter" && (
+ <div className="tui-cmdline">
+ <span className="tui-cmdline-prompt">/</span>
+ <span className="tui-cmdline-buf">{filter}</span>
+ <span className="tui-caret">โ</span>
+ </div>
+ )}
+
+ {/* Flash */}
+ {flash && <div className="tui-flash">{flash}</div>}
+
+ {/* Status line */}
+ <div className="tui-status">
+ {mode === "email" ? (
+ <>
+ <span className="seg">โโ r: reply</span>
+ <span className="seg">โต f: forward</span>
+ <span className="seg">โ d: delete</span>
+ <span className="seg">โก a: archive</span>
+ <span className="seg">โฅ tab: focus attachments</span>
+ <span className="seg">โ esc: back to inbox</span>
+ <span className="seg">โซ i: toggle images</span>
+ </>
+ ) : mode === "visual" ? (
+ <>
+ <span className="seg hi">-- VISUAL --</span>
+ <span className="seg">j/k expand</span>
+ <span className="seg">d delete</span>
+ <span className="seg">a archive</span>
+ <span className="seg">m move</span>
+ <span className="seg">v/esc exit</span>
+ </>
+ ) : mode === "filter" ? (
+ <>
+ <span className="seg hi">-- FILTER --</span>
+ <span className="seg">enter apply</span>
+ <span className="seg">esc cancel</span>
+ </>
+ ) : (
+ <>
+ <span className="seg">โ/k up</span>
+ <span className="seg">โ/j down</span>
+ <span className="seg">/ filter</span>
+ <span className="seg">v visual mode</span>
+ <span className="seg">โ d delete</span>
+ <span className="seg">โก a archive</span>
+ <span className="seg">โ r refresh</span>
+ <span className="seg">โ/h prev tab</span>
+ <span className="seg">โ/l next tab</span>
+ <span className="seg">tab next folder</span>
+ <span className="seg">shift+tab prev folder</span>
+ <span className="seg">m move</span>
+ <span className="seg">q quit</span>
+ </>
+ )}
+ </div>
+
+ {/* Focus hint */}
+ {!focused && (
+ <div className="tui-focus-hint">
+ click ยท try <kbd>j</kbd>
+ <kbd>k</kbd> <kbd>tab</kbd> <kbd>/</kbd> <kbd>โต</kbd>
+ </div>
+ )}
+ </div>
+ );
+}
+
+function EmailView({ msg, acct }) {
+ if (!msg) return null;
+ const body = bodyFor(msg);
+ const from = acct ? `<${acct.label}>` : "";
+ return (
+ <div className="tui-email">
+ <div className="tui-email-head">
+ To: {acct ? acct.label : "you"} | From: {msg.from} {from} | Subject:{" "}
+ {msg.subject}
+ </div>
+ <div className="tui-email-body">
+ {body.split("\n").map((line, i) => {
+ // highlight 'this guy' style blue token if present
+ const parts = line.split(/(this guy)/);
+ return (
+ <div key={i} className="tui-email-line">
+ {parts.map((p, j) =>
+ p === "this guy" ? (
+ <span key={j} className="tui-link">
+ {p}
+ </span>
+ ) : (
+ <span key={j}>{p || "\u00a0"}</span>
+ ),
+ )}
+ </div>
+ );
+ })}
+ </div>
+ <Watermark />
+ </div>
+ );
+}
+
+window.MatchaTUI = TUI;
@@ -1,11 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta http-equiv="refresh" content="0; url=/#contact" />
- <title>Redirecting...</title>
- </head>
- <body>
- <p>Redirecting to <a href="/#contact">contact</a>...</p>
- </body>
-</html>
@@ -1,11 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta http-equiv="refresh" content="0; url=/#features" />
- <title>Redirecting to Features...</title>
- </head>
- <body>
- <p>Redirecting to <a href="/#features">features</a>...</p>
- </body>
-</html>
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8" />
- <meta http-equiv="refresh" content="0; url=/#installation" />
- <title>Redirecting...</title>
- <script>window.location.href = "/#installation";</script>
-</head>
-<body>
- <p>Redirecting to <a href="/#installation">installation</a>...</p>
-</body>
-</html>
@@ -1,1644 +1,56 @@
<!doctype html>
<html lang="en">
<head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Matcha - Terminal Email Client</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Matcha โ a TUI email client, by Floatpane</title>
<meta
name="description"
- content="A beautiful, keyboard-driven terminal email client written in Go. Manage your inbox without ever leaving the command line."
+ content="Matcha is a modern TUI email client by Floatpane. Vim keybindings, PGP, IMAP multi-account, markdown composing, Lua plugins."
/>
- <link rel="stylesheet" href="style.css" />
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
- <link rel="icon" type="image/png" href="assets/favicon.ico" />
<link
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
+ href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&family=IBM+Plex+Serif:ital,wght@1,400;1,500&display=swap"
rel="stylesheet"
/>
- <link rel="me" href="https://fosstodon.org/@floatpane" />
- </head>
- <body>
- <style>
- /* Reset */
- *,
- *::before,
- *::after {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- }
-
- :root {
- --bg: #09090b;
- --bg-subtle: #0f0f12;
- --bg-card: #131316;
- --bg-elevated: #1a1a1f;
- --bg-hover: #222228;
-
- --text: #fafafa;
- --text-secondary: #a1a1aa;
- --text-muted: #52525b;
-
- --green: #22c55e;
- --green-dim: #16a34a;
- --green-glow: rgba(34, 197, 94, 0.15);
- --teal: #14b8a6;
-
- --border: rgba(255, 255, 255, 0.06);
- --border-hover: rgba(255, 255, 255, 0.12);
-
- --radius: 12px;
- --radius-sm: 8px;
-
- --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- --mono: "JetBrains Mono", "SF Mono", "Menlo", monospace;
- }
-
- html {
- scroll-behavior: smooth;
- }
-
- body {
- font-family: var(--font);
- background: var(--bg);
- color: var(--text);
- line-height: 1.6;
- overflow-x: hidden;
- -webkit-font-smoothing: antialiased;
- }
-
- /* Subtle noise texture */
- .noise {
- position: fixed;
- inset: 0;
- z-index: -1;
- opacity: 0.03;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
- pointer-events: none;
- }
-
- /* Ambient glow orbs */
- .gradient-orb {
- position: fixed;
- border-radius: 50%;
- filter: blur(120px);
- pointer-events: none;
- z-index: -1;
- }
-
- .orb-1 {
- width: 600px;
- height: 600px;
- top: -200px;
- left: 50%;
- transform: translateX(-50%);
- background: radial-gradient(circle, var(--green-glow), transparent 70%);
- }
-
- .orb-2 {
- width: 400px;
- height: 400px;
- bottom: -100px;
- right: -100px;
- background: radial-gradient(circle, rgba(20, 184, 166, 0.08), transparent 70%);
- }
-
- /* Header */
- header {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 100;
- background: rgba(9, 9, 11, 0.7);
- backdrop-filter: blur(24px);
- -webkit-backdrop-filter: blur(24px);
- border-bottom: 1px solid var(--border);
- }
-
- nav {
- max-width: 1100px;
- margin: 0 auto;
- padding: 0.875rem 1.5rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .logo {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- text-decoration: none;
- font-weight: 700;
- font-size: 1.15rem;
- color: var(--text);
- transition: opacity 0.2s;
- }
-
- .logo:hover {
- opacity: 0.8;
- }
-
- .nav-links {
- display: flex;
- align-items: center;
- gap: 1.75rem;
- }
-
- .nav-links a {
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.875rem;
- font-weight: 500;
- transition: color 0.2s;
- }
-
- .nav-links a:hover {
- color: var(--text);
- }
-
- .nav-btn {
- display: inline-flex !important;
- align-items: center;
- gap: 0.4rem;
- padding: 0.45rem 0.9rem;
- background: var(--bg-elevated);
- border: 1px solid var(--border-hover);
- border-radius: var(--radius-sm);
- transition: all 0.2s;
- }
-
- .nav-btn:hover {
- background: var(--bg-hover);
- border-color: rgba(255, 255, 255, 0.18);
- }
-
- /* Main */
- main {
- max-width: 1100px;
- margin: 0 auto;
- padding: 0 1.5rem;
- }
-
- /* Hero */
- .hero {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- padding-top: 8rem;
- padding-bottom: 4rem;
- }
-
- .hero-badge {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.4rem 1rem;
- font-size: 0.8rem;
- font-weight: 500;
- color: var(--green);
- background: rgba(34, 197, 94, 0.08);
- border: 1px solid rgba(34, 197, 94, 0.2);
- border-radius: 100px;
- margin-bottom: 2rem;
- animation: fadeIn 0.5s ease-out 0.1s both;
- }
-
- .hero-badge-dot {
- width: 6px;
- height: 6px;
- background: var(--green);
- border-radius: 50%;
- animation: pulse 2s ease-in-out infinite;
- }
-
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.4; }
- }
-
- .hero-title {
- font-size: clamp(2.75rem, 7vw, 4.5rem);
- font-weight: 800;
- line-height: 1.1;
- letter-spacing: -0.03em;
- margin-bottom: 1.5rem;
- animation: fadeIn 0.5s ease-out 0.2s both;
- }
-
- .gradient-text {
- background: linear-gradient(135deg, var(--green), var(--teal));
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- }
-
- .text-muted {
- color: var(--text-muted);
- }
-
- .hero-subtitle {
- font-size: 1.15rem;
- color: var(--text-secondary);
- max-width: 540px;
- line-height: 1.75;
- margin-bottom: 2.5rem;
- animation: fadeIn 0.5s ease-out 0.3s both;
- }
-
- .hero-actions {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 0.75rem;
- margin-bottom: 4rem;
- animation: fadeIn 0.5s ease-out 0.4s both;
- }
-
- /* Buttons */
- .btn {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.7rem 1.4rem;
- font-size: 0.9rem;
- font-weight: 600;
- font-family: var(--font);
- text-decoration: none;
- border-radius: var(--radius-sm);
- transition: all 0.2s;
- cursor: pointer;
- border: none;
- white-space: nowrap;
- }
-
- .btn-lg {
- padding: 0.85rem 1.75rem;
- font-size: 0.95rem;
- }
-
- .btn-primary {
- background: var(--green);
- color: #000;
- }
-
- .btn-primary:hover {
- background: var(--green-dim);
- transform: translateY(-1px);
- box-shadow: 0 0 40px var(--green-glow);
- }
-
- .btn-ghost {
- background: transparent;
- color: var(--text-secondary);
- border: 1px solid var(--border-hover);
- }
-
- .btn-ghost:hover {
- color: var(--text);
- background: var(--bg-elevated);
- border-color: rgba(255, 255, 255, 0.18);
- }
-
- /* Hero Terminal */
- .hero-terminal {
- width: 100%;
- max-width: 680px;
- border-radius: var(--radius);
- border: 1px solid var(--border-hover);
- background: var(--bg-subtle);
- overflow: hidden;
- box-shadow:
- 0 0 0 1px rgba(255, 255, 255, 0.03),
- 0 20px 50px rgba(0, 0, 0, 0.5),
- 0 0 80px var(--green-glow);
- animation: fadeUp 0.6s ease-out 0.5s both;
- }
-
- .terminal-chrome {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.7rem 1rem;
- background: var(--bg-card);
- border-bottom: 1px solid var(--border);
- }
-
- .terminal-dots {
- display: flex;
- gap: 6px;
- }
-
- .terminal-dots span {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: var(--bg-hover);
- }
-
- .terminal-dots span:nth-child(1) { background: #ff5f57; }
- .terminal-dots span:nth-child(2) { background: #ffbd2e; }
- .terminal-dots span:nth-child(3) { background: #28ca41; }
-
- .terminal-title {
- font-family: var(--mono);
- font-size: 0.75rem;
- color: var(--text-muted);
- }
-
- .terminal-body {
- padding: 1.25rem 1.5rem;
- font-family: var(--mono);
- font-size: 0.85rem;
- }
-
- .terminal-line {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- margin-bottom: 1rem;
- }
-
- .t-prompt { color: var(--green); }
- .t-cmd { color: var(--text-muted); }
- .t-text { color: var(--text); }
-
- .cursor {
- color: var(--green);
- animation: blink 1s step-end infinite;
- }
-
- @keyframes blink {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0; }
- }
-
- .terminal-content {
- opacity: 1;
- }
-
- .ascii-logo {
- color: var(--green);
- font-size: 0.7rem;
- line-height: 1.2;
- margin: 0 0 1rem 0;
- white-space: pre;
- }
-
- .tui-prompt {
- color: var(--text-secondary);
- font-size: 0.85rem;
- margin-bottom: 1rem;
- }
-
- .tui-menu {
- margin-bottom: 1.25rem;
- padding-left: 0.25rem;
- }
-
- .tui-item {
- line-height: 1.9;
- font-size: 0.85rem;
- color: var(--text-secondary);
- }
-
- .tui-item.active {
- color: var(--green);
- }
-
- .tui-arrow {
- color: var(--green);
- font-weight: 700;
- margin-right: 0.25rem;
- }
-
- .tui-space {
- display: inline-block;
- width: 0.85rem;
- margin-right: 0.25rem;
- }
-
- .tui-hint {
- color: var(--text-muted);
- font-size: 0.75rem;
- }
-
- .tui-key {
- color: var(--text-secondary);
- }
-
- /* Logos strip */
- .logos-strip {
- padding: 3rem 0;
- border-top: 1px solid var(--border);
- border-bottom: 1px solid var(--border);
- margin-bottom: 2rem;
- }
-
- .strip-items {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 2rem;
- flex-wrap: wrap;
- }
-
- .strip-item {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-secondary);
- }
-
- .strip-item svg {
- color: var(--text-muted);
- }
-
- .strip-tag {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0.15rem 0.5rem;
- font-size: 0.7rem;
- font-weight: 700;
- font-family: var(--mono);
- color: var(--text-muted);
- border: 1px solid var(--border-hover);
- border-radius: 4px;
- }
-
- .strip-divider {
- width: 1px;
- height: 20px;
- background: var(--border-hover);
- }
-
- /* Sections */
- .section-header {
- text-align: center;
- margin-bottom: 3.5rem;
- }
-
- .section-label {
- display: inline-block;
- font-size: 0.8rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- color: var(--green);
- margin-bottom: 0.75rem;
- }
-
- .section-title {
- font-size: clamp(1.75rem, 4vw, 2.5rem);
- font-weight: 700;
- line-height: 1.2;
- letter-spacing: -0.02em;
- }
-
- /* Features */
- .features {
- padding: 5rem 0;
- }
-
- .features-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 1rem;
- }
-
- .feature-card {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 1.75rem;
- transition: all 0.3s ease;
- opacity: 0;
- transform: translateY(12px);
- }
-
- .feature-card.visible {
- opacity: 1;
- transform: translateY(0);
- }
-
- .feature-card:hover {
- border-color: var(--border-hover);
- background: var(--bg-elevated);
- }
-
- .feature-highlight {
- grid-column: 1 / -1;
- display: grid;
- grid-template-columns: auto 1fr;
- grid-template-rows: auto auto;
- column-gap: 1.25rem;
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.04), rgba(20, 184, 166, 0.02));
- border-color: rgba(34, 197, 94, 0.15);
- }
-
- .feature-highlight .feature-icon-wrap {
- grid-row: 1 / 3;
- }
-
- .feature-highlight:hover {
- border-color: rgba(34, 197, 94, 0.25);
- }
-
- .feature-icon-wrap {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 44px;
- height: 44px;
- background: rgba(34, 197, 94, 0.08);
- border: 1px solid rgba(34, 197, 94, 0.15);
- border-radius: 10px;
- color: var(--green);
- margin-bottom: 1rem;
- flex-shrink: 0;
- }
-
- .feature-highlight .feature-icon-wrap {
- margin-bottom: 0;
- }
-
- .feature-card h3 {
- font-size: 1.05rem;
- font-weight: 600;
- margin-bottom: 0.5rem;
- color: var(--text);
- }
-
- .feature-card p {
- color: var(--text-secondary);
- font-size: 0.9rem;
- line-height: 1.65;
- }
-
- /* Installation */
- .installation {
- padding: 5rem 0;
- }
-
- .install-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
- gap: 1.25rem;
- max-width: 820px;
- margin: 0 auto;
- }
-
- .install-card {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 1.75rem;
- opacity: 0;
- transform: translateY(12px);
- transition: all 0.3s ease;
- }
-
- .install-card.visible {
- opacity: 1;
- transform: translateY(0);
- }
-
- .primary-install {
- border-color: rgba(34, 197, 94, 0.2);
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.04), transparent);
- }
-
- .install-card-header {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- margin-bottom: 1.25rem;
- }
-
- .install-card h3 {
- font-size: 1.1rem;
- font-weight: 600;
- margin-bottom: 1.25rem;
- }
-
- .install-card-header h3 {
- margin-bottom: 0;
- }
-
- .recommended-badge {
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- padding: 0.2rem 0.6rem;
- color: var(--green);
- background: rgba(34, 197, 94, 0.1);
- border: 1px solid rgba(34, 197, 94, 0.2);
- border-radius: 100px;
- }
-
- .code-block {
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- overflow: hidden;
- }
-
- .code-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.6rem 0.9rem;
- border-bottom: 1px solid var(--border);
- font-size: 0.75rem;
- color: var(--text-muted);
- }
-
- .copy-btn {
- display: inline-flex;
- align-items: center;
- gap: 0.35rem;
- padding: 0.25rem 0.6rem;
- font-size: 0.72rem;
- font-weight: 500;
- font-family: var(--font);
- color: var(--text-muted);
- background: transparent;
- border: 1px solid var(--border);
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.15s;
- }
-
- .copy-btn:hover {
- color: var(--text);
- background: var(--bg-elevated);
- border-color: var(--border-hover);
- }
-
- .copy-btn.copied {
- color: var(--green);
- border-color: rgba(34, 197, 94, 0.3);
- }
-
- .code-body {
- padding: 0.85rem 1rem;
- font-family: var(--mono);
- font-size: 0.82rem;
- }
-
- .code-line {
- display: flex;
- gap: 0.6rem;
- line-height: 2;
- }
-
- .line-prompt {
- color: var(--text-muted);
- user-select: none;
- }
-
- .code-text {
- color: var(--text);
- }
-
- .install-docs-hint {
- text-align: center;
- margin-top: 2rem;
- font-size: 0.9rem;
- color: var(--text-muted);
- }
-
- .install-docs-hint a {
- color: var(--green);
- text-decoration: none;
- }
-
- .install-docs-hint a:hover {
- text-decoration: underline;
- }
-
- /* CTA Section */
- .cta-section {
- padding: 5rem 0;
- }
-
- .cta-card {
- text-align: center;
- padding: 4rem 2rem;
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.05), rgba(20, 184, 166, 0.03));
- border: 1px solid rgba(34, 197, 94, 0.12);
- border-radius: 20px;
- opacity: 0;
- transform: translateY(12px);
- transition: all 0.3s ease;
- }
-
- .cta-card.visible {
- opacity: 1;
- transform: translateY(0);
- }
-
- .cta-card h2 {
- font-size: clamp(1.5rem, 3.5vw, 2.25rem);
- font-weight: 700;
- line-height: 1.2;
- letter-spacing: -0.02em;
- margin-bottom: 1rem;
- }
-
- .cta-card p {
- color: var(--text-secondary);
- font-size: 1.05rem;
- max-width: 520px;
- margin: 0 auto 2rem;
- line-height: 1.7;
- }
-
- .cta-actions {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- gap: 0.75rem;
- margin-bottom: 2rem;
- }
-
- .cta-links {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 0.75rem;
- font-size: 0.85rem;
- }
-
- .cta-links a {
- color: var(--text-muted);
- text-decoration: none;
- transition: color 0.2s;
- }
-
- .cta-links a:hover {
- color: var(--text-secondary);
- }
-
- .cta-divider {
- color: var(--text-muted);
- }
-
- /* Footer */
- footer {
- border-top: 1px solid var(--border);
- padding: 2rem 1.5rem;
- margin-top: 2rem;
- }
-
- .footer-content {
- max-width: 1100px;
- margin: 0 auto;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .footer-left {
- display: flex;
- align-items: center;
- gap: 1.5rem;
- }
-
- .footer-brand {
- display: flex;
- align-items: center;
- gap: 0.4rem;
- font-weight: 600;
- font-size: 0.95rem;
- }
-
- .footer-copyright {
- color: var(--text-muted);
- font-size: 0.8rem;
- }
-
- .footer-right {
- display: flex;
- gap: 1.5rem;
- }
-
- .footer-right a {
- color: var(--text-muted);
- text-decoration: none;
- font-size: 0.85rem;
- transition: color 0.2s;
- }
-
- .footer-right a:hover {
- color: var(--text-secondary);
- }
-
- /* Animations */
- @keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
-
- @keyframes fadeUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- /* Responsive */
- @media (max-width: 768px) {
- nav {
- padding: 0.75rem 1rem;
- }
-
- .nav-links {
- gap: 1rem;
- }
-
- .nav-links a:not(.nav-btn) {
- display: none;
- }
-
- .hero {
- padding-top: 7rem;
- }
-
- .hero-terminal {
- max-width: 100%;
- }
-
- .features-grid {
- grid-template-columns: 1fr;
- }
-
- .feature-highlight {
- grid-template-columns: 1fr;
- grid-template-rows: auto;
- }
-
- .feature-highlight .feature-icon-wrap {
- grid-row: auto;
- margin-bottom: 1rem;
- }
-
- .install-grid {
- grid-template-columns: 1fr;
- }
-
- .strip-divider {
- display: none;
- }
-
- .strip-items {
- gap: 1.25rem;
- }
-
- .footer-content {
- flex-direction: column;
- gap: 1rem;
- text-align: center;
- }
-
- .footer-left {
- flex-direction: column;
- gap: 0.5rem;
- }
- }
-
- @media (max-width: 480px) {
- .hero-actions {
- flex-direction: column;
- width: 100%;
- }
-
- .btn {
- width: 100%;
- justify-content: center;
- }
-
- .cta-actions {
- flex-direction: column;
- }
-
- .cta-card {
- padding: 2.5rem 1.5rem;
- }
- }
- </style>
- <div class="noise"></div>
- <div class="gradient-orb orb-1"></div>
- <div class="gradient-orb orb-2"></div>
-
- <header>
- <nav>
- <a href="/" class="logo">
- <img
- src="assets/logo-transparent.png"
- alt="Matcha"
- class="logo-icon"
- width="28"
- height="28"
- />
- <span class="logo-text">Matcha</span>
- </a>
- <div class="nav-links">
- <a href="#features">Features</a>
- <a href="#installation">Install</a>
- <a href="https://docs.matcha.floatpane.com" target="_blank"
- >Docs</a
- >
- <a
- href="https://github.com/floatpane/matcha"
- target="_blank"
- class="nav-btn"
- >
- <svg
- height="18"
- width="18"
- viewBox="0 0 16 16"
- fill="currentColor"
- >
- <path
- d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
- />
- </svg>
- Star on GitHub
- </a>
- </div>
- </nav>
- </header>
-
- <main>
- <section class="hero">
- <div class="hero-badge">
- <span class="hero-badge-dot"></span>
- Open Source · MIT Licensed
- </div>
-
- <h1 class="hero-title">
- Email belongs in<br />
- <span class="gradient-text">your terminal.</span>
- </h1>
-
- <p class="hero-subtitle">
- Matcha is a fast, keyboard-driven email client that lives in
- your terminal. Built with Go and designed for developers who
- never want to leave the command line.
- </p>
-
- <div class="hero-actions">
- <a href="#installation" class="btn btn-primary">
- Get Started
- <svg
- width="16"
- height="16"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path d="M5 12h14" />
- <path d="m12 5 7 7-7 7" />
- </svg>
- </a>
- <a
- href="https://github.com/floatpane/matcha"
- target="_blank"
- class="btn btn-ghost"
- >
- <svg
- height="18"
- width="18"
- viewBox="0 0 16 16"
- fill="currentColor"
- >
- <path
- d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z"
- />
- </svg>
- Star on GitHub
- </a>
- <a
- href="https://docs.matcha.floatpane.com"
- target="_blank"
- class="btn btn-ghost"
- >
- <svg
- width="18"
- height="18"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path
- d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"
- />
- <path
- d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"
- />
- </svg>
- Documentation
- </a>
- </div>
-
- <div class="hero-terminal">
- <div class="terminal-chrome">
- <div class="terminal-dots">
- <span></span><span></span><span></span>
- </div>
- <span class="terminal-title">matcha</span>
- <div class="terminal-dots" style="visibility: hidden">
- <span></span><span></span><span></span>
- </div>
- </div>
- <div class="terminal-body">
- <div class="terminal-line">
- <span class="t-prompt">~</span>
- <span class="t-cmd">$</span>
- <span class="t-text typing-anim">matcha</span>
- <span class="cursor">|</span>
- </div>
- <div
- class="terminal-content"
- id="terminal-content"
- ></div>
- </div>
- </div>
- </section>
-
- <section class="logos-strip">
- <div class="strip-items">
- <div class="strip-item">
- <svg
- width="18"
- height="18"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- >
- <path
- d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
- />
- <polyline points="22,6 12,13 2,6" />
- </svg>
- Gmail
- </div>
- <div class="strip-divider"></div>
- <div class="strip-item">
- <svg
- width="18"
- height="18"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- >
- <path
- d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"
- />
- </svg>
- iCloud
- </div>
- <div class="strip-divider"></div>
- <div class="strip-item">
- <svg
- width="18"
- height="18"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- >
- <circle cx="12" cy="12" r="3" />
- <path
- d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
- />
- </svg>
- Custom IMAP
- </div>
- <div class="strip-divider"></div>
- <div class="strip-item">
- <span class="strip-tag">Go</span>
- Written in Go
- </div>
- <div class="strip-divider"></div>
- <div class="strip-item">
- <span class="strip-tag">MIT</span>
- Open Source
- </div>
- </div>
- </section>
-
- <section class="features" id="features">
- <div class="section-header">
- <span class="section-label">Features</span>
- <h2 class="section-title">
- Everything you need.<br /><span class="text-muted"
- >Nothing you don't.</span
- >
- </h2>
- </div>
-
- <div class="features-grid">
- <div class="feature-card feature-highlight">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <rect
- x="2"
- y="4"
- width="20"
- height="16"
- rx="2"
- />
- <path
- d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"
- />
- </svg>
- </div>
- <h3>Full Inbox Management</h3>
- <p>
- Read, compose, reply, and manage your emails
- entirely from the terminal. Beautiful formatting,
- attachment support, and lightning-fast navigation.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path d="M12 20h9" />
- <path
- d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"
- />
- </svg>
- </div>
- <h3>Compose & Send</h3>
- <p>
- Intuitive compose interface with tab-based field
- navigation. Write and send emails without touching a
- browser.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path
- d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
- />
- <circle cx="9" cy="7" r="4" />
- <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
- <path d="M16 3.13a4 4 0 0 1 0 7.75" />
- </svg>
- </div>
- <h3>Multiple Accounts</h3>
- <p>
- Manage all your email accounts in one place. Switch
- between them seamlessly.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path d="m18 16 4-4-4-4" />
- <path d="m6 8-4 4 4 4" />
- <path d="m14.5 4-5 16" />
- </svg>
- </div>
- <h3>Keyboard-First</h3>
- <p>
- Vim-inspired keybindings. Navigate, select, and
- manage everything without ever touching the mouse.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
- </svg>
- </div>
- <h3>Blazing Fast</h3>
- <p>
- Local email caching means instant load times. No
- Electron, no bloat -- just pure terminal speed.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <rect
- width="18"
- height="11"
- x="3"
- y="11"
- rx="2"
- ry="2"
- />
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
- </svg>
- </div>
- <h3>Private & Secure</h3>
- <p>
- Your credentials stay on your machine. No telemetry,
- no tracking, no cloud middleman.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path
- d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"
- />
- <circle cx="9" cy="7" r="4" />
- <line x1="19" x2="19" y1="8" y2="14" />
- <line x1="22" x2="16" y1="11" y2="11" />
- </svg>
- </div>
- <h3>Contacts</h3>
- <p>
- Save and manage contacts locally with auto-complete
- support when composing.
- </p>
- </div>
-
- <div class="feature-card">
- <div class="feature-icon-wrap">
- <svg
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <rect
- width="18"
- height="18"
- x="3"
- y="3"
- rx="2"
- />
- <path d="M3 9h18" />
- <path d="M9 21V9" />
- </svg>
- </div>
- <h3>Beautiful TUI</h3>
- <p>
- Built with Bubble Tea. A clean, modern interface
- that makes terminal email a pleasure.
- </p>
- </div>
- </div>
- </section>
-
- <section class="installation" id="installation">
- <div class="section-header">
- <span class="section-label">Get Started</span>
- <h2 class="section-title">
- Up and running<br /><span class="text-muted"
- >in seconds.</span
- >
- </h2>
- </div>
-
- <div class="install-grid">
- <div class="install-card primary-install">
- <div class="install-card-header">
- <h3>Homebrew</h3>
- <span class="recommended-badge">Recommended</span>
- </div>
- <div class="code-block">
- <div class="code-header">
- <span>Terminal</span>
- <button
- class="copy-btn"
- onclick="copyCode(this)"
- >
- <svg
- width="14"
- height="14"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <rect
- width="14"
- height="14"
- x="8"
- y="8"
- rx="2"
- ry="2"
- />
- <path
- d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
- />
- </svg>
- Copy
- </button>
- </div>
- <div class="code-body">
- <div class="code-line">
- <span class="line-prompt">$</span
- ><span class="code-text"
- >brew install
- floatpane/matcha/matcha</span
- >
- </div>
- <div class="code-line">
- <span class="line-prompt">$</span
- ><span class="code-text">matcha</span>
- </div>
- </div>
- </div>
- </div>
-
- <div class="install-card">
- <h3>Build from Source</h3>
- <div class="code-block">
- <div class="code-header">
- <span>Terminal</span>
- <button
- class="copy-btn"
- onclick="copyCode(this)"
- >
- <svg
- width="14"
- height="14"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <rect
- width="14"
- height="14"
- x="8"
- y="8"
- rx="2"
- ry="2"
- />
- <path
- d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
- />
- </svg>
- Copy
- </button>
- </div>
- <div class="code-body">
- <div class="code-line">
- <span class="line-prompt">$</span
- ><span class="code-text"
- >git clone
- https://github.com/floatpane/matcha.git</span
- >
- </div>
- <div class="code-line">
- <span class="line-prompt">$</span
- ><span class="code-text"
- >cd matcha && make build</span
- >
- </div>
- <div class="code-line">
- <span class="line-prompt">$</span
- ><span class="code-text">matcha</span>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <p class="install-docs-hint">
- Need help setting up? Check the
- <a href="https://docs.matcha.floatpane.com" target="_blank"
- >documentation</a
- >
- for detailed guides.
- </p>
- </section>
-
- <section class="cta-section">
- <div class="cta-card">
- <h2>Ready to ditch the browser<br />for your email?</h2>
- <p>
- Join developers who manage their inbox from the
- terminal. Star the project to show your support and stay
- updated.
- </p>
- <div class="cta-actions">
- <a
- href="https://github.com/floatpane/matcha"
- target="_blank"
- class="btn btn-primary btn-lg"
- >
- <svg
- height="20"
- width="20"
- viewBox="0 0 16 16"
- fill="currentColor"
- >
- <path
- d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z"
- />
- </svg>
- Star on GitHub
- </a>
- <a
- href="https://docs.matcha.floatpane.com"
- target="_blank"
- class="btn btn-ghost btn-lg"
- >
- Read the Docs
- </a>
- </div>
- <div class="cta-links">
- <a href="https://andrinoff.com/sponsor" target="_blank"
- >Support the developer</a
- >
- </div>
- </section>
- </main>
-
- <footer>
- <div class="footer-content">
- <div class="footer-left">
- <div class="footer-brand">
- <img
- src="assets/logo-transparent.png"
- alt="Matcha"
- class="logo-icon"
- width="20"
- height="20"
- />
- <span>Matcha</span>
- </div>
- <p class="footer-copyright">
- © 2026 floatpane. MIT Licensed.
- </p>
- </div>
- <div class="footer-right">
- <a href="https://docs.matcha.floatpane.com" target="_blank"
- >Docs</a
- >
- <a
- href="https://github.com/floatpane/matcha"
- target="_blank"
- >GitHub</a
- >
- <a
- href="https://github.com/floatpane/matcha/blob/master/LICENSE"
- target="_blank"
- >License</a
- >
- </div>
- </div>
- </footer>
+ <link rel="icon" type="image/png" href="assets/favicon.png" />
+ <link rel="stylesheet" href="styles.css?v=3" />
<script>
- function copyCode(btn) {
- const codeBlock = btn.closest(".code-block");
- const codeLines = codeBlock.querySelectorAll(".code-text");
- const text = Array.from(codeLines)
- .map((line) => line.textContent)
- .join("\n");
-
- navigator.clipboard.writeText(text).then(() => {
- const originalHTML = btn.innerHTML;
- btn.innerHTML =
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Copied!';
- btn.classList.add("copied");
- setTimeout(() => {
- btn.innerHTML = originalHTML;
- btn.classList.remove("copied");
- }, 2000);
- });
- }
-
- document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
- anchor.addEventListener("click", function (e) {
- e.preventDefault();
- const target = document.querySelector(
- this.getAttribute("href"),
- );
- if (target) {
- target.scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
- }
- });
- });
-
- // Fade in elements on scroll
- const observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting) {
- entry.target.classList.add("visible");
- }
- });
- },
- { threshold: 0.1 },
- );
-
- document
- .querySelectorAll(".feature-card, .install-card, .cta-card")
- .forEach((el) => {
- observer.observe(el);
- });
+ /*EDITMODE-BEGIN*/
+ const TWEAK_DEFAULTS = {
+ datasetKey: "default",
+ };
+ /*EDITMODE-END*/
+ </script>
+ </head>
+ <body>
+ <div id="root"></div>
+
+ <script
+ src="https://unpkg.com/react@18.3.1/umd/react.development.js"
+ integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L"
+ crossorigin="anonymous"
+ ></script>
+ <script
+ src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"
+ integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm"
+ crossorigin="anonymous"
+ ></script>
+ <script
+ src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"
+ integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"
+ crossorigin="anonymous"
+ ></script>
+
+ <script type="text/babel" src="components/tui.jsx?v=3"></script>
+ <script type="text/babel" src="components/site.jsx?v=3"></script>
+
+ <script type="text/babel">
+ const root = ReactDOM.createRoot(document.getElementById("root"));
+ root.render(<window.MatchaApp />);
</script>
</body>
</html>
@@ -0,0 +1,1126 @@
+/* ============ Matcha site styles ============ */
+:root {
+ /* Dark blue-green terminal, like the real Matcha screenshots */
+ --bg: #05080a;
+ --bg-2: #060a0d;
+ --panel: #0a1013;
+ --panel-2: #0e1619;
+ --line: #14222b;
+ --line-2: #1d3441;
+ --ink: #c8d6d8;
+ --ink-dim: #7d9099;
+ --ink-dim2: #52666e;
+
+ /* green accent โ slightly darker, per request */
+ --accent: #6aa84f;
+ --accent-2: #85c26a;
+ --accent-deep: #3b6b28;
+ --green-soft: #8fb88b;
+
+ --blue: #5aa3d8;
+ --yellow: #d1b85a;
+
+ --mono: "IBM Plex Mono", ui-monospace, "JetBrains Mono", Menlo, monospace;
+ --sans:
+ "IBM Plex Sans", ui-sans-serif, -apple-system, system-ui, sans-serif;
+ --serif: "IBM Plex Serif", ui-serif, Georgia, serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+html,
+body {
+ margin: 0;
+ padding: 0;
+ background: var(--bg);
+ color: var(--ink);
+ font-family: var(--sans);
+ font-size: 15px;
+ line-height: 1.55;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ background:
+ radial-gradient(
+ 1000px 500px at 85% -10%,
+ rgba(106, 168, 79, 0.06),
+ transparent 60%
+ ),
+ radial-gradient(
+ 800px 400px at -10% 30%,
+ rgba(106, 168, 79, 0.03),
+ transparent 60%
+ );
+}
+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;
+}
+.underline-link:hover {
+ color: var(--accent-2);
+}
+
+.site {
+ position: relative;
+ z-index: 1;
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 0 32px;
+}
+
+/* ---------- Nav ---------- */
+.nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 22px 0;
+ border-bottom: 1px solid var(--line);
+ position: sticky;
+ top: 0;
+ background: rgba(5, 8, 10, 0.82);
+ backdrop-filter: blur(12px);
+ z-index: 50;
+}
+.nav-brand {
+ display: flex;
+ align-items: center;
+}
+.wordmark {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-family: var(--mono);
+ font-size: 15px;
+ letter-spacing: -0.01em;
+}
+.wm-bracket {
+ color: var(--accent);
+ font-size: 12px;
+ transform: translateY(-1px);
+}
+.wm-name {
+ color: var(--ink);
+ font-weight: 500;
+}
+.wm-dim {
+ color: var(--ink-dim2);
+ font-size: 12px;
+}
+
+.wm-logo {
+ display: flex;
+ align-items: center;
+ align-self: center;
+}
+
+.nav-links {
+ display: flex;
+ gap: 28px;
+ align-items: center;
+ font-family: var(--mono);
+ font-size: 13px;
+}
+.nav-links a {
+ color: var(--ink-dim);
+ transition: color 0.12s;
+}
+.nav-links a:hover {
+ color: var(--ink);
+}
+.nav-github {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+}
+.nav-star {
+ color: var(--accent);
+ font-size: 12px;
+}
+.nav-right .btn {
+ font-family: var(--mono);
+ font-size: 13px;
+}
+
+/* ---------- Buttons ---------- */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ border: 1px solid var(--line-2);
+ border-radius: 2px;
+ font-family: var(--mono);
+ font-size: 13px;
+ color: var(--ink);
+ background: transparent;
+ cursor: pointer;
+ transition: all 0.12s;
+}
+.btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+.btn-k {
+ font-size: 11px;
+ color: var(--ink-dim);
+}
+.btn:hover .btn-k {
+ color: var(--accent);
+}
+.btn-primary {
+ background: var(--accent);
+ color: #05080a;
+ border-color: var(--accent);
+ font-weight: 500;
+}
+.btn-primary:hover {
+ background: var(--accent-2);
+ border-color: var(--accent-2);
+ color: #05080a;
+}
+.btn-primary .btn-k {
+ color: #05080a;
+ opacity: 0.6;
+}
+.btn-ghost {
+ background: transparent;
+}
+.btn-lg {
+ padding: 14px 22px;
+ font-size: 14px;
+}
+
+/* ---------- Hero ---------- */
+.hero {
+ display: grid;
+ grid-template-columns: 1fr 1.35fr;
+ gap: 56px;
+ padding: 72px 0 80px;
+ align-items: start;
+}
+.hero-copy {
+ padding-top: 24px;
+}
+.hero-eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--ink-dim);
+ padding: 6px 12px;
+ border: 1px solid var(--line);
+ border-radius: 2px;
+ margin-bottom: 28px;
+}
+.dot-live {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 0 3px rgba(106, 168, 79, 0.2);
+ animation: pulse 2.4s ease-in-out infinite;
+}
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.4;
+ }
+}
+.hero-h1 {
+ font-family: var(--sans);
+ font-size: 56px;
+ font-weight: 500;
+ letter-spacing: -0.025em;
+ line-height: 1.04;
+ margin: 0 0 24px;
+ text-wrap: pretty;
+}
+.hero-sub {
+ font-size: 17px;
+ color: var(--ink-dim);
+ max-width: 46ch;
+ margin: 0 0 36px;
+ line-height: 1.55;
+}
+.hero-cta {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 44px;
+}
+.hero-meta {
+ display: grid;
+ grid-template-columns: repeat(3, auto);
+ gap: 32px;
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--ink);
+}
+.hero-meta .dim {
+ display: block;
+ color: var(--ink-dim2);
+ font-size: 11px;
+ margin-bottom: 2px;
+ text-transform: uppercase;
+ 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);
+ 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;
+ 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;
+ transition:
+ box-shadow 0.2s,
+ border-color 0.2s;
+ min-height: 520px;
+}
+.tui-root.is-focused {
+ border-color: var(--accent);
+ 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);
+}
+.tui-titlebar-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.tui-traffic {
+ display: inline-flex;
+ gap: 6px;
+}
+.tl {
+ width: 11px;
+ height: 11px;
+ border-radius: 50%;
+ display: inline-block;
+}
+.tl-r {
+ background: #2a1414;
+}
+.tl-y {
+ background: #2a2514;
+}
+.tl-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;
+ 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;
+}
+.tui-cmdline-prompt {
+ color: var(--accent);
+}
+.tui-cmdline-buf {
+ color: var(--ink);
+}
+.tui-caret {
+ display: inline-block;
+ width: 7px;
+ height: 13px;
+ background: var(--accent);
+ margin-left: 2px;
+ transform: translateY(2px);
+ animation: blink 1s step-end infinite;
+}
+@keyframes blink {
+ 50% {
+ opacity: 0;
+ }
+}
+
+.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;
+}
+.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);
+}
+
+/* ---------- Sections ---------- */
+.section-head {
+ display: grid;
+ grid-template-columns: 1.2fr 1fr;
+ gap: 48px;
+ padding: 80px 0 40px;
+ border-top: 1px solid var(--line);
+ align-items: end;
+}
+.section-eyebrow {
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--accent);
+ margin-bottom: 16px;
+ letter-spacing: 0.04em;
+}
+.section-h2 {
+ font-size: 44px;
+ font-weight: 500;
+ letter-spacing: -0.025em;
+ line-height: 1.08;
+ margin: 0;
+ text-wrap: pretty;
+}
+.section-h2 .dim {
+ color: var(--ink-dim);
+}
+.section-head-r {
+ color: var(--ink-dim);
+ font-size: 15px;
+ max-width: 44ch;
+ margin: 0;
+}
+.section-head-r code {
+ font-size: 13px;
+ color: var(--accent);
+ background: var(--panel);
+ padding: 1px 5px;
+ border-radius: 2px;
+}
+
+/* Features grid */
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1px;
+ background: var(--line);
+ border: 1px solid var(--line);
+ margin-top: 24px;
+}
+.feature {
+ background: var(--bg);
+ padding: 32px 28px;
+ min-height: 240px;
+ display: flex;
+ flex-direction: column;
+ transition: background 0.2s;
+}
+.feature:hover {
+ background: var(--bg-2);
+}
+.feature-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 24px;
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--accent);
+}
+.feature-dash {
+ color: var(--ink-dim2);
+}
+.feature-title {
+ font-family: var(--sans);
+ font-size: 20px;
+ font-weight: 500;
+ letter-spacing: -0.01em;
+ margin: 0 0 10px;
+ line-height: 1.2;
+}
+.feature-body {
+ color: var(--ink-dim);
+ font-size: 14px;
+ margin: 0 0 20px;
+ flex: 1;
+ line-height: 1.55;
+}
+.feature-mono {
+ margin: 0;
+ background: var(--panel-2);
+ border: 1px solid var(--line-2);
+ padding: 10px 12px;
+ font-size: 11.5px;
+ color: var(--accent);
+ border-radius: 2px;
+ white-space: pre-wrap;
+}
+@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);
+ }
+}
+
+/* Install */
+.install-card {
+ background: var(--bg-2);
+ border: 1px solid var(--line-2);
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 80px;
+}
+.install-tabs {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid var(--line);
+ background: var(--bg);
+ padding: 0 8px;
+ overflow-x: auto;
+}
+.install-tab {
+ background: transparent;
+ border: none;
+ padding: 12px 16px;
+ color: var(--ink-dim);
+ font-family: var(--mono);
+ font-size: 13px;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.12s;
+ white-space: nowrap;
+}
+.install-tab:hover {
+ color: var(--ink);
+}
+.install-tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+.install-tabs-spacer {
+ flex: 1;
+}
+.install-plat {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--ink-dim2);
+ padding-right: 12px;
+ white-space: nowrap;
+}
+.install-code {
+ margin: 0;
+ padding: 36px 28px;
+ font-size: 15px;
+ color: var(--accent);
+ background: var(--bg-2);
+ white-space: pre-wrap;
+ line-height: 1.7;
+ overflow-x: auto;
+}
+.install-foot {
+ display: flex;
+ gap: 32px;
+ flex-wrap: wrap;
+ padding: 12px 28px;
+ border-top: 1px solid var(--line);
+ background: var(--bg);
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--ink);
+}
+.install-foot .dim {
+ color: var(--ink-dim2);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ font-size: 10px;
+ margin-right: 6px;
+}
+
+/* CTA */
+.cta {
+ border-top: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+ margin-top: 20px;
+ padding: 100px 0;
+ text-align: center;
+ background: repeating-linear-gradient(
+ 0deg,
+ transparent 0 26px,
+ rgba(106, 168, 79, 0.02) 26px 27px
+ );
+}
+.cta-pre {
+ font-family: var(--mono);
+ color: var(--accent);
+ font-size: 14px;
+ margin-bottom: 16px;
+}
+.cta-h2 {
+ font-size: 60px;
+ font-weight: 500;
+ letter-spacing: -0.03em;
+ line-height: 1.04;
+ margin: 0 0 36px;
+}
+.cta-row {
+ display: flex;
+ gap: 14px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+@media (max-width: 960px) {
+ .cta-h2 {
+ font-size: 36px;
+ }
+}
+
+/* Footer */
+.footer {
+ padding: 64px 0 32px;
+}
+.footer-top {
+ display: grid;
+ grid-template-columns: 1.2fr 3fr;
+ gap: 48px;
+ padding-bottom: 48px;
+ border-bottom: 1px solid var(--line);
+}
+.footer-tag {
+ color: var(--ink-dim);
+ font-size: 13px;
+ margin: 16px 0 0;
+}
+.footer-cols {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 32px;
+}
+.footer-h {
+ font-family: var(--mono);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--ink-dim2);
+ margin-bottom: 12px;
+}
+.footer-cols a {
+ display: block;
+ color: var(--ink-dim);
+ font-size: 13px;
+ padding: 4px 0;
+ cursor: pointer;
+}
+.footer-cols a:hover {
+ color: var(--accent);
+}
+.footer-bot {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 24px;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--ink-dim2);
+}
+.footer-copy {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: var(--ink-dim);
+}
+.footer-meta {
+ display: flex;
+ gap: 8px;
+}
+.footer-meta .sep {
+ color: var(--line-2);
+}
+@media (max-width: 960px) {
+ .footer-top {
+ grid-template-columns: 1fr;
+ }
+ .footer-cols {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+/* 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);
+}
+.tweaks-dot {
+ color: var(--accent);
+ width: 10px;
+}