ava-engine 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ /* Ava embed: a "Talk to Ava" button for your own site or product.
2
+ *
3
+ * <script src="https://YOUR-AVA/embed.js" data-link="SHARE_LINK_ID" async></script>
4
+ *
5
+ * Optional attributes:
6
+ * data-label="Need a hand?" button text (default "Talk to Ava")
7
+ * data-color="#4F46E5" button colour
8
+ * data-position="left" bottom-left instead of bottom-right
9
+ * data-open="tab" open in a new tab instead of a side window
10
+ *
11
+ * The call opens in its own small window next to your page, so the customer can
12
+ * share this tab and keep working in it while Ava guides them. The button lives in
13
+ * a shadow root: your styles can't break it and it can't touch yours.
14
+ * Call window.Ava.open() to open it from your own button or menu.
15
+ */
16
+ (() => {
17
+ const me = document.currentScript || document.querySelector('script[src*="embed.js"][data-link]');
18
+ if (!me || window.Ava?.loaded) return;
19
+ const link = me.dataset.link || "";
20
+ if (!/^[A-Za-z0-9_-]{22}$/.test(link)) { console.warn("[Ava] embed.js needs data-link set to a share link id (Dashboard → Share links → Embed)."); return; }
21
+ const origin = new URL(me.src, location.href).origin;
22
+ const url = `${origin}/c/${link}`;
23
+ const label = (me.dataset.label || "Talk to Ava").slice(0, 40);
24
+ const color = /^#[0-9a-f]{3,8}$/i.test(me.dataset.color || "") ? me.dataset.color : "#4F46E5";
25
+ const left = me.dataset.position === "left";
26
+
27
+ let win = null;
28
+ function open() {
29
+ if (win && !win.closed) { win.focus(); return; }
30
+ if (me.dataset.open !== "tab") {
31
+ const w = 440, h = Math.min(820, screen.availHeight - 40);
32
+ const x = left ? 20 : Math.max(0, screen.availWidth - w - 20);
33
+ win = window.open(url, "ava-call", `popup=yes,width=${w},height=${h},left=${x},top=20`);
34
+ }
35
+ if (!win) win = window.open(url, "_blank"); // popup blocked, or asked for a tab
36
+ if (!win) location.href = url;
37
+ }
38
+ window.Ava = { loaded: true, open, url };
39
+
40
+ const host = document.createElement("div");
41
+ host.setAttribute("data-ava-embed", "");
42
+ const root = host.attachShadow({ mode: "open" });
43
+ root.innerHTML = `<style>
44
+ :host { all: initial; }
45
+ button { position: fixed; ${left ? "left" : "right"}: 20px; bottom: 20px; z-index: 2147483000; display: inline-flex; align-items: center; gap: 10px;
46
+ height: 48px; padding: 0 20px 0 8px; border: 0; border-radius: 999px; background: #0B0B12; color: #fff; cursor: pointer;
47
+ font: 600 15px/1 "Inter Tight", Inter, -apple-system, "Segoe UI", sans-serif; letter-spacing: -.01em;
48
+ box-shadow: 0 10px 30px -10px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.08); transition: transform .2s cubic-bezier(.16,1,.3,1); }
49
+ button:hover { transform: translateY(-2px); }
50
+ button:focus-visible { outline: 3px solid ${color}; outline-offset: 3px; }
51
+ .orb { width: 32px; height: 32px; border-radius: 50%; background: radial-gradient(circle at 34% 30%, #fff 0%, ${color} 55%, #0B0B12 140%); box-shadow: 0 0 18px ${color}; animation: b 2.8s ease-in-out infinite; }
52
+ @keyframes b { 50% { transform: scale(1.08); } }
53
+ @media (prefers-reduced-motion: reduce) { .orb { animation: none; } button { transition: none; } }
54
+ @media print { button { display: none; } }
55
+ </style><button type="button" part="button" aria-label="${label.replace(/"/g, "&quot;")}: opens a live guide in a new window"><span class="orb" aria-hidden="true"></span><span></span></button>`;
56
+ root.querySelector("button span:last-child").textContent = label;
57
+ root.querySelector("button").addEventListener("click", open);
58
+ (document.body ? Promise.resolve() : new Promise((r) => document.addEventListener("DOMContentLoaded", r, { once: true })))
59
+ .then(() => document.body.append(host));
60
+ })();
package/public/index.html CHANGED
@@ -22,8 +22,8 @@
22
22
  </div>
23
23
 
24
24
  <div class="ob-amid">
25
- <h2>An assistant that sees your screen.</h2>
26
- <p>Ava joins your call, watches the window you share, and helps you through whatever you're doing — live.</p>
25
+ <h2 id="obAsideTitle">An assistant that sees your screen.</h2>
26
+ <p id="obAsideLead">Ava joins your call, watches the window you share, and helps you through whatever you're doing — live.</p>
27
27
  <ul class="ob-features">
28
28
  <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>Understands your screen as it changes, in real time</li>
29
29
  <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>Natural voice that answers in under a second</li>
@@ -45,12 +45,19 @@
45
45
  <div class="ob-form">
46
46
  <div class="ob-panel">
47
47
  <div class="ob-header">
48
- <h1>Start a session</h1>
49
- <p>Choose how Ava should work, then check your audio before connecting.</p>
50
- <p class="ob-demo" id="obDemo" hidden><b>Demo mode.</b> Replies are canned until you add an OpenRouter key: run <code>npx ava-engine setup</code>.</p>
48
+ <a class="ob-dashlink" id="obDash" href="/dashboard">
49
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18 9 12l6-6"/></svg>Dashboard</a>
50
+ <h1 id="obTitle">Start a session</h1>
51
+ <p id="obLead">Choose how Ava should work, then check your audio before connecting.</p>
52
+ <p class="ob-demo" id="obDemo" hidden><b>Demo mode.</b> <span id="obDemoText">Replies are canned until you add an OpenRouter key: run <code>npx ava-engine setup</code>.</span></p>
51
53
  </div>
52
54
 
