diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..67caa57
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "outputDirectory": "website"
+}
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..6f9d58c
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+ Premove
+ Faster conversational voice agents, built in the open
+
+ I got obsessed with exploring voice agents after making the same frustrating phone call over and over again. I thought making the agent smart would be the hard part. But I kept running into the less glamorous problems underneath it: latency, transcription errors, tool calls, and all the small things that make a conversation feel off.
+
+ Premove is my effort to work on those problems in the open, one by one.
+
+ The broader project is still under development. As parts become useful on their own, I’ll release them independently instead of waiting for the whole thing to be finished.
+
+ Premove ITN: Context-aware inverse text normalization
+
+ The first released piece is Premove ITN , an open-source, context-aware inverse text normalizer for English voice-agent transcripts.
+
+ One of the first problems I hit was that an ASR transcript could look completely fine to me and still be wrong for the tool behind the agent.
+
+
+
⧉
+
my room number is two thirty
+→ room_number = 230
+
+meet me at two thirty
+→ 02:30
+
+
+ If a normalizer always rewrites “two thirty” as 02:30, it sends the wrong value for the room-number request:
+
+
+
⧉
+
my room number is two thirty
+→ room_number = 02:30 (wrong; the tool expects 230)
+
+
+ The same spoken words can need different written forms depending on context.
+
+ That is why I built Premove ITN : to turn spoken values like IDs, phone numbers, dates, times, amounts, and emails into the structured forms downstream tools expect.
+
+ Explore Premove ITN →
+
+
+
+
diff --git a/website/premove-icon.png b/website/premove-icon.png
new file mode 100644
index 0000000..92c171b
Binary files /dev/null and b/website/premove-icon.png differ
diff --git a/website/site.js b/website/site.js
new file mode 100644
index 0000000..bf3275b
--- /dev/null
+++ b/website/site.js
@@ -0,0 +1,31 @@
+const root = document.documentElement;
+const themeToggle = document.querySelector(".theme-toggle");
+const savedTheme = localStorage.getItem("premove-theme");
+
+if (savedTheme === "light" || savedTheme === "dark") root.dataset.theme = savedTheme;
+
+function updateThemeLabel() {
+ const isDark = root.dataset.theme
+ ? root.dataset.theme === "dark"
+ : matchMedia("(prefers-color-scheme: dark)").matches;
+ themeToggle.setAttribute("aria-label", `Switch to ${isDark ? "light" : "dark"} theme`);
+}
+
+themeToggle.addEventListener("click", () => {
+ root.dataset.theme = themeToggle.getAttribute("aria-label") === "Switch to light theme"
+ ? "light" : "dark";
+ localStorage.setItem("premove-theme", root.dataset.theme);
+ updateThemeLabel();
+});
+
+matchMedia("(prefers-color-scheme: dark)").addEventListener("change", updateThemeLabel);
+updateThemeLabel();
+
+document.querySelectorAll(".copy-button").forEach((button) => {
+ button.addEventListener("click", async () => {
+ const code = button.parentElement.querySelector("code").textContent;
+ await navigator.clipboard.writeText(code);
+ button.setAttribute("aria-label", "Copied");
+ setTimeout(() => button.setAttribute("aria-label", "Copy the contents from the code block"), 2000);
+ });
+});
diff --git a/website/style.css b/website/style.css
new file mode 100644
index 0000000..7edacbc
--- /dev/null
+++ b/website/style.css
@@ -0,0 +1,177 @@
+:root {
+ color-scheme: light dark;
+ font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ font-synthesis: none;
+ --background: #fff;
+ --foreground: #111;
+ --muted: #4b5563;
+ --border: #e5e7eb;
+ --code-background: #f9fafb;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0b0b0d;
+ --foreground: #f9fafb;
+ --muted: #d1d5db;
+ --border: #202024;
+ --code-background: #0d0d0f;
+ }
+}
+
+:root[data-theme="light"] {
+ color-scheme: light;
+ --background: #fff;
+ --foreground: #111;
+ --muted: #4b5563;
+ --border: #e5e7eb;
+ --code-background: #f9fafb;
+}
+
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --background: #0b0b0d;
+ --foreground: #f9fafb;
+ --muted: #d1d5db;
+ --border: #202024;
+ --code-background: #0d0d0f;
+}
+
+* { box-sizing: border-box; }
+
+html { background: var(--background); }
+
+body {
+ margin: 0;
+ min-width: 320px;
+ color: var(--foreground);
+ background: var(--background);
+}
+
+a { color: inherit; }
+
+.skip-link {
+ position: absolute;
+ left: 1rem;
+ top: -4rem;
+ z-index: 2;
+ padding: .5rem;
+ background: var(--background);
+}
+.skip-link:focus { top: .5rem; }
+
+.site-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 64px;
+ padding: 0 48px;
+ border-bottom: 1px solid var(--border);
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 1.25rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.brand img { filter: brightness(0); }
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) .brand img { filter: brightness(0) invert(1); }
+}
+:root[data-theme="dark"] .brand img { filter: brightness(0) invert(1); }
+
+.theme-toggle {
+ width: 32px;
+ height: 32px;
+ border: 0;
+ color: var(--foreground);
+ background: transparent;
+ cursor: pointer;
+ font-size: 1.15rem;
+}
+.theme-toggle::before { content: "☾"; }
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) .theme-toggle::before { content: "☀"; }
+}
+:root[data-theme="dark"] .theme-toggle::before { content: "☀"; }
+
+main {
+ max-width: 752px;
+ margin: 48px auto 64px;
+ padding: 0 24px;
+}
+
+.eyebrow {
+ margin: 0 0 8px;
+ font-size: .875rem;
+ font-weight: 600;
+ color: var(--foreground);
+}
+
+h1 {
+ margin: 0 0 24px;
+ font-size: 2.5rem;
+ font-weight: 700;
+ letter-spacing: -.025em;
+ line-height: 1.15;
+}
+
+h2 {
+ margin: 32px 0 16px;
+ font-size: 1.5rem;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+main p {
+ margin: 0 0 16px;
+ color: var(--muted);
+ font-size: 1rem;
+ line-height: 1.7;
+}
+
+main p a { color: var(--foreground); font-weight: 600; }
+main p strong { color: var(--foreground); }
+
+.code-block {
+ position: relative;
+ margin: 20px 0;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--code-background);
+}
+
+pre {
+ margin: 0;
+ padding: 18px 20px;
+ overflow-x: auto;
+ font-size: .875rem;
+ line-height: 1.6;
+}
+
+code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
+
+.copy-button {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ padding: 4px;
+ border: 0;
+ color: var(--muted);
+ background: var(--code-background);
+ cursor: pointer;
+ font-size: 1rem;
+}
+
+.explore-link { display: inline-block; margin-top: 4px; }
+
+@media (max-width: 640px) {
+ .site-header { height: 64px; padding: 0 16px; }
+ main { margin-top: 40px; padding: 0 16px; }
+ h1 { font-size: 2.5rem; }
+ h2 { font-size: 1.5rem; }
+}