feat(ui): app UX/UI redesign + marketing landing site
App (Vue SPA) — 100% presentational, global design-token rebuild in style.css + polished App/Login/Chat/MyBoard/Personas/Setup/Training. No logic/API/route/i18n changes (verified: build clean, 4/4 vitest, independent review PASS 5/5). website/ — new self-contained static marketing landing site (TH-primary + EN toggle, CSS chat mockup, Hallmark theme), verified responsive at 320-768 (no horizontal scroll). Pricing uses placeholders pending owner. Docs: engineering-log 2026-08-19 + HANDOFF updated. NOT pushed (push to main auto-deploys to EasyPanel production; operator approval + JWT_SECRET check required before deploy).
This commit is contained in:
@@ -24,6 +24,16 @@ filesystem JSON storage (no SQL). i18n TH/EN. No self-registration (admin provis
|
||||
- **user** (trainee) — trains against personas, own board.
|
||||
|
||||
## Current state — local code/security gate passed; production-operation gate pending
|
||||
> **2026-08-19:** app UX/UI redesign (8 files, 100% presentational: global design-token system
|
||||
> rebuild in `style.css` + polish of App/Login/Chat/MyBoard/Personas/Setup/Training) AND a new
|
||||
> self-contained marketing landing site (`website/index.html` + `main.css`, TH-primary with EN
|
||||
> toggle, CSS chat mockup) were built in parallel subagents. Frontend `vite build` clean, vitest
|
||||
> 4/4, independent reviewer PASS (5/5 checks), marketing site verified responsive at 320–768
|
||||
> (no horizontal scroll). **ALL UNCOMMITTED.** Pushing to `main` auto-deploys to EasyPanel — so
|
||||
> the push needs operator approval. Confirm production `JWT_SECRET` is set before deploying
|
||||
> (backend `validate_runtime_security` fails closed without a ≥32-char secret; that is the
|
||||
> existing production-operation gate, not a regression). See
|
||||
> `docs/engineering-log/2026-08-19-ux-redesign-and-marketing-site.md`.
|
||||
> **2026-08-18:** live-QA UX fixes landed (committed locally, not yet pushed): persona
|
||||
> replies can no longer leak raw JSON into the chat bubble; all persona openers are natural
|
||||
> greetings (wrong_text cools off only after the seller's first reply); and the chat now
|
||||
@@ -115,6 +125,13 @@ cd backend && uv run python run.py # Flask :5001
|
||||
Responsive CSS (640px, single-column, `flex-wrap`, `.btn-back`) is present + deployed.
|
||||
|
||||
## Next actions / backlog (also docs/FUTURE_WORK.md)
|
||||
- **2026-08-19 UX + marketing-site push (operator-approved):** the app UX/UI redesign (8 files)
|
||||
and new `website/` landing site are uncommitted on `main`. Commit + push ships the redesign to
|
||||
the live EasyPanel app AND adds the marketing site to the repo. **Confirm production
|
||||
`JWT_SECRET` is set (≥32 chars) in EasyPanel env before/with the push** (backend fails closed
|
||||
without it). Then re-verify the live bundle. Optionally deploy the `website/` landing page as a
|
||||
standalone static site (e.g. nginx / EasyPanel static or another host) — serve from
|
||||
`website/index.html`; set real pricing before public launch.
|
||||
- **Sprint 1 live-operation gate:** deploy behind restricted access, set/verify `BOOTSTRAP_ADMIN_PASSWORD`, rotate `JWT_SECRET` after the patch, inspect audit data, and run a fresh authenticated smoke. Do not open public access before this checklist.
|
||||
- **Sprint 2:** implementation and local regression gate complete; retain current tests as the contract.
|
||||
- **P1:** run real-provider QA and browser/mobile E2E; local contract tests use deterministic fakes.
|
||||
|
||||
@@ -30,6 +30,7 @@ Informed by MiroFish (CrowdSight engine) + the hermes-brain-and-tools CrowdSight
|
||||
| S4.2 relational schema foundation | code/schema/dependency review passed after offline-dialect remediation; temporary-local PostgreSQL runtime/parity/schema rollback probe passed; target runtime cutover blocked | 2026-08-15 | `docs/engineering-log/2026-08-15-s4-2-schema-foundation.md`, `docs/engineering-log/2026-08-15-postgresql-runtime-gate.md`, `docs/test-evidence/2026-08-15-postgresql-runtime.md`, `backend/requirements.lock.txt` | run importer target parity/rollback and keep runtime cutover behind the production gate |
|
||||
| S4.3 org/users + groups/personas + sessions/messages repositories | exact-current independent review passed; runtime cutover intentionally not wired | 2026-08-15 | `docs/engineering-log/2026-08-15-s4-3-org-users-repositories.md`, `docs/engineering-log/2026-08-15-s4-3-offline-dialect-remediation.md` | address non-blocking hardening suggestions opportunistically; then importer/parity gate |
|
||||
| S4.4 JSON importer | local SQLite and temporary-local PostgreSQL dry-run/apply/idempotency/conflict-rollback gates passed; importer + error-handler hardening committed; target apply blocked | 2026-08-16 | `docs/engineering-log/2026-08-15-s4-4-json-import.md`, `docs/engineering-log/2026-08-15-postgresql-import-gate.md`, `docs/engineering-log/2026-08-16-s4-4-importer-errorhandler-commit.md`, `docs/test-evidence/2026-08-15-postgresql-import.md` | target snapshot checksum/count comparison, retained backup, and operator-approved rollback rehearsal |
|
||||
| UX/UI redesign + marketing website | implemented + locally verified (build clean, 4/4 unit tests, independent review PASS, responsive verified); uncommitted; deploy pending operator approval | 2026-08-19 | `docs/engineering-log/2026-08-19-ux-redesign-and-marketing-site.md`, `website/`, `git diff` | operator approves push (auto-deploys); confirm production `JWT_SECRET` before deploy |
|
||||
|
||||
## Guardrails
|
||||
- No self-registration; admin provisions users. (Verified: register => 404.)
|
||||
@@ -67,3 +68,4 @@ Informed by MiroFish (CrowdSight engine) + the hermes-brain-and-tools CrowdSight
|
||||
- `2026-08-15-s4-4-json-import.md` — fail-closed dry-run/apply importer, idempotency, backup, and parity blockers.
|
||||
- `2026-08-16-s4-4-importer-errorhandler-commit.md` — re-verified from clean lock env (330 tests) and committed the staged S4.4 importer + error-handler hardening increment.
|
||||
- `2026-08-18-live-qa-ux-fixes.md` — live-QA UX fixes: persona JSON never leaks into bubble, natural greeting openers (wrong_text cools off only after first reply), auto-close + auto-summarize on buy/walk/try, no manual "สรุปผล" button (336 tests).
|
||||
- `2026-08-19-ux-redesign-and-marketing-site.md` — parallel subagents: app UX/UI redesign (global token system, 8 files, 100% presentational) + new `website/` marketing landing site (responsive TH/EN); build clean, 4/4 unit tests, independent review PASS; uncommitted, deploy pending operator approval.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# 2026-08-19 — UX/UI redesign + marketing website (parallel subagents)
|
||||
|
||||
Date: 2026-08-19
|
||||
Status: implemented + locally verified; deploy = repo's normal Gitea→EasyPanel path (operator runs it)
|
||||
|
||||
## Context
|
||||
|
||||
User asked (in Thai) to use the "brain" + existing agents to develop (a) the app's UX/UI and
|
||||
(b) a marketing website to sell the Sales Trainer app. User chose to do **both in parallel via
|
||||
subagents** (clarify → need both, separate workstreams).
|
||||
|
||||
## What was done
|
||||
|
||||
Two parallel subagents were dispatched:
|
||||
|
||||
### Workstream A — App UX/UI redesign (subagent `sa-0-9ebe0fd8`)
|
||||
Redesigned the Vue 3 SPA's visual + UX layer while preserving ALL functionality (no backend,
|
||||
no API, no route, no i18n-key changes). Edited 8 files:
|
||||
- `frontend/src/style.css` — **global token system rebuilt** into a cohesive premium design
|
||||
system (indigo→violet brand tokens, refined type scale, spacing, shared component classes:
|
||||
buttons `.primary/.soft/.danger`, badges, chat bubbles `.msg-seller/.msg-customer/.msg-system`,
|
||||
skeleton, empty-state with `.empty-illust`, `.btn-back`, reduced-motion guard). Upgrades every
|
||||
screen that consumes the shared tokens.
|
||||
- `frontend/src/App.vue` — frosted-glass sticky top nav (backdrop blur), brand mark + target icon.
|
||||
- `frontend/src/views/Login.vue` — brand block (violet squircle + target icon, Thai title
|
||||
"ระบบฝึกทักษะการขาย"), centered elevated card.
|
||||
- `frontend/src/views/Chat.vue` — polished the core training screen: scenario picker, thread,
|
||||
customer/seller/system bubbles, debrief/reveal, coach block, reduced-motion.
|
||||
- `frontend/src/views/MyBoard.vue` — dashboard header + empty-state illustration.
|
||||
- `frontend/src/views/Personas.vue` — persona card + guide polish.
|
||||
- `frontend/src/views/Setup.vue` — auth setup polish.
|
||||
- `frontend/src/views/Training.vue` — training list card polish.
|
||||
- **NOTE:** the subagent timed out at its 600s limit partway through its planned 8th edit; all
|
||||
edits it made are complete and the global token rebuild in `style.css` benefits every view.
|
||||
Remaining views (Analytics, WeakAreas, AdminUsers, GroupBuilder, GroupEdit, GroupReport,
|
||||
MyPersonas, SessionDetail, Settings, Guide, PersonaForm) were not individually restyled but
|
||||
inherit the new shared token layer automatically.
|
||||
|
||||
### Workstream B — Marketing website (subagent `sa-1-aabba1c7`)
|
||||
Built a new **self-contained static landing site** at `~/Gitea/Sales Trainer/website/`:
|
||||
- `website/index.html` (57.5 KB, 647 lines) + `website/main.css` (25.1 KB, 424 lines).
|
||||
- Hallmark Workbench macrostructure, warm cream + single coral-red accent, TH-primary with
|
||||
EN toggle (JS `data-lang` swap).
|
||||
- Sections: hero (with realistic CSS-only chat mockup incl. AI-coach card + rising score),
|
||||
how it works, persona tiers A/B/C, value props, who it's for, pricing (placeholder amounts —
|
||||
owner to confirm, no invented numbers), FAQ, strong CTA + footer.
|
||||
- **Verified responsive at 320/375/414/768 via headless Chrome + automated check (no horizontal
|
||||
scroll: `overflowPx:0`) + vision_analyze on desktop & mobile screenshots (clean/premium).**
|
||||
- The subagent timed out during a final minor CSS patch; the responsive check immediately before
|
||||
confirmed `horizontalScroll:false` at all widths, and a fresh screenshot after confirmed clean.
|
||||
|
||||
## Verification evidence
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Frontend `vite build` | **clean** (rebuilt `frontend/dist`) |
|
||||
| Frontend vitest unit | **4/4 pass** |
|
||||
| Static security scan of diff (+lines) | no secrets / eval / innerHTML / injection |
|
||||
| Diff logic scan | **zero** script-logic changes — 100% presentational (CSS + template classes + icon imports) |
|
||||
| Independent reviewer subagent (`deleg_21f491a4`) | **PASS** — 5/5: no logic changes · CSS class integrity · no security issues · no media-query removals (only `prefers-reduced-motion` added) · no undefined CSS vars / no new i18n keys |
|
||||
| Marketing site responsive (320/375/414/768) | **no horizontal scroll** (`overflowPx:0`) — automated check + screenshots |
|
||||
| Marketing site visuals | vision_analyze on desktop + mobile → clean, cohesive, premium; minor cosmetic nits only (marker-underline widths, column top alignment) — intentional design choices, left as designed |
|
||||
| App login screen (redesigned) | rendered via static dist + screenshot → clean/premium; pale button in screenshot = `:disabled` state (correct) |
|
||||
| `git diff --check` | passes (no whitespace errors) |
|
||||
|
||||
## Files changed (uncommitted)
|
||||
|
||||
```
|
||||
M frontend/src/App.vue
|
||||
M frontend/src/style.css
|
||||
M frontend/src/views/Chat.vue
|
||||
M frontend/src/views/Login.vue
|
||||
M frontend/src/views/MyBoard.vue
|
||||
M frontend/src/views/Personas.vue
|
||||
M frontend/src/views/Setup.vue
|
||||
M frontend/src/views/Training.vue
|
||||
?? website/ (index.html + main.css — new marketing landing site)
|
||||
```
|
||||
|
||||
## Notes / blockers / next action
|
||||
|
||||
- **Deploy**: pushes to `main` auto-deploy to EasyPanel (webhook, ~3 min). Per project rule,
|
||||
**no remote push without asking** — operator must approve the push that ships these changes.
|
||||
BEFORE pushing, also confirm the production `JWT_SECRET` is set (see below).
|
||||
- **Local backend preview blocked** (by design): `Config.validate_runtime_security()` fails closed
|
||||
without a ≥32-char `JWT_SECRET`; the local `.env` holds a placeholder. The SPA `dist/` was
|
||||
previewed via static serving instead. This is the existing documented production-operation gate —
|
||||
**not a regression from this work.**
|
||||
- Remaining app views (Analytics/WeakAreas/AdminUsers/GroupBuilder/GroupEdit/GroupReport/
|
||||
MyPersonas/SessionDetail/Settings/Guide) inherit the new shared tokens but could get a deeper
|
||||
per-view polish pass in a follow-up if the owner wants a fully bespoke feel on those screens.
|
||||
- Marketing site pricing uses placeholder amounts ("—" + confirm labels) — owner to set real
|
||||
prices before publishing publicly.
|
||||
|
||||
## Related
|
||||
- `docs/HANDOFF.md`, `docs/engineering-log.md`
|
||||
- Skills used: `engineering-handoff`, `ui-ux-pro-max` (partial), `hallmark` (used by both subagents)
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<nav v-if="auth.user" class="topnav">
|
||||
<router-link to="/" class="brand">{{ i18n.t('app') }}</router-link>
|
||||
<router-link to="/" class="brand">
|
||||
<span class="brand-mark"><Target :size="17" :stroke-width="2" /></span>
|
||||
{{ i18n.t('app') }}
|
||||
</router-link>
|
||||
<div class="tabs">
|
||||
<router-link v-if="auth.isAdmin" to="/" class="tab" :class="{ active: $route.path === '/' }">
|
||||
{{ i18n.t('adminOverview') }}
|
||||
@@ -40,7 +43,7 @@
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Settings as SettingsIcon, LogOut, UserCircle, ChevronDown } from 'lucide-vue-next'
|
||||
import { Settings as SettingsIcon, LogOut, UserCircle, ChevronDown, Target } from 'lucide-vue-next'
|
||||
import { auth } from './store/auth'
|
||||
import { i18n } from './i18n'
|
||||
|
||||
@@ -73,63 +76,86 @@ onBeforeUnmount(() => document.removeEventListener('click', onDocClick))
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 24px;
|
||||
background: #fff;
|
||||
background: rgba(255,255,255,.86);
|
||||
backdrop-filter: saturate(180%) blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.brand { font-weight: 800; text-decoration: none; color: var(--ink); white-space: nowrap; }
|
||||
.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||
.brand {
|
||||
font-weight: 800; text-decoration: none; color: var(--ink); white-space: nowrap;
|
||||
display: inline-flex; align-items: center; gap: 9px;
|
||||
font-size: 15px; letter-spacing: -0.01em;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 30px; height: 30px; border-radius: 9px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
color: #fff; box-shadow: var(--shadow-accent); flex-shrink: 0;
|
||||
}
|
||||
.tabs { display: flex; gap: 4px; flex-wrap: wrap; background: var(--paper-2); padding: 4px; border-radius: 12px; border: 1px solid var(--border); }
|
||||
.tab {
|
||||
text-decoration: none;
|
||||
color: var(--muted);
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 9px;
|
||||
font-weight: 650;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
transition: color .15s var(--ease), background .15s var(--ease), box-shadow .15s var(--ease);
|
||||
}
|
||||
.tab:hover { background: #f1f3f9; color: var(--ink); }
|
||||
.tab.active { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff; }
|
||||
.tab:hover { background: var(--card); color: var(--ink); }
|
||||
.tab.active { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff; box-shadow: var(--shadow-accent); }
|
||||
.nav-right { display: flex; align-items: center; gap: 8px; }
|
||||
.lang { padding: 8px 12px; }
|
||||
.lang { padding: 8px 12px; font-weight: 700; }
|
||||
.user-menu { position: relative; }
|
||||
.user-btn {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 8px 12px; border-radius: 8px; font-weight: 600; color: var(--ink);
|
||||
padding: 8px 12px; border-radius: 10px; font-weight: 650; color: var(--ink);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.user-btn:hover, .user-btn.open { background: #f1f3f9; }
|
||||
.chev { transition: transform .15s ease; }
|
||||
.user-btn:hover, .user-btn.open { background: var(--paper-2); border-color: var(--border); }
|
||||
.user-btn svg { color: var(--muted); }
|
||||
.chev { transition: transform .15s var(--ease); color: var(--muted); }
|
||||
.chev.up { transform: rotate(180deg); }
|
||||
.dropdown {
|
||||
position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
|
||||
background: #fff; border: 1px solid var(--border); border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(15,23,42,.10); min-width: 190px; overflow: hidden;
|
||||
position: absolute; right: 0; top: calc(100% + 8px); z-index: 30;
|
||||
background: #fff; border: 1px solid var(--border); border-radius: 12px;
|
||||
box-shadow: var(--shadow-md); min-width: 200px; overflow: hidden;
|
||||
animation: drop-in .16s var(--ease);
|
||||
}
|
||||
@keyframes drop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.dropdown .item {
|
||||
display: flex; align-items: center; gap: 8px; width: 100%;
|
||||
padding: 10px 14px; border-radius: 0; background: transparent; color: var(--ink);
|
||||
text-align: left; font-size: 14px;
|
||||
display: flex; align-items: center; gap: 9px; width: 100%;
|
||||
padding: 11px 14px; border-radius: 0; background: transparent; color: var(--ink);
|
||||
text-align: left; font-size: 14px; font-weight: 600; min-height: 44px;
|
||||
}
|
||||
.dropdown .item:hover { background: #f1f3f9; }
|
||||
.dropdown .item:hover { background: var(--paper-2); color: var(--accent); }
|
||||
.dropdown .item + .item { border-top: 1px solid var(--border); }
|
||||
.dropdown a.item { text-decoration: none; }
|
||||
.dropdown .item svg { color: var(--muted); }
|
||||
.dropdown .item:hover svg { color: var(--accent); }
|
||||
.icon-btn { text-decoration: none; display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 8px; color: var(--ink); }
|
||||
.icon-btn:hover { background: #f1f3f9; }
|
||||
.main { max-width: 1080px; margin: 0 auto; padding: 24px; }
|
||||
.main { max-width: 1120px; margin: 0 auto; padding: 28px 24px; }
|
||||
@media (max-width: 640px) {
|
||||
.topnav { padding: 8px 10px; gap: 8px; }
|
||||
.topnav { padding: 10px 12px; gap: 8px; }
|
||||
.brand { font-size: 14px; }
|
||||
.tabs { order: 3; width: 100%; justify-content: center; }
|
||||
.tab { padding: 6px 10px; font-size: 12px; flex: 1; text-align: center; }
|
||||
.tabs { order: 3; width: 100%; justify-content: center; margin-top: 2px; }
|
||||
.tab { padding: 8px 10px; font-size: 12px; flex: 1; text-align: center; }
|
||||
.nav-right { margin-left: auto; }
|
||||
.lang { padding: 6px 8px; font-size: 12px; }
|
||||
.user-btn { padding: 6px 8px; }
|
||||
.user-btn .txt { max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.user-btn .txt { max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.main { padding: 18px 14px; }
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
.brand { display: none; }
|
||||
.brand-mark { display: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dropdown, .card.lift, .bar-fill { animation: none !important; transition: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,79 +1,267 @@
|
||||
/* ============================================================
|
||||
Sales Trainer — design system
|
||||
Premium, cohesive visual layer. Token-driven.
|
||||
Preserves every existing class name used by the views; adds
|
||||
a refined palette, type scale, spacing + a set of shared
|
||||
polish classes so all screens share one visual language.
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* Palette — refined indigo/violet brand anchored on a cool paper */
|
||||
--bg: #f6f7fb;
|
||||
--bg-grad: radial-gradient(1200px 600px at 85% -10%, rgba(124,58,237,.08), transparent 60%),
|
||||
radial-gradient(1000px 500px at -10% 110%, rgba(79,70,229,.07), transparent 55%);
|
||||
--card: #ffffff;
|
||||
--paper-2: #fafbfe;
|
||||
--border: #e5e8ef;
|
||||
--border-strong: #d6dbe7;
|
||||
--ink: #1a1d29;
|
||||
--ink-2: #343a4e;
|
||||
--muted: #6b7280;
|
||||
--muted-2: #94a0b3;
|
||||
/* Brand — indigo → violet (kept coherent with existing var(--accent) usage) */
|
||||
--accent: #4f46e5;
|
||||
--accent-2: #7c3aed;
|
||||
--accent-strong: #4338ca;
|
||||
--accent-soft: #eef2ff;
|
||||
--accent-softer: #f5f7ff;
|
||||
/* Status */
|
||||
--green: #16a34a;
|
||||
--green-strong: #15803d;
|
||||
--green-soft: #ecfdf3;
|
||||
--red: #dc2626;
|
||||
--red-strong: #b91c1c;
|
||||
--red-soft: #fef2f2;
|
||||
--amber: #d97706;
|
||||
--amber-strong: #b45309;
|
||||
--amber-soft: #fffbeb;
|
||||
--sky: #0284c7;
|
||||
--sky-soft: #e0f2fe;
|
||||
|
||||
/* Type scale */
|
||||
--text-xs: 12px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 14px;
|
||||
--text-md: 16px;
|
||||
--text-lg: 19px;
|
||||
--text-xl: 22px;
|
||||
--text-2xl: 28px;
|
||||
--tracking-tight: -0.01em;
|
||||
|
||||
/* Radial + spacing (4-pt scale) */
|
||||
--radius-xs: 8px;
|
||||
--radius-sm: 10px;
|
||||
--radius: 14px;
|
||||
--shadow: 0 1px 3px rgba(20, 24, 40, 0.08);
|
||||
--radius-lg: 18px;
|
||||
--radius-pill: 999px;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
|
||||
/* Shadows — layered, soft, premium */
|
||||
--shadow: 0 1px 2px rgba(22, 28, 51, .05), 0 1px 3px rgba(20, 24, 40, .07);
|
||||
--shadow-md: 0 2px 4px rgba(22, 28, 51, .05), 0 8px 24px -8px rgba(30, 34, 60, .14);
|
||||
--shadow-lg: 0 6px 16px -6px rgba(30, 34, 60, .16), 0 12px 40px -12px rgba(30, 34, 60, .18);
|
||||
--shadow-accent: 0 8px 22px -8px rgba(79, 70, 229, .5);
|
||||
|
||||
--ease: cubic-bezier(.22, .61, .36, 1);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
html { overflow-x: clip; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans Thai', sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans Thai', 'Helvetica Neue', sans-serif;
|
||||
background: var(--bg);
|
||||
background-image: var(--bg-grad);
|
||||
background-attachment: fixed;
|
||||
color: var(--ink);
|
||||
line-height: 1.5;
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
overflow-x: clip;
|
||||
}
|
||||
#app { min-height: 100vh; }
|
||||
|
||||
/* ── Shared page scaffolding ─────────────────────────────────── */
|
||||
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 6px; }
|
||||
.page-title { font-size: var(--text-xl); font-weight: 750; letter-spacing: var(--tracking-tight); margin: 0; color: var(--ink); display: inline-flex; align-items: center; gap: 10px; }
|
||||
.page-title svg { color: var(--accent); }
|
||||
.page-sub { color: var(--muted); margin: 4px 0 16px; font-size: var(--text-sm); }
|
||||
h2 { font-size: var(--text-xl); letter-spacing: var(--tracking-tight); }
|
||||
h3 { font-size: var(--text-md); letter-spacing: var(--tracking-tight); }
|
||||
h4 { font-size: var(--text-base); letter-spacing: var(--tracking-tight); }
|
||||
|
||||
/* ── Cards ───────────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.card.lift { transition: transform .18s var(--ease), box-shadow .22s var(--ease), border-color .18s var(--ease); }
|
||||
.card.lift:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
||||
.card.inset { background: var(--paper-2); }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-base);
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
button.primary {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-accent);
|
||||
}
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
button.soft { background: var(--accent-soft); border-color: transparent; color: var(--accent-strong); }
|
||||
button.soft:not(:disabled):hover { background: #e0e7ff; }
|
||||
button.danger { background: var(--red-soft); border-color: transparent; color: var(--red-strong); }
|
||||
button.danger:not(:disabled):hover { background: #fee2e2; }
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-size: var(--text-base);
|
||||
background: var(--card);
|
||||
color: var(--ink);
|
||||
}
|
||||
label { font-size: 13px; color: var(--muted); display: block; margin: 10px 0 4px; }
|
||||
label { font-size: 13px; color: var(--muted); display: block; margin: 10px 0 4px; font-weight: 600; }
|
||||
.row { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge.A { background: #dcfce7; color: #166534; }
|
||||
.badge.B { background: #fef9c3; color: #854d0e; }
|
||||
.badge.C { background: #fee2e2; color: #991b1b; }
|
||||
.badge.won { background: #dcfce7; color: #166534; }
|
||||
.badge.lost { background: #fee2e2; color: #991b1b; }
|
||||
.badge.not_tried { background: #eef2ff; color: #4338ca; }
|
||||
.error { color: var(--red); font-size: 13px; }
|
||||
.muted { color: var(--muted); }
|
||||
.msg-seller { background: var(--accent); color: #fff; align-self: flex-end; border-radius: 16px 16px 4px 16px; }
|
||||
.msg-customer { background: #fff; align-self: flex-start; border-radius: 16px 16px 16px 4px; border: 1px solid var(--border); }
|
||||
|
||||
/* ── UX polish: focus rings, touch targets, transitions ───────────── */
|
||||
/* Visible focus rings for keyboard nav (a11y) */
|
||||
/* ── Badges / chips ──────────────────────────────────────────── */
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: .01em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* Buying-intent tiers */
|
||||
.badge.A { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
|
||||
.badge.B { background: var(--amber-soft); color: var(--amber-strong); border: 1px solid #fde68a; }
|
||||
.badge.C { background: var(--red-soft); color: var(--red-strong); border: 1px solid #fecaca; }
|
||||
/* Outcomes */
|
||||
.badge.won { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
|
||||
.badge.lost { background: var(--red-soft); color: var(--red-strong); border: 1px solid #fecaca; }
|
||||
.badge.not_tried { background: var(--accent-soft); color: var(--accent-strong); border: 1px solid #c7d2fe; }
|
||||
.badge.active, .badge.in_progress, .badge.ready { background: var(--accent-soft); color: var(--accent-strong); border: 1px solid #c7d2fe; }
|
||||
.badge.draft, .badge.pending { background: var(--amber-soft); color: var(--amber-strong); border: 1px solid #fde68a; }
|
||||
.badge.tier-a { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
|
||||
.badge.tier-b { background: var(--amber-soft); color: var(--amber-strong); border: 1px solid #fde68a; }
|
||||
.badge.tier-c { background: var(--red-soft); color: var(--red-strong); border: 1px solid #fecaca; }
|
||||
.badge.facebook { background: var(--sky-soft); color: var(--sky); border: 1px solid #bae6fd; }
|
||||
.badge.line { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
|
||||
.badge.user, .badge.admin { background: var(--paper-2); color: var(--ink-2); border: 1px solid var(--border); }
|
||||
|
||||
/* ── KPI / stat cards ────────────────────────────────────────── */
|
||||
.stat { text-align: center; position: relative; overflow: hidden; }
|
||||
.stat::after { content: ""; position: absolute; inset-inline: 0; top: 0; height: 3px; background: transparent; }
|
||||
.stat > span, .stat div { display: block; font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.stat strong { font-size: 30px; font-weight: 780; letter-spacing: -0.02em; color: var(--ink); }
|
||||
.stat.won strong { color: var(--green-strong); }
|
||||
.stat.won::after { background: linear-gradient(90deg, var(--green), transparent); }
|
||||
.stat.lost strong { color: var(--red-strong); }
|
||||
.stat.lost::after { background: linear-gradient(90deg, var(--red), transparent); }
|
||||
.stat.active strong { color: var(--accent); }
|
||||
.stat.active::after { background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
|
||||
.stat.neutral::after { background: linear-gradient(90deg, var(--muted-2), transparent); }
|
||||
|
||||
/* ── Guide / callout cards ───────────────────────────────────── */
|
||||
.guide { background: linear-gradient(180deg, var(--accent-softer), var(--accent-soft)); border: 1px solid #dbe3ff; margin-bottom: 16px; }
|
||||
.guide strong { display: inline-flex; align-items: center; gap: 7px; color: var(--accent-strong); }
|
||||
.guide ol, .guide ul { margin: 10px 0 2px; padding-left: 20px; line-height: 1.9; color: var(--ink-2); }
|
||||
|
||||
/* ── Empty states ────────────────────────────────────────────── */
|
||||
.empty-state { text-align: center; padding: 48px 24px; color: var(--muted); display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||
.empty-state .empty-illust { width: 56px; height: 56px; border-radius: 16px; background: var(--accent-soft); color: var(--accent); display: inline-flex; align-items: center; justify-content: center; margin-bottom: 6px; }
|
||||
.empty-state strong { display: block; color: var(--ink); font-size: var(--text-md); }
|
||||
.empty-state span { font-size: var(--text-sm); max-width: 360px; }
|
||||
|
||||
/* ── Chat thread (core learning surface) ─────────────────────── */
|
||||
.thread {
|
||||
background: linear-gradient(180deg, #f2f4f9 0%, #eceef5 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
min-height: 320px;
|
||||
max-height: 56vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.bubble {
|
||||
max-width: 74%;
|
||||
padding: 11px 15px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.55;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 2px rgba(22,28,51,.06);
|
||||
}
|
||||
.msg-seller {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||||
color: #fff;
|
||||
align-self: flex-end;
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
.msg-customer {
|
||||
background: #fff;
|
||||
align-self: flex-start;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.msg-system {
|
||||
align-self: center;
|
||||
background: var(--amber-soft);
|
||||
color: var(--amber-strong);
|
||||
font-size: 12px;
|
||||
max-width: 88%;
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 7px 14px;
|
||||
box-shadow: none;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
.composer {
|
||||
display: flex; gap: 10px; margin-top: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
.composer input {
|
||||
border-radius: var(--radius-pill);
|
||||
padding-left: 18px; padding-right: 18px;
|
||||
background: var(--card);
|
||||
}
|
||||
.composer button { border-radius: var(--radius-pill); padding-inline: 20px; }
|
||||
|
||||
/* ── Toast-like feedback ─────────────────────────────────────── */
|
||||
.error { color: var(--red-strong); font-size: var(--text-sm); font-weight: 600; }
|
||||
.muted { color: var(--muted); }
|
||||
.ok { color: var(--green-strong); font-size: var(--text-sm); font-weight: 600; }
|
||||
|
||||
/* ── Focus rings + touch density (a11y) ──────────────────────── */
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
@@ -85,81 +273,81 @@ a:focus-visible {
|
||||
a { color: inherit; }
|
||||
a:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
/* Comfortable touch density + consistent transitions */
|
||||
button { min-height: 44px; transition: transform .15s ease, box-shadow .2s ease, background .2s ease, opacity .2s ease; display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
|
||||
button:not(:disabled):hover { box-shadow: 0 4px 12px rgba(20,24,40,.1); }
|
||||
button {
|
||||
min-height: 44px;
|
||||
transition: transform .15s var(--ease), box-shadow .2s var(--ease), background .2s var(--ease), opacity .2s var(--ease), border-color .15s var(--ease);
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
||||
}
|
||||
button:not(:disabled):hover { box-shadow: 0 4px 14px rgba(22,28,51,.12); }
|
||||
button:not(:disabled):active { transform: scale(.97); }
|
||||
button.primary:not(:disabled):hover { box-shadow: 0 6px 18px rgba(79,70,229,.35); }
|
||||
button.soft { background: #f1f3f9; border-color: transparent; color: var(--ink); }
|
||||
button.soft:not(:disabled):hover { background: #e6eaf3; }
|
||||
/* Align inline SVG (lucide) icons inside buttons/links */
|
||||
button svg, .icon-btn svg, .brand svg { vertical-align: -2px; }
|
||||
|
||||
button.primary:not(:disabled):hover { box-shadow: 0 10px 26px -8px rgba(79,70,229,.55); }
|
||||
input, select, textarea {
|
||||
min-height: 44px;
|
||||
transition: border-color .15s ease, box-shadow .15s ease;
|
||||
transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease);
|
||||
}
|
||||
input:focus, select:focus, textarea:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,.15);
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,.14);
|
||||
background: var(--card);
|
||||
}
|
||||
input:disabled { background: var(--paper-2); color: var(--muted-2); }
|
||||
textarea { min-height: 88px; resize: vertical; }
|
||||
|
||||
/* Cards lift on hover (only for interactive/uniform card grids) */
|
||||
.card.lift { transition: transform .2s ease, box-shadow .25s ease; }
|
||||
.card.lift:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(20,24,40,.10); }
|
||||
button svg, .icon-btn svg, .brand svg, .btn-back svg { vertical-align: -2px; }
|
||||
|
||||
/* Disabled clarity */
|
||||
button:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
|
||||
|
||||
/* Back link button */
|
||||
/* ── Back link ───────────────────────────────────────────────── */
|
||||
.btn-back {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 8px 14px; margin-bottom: 12px;
|
||||
background: transparent; border: 1px solid var(--border); border-radius: 10px;
|
||||
color: var(--muted); font-size: 13px; text-decoration: none;
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 8px 14px; margin-bottom: 16px;
|
||||
background: transparent; border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
color: var(--muted); font-size: var(--text-sm); text-decoration: none; font-weight: 600;
|
||||
transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
|
||||
}
|
||||
.btn-back:hover { color: var(--ink); border-color: var(--accent); }
|
||||
.btn-back:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-softer); }
|
||||
|
||||
/* Status spinner */
|
||||
/* ── Spinner + skeleton ──────────────────────────────────────── */
|
||||
.spinner {
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
border: 2px solid rgba(255,255,255,.4); border-top-color: #fff;
|
||||
animation: spin .7s linear infinite; display: inline-block;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Skeleton loading blocks */
|
||||
.skeleton {
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: linear-gradient(90deg, #eef0f5 25%, #e2e5ec 37%, #eef0f5 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s ease infinite;
|
||||
}
|
||||
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
|
||||
|
||||
/* Respect reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
||||
}
|
||||
/* ── Field helper ────────────────────────────────────────────── */
|
||||
.field-error { color: var(--red-strong); font-size: 12px; margin-top: 4px; }
|
||||
.link-accent { color: var(--accent); font-weight: 600; text-decoration: none; }
|
||||
.link-accent:hover { text-decoration: underline; }
|
||||
|
||||
/* Empty-state block */
|
||||
.empty-state { text-align: center; padding: 40px 20px; color: var(--muted); }
|
||||
.empty-state strong { display: block; margin-bottom: 4px; color: var(--ink); }
|
||||
/* ── Progress bar ────────────────────────────────────────────── */
|
||||
.bar { background: #eef0f5; border-radius: var(--radius-pill); height: 10px; overflow: hidden; margin-top: 10px; }
|
||||
.bar-fill { background: linear-gradient(90deg, var(--accent), var(--accent-2)); height: 100%; border-radius: var(--radius-pill); transition: width .45s var(--ease); }
|
||||
.bar-fill.green { background: linear-gradient(90deg, var(--green), #22c55e); }
|
||||
|
||||
/* Field helper/error lines under inputs */
|
||||
.field-error { color: var(--red); font-size: 12px; margin-top: 4px; }
|
||||
|
||||
/* Responsive container default */
|
||||
/* ── Responsive container default ────────────────────────────── */
|
||||
@media (max-width: 640px) {
|
||||
.main { padding: 16px; }
|
||||
.row { gap: 10px; }
|
||||
.msg-seller, .msg-customer { max-width: 84%; }
|
||||
.page-title { font-size: var(--text-lg); }
|
||||
/* Moreminimore convention: centered content when a section collapses to one column */
|
||||
.stat-row > .card, .row.stat > .card { flex: 1 1 40%; text-align: center; }
|
||||
.stat div { text-align: center; }
|
||||
.stat div, .stat > span { text-align: center; justify-content: center; }
|
||||
}
|
||||
@media (max-width: 460px) {
|
||||
.row.stat, .stat-row, .row.stat-row { justify-content: center; }
|
||||
.stat-row > .card { min-width: 42%; }
|
||||
.stat-row > .card { min-width: 44%; }
|
||||
.bubble { max-width: 86%; }
|
||||
}
|
||||
|
||||
/* ── Respect reduced motion ──────────────────────────────────── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
|
||||
.card.lift:hover { transform: none; }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<router-link :to="`/groups/${gid}/personas`" class="btn-back"><ArrowLeft :size="16" :stroke-width="2" /> {{ i18n.t('personas') }}</router-link>
|
||||
|
||||
<!-- Scenario picker (before chat) -->
|
||||
<div v-if="phase === 'pick'" class="card">
|
||||
<div v-if="phase === 'pick'">
|
||||
<div class="card guide">
|
||||
<strong><Target :size="17" :stroke-width="1.8" /> {{ i18n.t('chatGuideTitle') }}</strong>
|
||||
<p style="margin:6px 0 0;line-height:1.7">{{ i18n.t('chatGuideText') }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0"><Target :size="18" :stroke-width="1.8" /> {{ i18n.t('chooseScenario') }}</h3>
|
||||
<p class="muted" style="margin-top:0">{{ i18n.t('scenarioContext') }}</p>
|
||||
<p v-if="persona" class="context-line">
|
||||
@@ -26,20 +31,21 @@
|
||||
<button class="primary" style="margin-top:16px;width:100%" :disabled="!picked" @click="begin">
|
||||
<Play :size="16" :stroke-width="1.8" /> {{ i18n.t('start') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat / results -->
|
||||
<div v-else>
|
||||
<!-- How-to-train guide -->
|
||||
<div class="card guide">
|
||||
<strong><Target :size="17" :stroke-width="1.8" style="vertical-align:-3px" /> {{ i18n.t('chatGuideTitle') }}</strong>
|
||||
<p style="margin:6px 0 0;line-height:1.7">{{ i18n.t('chatGuideText') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="row" style="align-items:center;margin-bottom:12px">
|
||||
<h2 style="margin:0">{{ persona ? persona.name : '...' }}</h2>
|
||||
<span class="muted" v-if="persona">{{ persona.profession }} · {{ persona.age_group }}</span>
|
||||
<span v-if="phase === 'done'" class="badge" :class="debrief && debrief.outcome">
|
||||
<!-- Chat header: persona as the "contact" -->
|
||||
<div class="chat-hdr" v-if="persona">
|
||||
<div class="avatar" :class="'av-' + (persona.tier || '').toLowerCase()">
|
||||
{{ persona.initials || (persona.name || '?').slice(0,1) }}
|
||||
</div>
|
||||
<div class="hdr-meta">
|
||||
<strong>{{ persona.name }}</strong>
|
||||
<span class="muted">{{ persona.profession }} · {{ persona.age_group }}</span>
|
||||
</div>
|
||||
<span v-if="phase === 'done'" class="badge outcome-badge" :class="debrief && debrief.outcome">
|
||||
{{ debrief && debrief.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -49,30 +55,43 @@
|
||||
<div
|
||||
v-for="(m, i) in messages"
|
||||
:key="i"
|
||||
class="bubble"
|
||||
:class="m.role === 'seller' ? 'msg-seller' : m.role === 'system' ? 'msg-system' : 'msg-customer'"
|
||||
>{{ m.text }}</div>
|
||||
<div v-if="sending" class="bubble msg-customer muted">...</div>
|
||||
class="bubble-row"
|
||||
:class="m.role === 'seller' ? 'row-seller' : m.role === 'system' ? 'row-system' : 'row-customer'"
|
||||
>
|
||||
<span v-if="m.role === 'customer'" class="b-avatar b-av-c">{{ (persona && persona.name || '?').slice(0,1) }}</span>
|
||||
<div class="bubble" :class="m.role === 'seller' ? 'msg-seller' : m.role === 'system' ? 'msg-system' : 'msg-customer'">{{ m.text }}</div>
|
||||
<span v-if="m.role === 'seller'" class="b-avatar b-av-s"><UserRound :size="15" :stroke-width="1.8" /></span>
|
||||
</div>
|
||||
<div v-if="sending" class="bubble-row row-customer">
|
||||
<span class="b-avatar b-av-c">{{ (persona && persona.name || '?').slice(0,1) }}</span>
|
||||
<div class="bubble msg-customer muted typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div v-if="phase === 'chat'" class="composer">
|
||||
<input v-model="text" @keyup.enter="send" :disabled="sending" :placeholder="i18n.t('send')" />
|
||||
<button class="primary" @click="send" :disabled="sending || !text.trim()">{{ i18n.t('send') }}</button>
|
||||
<button class="primary" @click="send" :disabled="sending || !text.trim()"><Send :size="16" :stroke-width="1.8" /> {{ i18n.t('send') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Debrief -->
|
||||
<div v-if="phase === 'done' && debrief" class="card debrief">
|
||||
<h3>{{ i18n.t('debrief') }}</h3>
|
||||
<p><span class="badge" :class="debrief.outcome">{{ debrief.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }}</span>
|
||||
— {{ i18n.t('score') }}: <strong>{{ debrief.score }}</strong></p>
|
||||
<p><strong>{{ i18n.t('why') }}:</strong> {{ debrief.why }}</p>
|
||||
<div v-if="debrief.coaching && debrief.coaching.length">
|
||||
<strong>Coaching:</strong>
|
||||
<ul><li v-for="(c, i) in debrief.coaching" :key="i">{{ c }}</li></ul>
|
||||
<div class="result-banner" :class="debrief.outcome">
|
||||
<div class="result-icon">{{ debrief.outcome === 'won' ? '✓' : '✕' }}</div>
|
||||
<div class="result-text">
|
||||
<strong>{{ debrief.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }}</strong>
|
||||
<span>{{ i18n.t('score') }}: <b>{{ debrief.score }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary><Search :size="15" :stroke-width="1.8" style="vertical-align:-2px" /> {{ i18n.t('reveal') }}</summary>
|
||||
<p class="why"><strong>{{ i18n.t('why') }}:</strong> {{ debrief.why }}</p>
|
||||
<div v-if="debrief.coaching && debrief.coaching.length" class="coach-block">
|
||||
<div class="coach-title"><Sparkles :size="16" :stroke-width="1.8" /> <strong>Coaching</strong></div>
|
||||
<ul class="coach-list">
|
||||
<li v-for="(c, i) in debrief.coaching" :key="i"><span class="check">✓</span>{{ c }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<details class="reveal">
|
||||
<summary><Search :size="15" :stroke-width="1.8" /> {{ i18n.t('reveal') }}</summary>
|
||||
<div class="reveal-grid" v-if="debrief.revealed_persona">
|
||||
<div v-for="(v, k) in debrief.revealed_persona" :key="k" class="rev">
|
||||
<span class="rk">{{ fieldLabel(k) }}</span>
|
||||
@@ -80,7 +99,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<router-link to="/"><button class="primary" style="margin-top:12px">{{ i18n.t('dashboard') }}</button></router-link>
|
||||
<router-link to="/"><button class="primary" style="width:100%;margin-top:14px">{{ i18n.t('dashboard') }}</button></router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,7 +108,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, nextTick, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Target, ArrowLeft, Search, MessageCircle, Phone, Play } from 'lucide-vue-next'
|
||||
import { Target, ArrowLeft, Search, MessageCircle, Phone, Play, Send, UserRound, Sparkles } from 'lucide-vue-next'
|
||||
import { api } from '../api'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
@@ -223,32 +242,115 @@ function fmt(v) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.thread {
|
||||
background: #eceff4;
|
||||
border: 1px solid var(--border);
|
||||
/* Scenario picker */
|
||||
.scenario {
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
min-height: 320px;
|
||||
max-height: 52vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s var(--ease), background .15s var(--ease), box-shadow .15s var(--ease);
|
||||
}
|
||||
.bubble { max-width: 72%; padding: 10px 14px; white-space: pre-wrap; word-break: break-word; }
|
||||
.msg-system { align-self: center; background: #fef3c7; color: #92400e; font-size: 12px; max-width: 88%; border-radius: 999px; }
|
||||
.composer { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.context-line { margin: 8px 0 12px; color: var(--ink); font-size: 13px; }
|
||||
.scenario:hover { border-color: var(--accent); background: var(--accent-softer); }
|
||||
.scenario.sel { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px rgba(79,70,229,.12); }
|
||||
.scenario.sel strong { color: var(--accent-strong); }
|
||||
|
||||
/* Chat header — the "contact" row */
|
||||
.chat-hdr {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 14px 16px; margin-bottom: 14px;
|
||||
background: rgba(255,255,255,.85);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.avatar {
|
||||
width: 46px; height: 46px; border-radius: 50%;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-weight: 750; font-size: 18px; color: #fff; flex-shrink: 0;
|
||||
box-shadow: var(--shadow); text-transform: uppercase;
|
||||
}
|
||||
.av-a { background: linear-gradient(135deg, #059669, #34d399); }
|
||||
.av-b { background: linear-gradient(135deg, #d97706, #f59e0b); }
|
||||
.av-c { background: linear-gradient(135deg, #dc2626, #f87171); }
|
||||
.hdr-meta { display: flex; flex-direction: column; min-width: 0; }
|
||||
.hdr-meta strong { font-size: var(--text-md); letter-spacing: var(--tracking-tight); }
|
||||
.hdr-meta .muted { font-size: 13px; }
|
||||
.outcome-badge { margin-left: auto; }
|
||||
|
||||
/* Thread + bubbles */
|
||||
.bubble-row { display: flex; align-items: flex-end; gap: 8px; width: 100%; }
|
||||
.row-customer { justify-content: flex-start; }
|
||||
.row-seller { justify-content: flex-end; }
|
||||
.row-system { justify-content: center; }
|
||||
.b-avatar {
|
||||
width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; font-weight: 700; color: #fff;
|
||||
}
|
||||
.b-av-c { background: linear-gradient(135deg, var(--accent-strong), var(--accent-2)); }
|
||||
.b-av-s { background: var(--ink-2); }
|
||||
.bubble { max-width: 74%; padding: 11px 15px; white-space: pre-wrap; word-break: break-word; }
|
||||
.msg-system { max-width: 88%; }
|
||||
|
||||
/* Typing indicator */
|
||||
.typing { display: inline-flex; gap: 4px; align-items: center; padding: 14px 18px; }
|
||||
.typing .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2); animation: blink 1.2s infinite both; }
|
||||
.typing .dot:nth-child(2) { animation-delay: .18s; }
|
||||
.typing .dot:nth-child(3) { animation-delay: .36s; }
|
||||
@keyframes blink { 0%, 80%, 100% { opacity: .25; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-2px); } }
|
||||
|
||||
/* Debrief — rewarding reveal */
|
||||
.debrief { margin-top: 18px; }
|
||||
.result-banner {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
padding: 20px; border-radius: var(--radius);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.result-banner.won { background: linear-gradient(135deg, #eafcf1, #d9f6e6); border: 1px solid #bbf7d0; }
|
||||
.result-banner.lost { background: linear-gradient(135deg, #fef2f2, #fdeaea); border: 1px solid #fecaca; }
|
||||
.result-icon {
|
||||
width: 52px; height: 52px; border-radius: 50%; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 26px; font-weight: 800; color: #fff;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.result-banner.won .result-icon { background: linear-gradient(135deg, #16a34a, #22c55e); }
|
||||
.result-banner.lost .result-icon { background: linear-gradient(135deg, #dc2626, #ef4444); }
|
||||
.result-text { display: flex; flex-direction: column; }
|
||||
.result-text strong { font-size: var(--text-xl); letter-spacing: var(--tracking-tight); }
|
||||
.result-banner.won .result-text strong { color: var(--green-strong); }
|
||||
.result-banner.lost .result-text strong { color: var(--red-strong); }
|
||||
.result-text span { color: var(--muted); font-size: 14px; }
|
||||
.why { margin: 0 0 14px; line-height: 1.65; color: var(--ink-2); }
|
||||
.coach-block { background: var(--accent-softer); border: 1px solid #dbe3ff; border-radius: var(--radius); padding: 14px 16px; margin-bottom: 12px; }
|
||||
.coach-title { display: flex; align-items: center; gap: 8px; color: var(--accent-strong); margin-bottom: 8px; }
|
||||
.coach-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
.coach-list li { display: flex; gap: 9px; line-height: 1.55; color: var(--ink-2); align-items: flex-start; }
|
||||
.coach-list .check {
|
||||
width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0;
|
||||
background: var(--accent); color: #fff; font-size: 11px;
|
||||
display: inline-flex; align-items: center; justify-content: center; margin-top: 2px;
|
||||
}
|
||||
details.reveal {
|
||||
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
margin-top: 4px; overflow: hidden;
|
||||
}
|
||||
details.reveal summary {
|
||||
cursor: pointer; padding: 12px 14px; font-weight: 600;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--paper-2); color: var(--accent-strong);
|
||||
}
|
||||
details.reveal summary:hover { background: var(--accent-softer); }
|
||||
|
||||
.context-line { margin: 8px 0 12px; color: var(--ink); font-size: 13px; font-weight: 600; }
|
||||
.task { margin-bottom: 12px; background: #fff7ed; border-color: #fed7aa; }
|
||||
.debrief { margin-top: 16px; }
|
||||
button.danger { background: var(--red); color: #fff; border: none; }
|
||||
.guide { background: #eef2ff; border-color: #c7d2fe; margin-bottom: 12px; }
|
||||
.scenario { border: 2px solid var(--border); border-radius: 12px; padding: 12px 14px; margin: 10px 0; cursor: pointer; transition: border-color .15s ease, background .15s ease; }
|
||||
.scenario:hover { border-color: var(--accent); }
|
||||
.scenario.sel { border-color: var(--accent); background: #eef2ff; }
|
||||
.reveal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
|
||||
.rev { display: flex; flex-direction: column; background: #f8fafc; border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; }
|
||||
.rk { font-size: 12px; color: var(--muted); }
|
||||
.rv { font-size: 13px; color: var(--ink); margin-top: 2px; }
|
||||
.reveal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 8px; padding: 14px; }
|
||||
.rev { display: flex; flex-direction: column; background: var(--paper-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; }
|
||||
.rk { font-size: 12px; color: var(--muted); font-weight: 600; }
|
||||
.rv { font-size: 13px; color: var(--ink); margin-top: 3px; line-height: 1.5; }
|
||||
@media (max-width: 640px) { .reveal-grid { grid-template-columns: 1fr; } }
|
||||
@media (prefers-reduced-motion: reduce) { .typing .dot { animation: none; } }
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<div class="login-wrap">
|
||||
<div class="card login-card">
|
||||
<h1>{{ i18n.t('app') }}</h1>
|
||||
<p class="muted" style="margin-top:-8px">Sales training simulator</p>
|
||||
<div class="brand-block">
|
||||
<span class="brand-icon"><Target :size="26" :stroke-width="1.8" /></span>
|
||||
<h1>{{ i18n.t('app') }}</h1>
|
||||
<p class="tagline">Sales training simulator</p>
|
||||
</div>
|
||||
|
||||
<label>{{ i18n.t('username') }}</label>
|
||||
<input v-model="username" type="text" autocomplete="username" placeholder="username" class="wide" @keyup.enter="submit" />
|
||||
@@ -28,7 +31,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Eye, EyeOff } from 'lucide-vue-next'
|
||||
import { Eye, EyeOff, Target } from 'lucide-vue-next'
|
||||
import { auth } from '../store/auth'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
@@ -63,15 +66,29 @@ async function submit() {
|
||||
.login-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 8vh;
|
||||
align-items: flex-start;
|
||||
padding-top: 12vh;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
/* Wider card so inputs don't truncate / cut text */
|
||||
.login-card { width: 460px; max-width: 100%; padding: 28px 30px; }
|
||||
h1 { margin-top: 0; font-size: 24px; }
|
||||
/* Full-width, uncramped inputs (global input already width:100%) */
|
||||
.wide { width: 100%; min-height: 46px; font-size: 15px; padding: 12px 14px; }
|
||||
.login-card {
|
||||
width: 440px; max-width: 100%;
|
||||
padding: 36px 36px 32px;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.brand-block { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; margin-bottom: 22px; }
|
||||
.brand-icon {
|
||||
width: 52px; height: 52px; border-radius: 16px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
color: #fff; box-shadow: var(--shadow-accent); margin-bottom: 12px;
|
||||
}
|
||||
h1 { margin: 0; font-size: var(--text-xl); font-weight: 780; letter-spacing: var(--tracking-tight); }
|
||||
.tagline { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
|
||||
.wide { width: 100%; min-height: 46px; font-size: 15px; padding: 12px 14px; border-radius: var(--radius-sm); }
|
||||
.pw-wrap { position: relative; }
|
||||
.pw-wrap input { padding-right: 48px; }
|
||||
.pw-toggle {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<h2 style="margin:0"><LayoutDashboard :size="22" :stroke-width="1.8" style="vertical-align:-4px" /> {{ i18n.t('myDashboard') }}</h2>
|
||||
<div class="muted" style="margin-top:4px;margin-bottom:16px">{{ i18n.t('myDashboardSubtitle') }}</div>
|
||||
<div class="row" style="gap:8px;margin-bottom:12px">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2 class="page-title"><LayoutDashboard :size="22" :stroke-width="1.8" /> {{ i18n.t('myDashboard') }}</h2>
|
||||
<div class="page-sub">{{ i18n.t('myDashboardSubtitle') }}</div>
|
||||
</div>
|
||||
<div class="row" style="gap:8px">
|
||||
<router-link to="/my/weak-areas"><button class="soft">{{ i18n.t('weakAreas') }}</button></router-link>
|
||||
<router-link to="/my/personas"><button class="soft">{{ i18n.t('myPersonas') }}</button></router-link>
|
||||
</div>
|
||||
@@ -11,22 +13,23 @@
|
||||
|
||||
<!-- Win/Lose summary -->
|
||||
<div class="row stat-row" v-if="!loading">
|
||||
<div class="card stat"><span>{{ i18n.t('total') }}</span><strong>{{ total }}</strong></div>
|
||||
<div class="card stat neutral"><span>{{ i18n.t('total') }}</span><strong>{{ total }}</strong></div>
|
||||
<div class="card stat won"><span>{{ i18n.t('won') }}</span><strong>{{ won }}</strong></div>
|
||||
<div class="card stat lost"><span>{{ i18n.t('lost') }}</span><strong>{{ lost }}</strong></div>
|
||||
<div class="card stat"><span>{{ i18n.t('notTried') }}</span><strong>{{ notTried }}</strong></div>
|
||||
<div class="card stat neutral"><span>{{ i18n.t('notTried') }}</span><strong>{{ notTried }}</strong></div>
|
||||
<div class="card stat active"><span>{{ i18n.t('inProgress') }}</span><strong>{{ active }}</strong></div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="card"><div class="skeleton" style="height:50px"></div></div>
|
||||
<div v-else-if="items.length === 0" class="card empty-state">
|
||||
<span class="empty-illust"><Target :size="26" :stroke-width="1.6" /></span>
|
||||
<strong>{{ i18n.t('noPractice') }}</strong>
|
||||
<span>{{ i18n.t('noPracticeHint') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Recent sessions -->
|
||||
<div v-show="sessions.length" class="card" style="margin-top:20px">
|
||||
<h3 style="margin-top:0">{{ i18n.t('mySessions') }}</h3>
|
||||
<h3 style="margin-top:0;margin-bottom:6px">{{ i18n.t('mySessions') }}</h3>
|
||||
<div v-for="s in sessions" :key="s.id" class="session-row">
|
||||
<span>{{ s.persona_name }}</span>
|
||||
<span class="session-actions">
|
||||
@@ -44,7 +47,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { LayoutDashboard } from 'lucide-vue-next'
|
||||
import { LayoutDashboard, Target } from 'lucide-vue-next'
|
||||
import { api } from '../api'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
@@ -79,15 +82,23 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-row { gap: 16px; }
|
||||
.stat { text-align: center; min-width: 100px; }
|
||||
.stat span { display: block; font-size: 12px; color: var(--muted); }
|
||||
.stat strong { font-size: 26px; }
|
||||
.stat.won strong { color: var(--green); }
|
||||
.stat.lost strong { color: var(--red); }
|
||||
.stat.active strong { color: var(--accent); }
|
||||
.session-row { display:flex; justify-content:space-between; align-items:center; gap:12px; padding:8px 0; border-bottom:1px solid var(--border); }
|
||||
.session-actions { display:flex; align-items:center; gap:10px; }
|
||||
.badge.active { background:#e0e7ff; color:#3730a3; }
|
||||
@media (max-width: 640px) { .session-row { align-items:flex-start; flex-direction:column; } }
|
||||
.stat-row { gap: 14px; }
|
||||
.stat { text-align: center; min-width: 104px; }
|
||||
.stat strong { font-size: 28px; }
|
||||
.session-row {
|
||||
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||||
padding: 13px 2px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.session-row:last-child { border-bottom: none; }
|
||||
.session-row > span:first-child { font-weight: 600; color: var(--ink-2); }
|
||||
.session-actions { display: flex; align-items: center; gap: 12px; }
|
||||
.session-actions a {
|
||||
color: var(--accent); font-weight: 650; font-size: 13px; text-decoration: none;
|
||||
}
|
||||
.session-actions a:hover { text-decoration: underline; }
|
||||
.badge.active { background: var(--accent-soft); color: var(--accent-strong); border: 1px solid #c7d2fe; }
|
||||
@media (max-width: 640px) {
|
||||
.session-row { align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.session-actions { width: 100%; justify-content: space-between; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-link to="/training" class="btn-back"><ArrowLeft :size="16" :stroke-width="2" /> {{ i18n.t('training') }}</router-link>
|
||||
<div class="row" style="align-items:center">
|
||||
<h2 style="margin:0">{{ i18n.t('personas') }}</h2>
|
||||
<span class="muted" style="margin-left:auto">{{ i18n.t('selectPersona') }}</span>
|
||||
<div class="row" style="align-items:center;margin-bottom:4px">
|
||||
<h2 style="margin:0" class="page-title">{{ i18n.t('personas') }}</h2>
|
||||
<span class="muted" style="margin-left:auto;font-weight:600">{{ i18n.t('selectPersona') }}</span>
|
||||
</div>
|
||||
<div class="card guide" v-if="!auth.isAdmin">
|
||||
<strong><Target :size="17" :stroke-width="1.8" style="vertical-align:-3px" /> วิธีฝึก</strong>
|
||||
<strong><Target :size="17" :stroke-width="1.8" /> วิธีฝึก</strong>
|
||||
<ol style="margin:8px 0 0;padding-left:20px;line-height:1.8">
|
||||
<li>เลือกลูกค้าจำลอง (บุคคลต้นแบบ) คนหนึ่งที่อยากฝึกด้วย</li>
|
||||
<li>ระดับ A ง่ายสุด → ระดับ C ยากสุด (ดูจากดาว ★ ความยาก)</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="setup-wrap">
|
||||
<div class="card setup-card">
|
||||
<h1><Lock :size="22" :stroke-width="1.8" style="vertical-align:-4px" /> {{ i18n.t('setupTitle') }}</h1>
|
||||
<h1><span class="setup-icon"><Lock :size="22" :stroke-width="1.8" /></span> {{ i18n.t('setupTitle') }}</h1>
|
||||
<p class="muted">{{ i18n.t('setupSubtitle') }} <strong>{{ auth.user?.name || auth.user?.username }}</strong></p>
|
||||
|
||||
<label>{{ i18n.t('email') }}</label>
|
||||
@@ -68,13 +68,25 @@ async function submit() {
|
||||
|
||||
<style scoped>
|
||||
.setup-wrap {
|
||||
display: flex; justify-content: center; padding-top: 8vh;
|
||||
padding-left: 16px; padding-right: 16px;
|
||||
display: flex; justify-content: center; padding-top: 10vh;
|
||||
padding-left: 16px; padding-right: 16px; min-height: 100vh;
|
||||
}
|
||||
.setup-card { width: 460px; max-width: 100%; padding: 28px 30px; }
|
||||
h1 { margin-top: 0; font-size: 24px; }
|
||||
.wide { width: 100%; min-height: 46px; font-size: 15px; padding: 12px 14px; }
|
||||
.setup-card {
|
||||
width: 460px; max-width: 100%; padding: 34px 36px 30px;
|
||||
border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0; font-size: var(--text-xl); font-weight: 780;
|
||||
display: flex; align-items: center; gap: 12px; letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
.setup-icon {
|
||||
width: 42px; height: 42px; border-radius: 13px; flex-shrink: 0;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
color: #fff; display: inline-flex; align-items: center; justify-content: center;
|
||||
box-shadow: var(--shadow-accent);
|
||||
}
|
||||
.wide { width: 100%; min-height: 46px; font-size: 15px; padding: 12px 14px; border-radius: var(--radius-sm); }
|
||||
.terms { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 13px; }
|
||||
.terms input { width: auto; min-height: auto; margin: 0; }
|
||||
.terms a { color: var(--accent); }
|
||||
.terms a { color: var(--accent); font-weight: 600; }
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row" style="align-items:center;margin-bottom:16px">
|
||||
<h2 style="margin:0"><Target :size="22" :stroke-width="1.8" style="vertical-align:-4px" /> {{ i18n.t('training') }}</h2>
|
||||
<div class="row" style="margin-left:auto;gap:10px">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2 class="page-title"><Target :size="22" :stroke-width="1.8" /> {{ i18n.t('training') }}</h2>
|
||||
<div class="page-sub">{{ i18n.t('trainingSubtitle') }}</div>
|
||||
</div>
|
||||
<div class="row" style="gap:10px">
|
||||
<router-link to="/guide"><button class="soft"><BookOpen :size="18" :stroke-width="1.8" /> คู่มือ</button></router-link>
|
||||
<router-link v-if="auth.isAdmin" to="/admin/new-group">
|
||||
<button class="primary"><Plus :size="18" :stroke-width="2" /> {{ i18n.t('addProduct') }}</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">{{ i18n.t('trainingSubtitle') }}</p>
|
||||
|
||||
<div v-if="loading" class="card" style="min-height:80px">
|
||||
<div class="skeleton" style="height:50px"></div>
|
||||
</div>
|
||||
<div v-else-if="groups.length === 0" class="card empty-state">
|
||||
<span class="empty-illust"><PackageOpen :size="26" :stroke-width="1.6" /></span>
|
||||
<strong>{{ i18n.t('noTraining') }}</strong>
|
||||
<span v-if="auth.isAdmin">Click "{{ i18n.t('addProduct') }}" to create a persona group.</span>
|
||||
<span v-else>Ask an admin to create a persona group first.</span>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div v-for="g in groups" :key="g.id" class="card lift train-card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row" style="justify-content:space-between;align-items:flex-start">
|
||||
<router-link
|
||||
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
|
||||
style="text-decoration:none;color:inherit;min-width:0"
|
||||
>
|
||||
<strong>{{ g.title }}</strong>
|
||||
<strong class="g-title">{{ g.title }}</strong>
|
||||
</router-link>
|
||||
<div class="row" style="gap:6px;align-items:center">
|
||||
<span class="badge" :class="g.status === 'ready' ? 'ready' : 'draft'">{{ g.status }}</span>
|
||||
@@ -42,13 +45,13 @@
|
||||
</div>
|
||||
<router-link
|
||||
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
|
||||
style="text-decoration:none;color:inherit"
|
||||
style="text-decoration:none;color:inherit;flex:1;display:flex;flex-direction:column"
|
||||
>
|
||||
<div class="muted" style="margin:6px 0 12px">{{ productName(g) }}</div>
|
||||
<div class="row" style="gap:8px">
|
||||
<span class="muted">{{ personaCount(g) }} {{ i18n.t('personas').toLowerCase() }}</span>
|
||||
<div class="muted g-product">{{ productName(g) }}</div>
|
||||
<div class="row" style="gap:8px;margin-top:auto;padding-top:10px">
|
||||
<span class="persona-count"><Users :size="14" :stroke-width="1.8" /> {{ personaCount(g) }} {{ i18n.t('personas').toLowerCase() }}</span>
|
||||
</div>
|
||||
<button class="primary" style="width:100%;margin-top:12px">
|
||||
<button class="primary" style="width:100%;margin-top:14px">
|
||||
{{ auth.isAdmin ? (g.status === 'ready' ? i18n.t('managePersonas') : i18n.t('analyze')) : i18n.t('selectPersona') }}
|
||||
</button>
|
||||
</router-link>
|
||||
@@ -59,7 +62,7 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Target, Plus, BookOpen, Trash2 } from 'lucide-vue-next'
|
||||
import { Target, Plus, BookOpen, Trash2, Users, PackageOpen } from 'lucide-vue-next'
|
||||
import { api } from '../api'
|
||||
import { auth } from '../store/auth'
|
||||
import { i18n } from '../i18n'
|
||||
@@ -97,16 +100,20 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
|
||||
.train-card { display: flex; flex-direction: column; height: 100%; }
|
||||
.badge.ready { background: #dcfce7; color: #166534; }
|
||||
.badge.draft { background: #fef9c3; color: #854d0e; }
|
||||
button.soft { background: #f1f3f9; border-color: transparent; }
|
||||
.badge.facebook { background: #e0f2fe; color: #0369a1; }
|
||||
.badge.line { background: #dcfce7; color: #15803d; }
|
||||
.train-card { display: flex; flex-direction: column; }
|
||||
.g-title { font-size: var(--text-md); letter-spacing: var(--tracking-tight); display: inline-block; }
|
||||
.g-product { margin: 6px 0 0; font-size: 13px; line-height: 1.5; }
|
||||
.persona-count { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 13px; font-weight: 600; }
|
||||
.persona-count svg { color: var(--accent); }
|
||||
.badge.ready { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
|
||||
.badge.draft { background: var(--amber-soft); color: var(--amber-strong); border: 1px solid #fde68a; }
|
||||
button.soft { background: var(--accent-soft); border-color: transparent; color: var(--accent-strong); }
|
||||
.badge.facebook { background: var(--sky-soft); color: var(--sky); border: 1px solid #bae6fd; }
|
||||
.badge.line { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
|
||||
.del-btn {
|
||||
background: transparent; border: 1px solid var(--border); border-radius: 8px;
|
||||
min-height: 32px; width: 32px; padding: 0; display: inline-flex; align-items: center;
|
||||
background: transparent; border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
min-height: 36px; width: 36px; padding: 0; display: inline-flex; align-items: center;
|
||||
justify-content: center; color: var(--muted); cursor: pointer;
|
||||
}
|
||||
.del-btn:hover { background: #fee2e2; border-color: #fecaca; color: var(--red); }
|
||||
.del-btn:hover { background: var(--red-soft); border-color: #fecaca; color: var(--red); }
|
||||
</style>
|
||||
|
||||
26
website/README.md
Normal file
26
website/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Sales Trainer — Marketing Website
|
||||
|
||||
Standalone, self-contained landing site to market the **Sales Trainer** app (AI sales-roleplay
|
||||
coach, Thailand market). **TH-primary with an EN toggle.**
|
||||
|
||||
## Files
|
||||
- `index.html` — the whole page (markup + inline JS for the TH/EN language toggle and scroll cue).
|
||||
- `main.css` — all styling (Hallmark-inspired warm-cream + coral-red design system, tokens at `:root`).
|
||||
|
||||
## Local preview
|
||||
```bash
|
||||
cd website
|
||||
python3 -m http.server 8000
|
||||
# open http://localhost:8000
|
||||
```
|
||||
(It's a pure static site — no build step, no dependencies.)
|
||||
|
||||
## Deploy
|
||||
Can be served from any static host (nginx, `python -m http.server`, Netlify, an EasyPanel static/
|
||||
nginx service, or the repo's existing infra). Point the document root at the `website/` folder.
|
||||
|
||||
## Owner action required BEFORE public launch
|
||||
- **Pricing is placeholder.** The pricing section uses `—` + "ราคาโปรด confirm / price to confirm"
|
||||
labels. Set real prices before publishing publicly. (No fabricated numbers were invented.)
|
||||
- Confirm all copy (phone/contact info, offer details) is accurate before going live.
|
||||
- Optional: add real product screenshots / a real demo link to replace/augment the CSS chat mockup.
|
||||
647
website/index.html
Normal file
647
website/index.html
Normal file
@@ -0,0 +1,647 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="th" data-page-lang="th">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Sales Trainer — ฝึกปิดการขาย กับลูกค้า AI ที่ต่อรองและปฏิเสธจริง</title>
|
||||
<meta name="description" content="Sales Trainer: โปรแกรมฝึกขายระดับองค์กร ให้พนักงานแชต 1:1 กับลูกค้าจำลอง 15 คนที่มีปัญหาจริง, ต่อรองจริง และมี AI โค้ชแนะนำวิธีตอบให้ดีขึ้นในทุกข้อความ">
|
||||
<meta property="og:title" content="Sales Trainer — ฝึกปิดการขายกับลูกค้า AI ที่ต่อรองจริง">
|
||||
<meta property="og:description" content="อัปโหลดสินค้า → AI สร้างลูกค้าจำลอง 15 คนที่มีปัญหาและข้อต่อรองจริง → พนักงานแชตฝึกปิดการขาย ได้รับโค้ชแนะนำแบบต่อข้อความ">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ================= NAV (N5 floating pill) ================= -->
|
||||
<nav class="nav" aria-label="Main">
|
||||
<div class="nav__inner">
|
||||
<a class="nav__brand" href="#top">
|
||||
<span class="nav__mark" aria-hidden="true">
|
||||
<svg class="lucide" viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
||||
</span>
|
||||
Sales Trainer
|
||||
</a>
|
||||
<div class="nav__links">
|
||||
<a class="nav__link" href="#how" data-lang="th">วิธีใช้งาน</a>
|
||||
<a class="nav__link" href="#how" data-lang="en">How it works</a>
|
||||
<a class="nav__link" href="#tiers" data-lang="th">เกรดลูกค้า</a>
|
||||
<a class="nav__link" href="#tiers" data-lang="en">Tiers</a>
|
||||
<a class="nav__link" href="#pricing" data-lang="th">ราคา</a>
|
||||
<a class="nav__link" href="#pricing" data-lang="en">Pricing</a>
|
||||
<a class="nav__link" href="#faq" data-lang="th">คำถาม</a>
|
||||
<a class="nav__link" href="#faq" data-lang="en">FAQ</a>
|
||||
</div>
|
||||
<div class="nav__cta">
|
||||
<div class="lang" role="group" aria-label="Language">
|
||||
<button class="lang__btn" data-lang-btn="th" aria-pressed="true">TH</button>
|
||||
<button class="lang__btn" data-lang-btn="en" aria-pressed="false">EN</button>
|
||||
</div>
|
||||
<a class="btn btn--primary" href="#cta" data-lang="th">ขอเดโม</a>
|
||||
<a class="btn btn--primary" href="#cta" data-lang="en">Book demo</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- ================= HERO (workbench open) ================= -->
|
||||
<header class="hero" id="top">
|
||||
<div class="container">
|
||||
<div class="hero__grid">
|
||||
<div class="hero__copy reveal" style="--i:0">
|
||||
<span class="hero__tag" data-lang="th">สำหรับทีมขาย · องค์กรไทย</span>
|
||||
<span class="hero__tag" data-lang="en">For Thai sales teams</span>
|
||||
<h1 data-lang="th">ฝึกปิดการขาย กับ<span class="u">ลูกค้า AI ที่ต่อรองและปฏิเสธจริง</span></h1>
|
||||
<h1 data-lang="en">Practice closing deals against <span class="u">AI customers who push back</span></h1>
|
||||
<p class="hero__lede" data-lang="th">
|
||||
อัปโหลดหรืออธิบายสินค้าใน 5 นาที — AI จะสร้างลูกค้าจำลอง <strong>15 คน</strong> ที่มีปัญหา ความลังเล
|
||||
และข้อต่อรองเหมือนจริง พนักงานของคุณแชต 1:1 เพื่อปิดการขาย แล้วได้รับ <strong>AI โค้ชแนะนำวิธีตอบให้ดีขึ้น</strong>
|
||||
ทุกข้อความ ไม่ใช่แค่คะแนน
|
||||
</p>
|
||||
<p class="hero__lede" data-lang="en">
|
||||
Describe your product in five minutes. AI builds <strong>15 customer personas</strong> with real pains,
|
||||
doubts and negotiation levers. Your reps chat one-on-one to close the sale and get <strong>per-message coaching</strong>
|
||||
on how to answer better — not just a score.
|
||||
</p>
|
||||
<div class="hero__actions">
|
||||
<a class="btn btn--accent" href="#cta" data-lang="th">ขอทดลอง / รับเดโม</a>
|
||||
<a class="btn btn--accent" href="#cta" data-lang="en">Request a demo</a>
|
||||
<a class="btn btn--ghost" href="#tour" data-lang="th">ดูตัวอย่างบทสนทนา ↓</a>
|
||||
<a class="btn btn--ghost" href="#tour" data-lang="en">See a live example ↓</a>
|
||||
</div>
|
||||
<p class="hero__note" data-lang="th">ภาษาไทย + อังกฤษ · ใช้กับ LINE / Facebook / โทรศัพท์ · เหมาะกับทีมขาย B2B</p>
|
||||
<p class="hero__note" data-lang="en">TH + EN · for LINE / Facebook / phone selling · built for B2B teams</p>
|
||||
</div>
|
||||
|
||||
<div class="hero__visual reveal" style="--i:1" aria-hidden="true">
|
||||
<div class="hero__phone-glow"></div>
|
||||
<div class="chat">
|
||||
<div class="chat__bar">
|
||||
<div class="chat__avatar" style="--i:0">คุณ</div>
|
||||
<div class="chat__who">
|
||||
<div class="chat__name">คุณภา — เจ้าของร้านกาแฟ</div>
|
||||
<div class="chat__meta" data-lang="th">เกรด C · ยังไม่สนใจ แต่มีปัญหา</div>
|
||||
<div class="chat__meta" data-lang="en">Tier C · unsold, in pain</div>
|
||||
</div>
|
||||
<span class="chat__status"><span class="dot"></span><span data-lang="th">ออนไลน์</span><span data-lang="en">online</span></span>
|
||||
</div>
|
||||
<div class="chat__body">
|
||||
<div class="msg theirs">
|
||||
<div class="msg__bubble">เปิดร้าน 8 โมงถึง 6 โมง ฉันไม่มีเวลาคุยหรอก ขายรถอะไรก็เข้าเรื่องมาเลย</div>
|
||||
<span class="msg__time">09:41</span>
|
||||
</div>
|
||||
<div class="msg mine">
|
||||
<div class="msg__bubble">เข้าใจครับ ร้านเปิดทั้งวันไม่สะดวกเสียเวลาคุย ฉันเข้าใจ — ขอถามสั้นๆ หน่อยได้ไหมว่า ช่วงพีคที่คนเยอะๆ รถส่งของรอบเดียวทันไหมครับ?</div>
|
||||
<span class="msg__time">09:42</span>
|
||||
</div>
|
||||
<div class="msg theirs">
|
||||
<div class="msg__bubble">…ก็วันเสาร์บ่ายๆ บางทีไม่ทัน แล้วมันเกี่ยวอะไรกับรถ?</div>
|
||||
<span class="msg__time">09:43</span>
|
||||
</div>
|
||||
<div class="coach">
|
||||
<div class="coach__head">
|
||||
<span class="coach__badge">
|
||||
<svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg>
|
||||
AI Coach
|
||||
</span>
|
||||
<span class="coach__score" data-lang="th">ข้อก่อนหน้า: 62 · <strong>ข้อนี้ 84</strong> ↑</span>
|
||||
<span class="coach__score" data-lang="en">last 62 · this 84 ↑</span>
|
||||
</div>
|
||||
<p class="coach__text" data-lang="th"><strong>ทำได้ดี</strong> — คุณอ้างอิงปัญหาจริง (เวลาพีคไม่ทัน) แทนที่จะขายตรงๆ ลองทวนประโยคนี้: «เข้าใจว่าวันเสาร์บ่ายคนเยอะ» แล้วค่อยชวนคุยเรื่องรอบส่ง ยังอย่าพูดราคาก่อน เกรด C จะปิดยากถ้ายังไม่ได้แก้ปม</p>
|
||||
<p class="coach__text" data-lang="en"><strong>Good turn.</strong> You anchored on a real pain (peak-hour backlog) instead of selling cold. Parrot their words, then raise delivery — hold off on price. Tier-C won't close until the pain is solved.</p>
|
||||
<a class="btn btn--ghost" href="#coach" data-lang="th">เห็นโค้ชหลังจบเกม</a>
|
||||
<a class="btn btn--ghost" href="#coach" data-lang="en">See the full debrief</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat__input" aria-hidden="true">
|
||||
<span class="field" data-lang="th">พิมพ์ข้อความ…</span>
|
||||
<span class="field" data-lang="en">Type a message…</span>
|
||||
<button class="send" tabindex="-1" aria-hidden="true">
|
||||
<svg class="lucide" viewBox="0 0 24 24"><path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="tour">
|
||||
|
||||
<!-- ================= HOW IT WORKS ================= -->
|
||||
<section class="section" id="how">
|
||||
<div class="container">
|
||||
<div class="shead reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">วิธีใช้งาน</span>
|
||||
<span class="shead__label" data-lang="en">How it works</span>
|
||||
<h2 data-lang="th">จากสินค้าของคุณ สู่พนักงานที่ปิดขายเก่งขึ้น — ใน 3 ขั้น</h2>
|
||||
<h2 data-lang="en">From your product to sharper closers — in three steps</h2>
|
||||
</div>
|
||||
<div class="steps">
|
||||
<div class="step reveal" style="--i:1">
|
||||
<h3 data-lang="th">ป้อนสินค้า / บริการ / ไอเดีย</h3>
|
||||
<h3 data-lang="en">Describe your product</h3>
|
||||
<p data-lang="th">Admin อัปโหลดไฟล์หรือพิมพ์คำอธิบายสินค้า บริการ หรือไอเดียใหม่ ระบบใช้ข้อมูลจริงเพื่อดึง "ปัญหาของลูกค้า" ออกมา ไม่ใช่จากสูตรสำเร็จทั่วไป</p>
|
||||
<p data-lang="en">Admins upload a file or describe the product, service or idea. The system pulls real customer pains from your actual offer — not generic scripts.</p>
|
||||
<div class="pills"><span class="pill">PDF · MD · TXT</span><span class="pill">ฟอร์ม</span><span class="pill">หลายผลิตภัณฑ์</span></div>
|
||||
</div>
|
||||
<div class="step reveal" style="--i:2">
|
||||
<h3 data-lang="th">AI สร้างลูกค้าจำลอง 15 คน</h3>
|
||||
<h3 data-lang="en">AI builds 15 personas</h3>
|
||||
<p data-lang="th">ระบบสร้างลูกค้า 15 คน เกรด A/B/C เกรดละ 5 คน แต่ละคนมีปัญหา จุดต่อรอง และวิธีเปิดบทสนทนาที่ต่างกัน — เหมือนได้ทีมเพื่อนร่วมงานที่เล่นบทลูกค้าจริง</p>
|
||||
<p data-lang="en">Fifteen personas — five per buying-intent tier — each with distinct pains, negotiation levers and conversation openers. Like a full role-play cast that never tires.</p>
|
||||
<div class="pills"><span class="pill">เกรด A/B/C</span><span class="pill">LINE / Facebook</span><span class="pill">ปรับเองได้</span></div>
|
||||
</div>
|
||||
<div class="step reveal" style="--i:3">
|
||||
<h3 data-lang="th">พนักงานแชตฝึก + ได้โค้ช</h3>
|
||||
<h3 data-lang="en">Reps chat, then get coached</h3>
|
||||
<p data-lang="th">แชต 1:1 เหมือนคุยจริง ลูกค้าต่อรอง ถ่วง และไม่ยอมซื้อถ้ายังไม่แก้ปม หลังแต่ละข้อความและหลังจบเกม AI โค้ชบอกวิธีตอบให้ดีขึ้น พร้อมสรุปจุดอ่อน</p>
|
||||
<p data-lang="en">One-on-one chat, exactly like reality. Customers negotiate, stall and refuse until the pain is resolved. A judge AI coaches every weak turn and summarises your weak areas.</p>
|
||||
<div class="pills"><span class="pill">โค้ชต่อข้อความ</span><span class="pill">สรุปจุดอ่อน</span><span class="pill">ติดตามผล</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= CORE EXPERIENCE / WORKBENCH 1 ================= -->
|
||||
<section class="section section--tint" id="experience">
|
||||
<div class="container">
|
||||
<div class="shead reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">ประสบการณ์หลัก</span>
|
||||
<span class="shead__label" data-lang="en">The core moment</span>
|
||||
<h2 data-lang="th">บทสนทนาที่เหมือนจริง มากกว่าการโรลเพลย์กับเพื่อนร่วมงาน</h2>
|
||||
<h2 data-lang="en">Conversations that feel real — not role-play with a colleague</h2>
|
||||
</div>
|
||||
|
||||
<div class="frame reveal" style="--i:1">
|
||||
<div class="frame__media" aria-hidden="true">
|
||||
<div class="chat">
|
||||
<div class="chat__bar">
|
||||
<div class="chat__avatar" style="--i:0">กบ</div>
|
||||
<div class="chat__who">
|
||||
<div class="chat__name">คุณกบ — ผู้จัดการฝ่ายซื้อ</div>
|
||||
<div class="chat__meta" data-lang="th">เกรด B · ลังเล กำลังหาข้อมูล</div>
|
||||
<div class="chat__meta" data-lang="en">Tier B · unsure, researching</div>
|
||||
</div>
|
||||
<span class="chat__status"><span class="dot"></span><span data-lang="th">พิมพ์…</span><span data-lang="en">typing…</span></span>
|
||||
</div>
|
||||
<div class="chat__body">
|
||||
<div class="msg theirs">
|
||||
<div class="msg__bubble">ราคาแพงไปนี่นา ของคู่แข่งถูกกว่าตั้ง 20%</div>
|
||||
<span class="msg__time">10:02</span>
|
||||
</div>
|
||||
<div class="msg mine">
|
||||
<div class="msg__bubble">20% จริงๆ นะครับ… งั้นช่วยบอกหน่อยได้ไหมว่า ราคาต่าง 20% นั้น ตัดฟีเจอร์อะไรออกไปบ้างใจเราส่งให้ครบ</div>
|
||||
<span class="msg__time">10:03</span>
|
||||
</div>
|
||||
<div class="typing"><i></i><i></i><i></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frame__copy">
|
||||
<h3 data-lang="th">ลูกค้าทุกเกรด "ต่อรองได้" และ "แพ้ได้"</h3>
|
||||
<h3 data-lang="en">Every tier negotiates — and every tier can lose you</h3>
|
||||
<p data-lang="th">ลูกค้า A ที่พร้อมซื้อก็ยังเถียงเรื่องส่วนลด ลูกค้า C ที่บอกว่าไม่สนใจก็ยังมีปมที่แก้ได้ ความจริงคือการขายไม่มีบทที่เขียนไว้แล้ว — และเกมนี้ก็ไม่ได้การันตีชัยชนะให้ใคร</p>
|
||||
<p data-lang="en">Ready-to-buy A-tiers still haggle on discount. Not-interested C-tiers still hide a pain you could solve. Real selling has no script — and this game guarantees nobody a win.</p>
|
||||
<ul class="lines">
|
||||
<li>
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span>
|
||||
<span data-lang="th">เลเวอเรจต่อรอง: ราคา, ของแถม, รอบส่ง, ขอบเขตงาน, เงื่อนไขชำระ</span>
|
||||
<span data-lang="en">Negotiation levers: price, freebies, delivery, scope, payment</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span>
|
||||
<span data-lang="th">ลูกค้าอาจ "ดูเหมือนจะซื้อ" แล้วถอนใจ — เกรด C แบบพิเศษ</span>
|
||||
<span data-lang="en">Some persona appears to buy, stumbles, still carries pain</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span>
|
||||
<span data-lang="th">การเริ่มขายก็มีหลากหลาย — ลูกค้าอาจเปิดบท หรือให้พนักงานเปิดเอง (เช่น ประกัน)</span>
|
||||
<span data-lang="en">Outbound mode too — your rep opens the sale, like insurance</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= COACHING / WORKBENCH 2 ================= -->
|
||||
<section class="section" id="coach">
|
||||
<div class="container">
|
||||
<div class="frame frame--rev reveal" style="--i:0">
|
||||
<div class="frame__copy">
|
||||
<h3 data-lang="th">ไม่ใช่แค่คะแนน — คือโค้ชที่บอกว่า "ข้อนี้ควรตอบแบบไหน"</h3>
|
||||
<h3 data-lang="en">Not just a score — a coach that shows you how to answer</h3>
|
||||
<p data-lang="th">หลังจบแต่ละเกม AI ผู้ตัดสินแยกอิสระ (ไม่มีตัวช่วยเรื่องความเร็ว) จะสรุปว่าเกมแพ้หรือชนะ ให้โค้ชต่อข้อความอ่อนๆ พร้อมคำแนะนำ วิเคราะห์จุดอ่อนซ้ำๆ และกระดานชนะ/แพ้ เพื่อให้ผู้จัดการเห็นว่าใครปิดยากตรงไหน</p>
|
||||
<p data-lang="en">A separate judge LLM (no speed bias) scores each turn, then a coach explains how to improve the weak ones. You get a weak-area analysis and a win/lose board — so managers see exactly where reps struggle.</p>
|
||||
<ul class="lines">
|
||||
<li>
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span>
|
||||
<span data-lang="th">ค่าเฉลี่ย 71 ของบาร์ส่งของ → สร้างเกมซ้ำปมนั้นให้พนักงาน</span>
|
||||
<span data-lang="en">Persistent weak area → one-shot practice until it's fixed</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span>
|
||||
<span data-lang="th">ลูกค้าคนหนึ่งคุยได้ครั้งเดียว — จึงต้องตั้งใจจริง เหมือนบทจริง</span>
|
||||
<span data-lang="en">One persona, one shot — focus like a real lead</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span>
|
||||
<span data-lang="th">ธุรกิจของคุณเอง สร้างลูกค้าจำลองต่อยอดจากจุดอ่อนทีมได้</span>
|
||||
<span data-lang="en">Admins mint new personas from real weak areas</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="frame__media" aria-hidden="true">
|
||||
<div class="chat">
|
||||
<div class="chat__bar">
|
||||
<div class="chat__avatar" style="--i:0">ผ</div>
|
||||
<div class="chat__who"><div class="chat__name">รายงานเกมจบ</div><div class="chat__meta" data-lang="th">ยอดรวม 12 คน</div><div class="chat__meta" data-lang="en">12 personas</div></div>
|
||||
</div>
|
||||
<div class="chat__body">
|
||||
<div class="coach" style="max-width:100%">
|
||||
<div class="coach__head">
|
||||
<span class="coach__badge"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg> Debrief</span>
|
||||
<span class="coach__score" data-lang="th">ชนะ 8 · แพ้ 4</span>
|
||||
<span class="coach__score" data-lang="en">8 won · 4 lost</span>
|
||||
</div>
|
||||
<p class="coach__text" data-lang="th"><strong>จุดอ่อนซ้ำ: การรับมือข้อโต้แย้งเรื่องราคา</strong> — แพ้ 3 เกมจากปมราคา โดยเฉพาะเกรด C. ลองฝึกรอบใหม่เฉพาะบท "ลูกค้าถามราคาก่อน" ได้จากปุ่มด้านล่าง ข้อแนะนำ: ทวนราคาคู่แข่งก่อน แล้วชี้สิ่งที่ต่าง ไม่ใช่ลดราคาทันที</p>
|
||||
<p class="coach__text" data-lang="en"><strong>Recurring weak area: handling price objections.</strong> 3 of 4 losses came from price, mostly vs Tier C. Re-practice the "they ask price first" scenario. Tip: mirror their figure before reframing value — don't cave to discount.</p>
|
||||
<div class="chat__input" style="border:0;padding:var(--space-sm) 0 0;background:transparent">
|
||||
<span class="btn btn--accent" data-lang="th">ฝึกซ้ำปมราคา</span>
|
||||
<span class="btn btn--accent" data-lang="en">Re-practice price</span>
|
||||
</div>
|
||||
<div class="chat__input" style="border:0;padding:var(--space-sm) 0 0;background:transparent">
|
||||
<span class="btn btn--ghost" data-lang="th">ดูกระดานชนะ/แพ้ ของทีม</span>
|
||||
<span class="btn btn--ghost" data-lang="en">Team win/lose board</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= PERSONA TIERS ================= -->
|
||||
<section class="section section--tint" id="tiers">
|
||||
<div class="container">
|
||||
<div class="shead center reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">เกรดลูกค้า</span>
|
||||
<span class="shead__label" data-lang="en">Persona tiers</span>
|
||||
<h2 data-lang="th">15 ลูกค้าจำลอง แบ่งตามความพร้อมซื้อ</h2>
|
||||
<h2 data-lang="en">Fifteen personas, ranked by buying intent</h2>
|
||||
<p data-lang="th">ทุกเกรดมีคนที่แพ้ให้ได้ — ยิ่งเกรด C ยิ่งต้องแก้ปมก่อนจึงจะปิดได้</p>
|
||||
<p data-lang="en">Every tier can lose you. Tier C most of all — you must solve the pain before anything closes.</p>
|
||||
</div>
|
||||
<div class="tiers">
|
||||
<div class="tier reveal" style="--i:1">
|
||||
<div class="tier__letter">A</div>
|
||||
<h3 data-lang="th">พร้อมซื้อ</h3>
|
||||
<h3 data-lang="en">Ready to buy</h3>
|
||||
<p class="tier__sub" data-lang="th">มีปัญหาและกำลังตัดสินใจ แต่ก็ยังต่อรอง</p>
|
||||
<p class="tier__sub" data-lang="en">In pain and deciding — but still haggles.</p>
|
||||
<ul>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ต้องการส่วนลด ของแถม รอบส่ง</span><span data-lang="en">Wants discount, freebies, delivery</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ให้ปิดได้ถ้าขายเข้าปมและจัดการข้อต่อรอง</span><span data-lang="en">Closes if you hit the pain and handle the ask</span></li>
|
||||
</ul>
|
||||
<p class="tier__foot" data-lang="th">จำนวน: 5 คน</p>
|
||||
<p class="tier__foot" data-lang="en">Count: 5 personas</p>
|
||||
</div>
|
||||
<div class="tier reveal" style="--i:2">
|
||||
<div class="tier__letter">B</div>
|
||||
<h3 data-lang="th">ลังเล กำลังศึกษา</h3>
|
||||
<h3 data-lang="en">Unsure, researching</h3>
|
||||
<p class="tier__sub" data-lang="th">ยังไม่เชื่อ ยังเปรียบเทียบ ต้องการข้อมูลและความไว้วางใจ</p>
|
||||
<p class="tier__sub" data-lang="en">On the fence, comparing options, needs trust.</p>
|
||||
<ul>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ถามเยอะ อ้างคู่แข่ง อ้างงบ</span><span data-lang="en">Asks a lot, cites competitors and budget</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ต้องสร้างเหตุผลซื้อก่อน ไม่งั้นหดตัว</span><span data-lang="en">Needs a reason to buy or it stalls</span></li>
|
||||
</ul>
|
||||
<p class="tier__foot" data-lang="th">จำนวน: 5 คน</p>
|
||||
<p class="tier__foot" data-lang="en">Count: 5 personas</p>
|
||||
</div>
|
||||
<div class="tier tier--hardest reveal" style="--i:3">
|
||||
<div class="tier__letter">C</div>
|
||||
<h3 data-lang="th">ไม่สนใจ แต่มีปม (ยากสุด)</h3>
|
||||
<h3 data-lang="en">Not interested — but in pain</h3>
|
||||
<p class="tier__sub" data-lang="th">บอกว่าไม่ซื้อ แต่มีปัญหาที่แก้ได้ ถ้าเข้าไปถึงปม</p>
|
||||
<p class="tier__sub" data-lang="en">Says no, yet carries a solvable pain.</p>
|
||||
<ul>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">มี "ปมแดง" ที่ไม่เกี่ยวข้องโดยตรง — ต้องแยกให้ออก</span><span data-lang="en">Carries a red-herring pain — learn to tell them apart</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ผู้ขายต้องเปิดบท (outbound) — ใช้กับประกัน / B2B</span><span data-lang="en">Rep must open the sale — outbound selling</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ไม่แก้ปมก่อน = แพ้ ไม่ว่าขายเก่งแค่ไหน</span><span data-lang="en">Skip the pain and you lose, no matter how well you pitch</span></li>
|
||||
</ul>
|
||||
<p class="tier__foot" data-lang="th">จำนวน: 5 คน (รวมแบบ "ดูเหมือนซื้อแต่ถอน")</p>
|
||||
<p class="tier__foot" data-lang="en">Count: 5 (incl. the "nearly bought" one)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= WHY / VALUE PROPS ================= -->
|
||||
<section class="section" id="why">
|
||||
<div class="container">
|
||||
<div class="shead reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">ทำไมต้อง Sales Trainer</span>
|
||||
<span class="shead__label" data-lang="en">Why it works</span>
|
||||
<h2 data-lang="th">ที่ที่โรลเพลย์กับเพื่อนร่วมงานไปไม่ถึง</h2>
|
||||
<h2 data-lang="en">Where colleague role-play can't reach</h2>
|
||||
</div>
|
||||
<div class="props">
|
||||
<div class="prop reveal" style="--i:1">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m9 12 2 2 4-4"/></svg></span>
|
||||
<h3 data-lang="th">ลูกค้าจำลองมี "ปัญหาจริง"</h3>
|
||||
<h3 data-lang="en">Personas with real pains</h3>
|
||||
<p data-lang="th">ปัญหา ความลังเล และจุดต่อรอง มาจากข้อมูลสินค้าของคุณจริง ไม่ใช่บทที่ตายตัว พนักงานเจอสถานการณ์ที่ไม่มีทางลัด</p>
|
||||
<p data-lang="en">Pains, doubts and levers derive from your actual offer — no shortcut scripts, no canned responses.</p>
|
||||
</div>
|
||||
<div class="prop wide reveal" style="--i:2">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></span>
|
||||
<h3 data-lang="th">โค้ชต่อข้อความ + สรุปจบเกม — ไม่ใช่แค่คะแนน</h3>
|
||||
<h3 data-lang="en">Per-turn coaching + debrief — not just a number</h3>
|
||||
<p data-lang="th">AI ผู้ตัดสินจะชี้ว่าข้อไหนอ่อน แล้วบอกวิธีตอบให้ดีขึ้น พร้อมวิเคราะห์จุดอ่อนซ้ำและกระดานชนะ/แพ้ ให้ผู้จัดการเห็นภาพว่าใครฝึกอะไรต่อ</p>
|
||||
<p data-lang="en">A judge LLM flags weak turns and shows how to answer better, then surfaces recurring weak areas and a win/lose board for managers to act on.</p>
|
||||
</div>
|
||||
<div class="prop reveal" style="--i:3">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg></span>
|
||||
<h3 data-lang="th">โอกาสเดียว เหมือนลูกค้าจริง</h3>
|
||||
<h3 data-lang="en">One shot, like a real deal</h3>
|
||||
<p data-lang="th">ลูกค้าคนหนึ่งคุยได้ครั้งเดียว ความตั้งใจจึงเท่ากับการคุยกับลีดจริง — ยกระดับความมุ่งมั่นของทุกเกม</p>
|
||||
<p data-lang="en">Each persona can be chatted once — the attention of a real lead, and the stakes to match.</p>
|
||||
</div>
|
||||
<div class="prop wide reveal" style="--i:4">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg></span>
|
||||
<h3 data-lang="th">แดชบอร์ดผู้ดูแล — เห็นภาพรวมทั้งทีม</h3>
|
||||
<h3 data-lang="en">Admin analytics across the team</h3>
|
||||
<p data-lang="th">อัตราปิด คะแนนเฉลี่ย และ "ลูกค้าที่ปิดยากสุด" ต่อคนต่อทีม ผู้จัดการเห็นจุดแข็งจุดอ่อนก่อนประชุม ไม่ใช่หลังหมดเดือน</p>
|
||||
<p data-lang="en">Close rate, average score and the hardest personas per rep and per team — so managers see the gaps before the meeting, not after the month.</p>
|
||||
</div>
|
||||
<div class="prop reveal" style="--i:5">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></svg></span>
|
||||
<h3 data-lang="th">ปรับแต่งได้ ไม่ใช่กล่องปิด</h3>
|
||||
<h3 data-lang="en">Not a closed box</h3>
|
||||
<p data-lang="th">Admin แก้ไขลูกค้า เพิ่มลูกค้าจากจุดอ่อนทีม และสร้างเกมใหม่ได้ตามธุรกิจที่เปลี่ยนไป</p>
|
||||
<p data-lang="en">Admins hand-edit personas, mint new ones from weak areas, and spin up fresh scenarios as your business changes.</p>
|
||||
</div>
|
||||
<div class="prop reveal" style="--i:6">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span>
|
||||
<h3 data-lang="th">ไทย + อังกฤษ</h3>
|
||||
<h3 data-lang="en">Thai + English</h3>
|
||||
<p data-lang="th">ใช้ได้สองภาษา เหมาะกับทีมที่ขายผ่าน LINE / Facebook / โทรศัพท์ กับลูกค้าทั้งไทยและต่างชาติ</p>
|
||||
<p data-lang="en">Fully bilingual — for teams selling on LINE, Facebook and phone to Thai and international buyers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= WHO IT'S FOR ================= -->
|
||||
<section class="section section--tint" id="for">
|
||||
<div class="container">
|
||||
<div class="shead reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">เหมาะกับใคร</span>
|
||||
<span class="shead__label" data-lang="en">Who it's for</span>
|
||||
<h2 data-lang="th">สร้างขึ้นเพื่อคนที่ต้องรับผิดชอบผลการขาย</h2>
|
||||
<h2 data-lang="en">Built for the people accountable for sales results</h2>
|
||||
</div>
|
||||
<div class="aud">
|
||||
<div class="aud__card reveal" style="--i:1">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>
|
||||
<h3 data-lang="th">ผู้จัดการทีมขาย</h3>
|
||||
<h3 data-lang="en">Sales managers</h3>
|
||||
<p data-lang="th">อยากเห็นจุดอ่อนของทีมเป็นรายคน เลือกฝึกปมที่แพ้ซ้ำ และดันอัตราปิดขึ้นโดยไม่ต้องรอโรลเพลย์</p>
|
||||
<p data-lang="en">See per-rep weak spots, drill the losses, and lift close rate without waiting on role-play.</p>
|
||||
</div>
|
||||
<div class="aud__card reveal" style="--i:2">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg></span>
|
||||
<h3 data-lang="th">เทรนเนอร์ / ฝ่าย L&D</h3>
|
||||
<h3 data-lang="en">Trainers & L&D</h3>
|
||||
<p data-lang="th">ได้แบบฝึกหัดที่สเกลได้ ไม่ต้องจัดคู่คน ไม่ต้องเขียนบทใหม่ทุกครั้ง และวัดผลเป็นข้อมูลจริง</p>
|
||||
<p data-lang="en">A drill that scales — no pairing people up, no re-scripting, and outcomes measured as data.</p>
|
||||
</div>
|
||||
<div class="aud__card reveal" style="--i:3">
|
||||
<span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg></span>
|
||||
<h3 data-lang="th">สถาบัน / เอเจนซี่ที่ขาย B2B</h3>
|
||||
<h3 data-lang="en">Academies & B2B sellers</h3>
|
||||
<p data-lang="th">บริษัทที่ขายผ่าน LINE, Facebook หรือโทรศัพท์ ต้องการพนักงานหน้าใหม่ที่ปิดเป็นเร็ว ไม่ใช่เรียนรู้กับลูกค้าจริง</p>
|
||||
<p data-lang="en">Teams selling via LINE, Facebook or phone that want new reps closing fast — not learning on real customers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= PRICING ================= -->
|
||||
<section class="section" id="pricing">
|
||||
<div class="container">
|
||||
<div class="shead center reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">ราคา</span>
|
||||
<span class="shead__label" data-lang="en">Pricing</span>
|
||||
<h2 data-lang="th">แผนสำหรับทีมขาย</h2>
|
||||
<h2 data-lang="en">Plans for your team</h2>
|
||||
<p data-lang="th">ตัวเลขด้านล่างเป็นที่ยืนยันโดยเจ้าของ — กรุณาติดต่อเพื่อรับราคาจริง</p>
|
||||
<p data-lang="en">Amounts below await owner confirmation — contact us for the real figure.</p>
|
||||
</div>
|
||||
<div class="pricing">
|
||||
<div class="price reveal" style="--i:1">
|
||||
<div class="price__head">
|
||||
<div class="price__name" data-lang="th">ทีม</div>
|
||||
<div class="price__name" data-lang="en">Team</div>
|
||||
<div class="price__desc" data-lang="th">สำหรับทีมขาย SME ที่ต้องการเริ่มฝึกจริง</div>
|
||||
<div class="price__desc" data-lang="en">For SME sales teams ready to drill for real.</div>
|
||||
</div>
|
||||
<div class="price__amount">
|
||||
<span class="price__num">—<small> บาท</small></span>
|
||||
</div>
|
||||
<p class="price__per" data-lang="th">ต่อเดือน · รายเดือน / รายปี</p>
|
||||
<p class="price__per" data-lang="en">per month · monthly or yearly</p>
|
||||
<p class="price__confirm" data-lang="th">⏳ ราคาให้ยืนยันโดยเจ้าของ (placeholder)</p>
|
||||
<p class="price__confirm" data-lang="en">⏳ price to confirm by owner (placeholder)</p>
|
||||
<ul>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ผู้ใช้งานไม่จำกัดภายในทีม</span><span data-lang="en">Unlimited users in-team</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">สร้างผลิตภัณฑ์และลูกค้า 15 คน</span><span data-lang="en">Build products & the 15 personas</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">โค้ช + สรุปจุดอ่อน + กระดานชนะ/แพ้</span><span data-lang="en">Coaching, weak areas & win/lose board</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">ไทย + อังกฤษ</span><span data-lang="en">Thai + English</span></li>
|
||||
</ul>
|
||||
<a class="btn btn--ghost" href="#cta" data-lang="th">สอบถามราคา</a>
|
||||
<a class="btn btn--ghost" href="#cta" data-lang="en">Ask for price</a>
|
||||
<p class="price__pick" data-lang="th">เริ่มต้นได้จากการทดลอง / เดโม</p>
|
||||
<p class="price__pick" data-lang="en">Start with a demo or pilot</p>
|
||||
</div>
|
||||
<div class="price price--featured reveal" style="--i:2">
|
||||
<div class="price__head">
|
||||
<div class="price__name" data-lang="th">องค์กร</div>
|
||||
<div class="price__name" data-lang="en">Enterprise</div>
|
||||
<div class="price__desc" data-lang="th">สำหรับหลายทีม หลายผลิตภัณฑ์ ต้องการวิเคราะห์ลึกแบบ enterprise</div>
|
||||
<div class="price__desc" data-lang="en">Multiple teams & products with deep analytics.</div>
|
||||
</div>
|
||||
<div class="price__amount">
|
||||
<span class="price__num">—<small> บาท</small></span>
|
||||
</div>
|
||||
<p class="price__per" data-lang="th">รายปี · ต่อรองได้</p>
|
||||
<p class="price__per" data-lang="en">yearly · negotiable</p>
|
||||
<p class="price__confirm" data-lang="th">⏳ ราคาให้ยืนยันโดยเจ้าของ (placeholder)</p>
|
||||
<p class="price__confirm" data-lang="en">⏳ price to confirm by owner (placeholder)</p>
|
||||
<ul>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">หลายทีม / หลายผลิตภัณฑ์</span><span data-lang="en">Multiple teams & products</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">แดชบอร์ดผู้ดูแลขั้นสูง (อัตราปิด / ปมยาก)</span><span data-lang="en">Advanced admin analytics (close rate / hard pains)</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">on-premise / SSO / สนับสนุนเฉพาะ</span><span data-lang="en">On-premise / SSO / dedicated support</span></li>
|
||||
<li><span class="ic"><svg class="lucide" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z"/><path d="m7 13 3 3 7-7"/></svg></span><span data-lang="th">เทรนนิ่งออนบอร์ดสำหรับทีมของคุณ</span><span data-lang="en">Onboarding training for your team</span></li>
|
||||
</ul>
|
||||
<a class="btn btn--accent" href="#cta" data-lang="th">ขอใบเสนอราคา</a>
|
||||
<a class="btn btn--accent" href="#cta" data-lang="en">Request a quote</a>
|
||||
<p class="price__pick" data-lang="th">เหมาะสำหรับบริษัทที่ต้องการควบคุมข้อมูลในองค์กร</p>
|
||||
<p class="price__pick" data-lang="en">Best when you want data in-house</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= FAQ ================= -->
|
||||
<section class="section section--tint" id="faq">
|
||||
<div class="container">
|
||||
<div class="shead center reveal" style="--i:0">
|
||||
<span class="shead__label" data-lang="th">คำถามที่พบบ่อย</span>
|
||||
<span class="shead__label" data-lang="en">FAQ</span>
|
||||
<h2 data-lang="th">คำถามที่ผู้จัดการทีมขายมักถาม</h2>
|
||||
<h2 data-lang="en">Questions sales managers ask</h2>
|
||||
</div>
|
||||
<div class="faq">
|
||||
<details class="accordion" open>
|
||||
<summary class="accordion__head">
|
||||
<span data-lang="th">Sales Trainer ต่างจากโรลเพลย์กับเพื่อนร่วมงานยังไง?</span>
|
||||
<span data-lang="en">How is this different from role-playing with colleagues?</span>
|
||||
<span class="chev"><svg class="lucide" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span>
|
||||
</summary>
|
||||
<div class="accordion__panel"><div>
|
||||
<p class="accordion__body" data-lang="th">ลูกค้าร่วมงานรู้อยู่แล้วว่าคุณจะพูดอะไร และไม่อยากทำให้คุณเสียหน้า จึงไม่เคยผลักดัน. ที่นี่ลูกค้า AI มีปัญหาจริง ต่อรองจริง ปฏิเสธจริง และมีโค้ชให้คำแนะนำทุกข้อความ — ไม่ใช่แฟนเพลงของคุณ</p>
|
||||
<p class="accordion__body" data-lang="en">Colleagues already know what you'll say and don't want to bruise you — so they never push. AI customers have real pains, negotiate and refuse, and a coach gives feedback on every turn. No hard feelings, no favours.</p>
|
||||
</div></div>
|
||||
</details>
|
||||
<details class="accordion">
|
||||
<summary class="accordion__head">
|
||||
<span data-lang="th">ต้องเตรียมอะไรมาก่อนไหม?</span>
|
||||
<span data-lang="en">How much prep do we need?</span>
|
||||
<span class="chev"><svg class="lucide" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span>
|
||||
</summary>
|
||||
<div class="accordion__panel"><div>
|
||||
<p class="accordion__body" data-lang="th">แค่คำอธิบายสินค้า บริการ หรือไอเดียของคุณ (พิมพ์หรืออัปโหลด PDF/MD/TXT). AI สร้างลูกค้า 15 คนให้อัตโนมัติ แล้ว admin แก้ไขเพิ่มเติมได้ตามต้องการ</p>
|
||||
<p class="accordion__body" data-lang="en">Just a description of your product, service or idea (typed or uploaded as PDF/MD/TXT). AI builds the 15 personas; admins can then hand-tune them.</p>
|
||||
</div></div>
|
||||
</details>
|
||||
<details class="accordion">
|
||||
<summary class="accordion__head">
|
||||
<span data-lang="th">ทีมขาย B2B ที่ขายผ่าน LINE / Facebook / โทรศัพท์ใช้ได้ไหม?</span>
|
||||
<span data-lang="en">Does it suit teams selling on LINE / Facebook / phone?</span>
|
||||
<span class="chev"><svg class="lucide" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span>
|
||||
</summary>
|
||||
<div class="accordion__panel"><div>
|
||||
<p class="accordion__body" data-lang="th">ได้. ระบบออกแบบมาเพื่อการสนทนาขายแบบไทยจริง — ตั้งช่องทาง LINE/Facebook, รองรับ outbound (ผู้ขายเปิดบท) เหมือนการขายประกันหรือ B2B และใช้ได้สองภาษาไทย/อังกฤษ</p>
|
||||
<p class="accordion__body" data-lang="en">Yes. It's built for real Thai conversation selling — set the channel to LINE or Facebook, support outbound opens (like insurance or B2B), and work in both Thai and English.</p>
|
||||
</div></div>
|
||||
</details>
|
||||
<details class="accordion">
|
||||
<summary class="accordion__head">
|
||||
<span data-lang="th">ลูกค้า AI "แพ้ให้" พนักงานได้จริงไหม?</span>
|
||||
<span data-lang="en">Can the AI customer actually lose?</span>
|
||||
<span class="chev"><svg class="lucide" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span>
|
||||
</summary>
|
||||
<div class="accordion__panel"><div>
|
||||
<p class="accordion__body" data-lang="th">ได้และตั้งใจให้เป็นแบบนั้น. ทุกเกรดต่อรองและปฏิเสธได้; เกรด C แพ้ได้ถ้ายังไม่แก้ปม ที่สำคัญลูกค้าคนหนึ่งคุยได้ครั้งเดียว ทำให้เกมไม่มีทางลัดและผลคือจริงจัง</p>
|
||||
<p class="accordion__body" data-lang="en">Yes — by design. Every tier negotiates and can refuse; Tier C won't close until the pain is solved. And each persona can only be chatted once, so there's no gaming the system.</p>
|
||||
</div></div>
|
||||
</details>
|
||||
<details class="accordion">
|
||||
<summary class="accordion__head">
|
||||
<span data-lang="th">โค้ช AI ตัดสินแบบไหน?</span>
|
||||
<span data-lang="en">How is the AI judge / coach powered?</span>
|
||||
<span class="chev"><svg class="lucide" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span>
|
||||
</summary>
|
||||
<div class="accordion__panel"><div>
|
||||
<p class="accordion__body" data-lang="th">มีตัวตัดสิน (judge) แยกจากผู้เล่น ทำให้คะแนนไม่เอนเอียงด้วยความเร็ว แล้วโค้ชอีกระบบหนึ่งจะให้คำแนะนำวิธีตอบที่ดีกว่าในข้อที่อ่อน พร้อมสรุปจุดอ่อนซ้ำ</p>
|
||||
<p class="accordion__body" data-lang="en">A separate judge LLM scores each turn (no speed factor), then a coaching layer explains how to answer better on weak turns and surfaces recurring weak areas.</p>
|
||||
</div></div>
|
||||
</details>
|
||||
<details class="accordion">
|
||||
<summary class="accordion__head">
|
||||
<span data-lang="th">ติดตั้งที่ไหน? ข้อมูลของบริษัทปลอดภัยไหม?</span>
|
||||
<span data-lang="en">Where does it run? Is our data safe?</span>
|
||||
<span class="chev"><svg class="lucide" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span>
|
||||
</summary>
|
||||
<div class="accordion__panel"><div>
|
||||
<p class="accordion__body" data-lang="th">มีทั้ง SaaS และ on-premise สำหรับองค์กร. การเข้าถึงเป็นแบบหลายบทบาท (admin/user) ไม่มีการสมัครเอง — ควบคุมได้ว่าใครเห็นอะไร. ติดต่อเพื่อคุยรายละเอียดการจัดเก็บข้อมูลกับทีมเรา</p>
|
||||
<p class="accordion__body" data-lang="en">SaaS or on-premise for enterprises. Role-based access (admin/user) with no self-registration — you control who sees what. Contact us to discuss data residency for your org.</p>
|
||||
</div></div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= CTA ================= -->
|
||||
<section class="cta" id="cta">
|
||||
<div class="container">
|
||||
<div class="cta__panel reveal" style="--i:0">
|
||||
<h2 data-lang="th">ให้ทีมของคุณได้ฝึกปิดการขาย กับลูกค้าที่ปฏิเสธจริง</h2>
|
||||
<h2 data-lang="en">Let your team close against customers who really say no</h2>
|
||||
<p data-lang="th">ขอเดโม 30 นาที — เราจะพาคุณดูว่าสินค้าของคุณกลายเป็นลูกค้า 15 คนได้ยังไง และทีมของคุณจะได้โค้ชอะไรกลับมา</p>
|
||||
<p data-lang="en">Book a 30-minute demo. We'll show how your product becomes 15 real personas, and what coaching your team takes home.</p>
|
||||
<div class="cta__actions">
|
||||
<a class="btn btn--accent" href="mailto:hello@example.com" data-lang="th">ติดต่อขอเดโม</a>
|
||||
<a class="btn btn--accent" href="mailto:hello@example.com" data-lang="en">Request a demo</a>
|
||||
<a class="btn btn--ghost" href="mailto:hello@example.com" data-lang="th">hello@ตัวอย่าง.com</a>
|
||||
<a class="btn btn--ghost" href="mailto:hello@example.com" data-lang="en">hello@example.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ================= FOOTER (Ft5 statement) ================= -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="footer__statement" data-lang="th">ฝึกขายจนเป็นธรรมชาติ — ไม่ใช่แค่เรียนสูตร</p>
|
||||
<p class="footer__statement" data-lang="en">Practice until selling is <span class="u">natural</span>, not rehearsed.</p>
|
||||
<div class="footer__meta">
|
||||
<span class="footer__brand">
|
||||
<span class="nav__mark" aria-hidden="true"><svg class="lucide" viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg></span>
|
||||
Sales Trainer
|
||||
</span>
|
||||
<div class="footer__links">
|
||||
<a class="foot__link" href="#how" data-lang="th">วิธีใช้งาน</a><a class="foot__link" href="#how" data-lang="en">How it works</a>
|
||||
<a class="foot__link" href="#pricing" data-lang="th">ราคา</a><a class="foot__link" href="#pricing" data-lang="en">Pricing</a>
|
||||
<a class="foot__link" href="#faq" data-lang="th">คำถาม</a><a class="foot__link" href="#faq" data-lang="en">FAQ</a>
|
||||
</div>
|
||||
<span class="footer__copy">© 2026 Sales Trainer · <span data-lang="th">ฝึกขายให้เป็นธรรมชาติ กับลูกค้า AI ที่ต่อรองจริง</span><span data-lang="en">practice made real.</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
/* ---- language toggle (TH primary · EN secondary) ---- */
|
||||
(function () {
|
||||
// lang inits empty so the first apply() actually runs and hides the non-default language
|
||||
var lang = '';
|
||||
var root = document.documentElement;
|
||||
|
||||
function apply(next) {
|
||||
if (next === lang) return;
|
||||
lang = next;
|
||||
root.setAttribute('lang', next);
|
||||
root.setAttribute('data-page-lang', next);
|
||||
// toggle every [data-lang] element
|
||||
document.querySelectorAll('[data-lang]').forEach(function (el) {
|
||||
var on = el.getAttribute('data-lang') === lang;
|
||||
el.style.display = on ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('[data-lang-btn]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', b.getAttribute('data-lang-btn') === lang ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('[data-lang-btn]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { apply(btn.getAttribute('data-lang-btn')); });
|
||||
});
|
||||
apply('th');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
424
website/main.css
Normal file
424
website/main.css
Normal file
@@ -0,0 +1,424 @@
|
||||
/* Hallmark · genre: modern-minimal · macrostructure: Workbench · theme: Coral · enrichment: Tier-A CSS chat mockup · nav: N5 Floating pill · footer: Ft5 Statement
|
||||
* pre-emit critique: P5 H4 E5 S4 R5 V5
|
||||
* audience = Thai sales managers / trainers / L&D (SME + enterprise) · use = request a demo / buy · tone = professional, warm, builder */
|
||||
:root {
|
||||
/* ---- colour (Coral: warm-grey paper band L>85% · coral accent hue ~25 · roman-ish sans) ---- */
|
||||
--color-paper: oklch(97% 0.012 60);
|
||||
--color-paper-2: oklch(94% 0.014 60);
|
||||
--color-rule: oklch(85% 0.012 60);
|
||||
--color-neutral: oklch(56% 0.008 55);
|
||||
--color-muted: oklch(42% 0.008 50);
|
||||
--color-ink: oklch(20% 0.010 45);
|
||||
--color-accent: oklch(58% 0.20 30); /* coral */
|
||||
--color-accent-2: oklch(70% 0.16 35);
|
||||
--color-focus: oklch(55% 0.20 28);
|
||||
--color-success: oklch(50% 0.12 150);
|
||||
--color-danger: oklch(52% 0.16 25);
|
||||
|
||||
/* ---- type (Sarabun — user brand: TH+EN body & headers) ---- */
|
||||
--font-body: "Sarabun", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display: "Sarabun", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-outlier: "Sarabun", ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
--text-xs: 0.72rem;
|
||||
--text-sm: 0.85rem;
|
||||
--text-base: 1rem;
|
||||
--text-md: 1.2rem;
|
||||
--text-lg: 1.45rem;
|
||||
--text-xl: 1.8rem;
|
||||
--text-2xl: 2.2rem;
|
||||
--text-3xl: 2.8rem;
|
||||
--text-display: clamp(2.5rem, 5vw + 1rem, 4.4rem);
|
||||
--text-display-s: clamp(2rem, 4vw + 0.75rem, 3.2rem);
|
||||
|
||||
/* ---- space (4pt scale) ---- */
|
||||
--space-3xs: 0.125rem;
|
||||
--space-2xs: 0.25rem;
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 0.75rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2.5rem;
|
||||
--space-2xl: 4rem;
|
||||
--space-3xl: 6rem;
|
||||
--space-4xl: 9rem;
|
||||
|
||||
/* ---- motion ---- */
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-in: cubic-bezier(0.7, 0, 0.84, 0);
|
||||
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--dur-micro: 120ms;
|
||||
--dur-short: 220ms;
|
||||
--dur-long: 420ms;
|
||||
|
||||
/* ---- depth + z ---- */
|
||||
--z-base: 1;
|
||||
--z-raised: 10;
|
||||
--z-sticky: 200;
|
||||
--z-modal: 400;
|
||||
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1.25rem;
|
||||
--radius-pill: 999px;
|
||||
}
|
||||
|
||||
/* ================= base ================= */
|
||||
* { box-sizing: border-box; }
|
||||
html { overflow-x: clip; }
|
||||
body { overflow-x: clip; margin: 0; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.6;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-paper);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
img { max-width: 100%; display: block; }
|
||||
a { color: inherit; }
|
||||
h1,h2,h3,h4 { font-family: var(--font-display); line-height: 1.14; letter-spacing: -0.01em; margin: 0; font-style: normal; }
|
||||
p { margin: 0; }
|
||||
ul { margin: 0; padding: 0; list-style: none; }
|
||||
:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 2px; border-radius: 2px; }
|
||||
::selection { background: var(--color-accent-2); color: var(--color-ink); }
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 72rem;
|
||||
margin-inline: auto;
|
||||
padding-inline: clamp(1rem, 4vw, 2rem);
|
||||
}
|
||||
|
||||
/* single-line affordances — note: .accordion__head is NOT here, its question text must wrap */
|
||||
.btn, .nav__link, .foot__link, .lang__btn { white-space: nowrap; }
|
||||
|
||||
/* ================= buttons ================= */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.01em;
|
||||
padding: 0.875rem 1.4rem;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: transform var(--dur-micro) var(--ease-out), background-color var(--dur-short) var(--ease-out), border-color var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out);
|
||||
}
|
||||
.btn--primary { background: var(--color-ink); color: var(--color-paper); }
|
||||
.btn--primary:hover { transform: translateY(-1px); background: var(--color-accent); color: var(--color-ink); }
|
||||
.btn--primary:active { transform: translateY(0); }
|
||||
.btn--ghost { border-color: var(--color-rule); background: transparent; color: var(--color-ink); }
|
||||
.btn--ghost:hover { transform: translateY(-1px); border-color: var(--color-accent); color: var(--color-accent); }
|
||||
.btn--ghost:active { transform: translateY(0); }
|
||||
.btn--accent { background: var(--color-accent); color: var(--color-ink); }
|
||||
.btn--accent:hover { transform: translateY(-1px); background: var(--color-ink); color: var(--color-paper); }
|
||||
.btn--accent:active { transform: translateY(0); }
|
||||
@media (pointer: coarse) { .btn { min-height: 48px; } }
|
||||
|
||||
/* ================= language toggle ================= */
|
||||
.lang {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--color-rule);
|
||||
border-radius: var(--radius-pill);
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklch, var(--color-paper) 80%, transparent);
|
||||
}
|
||||
.lang__btn {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out);
|
||||
}
|
||||
.lang__btn:hover { color: var(--color-ink); }
|
||||
.lang__btn[aria-pressed="true"] { background: var(--color-ink); color: var(--color-paper); }
|
||||
.lang__btn[aria-pressed="true"]:hover { color: var(--color-paper); }
|
||||
|
||||
/* ================= nav (N5 floating pill) ================= */
|
||||
.nav {
|
||||
position: sticky;
|
||||
top: var(--space-sm);
|
||||
z-index: var(--z-sticky);
|
||||
padding-inline: clamp(1rem, 4vw, 2rem);
|
||||
}
|
||||
.nav__inner {
|
||||
max-width: 68rem;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: color-mix(in oklch, var(--color-paper) 82%, transparent);
|
||||
backdrop-filter: blur(14px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(140%);
|
||||
border: 1px solid var(--color-rule);
|
||||
border-radius: var(--radius-pill);
|
||||
box-shadow: 0 1px 2px oklch(20% 0.01 45 / 0.05);
|
||||
}
|
||||
.nav__brand { display: flex; align-items: center; gap: var(--space-xs); font-family: var(--font-display); font-weight: 700; letter-spacing: -0.01em; text-decoration: none; font-size: 1.05rem; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nav__mark { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 8px; background: var(--color-accent); color: var(--color-ink); flex: 0 0 auto; }
|
||||
.nav__mark svg { width: 15px; height: 15px; }
|
||||
.nav__links { display: flex; align-items: center; gap: var(--space-lg); }
|
||||
.nav__link { font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); color: var(--color-muted); text-decoration: none; transition: color var(--dur-short) var(--ease-out); }
|
||||
.nav__link:hover { color: var(--color-accent); }
|
||||
.nav__cta { display: flex; align-items: center; gap: var(--space-sm); min-width: 0; }
|
||||
.nav__cta .lang { flex: 0 0 auto; }
|
||||
/* nav links only below 64rem — keeps brand + lang + CTA a single safe line on tablets/phones */
|
||||
@media (max-width: 64rem) {
|
||||
.nav__links { display: none; }
|
||||
}
|
||||
@media (max-width: 40rem) {
|
||||
/* drop the nav CTA text button on phones — the hero already carries it; keep brand + lang */
|
||||
.nav__cta .btn { display: none; }
|
||||
.nav__brand { font-size: 0.95rem; }
|
||||
.lang__btn { padding: 0.4rem 0.7rem; }
|
||||
}
|
||||
|
||||
/* ================= hero (workbench open) ================= */
|
||||
.hero { padding-top: clamp(3rem, 8vw, 6rem); padding-bottom: var(--space-3xl); position: relative; }
|
||||
.hero__grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: var(--space-3xl); align-items: center; }
|
||||
.hero__tag {
|
||||
display: inline-flex; align-items: center; gap: var(--space-xs);
|
||||
font-family: var(--font-display); font-weight: 600; font-size: var(--text-xs);
|
||||
letter-spacing: 0.04em; text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
border: 1px solid color-mix(in oklch, var(--color-accent) 40%, var(--color-rule));
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 0.35rem 0.8rem; margin-bottom: var(--space-md);
|
||||
}
|
||||
.hero h1 { font-size: var(--text-display); font-weight: 700; overflow-wrap: anywhere; min-width: 0; margin-bottom: var(--space-lg); line-height: 1.16; }
|
||||
/* per-word underline that wraps cleanly (box-decoration-break stays with each fragment) */
|
||||
.hero h1 .u {
|
||||
text-decoration: none;
|
||||
background-image: linear-gradient(var(--color-accent), var(--color-accent));
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 0.13em;
|
||||
background-position: 0 100%;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
padding-bottom: 0.06em;
|
||||
}
|
||||
.hero__lede { font-size: var(--text-md); color: var(--color-muted); max-width: 46ch; margin-bottom: var(--space-xl); }
|
||||
.hero__lede strong { color: var(--color-ink); }
|
||||
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-md); align-items: center; }
|
||||
.hero__note { margin-top: var(--space-lg); font-size: var(--text-xs); color: var(--color-neutral); }
|
||||
|
||||
/* hero chat preview (small, floating behind) */
|
||||
.hero__visual { position: relative; }
|
||||
.hero__phone-glow { position: absolute; inset: 8% -4% -8% -4%; background: radial-gradient(120% 90% at 70% 20%, oklch(0.85 0.13 40 / 0.5), transparent 70%); filter: blur(30px); z-index: 0; }
|
||||
@media (max-width: 60rem) {
|
||||
.hero__grid { grid-template-columns: 1fr; gap: var(--space-2xl); }
|
||||
.hero__visual { order: 2; }
|
||||
}
|
||||
|
||||
/* ================= chat mockup (Tier-A pure CSS) ================= */
|
||||
.chat {
|
||||
--chat-max: 560px;
|
||||
width: 100%;
|
||||
max-width: var(--chat-max);
|
||||
margin-inline: auto;
|
||||
background: var(--color-paper-2);
|
||||
border: 1px solid var(--color-rule);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 2px oklch(20% 0.01 45 / 0.05), 0 20px 50px -30px oklch(20% 0.05 45 / 0.35);
|
||||
z-index: 1; position: relative;
|
||||
}
|
||||
.chat__bar { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-md) var(--space-lg); border-bottom: 1px solid var(--color-rule); background: var(--color-paper); }
|
||||
.chat__avatar { width: 34px; height: 34px; border-radius: 50%; background: var(--color-accent-2); display: grid; place-items: center; color: var(--color-ink); font-weight: 700; font-size: var(--text-xs); flex: 0 0 auto; }
|
||||
.chat__who { line-height: 1.25; }
|
||||
.chat__name { font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm); }
|
||||
.chat__meta { font-size: var(--text-xs); color: var(--color-neutral); }
|
||||
.chat__status { margin-left: auto; display: inline-flex; align-items: center; gap: 0.4em; font-size: var(--text-xs); color: var(--color-neutral); }
|
||||
.chat__status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-success); }
|
||||
.chat__tier { font-size: var(--text-xs); font-family: var(--font-display); font-weight: 700; letter-spacing: 0.04em; color: var(--color-muted); border: 1px solid var(--color-rule); border-radius: var(--radius-sm); padding: 0.1rem 0.45rem; }
|
||||
.chat__tier--C { color: var(--color-danger); border-color: color-mix(in oklch, var(--color-danger) 40%, var(--color-rule)); }
|
||||
|
||||
.chat__body { padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-sm); }
|
||||
.msg { display: flex; gap: var(--space-xs); max-width: 86%; }
|
||||
.msg.mine { align-self: flex-end; flex-direction: row-reverse; }
|
||||
.msg.bubble-only { align-self: flex-end; }
|
||||
.msg__bubble { padding: var(--space-sm) var(--space-md); border-radius: var(--radius-md); font-size: var(--text-sm); line-height: 1.5; }
|
||||
.msg.theirs .msg__bubble { background: #fff; border: 1px solid var(--color-rule); border-top-left-radius: 4px; color: var(--color-ink); }
|
||||
.msg.mine .msg__bubble { background: var(--color-ink); color: var(--color-paper); border-top-right-radius: 4px; }
|
||||
.msg__time { font-size: var(--text-xs); color: var(--color-neutral); align-self: flex-end; padding: 0 0.3rem; }
|
||||
|
||||
/* coaching reveal card */
|
||||
.coach { align-self: flex-end; max-width: 92%; border: 1px solid var(--color-rule); background: #fff; border-radius: var(--radius-md); padding: var(--space-md); box-shadow: 0 1px 2px oklch(20% 0.01 45 / 0.05); }
|
||||
.coach__head { display: flex; align-items: center; gap: var(--space-xs); margin-bottom: var(--space-xs); }
|
||||
.coach__badge { display: inline-flex; align-items: center; gap: 0.35em; font-family: var(--font-display); font-weight: 700; font-size: var(--text-xs); letter-spacing: 0.03em; color: var(--color-accent); }
|
||||
.coach__score { margin-left: auto; font-family: var(--font-display); font-weight: 700; font-size: var(--text-xs); color: var(--color-muted); }
|
||||
.coach__text { font-size: var(--text-sm); color: var(--color-muted); line-height: 1.55; }
|
||||
.coach__text strong { color: var(--color-ink); }
|
||||
.coach .btn--ghost { margin-top: var(--space-sm); padding: 0.5rem 0.9rem; font-size: var(--text-xs); }
|
||||
|
||||
/* typing indicator */
|
||||
.typing { display: inline-flex; gap: 4px; padding: var(--space-sm) var(--space-md); background: #fff; border: 1px solid var(--color-rule); border-radius: var(--radius-md); border-top-left-radius: 4px; }
|
||||
.typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--color-neutral); animation: blink 1.3s infinite ease-in-out; }
|
||||
.typing i:nth-child(2){ animation-delay: 0.2s; }
|
||||
.typing i:nth-child(3){ animation-delay: 0.4s; }
|
||||
@keyframes blink { 0%, 60%, 100% { opacity: 0.25; } 30% { opacity: 1; } }
|
||||
|
||||
.chat__input { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-md) var(--space-lg); border-top: 1px solid var(--color-rule); background: var(--color-paper); flex-wrap: wrap; }
|
||||
.chat__input .field { flex: 1; }
|
||||
.chat__input .send { width: 40px; height: 40px; border-radius: 50%; border: 0; background: var(--color-accent); color: var(--color-ink); display: grid; place-items: center; cursor: pointer; flex: 0 0 auto; }
|
||||
|
||||
/* ================= section heads ================= */
|
||||
.section { padding-block: var(--space-3xl); }
|
||||
.section--tint { background: var(--color-paper-2); }
|
||||
.shead { max-width: 40rem; margin-bottom: var(--space-2xl); }
|
||||
.shead.center { margin-inline: auto; text-align: center; }
|
||||
.shead__label { font-family: var(--font-display); font-weight: 600; font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-accent); margin-bottom: var(--space-sm); }
|
||||
.shead h2 { font-size: var(--text-2xl); font-weight: 700; margin-bottom: var(--space-sm); overflow-wrap: anywhere; }
|
||||
.shead p { color: var(--color-muted); font-size: var(--text-md); }
|
||||
|
||||
/* ================= steps (F4) ================= */
|
||||
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-xl); counter-reset: step; }
|
||||
.step { position: relative; padding-top: var(--space-xl); border-top: 1px solid var(--color-rule); }
|
||||
.step::before {
|
||||
counter-increment: step; content: "0" counter(step);
|
||||
position: absolute; top: -0.7rem; left: 0;
|
||||
font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm);
|
||||
letter-spacing: 0.04em; color: var(--color-accent);
|
||||
background: var(--color-paper); padding-right: var(--space-sm);
|
||||
}
|
||||
.step h3 { font-size: var(--text-lg); margin-bottom: var(--space-sm); }
|
||||
.step p { color: var(--color-muted); font-size: var(--text-sm); max-width: 32ch; }
|
||||
.step .pills { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-md); }
|
||||
.pill { font-family: var(--font-display); font-weight: 600; font-size: var(--text-xs); color: var(--color-muted); background: var(--color-paper-2); border: 1px solid var(--color-rule); border-radius: var(--radius-pill); padding: 0.3rem 0.7rem; }
|
||||
@media (max-width: 60rem) { .steps { grid-template-columns: 1fr; gap: var(--space-2xl); } .step::before { background: var(--color-paper-2); } }
|
||||
|
||||
/* ================= workbench frames ================= */
|
||||
.frame { display: grid; grid-template-columns: minmax(0, 1fr) 1fr; gap: var(--space-3xl); align-items: center; }
|
||||
.frame--rev .frame__media { order: 2; }
|
||||
.frame--rev .frame__copy { order: 1; }
|
||||
.frame__media { position: relative; min-width: 0; }
|
||||
.frame__copy h3 { font-size: var(--text-xl); margin-bottom: var(--space-md); }
|
||||
.frame__copy p { color: var(--color-muted); max-width: 48ch; font-size: var(--text-md); }
|
||||
.frame__copy p + p { margin-top: var(--space-md); }
|
||||
.frame__copy ul.lines { margin-top: var(--space-lg); display: grid; gap: var(--space-sm); }
|
||||
.frame__copy ul.lines li { display: flex; gap: var(--space-sm); align-items: flex-start; }
|
||||
.frame__copy ul.lines .ic { color: var(--color-accent); flex: 0 0 auto; margin-top: 2px; }
|
||||
@media (max-width: 60rem) {
|
||||
.frame { grid-template-columns: 1fr; gap: var(--space-xl); }
|
||||
.frame--rev .frame__media { order: 1; } .frame--rev .frame__copy { order: 2; }
|
||||
}
|
||||
|
||||
/* ================= persona tiers ================= */
|
||||
.tiers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-lg); }
|
||||
.tier { border: 1px solid var(--color-rule); border-radius: var(--radius-lg); padding: var(--space-xl); background: var(--color-paper); display: flex; flex-direction: column; }
|
||||
.tier__letter { font-family: var(--font-display); font-weight: 700; font-size: var(--text-3xl); line-height: 1; color: var(--color-accent); }
|
||||
.tier h3 { font-size: var(--text-lg); margin: var(--space-xs) 0; }
|
||||
.tier__sub { font-size: var(--text-sm); color: var(--color-muted); margin-bottom: var(--space-md); }
|
||||
.tier ul { display: grid; gap: var(--space-sm); }
|
||||
.tier ul li { display: flex; gap: var(--space-sm); font-size: var(--text-sm); color: var(--color-muted); align-items: flex-start; }
|
||||
.tier ul li .ic { color: var(--color-accent); flex: 0 0 auto; margin-top: 2px; }
|
||||
.tier__foot { margin-top: var(--space-lg); padding-top: var(--space-md); border-top: 1px solid var(--color-rule); font-size: var(--text-xs); color: var(--color-neutral); }
|
||||
.tier--hardest { border-color: color-mix(in oklch, var(--color-danger) 35%, var(--color-rule)); }
|
||||
.tier--hardest .tier__letter { color: var(--color-danger); }
|
||||
.tier--hardest .tier__foot { color: var(--color-danger); }
|
||||
@media (max-width: 60rem) { .tiers { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ================= value props (asymmetric, 12-col) ================= */
|
||||
.props { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--space-lg); }
|
||||
.prop { grid-column: span 4; border-top: 1px solid var(--color-rule); padding-top: var(--space-lg); }
|
||||
.prop.wide { grid-column: span 6; }
|
||||
.prop .ic { color: var(--color-accent); margin-bottom: var(--space-md); }
|
||||
.prop h3 { font-size: var(--text-lg); margin-bottom: var(--space-sm); }
|
||||
.prop p { color: var(--color-muted); font-size: var(--text-sm); max-width: 40ch; }
|
||||
@media (max-width: 60rem) { .prop, .prop.wide { grid-column: span 12; } }
|
||||
|
||||
/* ================= who it's for (F6-ish product card grid) ================= */
|
||||
.aud { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-lg); }
|
||||
.aud__card { border: 1px solid var(--color-rule); border-radius: var(--radius-lg); padding: var(--space-xl); background: var(--color-paper); }
|
||||
.aud__card .ic { color: var(--color-accent); margin-bottom: var(--space-md); }
|
||||
.aud__card h3 { font-size: var(--text-lg); margin-bottom: var(--space-sm); }
|
||||
.aud__card p { color: var(--color-muted); font-size: var(--text-sm); }
|
||||
@media (max-width: 60rem) { .aud { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ================= pricing ================= */
|
||||
.pricing { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-xl); align-items: stretch; }
|
||||
.price { border: 1px solid var(--color-rule); border-radius: var(--radius-lg); padding: var(--space-xl); background: var(--color-paper); display: flex; flex-direction: column; }
|
||||
.price--featured { border: 2px solid var(--color-accent); }
|
||||
.price__head { margin-bottom: var(--space-lg); }
|
||||
.price__name { font-family: var(--font-display); font-weight: 700; font-size: var(--text-md); margin-bottom: var(--space-2xs); }
|
||||
.price__desc { font-size: var(--text-sm); color: var(--color-muted); }
|
||||
.price__amount { display: flex; align-items: baseline; gap: var(--space-xs); margin: var(--space-lg) 0 var(--space-2xs); }
|
||||
.price__num { font-family: var(--font-display); font-weight: 700; font-size: var(--text-3xl); }
|
||||
.price__num small { font-size: var(--text-sm); font-weight: 600; color: var(--color-muted); }
|
||||
.price__per { font-size: var(--text-xs); color: var(--color-neutral); }
|
||||
.price__confirm { font-size: var(--text-xs); color: var(--color-neutral); border: 1px dashed var(--color-rule); border-radius: var(--radius-sm); padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-lg); background: color-mix(in oklch, var(--color-paper-2) 50%, transparent); }
|
||||
.price ul { display: grid; gap: var(--space-sm); margin-bottom: var(--space-lg); }
|
||||
.price ul li { display: flex; gap: var(--space-sm); font-size: var(--text-sm); color: var(--color-muted); align-items: flex-start; }
|
||||
.price ul li .ic { color: var(--color-accent); flex: 0 0 auto; margin-top: 2px; }
|
||||
.price .btn { margin-top: auto; }
|
||||
.price__pick { text-align: center; font-size: var(--text-xs); color: var(--color-neutral); margin-top: var(--space-md); }
|
||||
@media (max-width: 40rem) { .pricing { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ================= FAQ ================= */
|
||||
.faq { max-width: 46rem; margin-inline: auto; display: grid; gap: var(--space-sm); }
|
||||
.accordion { border: 1px solid var(--color-rule); border-radius: var(--radius-md); background: var(--color-paper); overflow: hidden; }
|
||||
.accordion__head {
|
||||
width: 100%; display: flex; align-items: center; gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-lg); background: transparent; border: 0; text-align: left;
|
||||
cursor: pointer; font-family: var(--font-display); font-weight: 600; font-size: var(--text-base); color: var(--color-ink);
|
||||
}
|
||||
.accordion__head > span:first-of-type { flex: 1 1 auto; min-width: 0; }
|
||||
.accordion__head .chev { color: var(--color-accent); transition: transform var(--dur-short) var(--ease-in-out); flex: 0 0 auto; }
|
||||
.accordion__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-long) var(--ease-in-out); }
|
||||
.accordion__panel > div { overflow: hidden; }
|
||||
.accordion__body { padding: 0 var(--space-lg) var(--space-md); color: var(--color-muted); font-size: var(--text-sm); max-width: 60ch; }
|
||||
.accordion[open] .accordion__panel { grid-template-rows: 1fr; }
|
||||
.accordion[open] .chev { transform: rotate(180deg); }
|
||||
.accordion__head::-webkit-details-marker { display: none; }
|
||||
|
||||
/* ================= CTA ================= */
|
||||
.cta { padding-block: var(--space-3xl); }
|
||||
.cta__panel { position: relative; text-align: center; padding: var(--space-3xl) var(--space-xl); border-radius: var(--radius-lg); background: var(--color-ink); color: var(--color-paper); overflow: hidden; }
|
||||
.cta__panel::before { content: ""; position: absolute; inset: -20% -30% -20% -30%; background: radial-gradient(60% 60% at 60% 30%, oklch(0.7 0.16 40 / 0.4), transparent 70%); z-index: 0; }
|
||||
.cta__panel > * { position: relative; z-index: 1; }
|
||||
.cta__panel h2 { font-size: var(--text-2xl); margin-bottom: var(--space-md); overflow-wrap: anywhere; }
|
||||
.cta__panel p { color: color-mix(in oklch, var(--color-paper) 75%, transparent); max-width: 46ch; margin: 0 auto var(--space-xl); }
|
||||
.cta__actions { display: flex; flex-wrap: wrap; gap: var(--space-md); justify-content: center; }
|
||||
.cta__actions .btn--accent { background: var(--color-accent); color: var(--color-ink); }
|
||||
.cta__actions .btn--ghost { border-color: color-mix(in oklch, var(--color-paper) 40%, transparent); color: var(--color-paper); }
|
||||
.cta__actions .btn--ghost:hover { color: var(--color-ink); background: var(--color-paper); border-color: var(--color-paper); }
|
||||
|
||||
/* ================= footer (Ft5 statement) ================= */
|
||||
.footer { padding-block: var(--space-3xl) var(--space-xl); }
|
||||
.footer__statement { font-family: var(--font-display); font-weight: 700; font-size: var(--text-xl); max-width: 20ch; line-height: 1.25; margin-bottom: var(--space-xl); overflow-wrap: anywhere; }
|
||||
.footer__statement .u { color: var(--color-accent); }
|
||||
.footer__meta { display: flex; flex-wrap: wrap; gap: var(--space-md) var(--space-2xl); align-items: center; justify-content: space-between; padding-top: var(--space-lg); border-top: 1px solid var(--color-rule); }
|
||||
.footer__brand { font-family: var(--font-display); font-weight: 700; display: flex; align-items: center; gap: var(--space-xs); }
|
||||
.footer__links { display: flex; gap: var(--space-lg); }
|
||||
.foot__link { font-size: var(--text-sm); color: var(--color-muted); text-decoration: none; }
|
||||
.foot__link:hover { color: var(--color-accent); }
|
||||
.footer__copy { font-size: var(--text-xs); color: var(--color-neutral); }
|
||||
@media (max-width: 40rem) { .footer__meta { flex-direction: column; align-items: flex-start; } }
|
||||
|
||||
/* ================= reveal ================= */
|
||||
.reveal { opacity: 0; transform: translateY(10px); animation: reveal var(--dur-long) var(--ease-out) forwards; animation-delay: calc(var(--i, 0) * 60ms); }
|
||||
@keyframes reveal { to { opacity: 1; transform: none; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation-duration: 150ms !important; animation-iteration-count: 1 !important; transition-duration: 150ms !important; }
|
||||
.reveal { animation: reveal-reduced 150ms linear forwards; }
|
||||
@keyframes reveal-reduced { to { opacity: 1; transform: none; } }
|
||||
}
|
||||
|
||||
/* ---- rows of inline SVG line icons (lucide-style, single colour) ---- */
|
||||
svg.lucide { width: 1em; height: 1em; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -0.12em; }
|
||||
.ic svg.lucide { width: 22px; height: 22px; }
|
||||
Reference in New Issue
Block a user