From 2a913b808c94d0f94357f54e768f059d06ea2a6b Mon Sep 17 00:00:00 2001 From: Stuart Date: Wed, 23 Sep 2026 07:43:57 +1000 Subject: [PATCH 1/5] STU-391: bring tutorials and onboarding into the new visual system Restyle the welcome/tutorial modal with the new design tokens (Delight, coral accent, OKLch surfaces), add an accessible, non-autoplaying tutorial video (CCC.mp4) with native keyboard-reachable controls, add a first onboarding step that routes to the real FlutterFlow account editor, and return to the originatng step on success (forward) or failure (same step). Closing the shared settings editor opened OUTSIDE onboarding no longer launches or advances the walkthrough (onboarding-scoped re-entry flag). Add behavioral e2e coverage: fresh onboarding, dismiss remembered, reopen via the Tutorial nav entry, connection success->composer and failure-> same-step, settings-outside-onboarding, reduced-motion usability, and video non-autoplay/keyboard/controls. --- app.js | 84 +-- e2e/stu391-tutorial-onboarding.spec.js | 216 +++++++ index.html | 550 ++++++------------ .../stu391-walkthrough-desktop-1440x900.png | Bin 0 -> 270009 bytes .../stu391-walkthrough-mobile-360x800.png | Bin 0 -> 111670 bytes 5 files changed, 436 insertions(+), 414 deletions(-) create mode 100644 e2e/stu391-tutorial-onboarding.spec.js create mode 100644 screenshots/stu391-walkthrough-desktop-1440x900.png create mode 100644 screenshots/stu391-walkthrough-mobile-360x800.png diff --git a/app.js b/app.js index 792ba4b..69a7654 100644 --- a/app.js +++ b/app.js @@ -989,7 +989,14 @@ async function initializeApiKeys() { // --- API KEY UI FUNCTIONS --- -function openApiKeysModal() { +// Onboarding-scoped editor re-entry. The walkthrough's own "connect account" +// step sets this before opening the shared API-key editor so that closing it +// returns to (and advances) the tour. Settings opened from any other surface +// (account card, gear, commit flow) stays false, so closing those never +// launches or advances the walkthrough. +let walkthroughSettingsActive = false; + +function openApiKeysModalBody() { const modal = document.getElementById("api-keys-modal"); openModal(modal); @@ -1003,17 +1010,44 @@ function openApiKeysModal() { } } +function openApiKeysModal() { + // A standalone settings visit, not onboarding: closing it must not touch the + // walkthrough. + walkthroughSettingsActive = false; + openApiKeysModalBody(); +} + +// Entered by the walkthrough's "Connect your FlutterFlow account" step. Routes +// the user to the real account control and remembers, on this one close, to +// return to the originating tour step. +function walkthroughConnectAccount() { + walkthroughSettingsActive = true; + // Close the tour first so the shared editor never stacks on top of it. + const wt = document.getElementById("walkthrough-modal"); + if (wt) closeModal(wt); + openApiKeysModalBody(); +} + function closeApiKeysModal(event) { if (event && event.target !== event.currentTarget) return; const modal = document.getElementById("api-keys-modal"); if (modal) { closeModal(modal); } - // Show walkthrough again after closing API keys - const walkthroughModal = document.getElementById("walkthrough-modal"); - if (walkthroughModal) { - advanceWalkthrough(); - openModal(walkthroughModal); + const fromWalkthrough = walkthroughSettingsActive; + walkthroughSettingsActive = false; + if (fromWalkthrough) { + // Return to the originating walkthrough step. Advance only when the + // account is actually connected (a FlutterFlow key is stored); a failed or + // cancelled connection returns to the same connect step so the user can + // retry instead of being pushed forward past an unconnected account. + const walkthroughModal = document.getElementById("walkthrough-modal"); + if (walkthroughModal) { + if (hasStoredKey("flutterflow")) { + advanceWalkthrough(); + } + openModal(walkthroughModal); + } } } @@ -1030,38 +1064,17 @@ function updateWalkthroughUI() { if (!steps.length) return; steps.forEach((stepEl, idx) => { const i = idx + 1; + stepEl.classList.remove("wt-current", "wt-done", "wt-pending"); + const numEl = stepEl.querySelector(".wt-step-num"); if (i === walkthroughStep) { - stepEl.classList.remove("opacity-60", "bg-gray-50", "border-gray-200"); - stepEl.classList.add("bg-blue-50", "border-blue-200"); - const numEl = stepEl.querySelector("div:first-child"); - if (numEl) { - numEl.classList.remove("bg-gray-400"); - numEl.classList.add("bg-blue-500"); - numEl.innerHTML = i; - } + stepEl.classList.add("wt-current"); + if (numEl) numEl.textContent = i; } else if (i < walkthroughStep) { - stepEl.classList.remove("opacity-60", "bg-blue-50", "border-blue-200"); - stepEl.classList.add("bg-green-50", "border-green-200"); - const numEl = stepEl.querySelector("div:first-child"); - if (numEl) { - numEl.classList.remove("bg-blue-500", "bg-gray-400"); - numEl.classList.add("bg-green-500"); - numEl.innerHTML = "✓"; - } + stepEl.classList.add("wt-done"); + if (numEl) numEl.textContent = "✓"; } else { - stepEl.classList.add("opacity-60", "bg-gray-50", "border-gray-200"); - stepEl.classList.remove( - "bg-blue-50", - "border-blue-200", - "bg-green-50", - "border-green-200", - ); - const numEl = stepEl.querySelector("div:first-child"); - if (numEl) { - numEl.classList.remove("bg-blue-500", "bg-green-500"); - numEl.classList.add("bg-gray-400"); - numEl.innerHTML = i; - } + stepEl.classList.add("wt-pending"); + if (numEl) numEl.textContent = i; } }); } @@ -6894,6 +6907,7 @@ window.copyCode = copyCode; window.retryWithDifferentModel = retryWithDifferentModel; window.openApiKeysModal = openApiKeysModal; window.closeApiKeysModal = closeApiKeysModal; +window.walkthroughConnectAccount = walkthroughConnectAccount; window.closeWalkthroughModal = closeWalkthroughModal; window.openWalkthroughModal = openWalkthroughModal; window.advanceWalkthrough = advanceWalkthrough; diff --git a/e2e/stu391-tutorial-onboarding.spec.js b/e2e/stu391-tutorial-onboarding.spec.js new file mode 100644 index 0000000..d67ae3a --- /dev/null +++ b/e2e/stu391-tutorial-onboarding.spec.js @@ -0,0 +1,216 @@ +import { test, expect } from "@playwright/test"; +import { + applyDefaultRoutes, + guestIdentity, + signedInSession, + freeSubscription, + err, + sampleProjectList, + ENDPOINTS, +} from "./fixtures/apiFixtures.js"; + +/** + * STU-391 — tutorials and connection onboarding in the new visual system. + * + * The welcome/tutorial modal is the onboarding surface. A fresh user sees it + * once; dismiss is remembered through the existing hasSeenWalkthrough flag and + * a Tutorial nav entry reopens it. The walkthrough's "connect account" step + * routes to the REAL API-key editor (STU-384): on success the walkthrough + * returns to its next step and then to the composer, and on failure it returns + * to the SAME step instead of advancing. Closing settings opened OUTSIDE + * onboarding must never launch or advance the walkthrough. The tutorial video + * is keyboard-capable (native controls), responsive and never autoplays. + */ +const KEY = "ff-secret-token-abc123"; +const PROJ = "proj-abc-123"; + +function signedInContext(email = "metered@example.com") { + return { + [ENDPOINTS.identity]: { + status: 200, + body: JSON.stringify({ + status: "recognized", + user_id: "acct-0001", + identity_token: "identity-token", + usage_count: 2, + usage_month: new Date().toISOString().slice(0, 7), + }), + contentType: "application/json", + }, + [ENDPOINTS.authRefreshSession]: { + status: 200, + body: JSON.stringify({ email, sessionToken: "session-token" }), + contentType: "application/json", + }, + [ENDPOINTS.getSubscription]: freeSubscription(), + }; +} + +const WALKTHROUGH = "#walkthrough-modal"; + +test.describe("STU-391 tutorial + connection onboarding", () => { + test("fresh user sees the tutorial once with a keyboard-capable, non-autoplaying video", async ({ page }) => { + await applyDefaultRoutes(page, { identity: guestIdentity() }); + await page.goto("/"); + + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + await expect(page.locator("#walkthrough-step1")).toHaveClass(/wt-current/); + await expect(page.locator("#walkthrough-step1")).toContainText( + "Connect your FlutterFlow account", + ); + + const video = page.locator("#wt-tutorial-video"); + await expect(video).toHaveAttribute("controls", ""); + // The video must not autoplay unexpectedly. + expect(await video.evaluate((el) => el.hasAttribute("autoplay"))).toBe(false); + expect(await video.evaluate((el) => el.paused)).toBe(true); + // preload is conservative (metadata), not autoplay/buffering-first. + await expect(video).toHaveAttribute("preload", "metadata"); + // Native controls keep it keyboard reachable and tab-sequence capable. + expect(await video.evaluate((el) => el.tabIndex >= 0)).toBe(true); + }); + + test("reduced-motion still renders the onboarding usable", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + await applyDefaultRoutes(page, { identity: guestIdentity() }); + await page.goto("/"); + + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + await expect(page.locator("#walkthrough-step1")).toBeVisible(); + // The primary action remains clickable under reduced motion. + await page.click("#walkthrough-step1 .wt-step-link"); + await expect(page.locator("#api-keys-modal")).toBeVisible(); + }); + + test("dismissing onboarding is remembered for the returning user", async ({ page }) => { + await applyDefaultRoutes(page, { identity: guestIdentity() }); + await page.goto("/"); + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + + await page.check("#walkthrough-dont-show"); + await page.click(".wt-gotit-btn"); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + + expect( + await page.evaluate(() => localStorage.getItem("hasSeenWalkthrough")), + ).toBe("true"); + + // Returning-user load does not re-show it. + await page.reload(); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + }); + + test("a returning user reopens the tutorial from the Tutorial entry", async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("hasSeenWalkthrough", "true"); + }); + await applyDefaultRoutes(page, { identity: guestIdentity() }); + await page.goto("/"); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + + await page.click("#wt-reopen"); + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + await expect(page.locator("#walkthrough-step1")).toHaveClass(/wt-current/); + }); + + test("connecting from onboarding routes to the real editor and, on success, returns to the next step then the composer", async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("hasSeenWalkthrough", "true"); + }); + await applyDefaultRoutes(page, signedInContext()); + await page.goto("/"); + await page.click("#wt-reopen"); + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + + // Connect step opens the REAL account editor, not a fake flow. + await page.click("#walkthrough-step1 .wt-step-link"); + await expect(page.locator("#api-keys-modal")).toBeVisible(); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + + const input = page.locator("#flutterflow-api-key-input"); + await input.fill(KEY); + await input.blur(); + await expect( + page.locator(`#flutterflow-projects-select option[value="${PROJ}"]`), + ).toHaveCount(1); + await page.locator("#flutterflow-projects-select").selectOption(PROJ); + await page.locator("#api-keys-modal .bg-blue-500").click(); + await expect(page.locator("#api-keys-modal")).toBeHidden(); + + // saveApiKeys closes the editor inside a 1s timeout; wait it out. + await page.waitForTimeout(1300); + + // Success advances to the next onboarding step and returns to it. + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + await expect(page.locator("#walkthrough-step2")).toHaveClass(/wt-current/); + + // "Add Prompt" returns to the composer. + await page.click("#walkthrough-step2 .wt-step-link"); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + await expect(page.locator("#pipeline-input")).toBeFocused(); + // Advancing only happens when a FlutterFlow key is stored, which is the + // behavioural proof that connecting wrote through the real editor. + }); + + test("a failed or cancelled connection returns to the same onboarding step, not forward", async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("hasSeenWalkthrough", "true"); + }); + await applyDefaultRoutes(page, { + ...signedInContext(), + // Listed projects fail: the real editor rejects the key. + [ENDPOINTS.flutterFlowListProjects]: err(401, { error: "unauthorized" }), + [ENDPOINTS.flutterFlowLegacyListProjects]: err(401, { error: "unauthorized" }), + }); + await page.goto("/"); + await page.click("#wt-reopen"); + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + await expect(page.locator("#walkthrough-step1")).toHaveClass(/wt-current/); + + await page.click("#walkthrough-step1 .wt-step-link"); + await expect(page.locator("#api-keys-modal")).toBeVisible(); + + // Close the editor without having stored a key (failure / cancel path). + await page.evaluate(() => window.closeApiKeysModal()); + await expect(page.locator("#api-keys-modal")).toBeHidden(); + + // Returned to the SAME step — no advance past an unconnected account. + await expect(page.locator(WALKTHROUGH)).toBeVisible(); + await expect(page.locator("#walkthrough-step1")).toHaveClass(/wt-current/); + }); + + test("closing settings opened outside onboarding does not launch or advance the walkthrough", async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("hasSeenWalkthrough", "true"); + localStorage.setItem( + "ccc_auth_session", + JSON.stringify({ email: "metered@example.com", sessionToken: "session-token" }), + ); + }); + await applyDefaultRoutes(page, signedInContext()); + await page.goto("/"); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + + // Open the settings editor from the account connection card — NOT onboarding. + await page.locator('a.nav-link[data-view="account"]').click(); + await expect(page.locator("#account-view")).toBeVisible(); + await page + .locator(".acct-connection button", { hasText: "Configure" }) + .first() + .scrollIntoViewIfNeeded(); + await page + .locator(".acct-connection button", { hasText: "Configure" }) + .first() + .click(); + await expect(page.locator("#api-keys-modal")).toBeVisible(); + + // Closing it must not launch the walkthrough at all. + await page.evaluate(() => window.closeApiKeysModal()); + await expect(page.locator("#api-keys-modal")).toBeHidden(); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + + // Nor later (the old buggy path reopened it after the save window). + await page.waitForTimeout(1200); + await expect(page.locator(WALKTHROUGH)).toBeHidden(); + }); +}); diff --git a/index.html b/index.html index 185aa57..0584677 100644 --- a/index.html +++ b/index.html @@ -2128,345 +2128,171 @@ color: var(--accent); } - /* ── Walkthrough Modal Redesign ─────────────────────── */ - .wt-hero { - display: flex; - align-items: center; - gap: 28px; - padding: 32px 32px 24px; - background: linear-gradient(168deg, #f0f4ff 0%, #f8f9ff 40%, #fff 100%); - position: relative; - overflow: hidden; - } - - .wt-hero::before { - content: ''; - position: absolute; - top: -40px; - right: -40px; - width: 180px; - height: 180px; - background: radial-gradient(circle, rgba(139, 92, 246, 0.06) 0%, transparent 70%); - border-radius: 50%; - pointer-events: none; - } - - .wt-illustration { - position: relative; - width: 200px; - height: 200px; - flex-shrink: 0; - } - - .wt-orbit { - position: absolute; - inset: 0; - border-radius: 50%; - border: 2px solid transparent; - animation: wt-orbit-spin 20s linear infinite; - } - - .wt-orbit-1 { - border-image: linear-gradient(135deg, rgba(99, 102, 241, 0.3), rgba(168, 85, 247, 0.15), transparent 60%) 1; - border-radius: 50%; - border: 2px solid rgba(99, 102, 241, 0.15); - } - - .wt-orbit-2 { - inset: 16px; - border: 2px dashed rgba(139, 92, 246, 0.12); - animation-direction: reverse; - animation-duration: 28s; - } - - .wt-orbit-3 { - inset: 32px; - border: 1.5px solid rgba(59, 130, 246, 0.1); - animation-duration: 15s; - } - - @keyframes wt-orbit-spin { - to { transform: rotate(360deg); } - } - - .wt-center-gem { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 72px; - height: 72px; - } - - .wt-gem-shape { - width: 100%; - height: 100%; - filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.3)); - } - - .wt-code-tag { - position: absolute; - font-family: 'DM Mono', monospace; - font-size: 13px; - font-weight: 500; - color: #8b5cf6; - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(4px); - padding: 4px 8px; - border-radius: 6px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); - line-height: 1; - } - - .wt-code-tag-1 { bottom: 28px; left: 4px; color: #6366f1; } - .wt-code-tag-2 { bottom: 32px; right: 0px; color: #7c3aed; } - - .wt-gear { - position: absolute; - color: #94a3b8; - opacity: 0.5; - animation: wt-gear-spin 8s linear infinite; - } - .wt-gear-1 { top: 12px; right: 28px; width: 22px; height: 22px; } - .wt-gear-2 { top: 36px; right: 8px; width: 16px; height: 16px; animation-direction: reverse; animation-duration: 12s; } - - @keyframes wt-gear-spin { - to { transform: rotate(360deg); } - } - - .wt-sparkle { - position: absolute; - width: 8px; - height: 8px; - animation: wt-sparkle-pulse 2s ease-in-out infinite; - } - .wt-sparkle::before, - .wt-sparkle::after { - content: ''; - position: absolute; - background: currentColor; - border-radius: 1px; - } - .wt-sparkle::before { - width: 2px; - height: 100%; - left: 50%; - transform: translateX(-50%); - } - .wt-sparkle::after { - width: 100%; - height: 2px; - top: 50%; - transform: translateY(-50%); - } - - .wt-sparkle-1 { top: 20px; left: 24px; color: #818cf8; animation-delay: 0s; } - .wt-sparkle-2 { top: 8px; left: 80px; color: #a78bfa; animation-delay: 0.6s; } - .wt-sparkle-3 { top: 44px; left: 56px; color: #6366f1; animation-delay: 1.2s; font-size: 6px; } - .wt-sparkle-4 { bottom: 16px; right: 44px; color: #818cf8; animation-delay: 0.3s; } + /* ── Walkthrough / Tutorial Modal (new visual system) ─── */ + .wt-modal { max-width: 640px; overflow: hidden; background: var(--surface); } - @keyframes wt-sparkle-pulse { - 0%, 100% { opacity: 0.3; transform: scale(0.8); } - 50% { opacity: 1; transform: scale(1.2); } + .wt-topbar { + display: flex; align-items: center; justify-content: space-between; + gap: 16px; padding: 18px 24px; + border-bottom: 1px solid var(--border); } - - .wt-free-badge { - position: absolute; - top: 8px; - left: 50%; - transform: translateX(-50%); - background: linear-gradient(135deg, #22c55e, #16a34a); - color: #fff; - font-family: 'Delight', 'Outfit', sans-serif; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.08em; - padding: 3px 12px; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3); - z-index: 2; + .wt-title { + font-family: var(--font-display); + font-size: var(--fs-h4); + font-weight: var(--fw-bold); + color: var(--fg); + letter-spacing: var(--ls-heading); } - .wt-hero-text { - flex: 1; - min-width: 0; + .wt-hero { padding: 22px 24px 0; } + .wt-hero-kicker { + font-family: var(--font-mono); + font-size: var(--fs-micro); + font-weight: 600; + letter-spacing: var(--ls-mono); + text-transform: uppercase; + color: var(--accent); + margin-bottom: 8px; } - .wt-hero-heading { - font-family: 'Delight', 'Outfit', sans-serif; - font-size: 26px; - font-weight: 700; + font-family: var(--font-display); + font-size: var(--fs-h3); + font-weight: var(--fw-black); color: var(--fg); - letter-spacing: -0.03em; - line-height: 1.2; - } - - .wt-info-card { - margin: 0 24px; - padding: 20px 24px; - background: linear-gradient(135deg, #eef2ff 0%, #f0f4ff 50%, #ede9fe 100%); - border-radius: 14px; - text-align: center; - position: relative; - } - - .wt-info-text { - font-family: 'Delight', 'DM Sans', sans-serif; - font-size: 14px; - color: #334155; - line-height: 1.6; - margin-bottom: 16px; - } - - .wt-info-text strong { - font-weight: 700; - color: #1e293b; + letter-spacing: var(--ls-heading); + line-height: 1.15; } - - .wt-upgrade-btn { - display: block; - width: 100%; - padding: 14px 24px; - background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 50%, #4f46e5 100%); - color: #fff; - font-family: 'Delight', 'DM Sans', sans-serif; - font-size: 14px; - font-weight: 600; - border: none; - border-radius: 10px; - cursor: pointer; - letter-spacing: -0.01em; - transition: all 0.2s ease; - box-shadow: 0 4px 14px -3px rgba(99, 102, 241, 0.4); + .wt-hero-sub { + margin: 8px 0 16px; + font-size: var(--fs-small); + color: var(--muted); + line-height: var(--lh-body); + max-width: 58ch; } - - .wt-upgrade-btn:hover { - transform: translateY(-1px); - box-shadow: 0 6px 20px -3px rgba(99, 102, 241, 0.5); - background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 50%, #4338ca 100%); + .wt-video { + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border-strong); + background: oklch(16% 0.02 265); + aspect-ratio: 16 / 9; } + .wt-video video { display: block; width: 100%; height: 100%; object-fit: cover; } .wt-steps { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 14px; - padding: 20px 24px 0; + list-style: none; + margin: 20px 0 0; + padding: 0 24px; + display: grid; gap: 10px; } - .wt-step-card { - background: #fff; - border: 1px solid #e8ebf0; - border-radius: 12px; - padding: 18px; - transition: border-color 0.2s, box-shadow 0.2s; + display: flex; align-items: flex-start; gap: 14px; + padding: 14px 16px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + transition: border-color var(--t-fast) var(--ease), + box-shadow var(--t-fast) var(--ease), + opacity var(--t-fast) var(--ease); } - - .wt-step-card:hover { - border-color: #c7d2fe; - box-shadow: 0 2px 8px rgba(99, 102, 241, 0.08); + .wt-step-card.wt-current { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); } + .wt-step-card.wt-pending { opacity: 0.72; } .wt-step-num { - width: 28px; - height: 28px; - background: #3b82f6; - color: #fff; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; - font-weight: 700; - font-family: 'Delight', 'DM Sans', sans-serif; - margin-bottom: 10px; + flex: none; width: 28px; height: 28px; border-radius: 50%; + display: grid; place-items: center; + font-family: var(--font-mono); + font-size: var(--fs-caption); + font-weight: 600; + color: var(--fg); + border: 1px solid var(--border-strong); + background: var(--surface-sunken); } + .wt-current .wt-step-num { background: var(--accent); border-color: var(--accent); color: var(--on-dark); } + .wt-done .wt-step-num { background: var(--ok); border-color: var(--ok); color: var(--on-dark); } + .wt-step-body { flex: 1; min-width: 0; } .wt-step-title { - font-family: 'Delight', 'Outfit', sans-serif; - font-size: 14px; - font-weight: 600; - color: #1e293b; - margin-bottom: 4px; + display: block; + font-family: var(--font-body); + font-size: var(--fs-body); + font-weight: var(--fw-bold); + color: var(--fg); + letter-spacing: -0.01em; } - .wt-step-desc { - font-size: 12.5px; - color: #64748b; + display: block; margin-top: 3px; + font-size: var(--fs-caption); + color: var(--muted); line-height: 1.5; - margin-bottom: 10px; } - .wt-step-link { - font-size: 12.5px; - font-weight: 600; - color: #3b82f6; + margin-top: 9px; padding: 0; + background: none; border: none; cursor: pointer; + font-family: var(--font-body); + font-size: var(--fs-small); + font-weight: var(--fw-bold); + color: var(--accent); text-decoration: none; - cursor: pointer; - background: none; - border: none; - padding: 0; - font-family: 'Delight', 'DM Sans', sans-serif; - transition: color 0.15s; } - .wt-step-link:hover { color: #2563eb; } + .wt-step-link:hover { text-decoration: underline; } - .wt-footer { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 24px 20px; + .wt-upgrade { + margin: 16px 24px 0; padding: 14px 18px; + border: 1px dashed var(--border-strong); + border-radius: var(--radius-sm); + background: var(--surface-sunken); + display: flex; flex-wrap: wrap; align-items: center; + justify-content: space-between; gap: 12px; } - - .wt-dont-show { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; + .wt-info-text { + flex: 1 1 250px; + font-size: var(--fs-small); color: var(--muted); + line-height: var(--lh-body); } - - .wt-dont-show input[type="checkbox"] { - width: 16px; - height: 16px; - border-radius: 4px; - border: 1.5px solid #cbd5e1; - accent-color: #3b82f6; + .wt-info-text strong { font-weight: var(--fw-bold); color: var(--fg); } + .wt-upgrade-btn { + padding: 10px 16px; + background: var(--accent); color: var(--on-dark); + font-family: var(--font-body); + font-size: var(--fs-small); font-weight: var(--fw-bold); + border: none; border-radius: var(--radius-control); cursor: pointer; + transition: box-shadow var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); } + .wt-upgrade-btn:hover { box-shadow: var(--shadow-control); } - .wt-dont-show span { - font-size: 12px; - color: #94a3b8; - font-family: 'Delight', 'DM Sans', sans-serif; + .wt-footer { + display: flex; align-items: center; justify-content: space-between; + gap: 12px; padding: 16px 24px 20px; } - + .wt-dont-show { display: flex; align-items: center; gap: 8px; cursor: pointer; } + .wt-dont-show input[type="checkbox"] { + width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; + } + .wt-dont-show span { font-size: var(--fs-caption); color: var(--muted-2); } .wt-gotit-btn { - padding: 10px 22px; - background: #3b82f6; - color: #fff; - font-family: 'Delight', 'DM Sans', sans-serif; - font-size: 14px; - font-weight: 600; - border: none; - border-radius: 10px; + min-height: var(--control-h); + padding: 10px 20px; + background: var(--fg); color: var(--surface); + font-family: var(--font-body); + font-size: var(--fs-body); font-weight: var(--fw-bold); + border: none; border-radius: var(--radius-control); cursor: pointer; - transition: background 0.15s; - display: flex; - align-items: center; - gap: 6px; - } - .wt-gotit-btn:hover { background: #2563eb; } - - .wt-gotit-btn svg { - width: 16px; - height: 16px; + display: flex; align-items: center; gap: 6px; + transition: background-color var(--t-fast) var(--ease); } + .wt-gotit-btn:hover { background: oklch(from var(--fg) calc(l + 0.12) c h); } + .wt-gotit-btn svg { width: 15px; height: 15px; } @media (max-width: 640px) { - .wt-hero { flex-direction: column; text-align: center; gap: 16px; padding: 24px; } - .wt-illustration { width: 160px; height: 160px; } - .wt-hero-heading { font-size: 22px; } - .wt-steps { grid-template-columns: 1fr; } - .wt-info-card { margin: 0 16px; } + .wt-hero { padding: 18px 16px 0; } + .wt-steps { padding: 0 16px; } + .wt-upgrade { margin-left: 16px; margin-right: 16px; flex-direction: column; align-items: stretch; } + .wt-upgrade-btn { text-align: center; } + .wt-footer { padding-left: 16px; padding-right: 16px; } } /* ── Pricing Modal Redesign ─────────────────────── */ @@ -3462,6 +3288,7 @@ Home Account Plans + Tutorial
@@ -4071,96 +3898,62 @@

API Keys

- +