diff --git a/bookmark/static/app.js b/bookmark/static/app.js
new file mode 100644
index 0000000..d13aca6
--- /dev/null
+++ b/bookmark/static/app.js
@@ -0,0 +1,42 @@
+// bookmark/bookmark/static/app.js
+const statusEl = document.getElementById("status");
+const hintEl = document.getElementById("hint");
+const previewEl = document.getElementById("preview");
+const triggerButton = document.getElementById("trigger-button");
+
+const STATE_COPY = {
+ waiting_front: { status: "Waiting for book...", hint: "Center the front cover in the guide box." },
+ waiting_back: { status: "Front captured — flip it over", hint: "Center the back cover in the guide box." },
+ book_complete: { status: "Book complete!", hint: "Tap Next Book to continue." },
+};
+
+async function refreshStatus() {
+ const response = await fetch("/api/session-state");
+ const body = await response.json();
+ const copy = STATE_COPY[body.state] || { status: body.state, hint: "" };
+ statusEl.textContent = copy.status;
+ hintEl.textContent = copy.hint;
+ triggerButton.textContent = body.state === "book_complete" ? "Next Book" : "Capture";
+}
+
+function refreshPreview() {
+ previewEl.src = `/api/preview.jpg?ts=${Date.now()}`;
+}
+
+async function observe() {
+ await fetch("/api/observe", { method: "POST" });
+ await refreshStatus();
+}
+
+triggerButton.addEventListener("click", async () => {
+ if (statusEl.textContent === "Book complete!") {
+ await fetch("/api/next-book", { method: "POST" });
+ } else {
+ await fetch("/api/manual-trigger", { method: "POST" });
+ }
+ await refreshStatus();
+});
+
+setInterval(observe, 500);
+setInterval(refreshPreview, 500);
+refreshStatus();
diff --git a/bookmark/static/index.html b/bookmark/static/index.html
index 9bb88d0..132f6e7 100644
--- a/bookmark/static/index.html
+++ b/bookmark/static/index.html
@@ -1,5 +1,22 @@
+
-
Books in Hand — Intake
-Books in Hand — Intake
+
+
+
+ Books in Hand — Intake
+
+
+
+
+
+
+
![Camera preview]()
+
+ Waiting for book...
+ Center the front cover in the guide box.
+
+
+
+
diff --git a/bookmark/static/style.css b/bookmark/static/style.css
new file mode 100644
index 0000000..1fd3f08
--- /dev/null
+++ b/bookmark/static/style.css
@@ -0,0 +1,48 @@
+/* bookmark/bookmark/static/style.css */
+.kiosk {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ padding: var(--space-lg);
+ gap: var(--space-md);
+ text-align: center;
+}
+
+.kiosk__preview {
+ width: min(90vw, 640px);
+ aspect-ratio: 4 / 3;
+ background: var(--color-surface);
+ border-radius: var(--radius);
+ overflow: hidden;
+}
+
+.kiosk__preview img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.kiosk__status {
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+.kiosk__hint {
+ color: var(--color-muted);
+}
+
+.kiosk__button {
+ padding: var(--space-md) var(--space-lg);
+ font-size: 1.25rem;
+ border: none;
+ border-radius: var(--radius);
+ background: var(--color-accent);
+ color: var(--color-bg);
+ cursor: pointer;
+}
+
+.kiosk__button:active {
+ opacity: 0.8;
+}
diff --git a/bookmark/static/theme.css b/bookmark/static/theme.css
new file mode 100644
index 0000000..38388a2
--- /dev/null
+++ b/bookmark/static/theme.css
@@ -0,0 +1,35 @@
+/* bookmark/bookmark/static/theme.css */
+:root {
+ --color-bg: #101418;
+ --color-surface: #1b2128;
+ --color-text: #f2f4f6;
+ --color-muted: #9aa5b1;
+ --color-accent: #4fb0ae;
+ --color-success: #5cb85c;
+ --color-warning: #e0a13a;
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 2rem;
+ --radius: 0.5rem;
+ --font-base: system-ui, -apple-system, "Segoe UI", sans-serif;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --color-bg: #f2f4f6;
+ --color-surface: #ffffff;
+ --color-text: #101418;
+ --color-muted: #5a6472;
+ }
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ background: var(--color-bg);
+ color: var(--color-text);
+ font-family: var(--font-base);
+}