ava-engine 0.2.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,11 +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>
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>
50
53
  </div>
51
54
 
52
- <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">
53
61
  <span class="ob-seclabel">Agent</span>
54
62
  <div class="ob-modes" id="obModes"><!-- mode cards injected --></div>
55
63
  <div class="ob-platwrap" id="obPlatformBlock" hidden>
@@ -62,7 +70,7 @@
62
70
  <div class="ob-divider"></div>
63
71
 
64
72
  <div class="ob-section">
65
- <span class="ob-seclabel">Audio &amp; recognition</span>
73
+ <span class="ob-seclabel" id="obAudioLabel">Audio &amp; recognition</span>
66
74
  <div class="ob-gear">
67
75
  <div class="ob-field">
68
76
  <label class="ob-flabel" for="micsel">Microphone</label>
@@ -72,7 +80,7 @@
72
80
  </div>
73
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>
74
82
  </div>
75
- <div class="ob-field">
83
+ <div class="ob-field" id="obVoiceField">
76
84
  <label class="ob-flabel" for="voicesel2">Ava's voice</label>
77
85
  <div class="ob-fctl">
78
86
  <select id="voicesel2" class="ob-select"></select>
@@ -80,7 +88,7 @@
80
88
  </div>
81
89
  <span class="ob-fnote">Preview also warms the voice, so the call starts fast.</span>
82
90
  </div>
83
- <div class="ob-field ob-field-wide">
91
+ <div class="ob-field ob-field-wide" id="obSttField">
84
92
  <label class="ob-flabel" for="sttsel">Speech recognition</label>
85
93
  <select id="sttsel" class="ob-select"></select>
86
94
  <span class="ob-fnote" id="sttnote">Choose before the call.</span>
@@ -92,7 +100,7 @@
92
100
  <div class="ob-actions">
93
101
  <button type="button" id="joinvoice" class="amberbtn ob-bigbtn">
94
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>
95
- Join with voice
103
+ <span id="joinvoiceText">Join with voice</span>
96
104
  </button>
97
105
  <button type="button" id="jointext" class="ghostbtn ob-bigbtn">Join text-only</button>
98
106
  <span class="ob-joinstate" id="joinstate"></span>
@@ -129,6 +137,7 @@
129
137
  <span class="vdiv"></span>
130
138
  <span class="apptitle" id="apptitle">Flexible Agent</span>
131
139
  <span class="mono meta hide-narrow" id="topsub">screen-aware assistant</span>
140
+ <span id="demoPill" class="pill demo" hidden title="Replies are canned until an OpenRouter key is added">DEMO MODE</span>
132
141
  <span id="livepill" class="pill standby"><span class="dot"></span><span id="livepilltext">STANDBY</span></span>
133
142
  <div class="grow"></div>
134
143
  <span id="privacy" class="privacypill" hidden>
@@ -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,94 +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); }
125
+ .connect { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
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; }
127
+ .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
56
128
 
57
129
  .status { margin: 0; min-height: 22px; font-size: 14.5px; }
58
130
  .status.ok { color: var(--ok); } .status.err { color: var(--err); }
59
131
  .banner { padding: 14px 18px; border-radius: 12px; margin-bottom: 24px; }
60
132
  .banner.error { background: var(--err-tint); color: var(--err); }
61
133
 
62
- .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); }
63
136
  .job-head { display: flex; justify-content: space-between; font-size: 14.5px; margin-bottom: 10px; font-weight: 500; }
64
- .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); }
65
138
  .bar { height: 8px; background: var(--soft); border-radius: 99px; overflow: hidden; }
66
- .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; }
67
140
  .job.done .bar i { background: var(--ok); } .job.error .bar i { background: var(--err); }
68
141
 
69
- .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; }
70
- .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; }
71
148
 
72
- .nav { display: flex; justify-content: space-between; align-items: center; margin-top: 32px; gap: 12px; }
73
- .btn { height: 46px; padding: 0 22px; border-radius: 99px; font: 600 15px "Inter Tight", sans-serif; cursor: pointer; border: 1.5px solid transparent; }
74
- .btn.primary { background: var(--ink); color: #fff; }
75
- .btn.primary:hover { background: #24242C; }
76
- .btn.primary:disabled { background: #B9B9C3; cursor: not-allowed; }
77
- .btn.ghost { background: #fff; color: var(--ink); border-color: var(--line); }
78
- .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); }
79
159
  .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
80
160
 
81
- .launching { text-align: center; padding-top: 12vh; }
161
+ .launching { text-align: center; padding-top: 10vh; }
82
162
  .launching .lead { margin-inline: auto; }
83
- .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; }
84
- @keyframes pulse { 50% { transform: scale(1.06); box-shadow: 0 0 120px rgba(124,108,246,.75); } }
85
- @media (prefers-reduced-motion: reduce) { .orb { animation: none; } .bar i { transition: none; } }
86
163
 
87
- @media (max-width: 760px) {
164
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
165
+
166
+ @media (max-width: 860px) {
88
167
  .shell { grid-template-columns: 1fr; }
89
- .rail { padding: 20px; gap: 16px; }
168
+ .rail { position: relative; height: auto; padding: 18px 16px; gap: 14px; }
90
169
  .steps { flex-direction: row; overflow-x: auto; gap: 4px; }
91
- .steps li { padding: 6px 8px; font-size: 0; gap: 0; } .steps li.current { font-size: 14px; gap: 8px; }
92
- .rail-note { display: none; }
93
- .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; }
94
178
  }