53
- <div class="ob-section">
55
+ <div class="ob-section" id="obJourneyBlock" hidden>
56
+ <span class="ob-seclabel">What we'll do together</span>
57
+ <ol class="ob-journey" id="obJourney"></ol>
58
+ </div>
59
+
60
+ <div class="ob-section" id="obAgent">
54
61
  <span class="ob-seclabel">Agent</span>
55
62
  <div class="ob-modes" id="obModes"><!-- mode cards injected --></div>
56
63
  <div class="ob-platwrap" id="obPlatformBlock" hidden>
@@ -63,7 +70,7 @@
63
70
  <div class="ob-divider"></div>
64
71
 
65
72
  <div class="ob-section">
66
- <span class="ob-seclabel">Audio &amp; recognition</span>
73
+ <span class="ob-seclabel" id="obAudioLabel">Audio &amp; recognition</span>
67
74
  <div class="ob-gear">
68
75
  <div class="ob-field">
69
76
  <label class="ob-flabel" for="micsel">Microphone</label>
@@ -73,7 +80,7 @@
73
80
  </div>
74
81
  <div class="miclevel" id="miclevel"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
75
82
  </div>
76
- <div class="ob-field">
83
+ <div class="ob-field" id="obVoiceField">
77
84
  <label class="ob-flabel" for="voicesel2">Ava's voice</label>
78
85
  <div class="ob-fctl">
79
86
  <select id="voicesel2" class="ob-select"></select>
@@ -81,7 +88,7 @@
81
88
  </div>
82
89
  <span class="ob-fnote">Preview also warms the voice, so the call starts fast.</span>
83
90
  </div>
84
- <div class="ob-field ob-field-wide">
91
+ <div class="ob-field ob-field-wide" id="obSttField">
85
92
  <label class="ob-flabel" for="sttsel">Speech recognition</label>
86
93
  <select id="sttsel" class="ob-select"></select>
87
94
  <span class="ob-fnote" id="sttnote">Choose before the call.</span>
@@ -93,7 +100,7 @@
93
100
  <div class="ob-actions">
94
101
  <button type="button" id="joinvoice" class="amberbtn ob-bigbtn">
95
102
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11.5a6.5 6.5 0 0 0 13 0M12 18v3"/></svg>
96
- Join with voice
103
+ <span id="joinvoiceText">Join with voice</span>
97
104
  </button>
98
105
  <button type="button" id="jointext" class="ghostbtn ob-bigbtn">Join text-only</button>
99
106
  <span class="ob-joinstate" id="joinstate"></span>
@@ -0,0 +1,30 @@
1
+ @font-face { font-family: "Inter Tight"; src: url(/fonts/inter-tight-var.woff2) format("woff2"); font-weight: 300 700; font-display: swap; }
2
+ @font-face { font-family: "Inter"; src: url(/fonts/inter-var.woff2) format("woff2"); font-weight: 400 700; font-display: swap; }
3
+ @font-face { font-family: "Plex Mono"; src: url(/fonts/plex-mono-400.woff2) format("woff2"); font-weight: 400; font-display: swap; }
4
+ :root { --ink: #0A0A0C; --ink2: #4A4B55; --meta: #686975; --edge: #DCDCE3; --accent: #4F46E5; --accent-ink: #3730A3; --tint: rgba(79,70,229,.10); --err: #B42318; }
5
+ * { box-sizing: border-box; }
6
+ [hidden] { display: none !important; }
7
+ html, body { margin: 0; min-height: 100%; background: #0B0B12; color: var(--ink); font: 16px/1.5 Inter, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; }
8
+ .wrap { position: relative; min-height: 100vh; min-height: 100dvh; display: grid; place-items: center; padding: 24px 16px; overflow: hidden; }
9
+ .glow { position: absolute; width: 900px; height: 900px; left: 50%; top: 50%; transform: translate(-50%, -55%); background: radial-gradient(closest-side, rgba(99,102,241,.38), transparent 70%); pointer-events: none; }
10
+ .card { position: relative; width: min(420px, 100%); background: #fff; border-radius: 24px; padding: 34px 32px 28px; box-shadow: 0 30px 80px -30px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.06); animation: rise .5s cubic-bezier(.16,1,.3,1); }
11
+ @keyframes rise { from { opacity: 0; transform: translateY(10px); } }
12
+ .brand { display: flex; align-items: center; justify-content: space-between; margin-bottom: 28px; }
13
+ .wm { width: 52px; height: 22px; }
14
+ .orb { width: 34px; height: 34px; border-radius: 50%; background: radial-gradient(circle at 34% 30%, #E4E5FF, #6366F1 48%, #3730A3 100%); box-shadow: 0 0 30px rgba(124,108,246,.55); animation: breathe 2.8s ease-in-out infinite; }
15
+ @keyframes breathe { 50% { transform: scale(1.07); } }
16
+ h1 { margin: 0 0 6px; font: 300 32px/1.1 "Inter Tight", Inter, sans-serif; letter-spacing: -.03em; }
17
+ .lead { margin: 0 0 24px; color: var(--ink2); font-size: 15px; }
18
+ .field { display: flex; flex-direction: column; gap: 7px; margin-bottom: 16px; }
19
+ label { font-weight: 600; font-size: 14px; }
20
+ input { height: 48px; padding: 0 15px; border: 1.5px solid var(--edge); border-radius: 12px; font: 15px Inter, sans-serif; color: var(--ink); background: #fff; }
21
+ input:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px var(--tint); }
22
+ .error { margin: 0 0 12px; min-height: 0; color: var(--err); font-size: 14px; }
23
+ .error:empty { display: none; }
24
+ .btn { width: 100%; height: 48px; border: 0; border-radius: 99px; background: var(--accent); color: #fff; font: 600 15px "Inter Tight", Inter, sans-serif; cursor: pointer; box-shadow: 0 8px 24px -10px rgba(79,70,229,.7); }
25
+ .btn:hover { background: var(--accent-ink); }
26
+ .btn:disabled { opacity: .6; cursor: wait; }
27
+ .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
28
+ .foot { margin: 18px 0 0; font-size: 13px; color: var(--meta); text-align: center; }
29
+ code { font: 12px "Plex Mono", ui-monospace, monospace; background: #F5F5F7; border: 1px solid #E9E9ED; padding: 1px 5px; border-radius: 6px; }
30
+ @media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="robots" content="noindex">
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
8
+ <title>Sign in · Ava</title>
9
+ <link rel="preload" href="/fonts/inter-tight-var.woff2" as="font" type="font/woff2" crossorigin>
10
+ <link rel="stylesheet" href="/login.css">
11
+ <script src="/login.js" defer></script>
12
+ </head>
13
+ <body>
14
+ <main class="wrap">
15
+ <div class="glow" aria-hidden="true"></div>
16
+ <form class="card" id="form" novalidate>
17
+ <div class="brand">
18
+ <svg class="wm" viewBox="0 0 66 27.8" role="img" aria-label="Ava"><path transform="translate(1.9 1.9)" d="M0 24L9.5 0L19 24M21.6 0L31.1 24L40.6 0M43.2 24L52.7 0L62.2 24" fill="none" stroke="currentColor" stroke-width="3.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
19
+ <span class="orb" aria-hidden="true"></span>
20
+ </div>
21
+ <h1>Sign in to Ava</h1>
22
+ <p class="lead" id="lead">Your dashboard, test calls and the specialist console.</p>
23
+ <div class="field" id="emailField" hidden>
24
+ <label for="email">Email</label>
25
+ <input id="email" type="email" autocomplete="username" spellcheck="false">
26
+ </div>
27
+ <div class="field">
28
+ <label for="password" id="passwordLabel">Password</label>
29
+ <input id="password" type="password" autocomplete="current-password">
30
+ </div>
31
+ <p class="error" id="error" role="alert"></p>
32
+ <button class="btn" type="submit" id="submit">Sign in</button>
33
+ <p class="foot" id="foot">Forgot it? An owner can reset it under Team, or run <code>npx ava-engine team</code> on the server.</p>
34
+ </form>
35
+ </main>
36
+ </body>
37
+ </html>
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ // Sign in with a team account (email + password) or the shared access password.
3
+ (async () => {
4
+ const $ = (id) => document.getElementById(id);
5
+ const next = new URLSearchParams(location.search).get("next") || "/dashboard";
6
+ let mode = { accounts: false, password: true };
7
+ try { mode = await (await fetch("/api/auth/mode")).json(); } catch {}
8
+ if (mode.accounts) { $("emailField").hidden = false; $("email").focus(); }
9
+ else {
10
+ $("passwordLabel").textContent = "Access password";
11
+ $("lead").textContent = "Enter the password this Ava was set up with.";
12
+ $("foot").innerHTML = "Forgot it? Run <code>npx ava-engine setup</code> on the server to set a new one.";
13
+ $("password").focus();
14
+ }
15
+ $("form").addEventListener("submit", async (e) => {
16
+ e.preventDefault();
17
+ $("submit").disabled = true; $("error").textContent = "";
18
+ try {
19
+ const r = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: mode.accounts ? $("email").value : undefined, password: $("password").value, next }) });
20
+ const j = await r.json().catch(() => ({}));
21
+ if (!r.ok) throw new Error(j.error || "Couldn't sign in.");
22
+ location.replace(j.next || "/dashboard");
23
+ } catch (err) {
24
+ $("error").textContent = err.message; $("submit").disabled = false; $("password").select();
25
+ }
26
+ });
27
+ })();
package/public/setup.css CHANGED
@@ -1,97 +1,178 @@
1
- @font-face { font-family: "Inter Tight"; src: url(/fonts/inter-tight-var.woff2) format("woff2"); font-weight: 100 900; font-display: swap; }
2
- @font-face { font-family: "Inter"; src: url(/fonts/inter-var.woff2) format("woff2"); font-weight: 100 900; font-display: swap; }
1
+ /* ── Ava · Setup ──
2
+ The first thing anyone sees of Ava, so it speaks the app's language: a dark
3
+ rail with the indigo glow of the call screen, the ΛVΛ wordmark, light Inter
4
+ Tight display type, mono labels, soft cards and the breathing orb. */
5
+
6
+ @font-face { font-family: "Inter Tight"; src: url(/fonts/inter-tight-var.woff2) format("woff2"); font-weight: 300 700; font-display: swap; }
7
+ @font-face { font-family: "Inter"; src: url(/fonts/inter-var.woff2) format("woff2"); font-weight: 400 700; font-display: swap; }
3
8
  @font-face { font-family: "Plex Mono"; src: url(/fonts/plex-mono-400.woff2) format("woff2"); font-weight: 400; font-display: swap; }
9
+ @font-face { font-family: "Plex Mono"; src: url(/fonts/plex-mono-500.woff2) format("woff2"); font-weight: 500 600; font-display: swap; }
4
10
 
5
11
  :root {
6
- --ink: #0A0A0C; --ink2: #4A4B55; --meta: #686975; --line: #E6E6EB; --soft: #F5F5F7; --card: #FFFFFF;
7
- --accent: #4F46E5; --accent-tint: rgba(79,70,229,0.08); --ok: #15803D; --ok-tint: #ECFDF3; --err: #B42318; --err-tint: #FEF3F2;
8
- --radius: 14px;
12
+ --ink: #0A0A0C; --ink2: #4A4B55; --meta: #686975; --line: #E6E6EB; --edge: #DCDCE3; --soft: #F5F5F7; --card: #FFFFFF;
13
+ --accent: #4F46E5; --accent-ink: #3730A3; --accent-tint: rgba(79,70,229,.08); --accent-line: rgba(79,70,229,.30);
14
+ --ok: #15803D; --ok-tint: #ECFDF3; --err: #B42318; --err-tint: #FEF3F2;
15
+ --night: #0B0B12;
16
+ --orb: radial-gradient(circle at 34% 30%, #E4E5FF, #6366F1 48%, #3730A3 100%);
17
+ --shadow-sm: 0 1px 2px rgba(10,10,12,.04), 0 1px 3px rgba(10,10,12,.05);
18
+ --shadow-md: 0 1px 2px rgba(10,10,12,.04), 0 12px 32px -12px rgba(10,10,12,.16);
19
+ --ease: cubic-bezier(.16,1,.3,1);
20
+ --disp: "Inter Tight", Inter, sans-serif; --mono: "Plex Mono", ui-monospace, monospace;
21
+ --radius: 16px;
9
22
  }
10
23
  * { box-sizing: border-box; }
11
24
  [hidden] { display: none !important; } /* author display rules (e.g. .field) must not un-hide things */
12
- html, body { margin: 0; background: var(--soft); color: var(--ink); font: 16px/1.5 Inter, -apple-system, "Segoe UI", sans-serif; }
25
+ html, body { margin: 0; background: var(--soft); color: var(--ink); font: 16px/1.5 Inter, -apple-system, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
13
26
  a { color: var(--accent); }
14
- code { font: 13px "Plex Mono", ui-monospace, monospace; background: var(--soft); padding: 1px 5px; border-radius: 5px; }
15
-
16
- .shell { display: grid; grid-template-columns: 300px 1fr; min-height: 100vh; min-height: 100dvh; }
17
- .rail { background: var(--ink); color: #fff; padding: 36px 30px; display: flex; flex-direction: column; gap: 36px; }
18
- .brand { display: flex; align-items: center; gap: 12px; font: 600 20px "Inter Tight", sans-serif; }
19
- .brand svg { width: 34px; height: 34px; }
20
- .brand svg rect { fill: #fff; } .brand svg path { stroke: #0A0A0C; }
21
- .steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; counter-reset: s; }
22
- .steps li { counter-increment: s; display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 10px; color: #9D9DAB; font-weight: 500; }
23
- .steps li::before { content: counter(s); width: 26px; height: 26px; flex: none; display: grid; place-items: center; border-radius: 50%; border: 1.5px solid #3A3A46; font: 500 13px "Plex Mono", monospace; }
24
- .steps li.done { color: #D6D6E0; } .steps li.done::before { content: "✓"; background: #2B2B36; border-color: #2B2B36; }
25
- .steps li.current { color: #fff; background: #1C1C24; } .steps li.current::before { border-color: #A5A0FF; color: #A5A0FF; }
26
- .rail-note { margin-top: auto; font-size: 13px; color: #9D9DAB; }
27
- .rail-note code { background: #1C1C24; color: #D6D6E0; }
28
-
29
- .panel { padding: 56px clamp(20px, 6vw, 88px); max-width: 860px; width: 100%; }
30
- .eyebrow { margin: 0 0 8px; font: 500 13px "Plex Mono", monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--accent); }
31
- h1 { margin: 0 0 12px; font: 600 clamp(28px, 4vw, 40px)/1.1 "Inter Tight", sans-serif; letter-spacing: -0.025em; }
32
- .lead { margin: 0 0 28px; color: var(--ink2); font-size: 17px; max-width: 640px; }
33
-
34
- .cards { display: grid; gap: 12px; margin-bottom: 24px; }
35
- .card { position: relative; display: flex; gap: 14px; align-items: flex-start; padding: 18px 20px; background: var(--card); border: 1.5px solid var(--line); border-radius: var(--radius); cursor: pointer; transition: border-color .15s, box-shadow .15s; }
36
- .card:hover { border-color: #CFCFD8; }
37
- .card:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-tint); }
27
+ code { font: 13px var(--mono); background: var(--soft); border: 1px solid var(--line); padding: 1px 6px; border-radius: 6px; }
28
+ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; }
29
+
30
+ .shell { display: grid; grid-template-columns: 320px 1fr; min-height: 100vh; min-height: 100dvh; }
31
+
32
+ /* ── rail ── */
33
+ .rail { position: sticky; top: 0; height: 100vh; height: 100dvh; overflow: hidden; background: var(--night); color: #fff; padding: 34px 28px 26px; display: flex; flex-direction: column; gap: 34px; isolation: isolate; }
34
+ .rail::before { content: ""; position: absolute; z-index: -1; width: 560px; height: 560px; left: -220px; bottom: -260px; background: radial-gradient(closest-side, rgba(99,102,241,.42), transparent 72%); }
35
+ .rail::after { content: ""; position: absolute; z-index: -1; width: 300px; height: 300px; right: -170px; top: -120px; background: radial-gradient(closest-side, rgba(165,160,255,.18), transparent 70%); }
36
+ .brand { display: flex; align-items: center; gap: 12px; }
37
+ .brand .wm { width: 52px; height: 22px; stroke-width: 0; }
38
+ .brand .wm path { stroke: #fff; }
39
+ .brand-tag { font: 500 12px var(--mono); letter-spacing: .14em; text-transform: uppercase; color: #A5A0FF; border-left: 1px solid #2A2A36; padding-left: 12px; }
40
+
41
+ .steps { position: relative; list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; counter-reset: s; }
42
+ .steps::before { content: ""; position: absolute; left: 23px; top: 26px; bottom: 26px; width: 1.5px; background: #262632; }
43
+ .steps li { position: relative; counter-increment: s; display: grid; grid-template-columns: 30px 1fr; column-gap: 14px; align-items: center; padding: 10px 10px; border-radius: 12px; color: #8E8EA0; transition: background .25s, color .25s; }
44
+ .steps li::before { content: counter(s); grid-row: 1 / span 2; width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%; background: var(--night); border: 1.5px solid #34343F; font: 500 12px var(--mono); transition: all .25s var(--ease); }
45
+ .steps-name { font-weight: 500; font-size: 15px; }
46
+ .steps-sub { font: 12px var(--mono); color: #8A8A9C; }
47
+ .steps li.done { color: #D6D6E0; }
48
+ .steps li.done::before { content: "✓"; background: #2B2B3A; border-color: #2B2B3A; color: #A5A0FF; }
49
+ .steps li.current { color: #fff; background: rgba(255,255,255,.05); }
50
+ .steps li.current::before { border-color: #A5A0FF; color: #fff; background: #3730A3; box-shadow: 0 0 0 5px rgba(99,102,241,.22); }
51
+ .steps li.current .steps-sub { color: #B7B3FF; }
52
+
53
+ .rail-card { margin-top: auto; display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 16px; background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.08); backdrop-filter: blur(8px); }
54
+ .rail-orb { width: 30px; height: 30px; border-radius: 50%; background: var(--orb); box-shadow: 0 0 22px rgba(124,108,246,.6); animation: breathe 2.8s ease-in-out infinite; }
55
+ .rail-card-text { display: flex; flex-direction: column; flex: 1; }
56
+ .rail-card-text b { font: 600 14px var(--disp); }
57
+ .rail-card-text span { font-size: 11.5px; color: #B7B3FF; }
58
+ .rail-wave { display: flex; gap: 3px; align-items: center; height: 18px; }
59
+ .rail-wave i { width: 3px; border-radius: 3px; background: #A5A0FF; animation: wave 1.2s ease-in-out infinite; }
60
+ .rail-wave i:nth-child(1) { height: 6px; } .rail-wave i:nth-child(2) { height: 12px; animation-delay: .15s; } .rail-wave i:nth-child(3) { height: 17px; animation-delay: .3s; } .rail-wave i:nth-child(4) { height: 10px; animation-delay: .45s; } .rail-wave i:nth-child(5) { height: 6px; animation-delay: .6s; }
61
+ @keyframes wave { 50% { transform: scaleY(.45); opacity: .6; } }
62
+ .rail-note { margin: 0; font-size: 13px; color: #A3A3B5; }
63
+ .rail-note code { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.1); color: #D6D6E0; word-break: break-all; }
64
+
65
+ /* ── panel ── */
66
+ .panel { position: relative; padding: 72px clamp(20px, 6vw, 96px) 64px; width: 100%; min-width: 0; }
67
+ .panel > .step, .panel > .banner { max-width: 720px; }
68
+ .progress { position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--line); }
69
+ .progress i { display: block; height: 100%; width: 4%; background: linear-gradient(90deg, #6366F1, var(--accent)); transition: width .6s var(--ease); }
70
+ .step { animation: rise .5s var(--ease); }
71
+ @keyframes rise { from { opacity: 0; transform: translateY(10px); } }
72
+ .eyebrow { margin: 0 0 12px; font: 500 12px var(--mono); letter-spacing: .16em; text-transform: uppercase; color: var(--accent); }
73
+ h1 { margin: 0 0 14px; font: 300 clamp(32px, 4.4vw, 48px)/1.05 var(--disp); letter-spacing: -.035em; }
74
+ .lead { margin: 0 0 30px; color: var(--ink2); font-size: 17px; max-width: 620px; }
75
+
76
+ /* welcome */
77
+ .welcome { padding-top: 12px; }
78
+ .hero-orb { position: relative; width: 112px; height: 112px; margin: 0 0 34px; display: grid; place-items: center; }
79
+ .hero-orb span { width: 72px; height: 72px; border-radius: 50%; background: var(--orb); box-shadow: 0 0 60px rgba(124,108,246,.55); animation: breathe 2.8s ease-in-out infinite; }
80
+ .hero-orb i { position: absolute; inset: 0; border-radius: 50%; border: 1.5px solid var(--accent-line); animation: ring 2.8s var(--ease) infinite; }
81
+ .hero-orb i:nth-child(2) { animation-delay: 1.4s; }
82
+ .hero-orb.big { width: 150px; height: 150px; margin: 0 auto 38px; }
83
+ .hero-orb.big span { width: 96px; height: 96px; }
84
+ @keyframes breathe { 50% { transform: scale(1.06); } }
85
+ @keyframes ring { from { transform: scale(.65); opacity: 1; } to { transform: scale(1.25); opacity: 0; } }
86
+ .welcome-list { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 12px; max-width: 620px; }
87
+ .welcome-list li { display: flex; gap: 16px; align-items: flex-start; padding: 16px 18px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
88
+ .welcome-list svg { width: 22px; height: 22px; color: var(--accent); margin-top: 2px; }
89
+ .welcome-list li > span { display: flex; flex-direction: column; gap: 2px; }
90
+ .welcome-list b { font: 600 16px var(--disp); }
91
+ .welcome-list span span { color: var(--ink2); font-size: 14.5px; }
92
+
93
+ /* option cards */
94
+ .cards { display: grid; gap: 12px; margin-bottom: 26px; }
95
+ .card { position: relative; display: flex; gap: 16px; align-items: center; padding: 18px 20px; background: var(--card); border: 1.5px solid var(--line); border-radius: var(--radius); cursor: pointer; box-shadow: var(--shadow-sm); transition: border-color .2s, box-shadow .2s, transform .2s var(--ease); }
96
+ .card:hover { border-color: #CFCFD8; transform: translateY(-1px); box-shadow: var(--shadow-md); }
97
+ .card:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-tint), var(--shadow-md); }
38
98
  .card:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 3px; }
39
- .card input { margin: 4px 0 0; width: 18px; height: 18px; accent-color: var(--accent); flex: none; }
99
+ .card:has(input:disabled) { cursor: default; }
100
+ .card input { position: absolute; opacity: 0; width: 1px; height: 1px; } /* the whole card is the control */
101
+ .card-icon { width: 46px; height: 46px; flex: none; display: grid; place-items: center; border-radius: 14px; background: var(--soft); border: 1px solid var(--line); color: var(--ink2); transition: all .2s; }
102
+ .card-icon svg { width: 21px; height: 21px; }
103
+ .card:has(input:checked) .card-icon { background: var(--accent); border-color: var(--accent); color: #fff; }
40
104
  .card-body { display: flex; flex-direction: column; gap: 3px; flex: 1; }
41
- .card-body b { font: 600 17px "Inter Tight", sans-serif; }
105
+ .card-body b { font: 600 17px var(--disp); letter-spacing: -.005em; }
42
106
  .card-body span { color: var(--ink2); font-size: 14.5px; }
107
+ .card::after { content: ""; width: 22px; height: 22px; flex: none; border-radius: 50%; border: 1.5px solid var(--edge); transition: all .2s; }
108
+ .card:has(input[type=checkbox])::after { border-radius: 7px; }
109
+ .card:has(input:checked)::after { border-color: var(--accent); background: var(--accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 13px no-repeat; }
43
110
  .card:has(.badge) .card-body { padding-right: 84px; }
44
- .badge { position: absolute; top: 16px; right: 18px; font: 500 12px "Plex Mono", monospace; color: var(--ok); background: var(--ok-tint); padding: 2px 8px; border-radius: 99px; }
111
+ .badge { position: absolute; top: 14px; right: 54px; font: 500 12px var(--mono); color: var(--ok); background: var(--ok-tint); padding: 2px 8px; border-radius: 99px; }
45
112
 
113
+ /* fields */
46
114
  .field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 24px; }
47
115
  .field label { font-weight: 600; font-size: 15px; }
116
+ .keybox { padding: 22px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
48
117
  .opt { font-weight: 400; color: var(--meta); }
49
118
  .row { display: flex; gap: 8px; flex-wrap: wrap; }
50
- input[type=text], input[type=password], input[type=number] { flex: 1; min-width: 200px; height: 46px; padding: 0 14px; border: 1.5px solid var(--line); border-radius: 10px; font: 15px "Plex Mono", monospace; background: #fff; color: var(--ink); }
119
+ input[type=text], input[type=password], input[type=number] { flex: 1; min-width: 200px; height: 48px; padding: 0 15px; border: 1.5px solid var(--edge); border-radius: 12px; font: 15px var(--mono); background: #fff; color: var(--ink); transition: border-color .15s, box-shadow .15s; }
51
120
  input:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-tint); }
52
121
  input.short { max-width: 160px; flex: none; }
53
122
  .hint { margin: 0; color: var(--meta); font-size: 14px; }
54
123
  .keep label, .check { font-weight: 400 !important; display: flex; gap: 10px; align-items: center; font-size: 15px; }
55
124
  .keep input, .check input { width: 18px; height: 18px; accent-color: var(--accent); }
56
-
57
125
  .connect { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
58
- .or { display: flex; align-items: center; gap: 12px; margin: 10px 0 2px; color: var(--meta); font-size: 14px; }
126
+ .or { display: flex; align-items: center; gap: 12px; margin: 14px 0 4px; color: var(--meta); font-size: 13px; font-family: var(--mono); letter-spacing: .04em; }
59
127
  .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
128
+
60
129
  .status { margin: 0; min-height: 22px; font-size: 14.5px; }
61
130
  .status.ok { color: var(--ok); } .status.err { color: var(--err); }
62
131
  .banner { padding: 14px 18px; border-radius: 12px; margin-bottom: 24px; }
63
132
  .banner.error { background: var(--err-tint); color: var(--err); }
64
133
 
65
- .job { background: #fff; border: 1.5px solid var(--line); border-radius: 12px; padding: 14px 16px; margin-bottom: 12px; }
134
+ /* downloads */
135
+ .job { background: #fff; border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; margin-bottom: 12px; box-shadow: var(--shadow-sm); }
66
136
  .job-head { display: flex; justify-content: space-between; font-size: 14.5px; margin-bottom: 10px; font-weight: 500; }
67
- .job-head span:last-child { font-family: "Plex Mono", monospace; color: var(--meta); }
137
+ .job-head span:last-child { font-family: var(--mono); color: var(--meta); }
68
138
  .bar { height: 8px; background: var(--soft); border-radius: 99px; overflow: hidden; }
69
- .bar i { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 99px; transition: width .3s; }
139
+ .bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, #6366F1, var(--accent)); border-radius: 99px; transition: width .3s; }
70
140
  .job.done .bar i { background: var(--ok); } .job.error .bar i { background: var(--err); }
71
141
 
72
- .summary { display: grid; grid-template-columns: max-content 1fr; gap: 14px 28px; background: #fff; border: 1.5px solid var(--line); border-radius: var(--radius); padding: 22px 24px; margin: 0 0 20px; }
73
- .summary dt { color: var(--meta); font-size: 14.5px; } .summary dd { margin: 0; font-weight: 500; word-break: break-word; }
142
+ /* review */
143
+ .summary { display: grid; grid-template-columns: max-content 1fr; gap: 0; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); margin: 0 0 20px; box-shadow: var(--shadow-sm); overflow: hidden; }
144
+ .summary dt, .summary dd { padding: 15px 22px; border-bottom: 1px solid var(--line); margin: 0; }
145
+ .summary dt { color: var(--meta); font-size: 14.5px; }
146
+ .summary dd { font-weight: 500; word-break: break-word; }
147
+ .summary > :nth-last-child(-n+2) { border-bottom: 0; }
74
148
 
75
- .nav { display: flex; justify-content: space-between; align-items: center; margin-top: 32px; gap: 12px; }
76
- .btn { height: 46px; padding: 0 22px; border-radius: 99px; font: 600 15px "Inter Tight", sans-serif; cursor: pointer; border: 1.5px solid transparent; }
77
- .btn.primary { background: var(--ink); color: #fff; }
78
- .btn.primary:hover { background: #24242C; }
79
- .btn.primary:disabled { background: #B9B9C3; cursor: not-allowed; }
80
- .btn.ghost { background: #fff; color: var(--ink); border-color: var(--line); }
81
- .btn.ghost:hover { border-color: #CFCFD8; }
149
+ /* buttons */
150
+ .nav { display: flex; justify-content: space-between; align-items: center; margin-top: 36px; gap: 12px; }
151
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 9px; height: 48px; padding: 0 24px; border-radius: 99px; font: 600 15px var(--disp); cursor: pointer; border: 1.5px solid transparent; transition: background .15s, border-color .15s, transform .15s var(--ease), box-shadow .2s; }
152
+ .btn:active { transform: scale(.98); }
153
+ .btn svg { width: 17px; height: 17px; }
154
+ .btn.primary { background: var(--accent); color: #fff; box-shadow: 0 8px 24px -10px rgba(79,70,229,.7); }
155
+ .btn.primary:hover { background: var(--accent-ink); }
156
+ .btn.primary:disabled { background: #B9B9C3; box-shadow: none; cursor: not-allowed; }
157
+ .btn.ghost { background: #fff; color: var(--ink); border-color: var(--edge); }
158
+ .btn.ghost:hover { border-color: #C6C6D0; background: var(--soft); }
82
159
  .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
83
160
 
84
- .launching { text-align: center; padding-top: 12vh; }
161
+ .launching { text-align: center; padding-top: 10vh; }
85
162
  .launching .lead { margin-inline: auto; }
86
- .orb { width: 120px; height: 120px; margin: 0 auto 36px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #C9C4FF 0%, #8B7CFF 28%, #5B4FF0 60%, #3B2FC4 100%); box-shadow: 0 0 80px rgba(124,108,246,.55); animation: pulse 1.6s ease-in-out infinite; }
87
- @keyframes pulse { 50% { transform: scale(1.06); box-shadow: 0 0 120px rgba(124,108,246,.75); } }
88
- @media (prefers-reduced-motion: reduce) { .orb { animation: none; } .bar i { transition: none; } }
89
163
 
90
- @media (max-width: 760px) {
164
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
165
+
166
+ @media (max-width: 860px) {
91
167
  .shell { grid-template-columns: 1fr; }
92
- .rail { padding: 20px; gap: 16px; }
168
+ .rail { position: relative; height: auto; padding: 18px 16px; gap: 14px; }
93
169
  .steps { flex-direction: row; overflow-x: auto; gap: 4px; }
94
- .steps li { padding: 6px 8px; font-size: 0; gap: 0; } .steps li.current { font-size: 14px; gap: 8px; }
95
- .rail-note { display: none; }
96
- .panel { padding: 32px 16px; }
170
+ .steps::before, .steps-sub, .rail-card, .rail-note { display: none; }
171
+ .steps li { grid-template-columns: 28px; padding: 6px; }
172
+ .steps-name { display: none; }
173
+ .steps li.current { grid-template-columns: 28px auto; column-gap: 8px; }
174
+ .steps li.current .steps-name { display: inline; font-size: 14px; }
175
+ .panel { padding: 36px 16px 48px; }
176
+ .card { padding: 16px; gap: 12px; }
177
+ .card-icon { width: 40px; height: 40px; }
97
178
  }
package/public/setup.html CHANGED
@@ -12,36 +12,56 @@
12
12
  <div class="shell">
13
13
  <aside class="rail" aria-label="Setup progress">
14
14
  <div class="brand">
15
- <svg viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="#0A0A0C"/><path transform="translate(2.67 8.4) scale(.3)" d="M0 24L9.5 0L19 24M21.6 0L31.1 24L40.6 0M43.2 24L52.7 0L62.2 24" stroke="#fff" fill="none" stroke-width="3.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
16
- <span>Ava Engine</span>
15
+ <svg class="wm" viewBox="0 0 66 27.8" role="img" aria-label="Ava"><path transform="translate(1.9 1.9)" d="M0 24L9.5 0L19 24M21.6 0L31.1 24L40.6 0M43.2 24L52.7 0L62.2 24" fill="none" stroke="currentColor" stroke-width="3.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
16
+ <span class="brand-tag">Engine setup</span>
17
17
  </div>
18
18
  <ol class="steps" id="steps">
19
- <li data-step="ai">AI replies</li>
20
- <li data-step="voice">Voice</li>
21
- <li data-step="speech">Speech recognition</li>
22
- <li data-step="access">Access</li>
23
- <li data-step="review">Launch</li>
19
+ <li data-step="ai"><span class="steps-name">AI replies</span><span class="steps-sub">OpenRouter or demo</span></li>
20
+ <li data-step="voice"><span class="steps-name">Voice</span><span class="steps-sub">How Ava sounds</span></li>
21
+ <li data-step="speech"><span class="steps-name">Speech recognition</span><span class="steps-sub">Offline, optional</span></li>
22
+ <li data-step="access"><span class="steps-name">Access</span><span class="steps-sub">Who can reach Ava</span></li>
23
+ <li data-step="review"><span class="steps-name">Launch</span><span class="steps-sub">Open the dashboard</span></li>
24
24
  </ol>
25
- <p class="rail-note">Settings are saved on this computer only, in <code id="configFile">~/.ava-engine/.env</code>.</p>
25
+ <div class="rail-card" aria-hidden="true">
26
+ <span class="rail-orb"></span>
27
+ <span class="rail-card-text"><b>Ava</b><span class="mono">getting ready</span></span>
28
+ <span class="rail-wave"><i></i><i></i><i></i><i></i><i></i></span>
29
+ </div>
30
+ <p class="rail-note">Saved on this computer only, in <code id="configFile">~/.ava-engine/.env</code>.</p>
26
31
  </aside>
27
32
 
28
33
  <main class="panel">
34
+ <div class="progress" aria-hidden="true"><i id="progressBar"></i></div>
29
35
  <div id="fatal" class="banner error" role="alert" hidden></div>
30
36
 
37
+ <!-- 0 · Welcome -->
38
+ <section class="step welcome" data-step="welcome" aria-labelledby="h-welcome">
39
+ <div class="hero-orb" aria-hidden="true"><i></i><i></i><span></span></div>
40
+ <p class="eyebrow">Welcome</p>
41
+ <h1 id="h-welcome">Let's get Ava ready</h1>
42
+ <p class="lead">A live, screen-aware guide for your customers. Five short steps, about two minutes. You can change any of it later.</p>
43
+ <ul class="welcome-list">
44
+ <li><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="3.2"/></svg><span><b>Pick its brain</b><span>Connect OpenRouter for real answers, or start in demo mode</span></span></li>
45
+ <li><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11.5 4.5 6.8 8.6H3.2v6.8h3.6l4.7 4.1V4.5Z"/><path d="M15.4 9.6a3.6 3.6 0 0 1 0 4.8M18 7a7 7 0 0 1 0 10"/></svg><span><b>Give it a voice</b><span>Free and offline, or premium cloud voices</span></span></li>
46
+ <li><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5"/></svg><span><b>Decide who gets in</b><span>Just you, or your team and customers with a password</span></span></li>
47
+ </ul>
48
+ <div class="nav"><span></span><button type="button" class="btn primary" data-next>Get started<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button></div>
49
+ </section>
50
+
31
51
  <!-- 1 · AI -->
32
- <section class="step" data-step="ai" aria-labelledby="h-ai">
52
+ <section class="step" data-step="ai" aria-labelledby="h-ai" hidden>
33
53
  <p class="eyebrow">Step 1 of 5</p>
34
54
  <h1 id="h-ai">How should Ava think?</h1>
35
55
  <p class="lead">Ava's AI runs through <a href="https://openrouter.ai" target="_blank" rel="noopener">OpenRouter</a>: one key for every model. You can also look around first with canned replies.</p>
36
56
  <div class="cards" role="radiogroup" aria-label="AI mode">
37
- <label class="card"><input type="radio" name="mode" value="full" checked><span class="card-body"><b>With real AI</b><span>Recommended. Needs an OpenRouter key.</span></span></label>
38
- <label class="card"><input type="radio" name="mode" value="mock"><span class="card-body"><b>Just look around first</b><span>No key. Canned replies; voice, screen sharing and everything else work.</span></span></label>
57
+ <label class="card"><input type="radio" name="mode" value="full" checked><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="3.2"/></svg></span><span class="card-body"><b>With real AI</b><span>Recommended. Needs an OpenRouter key.</span></span></label>
58
+ <label class="card"><input type="radio" name="mode" value="mock"><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/></svg></span><span class="card-body"><b>Just look around first</b><span>No key. Canned replies; voice, screen sharing and everything else work.</span></span></label>
39
59
  </div>
40
- <div class="field" id="keyField">
60
+ <div class="field keybox" id="keyField">
41
61
  <div id="keepKeyRow" class="keep" hidden><label><input type="checkbox" id="keepKey" checked> Keep my current key <code id="currentKey"></code></label></div>
42
62
  <div id="newKeyRow">
43
63
  <div class="connect">
44
- <button type="button" class="btn primary" id="connectOr">Connect OpenRouter</button>
64
+ <button type="button" class="btn primary" id="connectOr"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>Connect OpenRouter</button>
45
65
  <p class="hint">Sign in to OpenRouter and approve, and Ava gets its own key. Nothing to copy.</p>
46
66
  <p class="status" id="connectStatus" role="status" aria-live="polite"></p>
47
67
  </div>
@@ -55,7 +75,7 @@
55
75
  <p class="status" id="keyStatus" role="status" aria-live="polite"></p>
56
76
  </div>
57
77
  </div>
58
- <div class="nav"><span></span><button type="button" class="btn primary" data-next>Continue</button></div>
78
+ <div class="nav"><button type="button" class="btn ghost" data-back>Back</button><button type="button" class="btn primary" data-next>Continue</button></div>
59
79
  </section>
60
80
 
61
81
  <!-- 2 · Voice -->
@@ -95,8 +115,8 @@
95
115
  <p class="eyebrow">Step 4 of 5</p>
96
116
  <h1 id="h-access">Who will use this Ava?</h1>
97
117
  <div class="cards" role="radiogroup" aria-label="Who will use Ava">
98
- <label class="card"><input type="radio" name="audience" value="me" checked><span class="card-body"><b>Just me, on this computer</b><span>Safest. Reachable only at localhost.</span></span></label>
99
- <label class="card"><input type="radio" name="audience" value="others"><span class="card-body"><b>My team or customers</b><span>Over a network or the internet. Needs a password, because every call spends your API credit.</span></span></label>
118
+ <label class="card"><input type="radio" name="audience" value="me" checked><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4.5" width="18" height="12" rx="2"/><path d="M8 20h8M12 16.5V20"/></svg></span><span class="card-body"><b>Just me, on this computer</b><span>Safest. Reachable only at localhost.</span></span></label>
119
+ <label class="card"><input type="radio" name="audience" value="others"><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg></span><span class="card-body"><b>My team or customers</b><span>Over a network or the internet. Needs a password, because every call spends your API credit.</span></span></label>
100
120
  </div>
101
121
  <div class="field" id="passwordField" hidden>
102
122
  <div id="keepPassRow" class="keep" hidden><label><input type="checkbox" id="keepPass" checked> Keep my current password</label></div>
@@ -118,16 +138,17 @@
118
138
  <section class="step" data-step="review" aria-labelledby="h-review" hidden>
119
139
  <p class="eyebrow">Step 5 of 5</p>
120
140
  <h1 id="h-review">Ready to launch</h1>
141
+ <p class="lead">Here's your setup. Launch Ava and your dashboard opens: test a call, describe your product, then share a link with customers.</p>
121
142
  <dl class="summary" id="summary"></dl>
122
143
  <p class="status" id="launchStatus" role="status" aria-live="polite"></p>
123
- <div class="nav"><button type="button" class="btn ghost" data-back>Back</button><button type="button" class="btn primary" id="launch">Launch Ava</button></div>
144
+ <div class="nav"><button type="button" class="btn ghost" data-back>Back</button><button type="button" class="btn primary" id="launch">Launch Ava<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button></div>
124
145
  </section>
125
146
 
126
147
  <!-- launching -->
127
148
  <section class="step launching" data-step="launching" aria-live="polite" hidden>
128
- <div class="orb" aria-hidden="true"></div>
149
+ <div class="hero-orb big" aria-hidden="true"><i></i><i></i><span></span></div>
129
150
  <h1>Starting Ava…</h1>
130
- <p class="lead" id="launchMsg">This takes a few seconds. The app opens here when it's ready.</p>
151
+ <p class="lead" id="launchMsg">This takes a few seconds. Your dashboard opens here when it's ready.</p>
131
152
  </section>
132
153
  </main>
133
154
  </div>