Compare commits

..

34 Commits

Author SHA1 Message Date
SeekingGamer
f3ee755f47 fix(docs): Update Readme.md
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 29s
2026-05-21 10:19:30 +08:00
SeekingGamer
f8d97f46c5 Update Base.astro with new js
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 31s
2026-05-21 10:15:47 +08:00
SeekingGamer
edec5370b6 fix(style): Change IOS buttons style
Some checks failed
Deploy to talkpro / build-and-sync (push) Has been cancelled
2026-05-20 16:17:12 +08:00
08699e6d0d Merge pull request 'fix(style): fix gaps and pop up screen ui' (#13) from finn-staging into main
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 35s
Reviewed-on: #13
2026-05-20 06:37:35 +00:00
SeekingGamer
74793fbc11 fix(style): fix gaps and pop up screen ui 2026-05-20 11:36:04 +08:00
1de7b09ceb Merge pull request 'fix(docs): Update Translation ts' (#12) from finn-staging into main
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 28s
Reviewed-on: #12
2026-05-19 03:31:02 +00:00
SeekingGamer
a8229e543e fix(docs): Update Translation ts 2026-05-19 11:30:10 +08:00
b34e4b8538 Merge pull request 'fix(style):Removed the site link in Downloads astro' (#11) from finn-staging into main
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 29s
Reviewed-on: #11
2026-05-19 03:00:49 +00:00
SeekingGamer
37055ca74a fix(style):Removed the site link in Downloads astro 2026-05-19 10:59:27 +08:00
9f58001a56 Merge pull request 'finn-staging' (#10) from finn-staging into main
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 33s
Reviewed-on: #10
2026-05-19 02:55:57 +00:00
SeekingGamer
2a2d5fb9e5 fix(feat): Change styling for responsive layout 2026-05-19 10:51:38 +08:00
SeekingGamer
0220aa5ff8 fix(style): compact reliability row at 1024 2026-05-18 16:03:09 +08:00
SeekingGamer
7f7b44415e fix(style): keep reliability grid at tablet widths 2026-05-18 15:58:05 +08:00
265a867602 Merge pull request 'fix(asset): bust cached public assets' (#9) from finn-staging into main
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 37s
Reviewed-on: #9
2026-05-18 07:42:03 +00:00
SeekingGamer
61ef7c14de fix(asset): bust cached public assets 2026-05-18 15:39:38 +08:00
b8103ea072 1
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 27s
2026-05-18 15:23:09 +08:00
a481c382a6 CI deploy without sudo: tar over ssh when rsync missing
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 33s
Remove apt/sudo install step (act runners lack sudo). Use rsync when
available; otherwise clear remote web root and stream dist/ via tar|ssh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:19:16 +08:00
5f9e8db7e5 CI: install rsync and openssh-client before deploy
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 11s
Gitea/act runners often lack rsync; apt/apk/dnf install step runs
before ssh/rsync in deploy-talkpro.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:17:13 +08:00
fe14ca30ff Fix deploy script: use SSH_OPTS array for ssh mkdir
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 31s
Typo SSH[@] left ssh out of the command so the shell tried to run
ubuntu@host as a program (exit 127).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:15:31 +08:00
cf9cbb6134 Fix CI: remove optional secret overrides step
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 31s
Empty optional secrets made [ -n ... ] && echo fail under set -e.
Job env already defines host, user, and deploy path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:13:48 +08:00
77485eee63 Fix CI deploy: default talkpro host, require only SSH secret
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 14s
Gitea workflow no longer needs TALKPRO_HOST secret; defaults match
talkpro VPS. Fail fast with a clear message if TALKPRO_SSH_PRIVATE_KEY
is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:06:14 +08:00
6e90a4adb6 Add Gitea Actions deploy workflow for talkpro.info
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 18s
Build Astro on push to main/master and rsync dist/ to the marketing
server via SSH. Includes deploy-talkpro.sh, env example, and README.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:01:58 +08:00
3933cf42c0 1 2026-05-18 14:58:09 +08:00
df6cff4895 Merge pull request 'fix(style): Changes responsive layout breakpoint' (#8) from finn-staging into main
Reviewed-on: #8
2026-05-18 06:41:21 +00:00
SeekingGamer
7b45ca94a6 fix(style): Changes responsive layout breakpoint 2026-05-18 14:39:56 +08:00
82e3a23df3 Merge pull request 'fix(feat): Refine responsive layout and section spacing' (#7) from finn-staging into main
Reviewed-on: #7
2026-05-15 09:46:01 +00:00
SeekingGamer
c09ba76350 fix(feat): Refine responsive layout and section spacing 2026-05-15 17:44:31 +08:00
66f52a2d6e Merge pull request 'fix(feat): add locales and refine responsive landing page UI' (#6) from finn-staging into main
Reviewed-on: #6
2026-05-15 05:41:46 +00:00
SeekingGamer
ff7e4395ea fix(feat): add locales and refine responsive landing page UI 2026-05-15 13:17:37 +08:00
b6e6178466 Merge pull request 'finn-staging' (#5) from finn-staging into main
Reviewed-on: #5
2026-05-14 10:02:48 +00:00
ce1095088d Merge branch 'main' into finn-staging 2026-05-14 10:02:00 +00:00
SeekingGamer
92bd81aed4 Merge finn-staging into main 2026-05-13 15:09:28 +08:00
TerryM
d31a13cbbe Revert "fix(style): Seperate CSS style to src\styles"
This reverts commit dbda554d28.
2026-05-13 14:55:01 +08:00
TerryM
6f8d36140a Revert "style: Seperate and Reformat inline style into css seperate files"
This reverts commit 93049e9044.
2026-05-13 14:55:01 +08:00
41 changed files with 3904 additions and 517 deletions

10
.env.deploy.example Normal file
View File

@@ -0,0 +1,10 @@
# Copy to .env.deploy for local runs: set -a && source .env.deploy && set +a && ./scripts/deploy-talkpro.sh
#
# Gitea Actions: only this secret is required (Settings → Secrets → Actions):
# TALKPRO_SSH_PRIVATE_KEY = full PEM file contents
# Host/user/path defaults are in .gitea/workflows/deploy-talkpro.yml
TALKPRO_HOST=13.214.179.69
TALKPRO_USER=ubuntu
TALKPRO_REMOTE_ROOT=/home/ubuntu/talkpro
TALKPRO_SSH_KEY_FILE=/absolute/path/to/luis-only.pem

View File

@@ -0,0 +1,57 @@
# Build talk-pro and rsync dist/ to the marketing VPS (talkpro.info).
#
# Required Gitea repo secret (Settings → Secrets → Actions):
# TALKPRO_SSH_PRIVATE_KEY full PEM for ubuntu@talkpro (same as luis-only.pem)
#
# Host/user/path defaults are in the `env:` block below (edit there if needed).
#
# Requires a runner with: node 22+, ssh, ssh-keyscan, tar (rsync optional; no sudo needed).
name: Deploy to talkpro
on:
push:
branches:
- main
- master
workflow_dispatch:
env:
TALKPRO_HOST: "13.214.179.69"
TALKPRO_USER: ubuntu
TALKPRO_REMOTE_ROOT: /home/ubuntu/talkpro
jobs:
build-and-sync:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- name: Check deploy secrets
env:
TALKPRO_SSH_PRIVATE_KEY: ${{ secrets.TALKPRO_SSH_PRIVATE_KEY }}
run: |
if [ -z "${TALKPRO_SSH_PRIVATE_KEY}" ]; then
echo "ERROR: Missing Gitea secret TALKPRO_SSH_PRIVATE_KEY"
echo "Add it under Repository → Settings → Secrets (Actions)."
echo "Value: full contents of your ubuntu@talkpro SSH private key (PEM)."
exit 1
fi
- name: Trust host key
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -H "$TALKPRO_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Build and deploy to talkpro
env:
TALKPRO_SSH_PRIVATE_KEY: ${{ secrets.TALKPRO_SSH_PRIVATE_KEY }}
run: bash scripts/deploy-talkpro.sh

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ dist/
.env .env
.env.* .env.*
!.env.example !.env.example
!.env.deploy.example
.env.deploy

View File

@@ -26,6 +26,28 @@ Open [http://localhost:4321](http://localhost:4321)
| `npm run build` | Build for production into `dist/` | | `npm run build` | Build for production into `dist/` |
| `npm run preview` | Preview production build locally | | `npm run preview` | Preview production build locally |
## Deploy (Gitea Actions → talkpro.info)
Pushes to **`main`** or **`master`** run [`.gitea/workflows/deploy-talkpro.yml`](.gitea/workflows/deploy-talkpro.yml): `npm ci``npm run build``rsync` `dist/` to the marketing server (`/home/ubuntu/talkpro`).
**Repository secret** (Gitea → Settings → Secrets → Actions) — **required**:
| Secret | Value |
|--------|--------|
| `TALKPRO_SSH_PRIVATE_KEY` | Full PEM text of the `ubuntu@talkpro` deploy key |
Host (`13.214.179.69`), user (`ubuntu`), and web root (`/home/ubuntu/talkpro`) are set in the workflow. Optional secrets `TALKPRO_HOST`, `TALKPRO_USER`, `TALKPRO_REMOTE_ROOT` override those defaults.
Manual deploy from this repo:
```bash
cp .env.deploy.example .env.deploy # edit paths
set -a && source .env.deploy && set +a
bash scripts/deploy-talkpro.sh
```
`/api/site-links` (APK / App Store URLs) is still updated via the parent **talkpro** repo: `./scripts/post-talkpro-site-links.sh` on your laptop.
## Project Structure ## Project Structure
``` ```
@@ -72,6 +94,41 @@ Figma assets are stored in `public/assets/`. To re-download them (valid for 7 da
bash scripts/download-assets.sh bash scripts/download-assets.sh
``` ```
## Cloudflare Cache & the `site-links-client.js` Version Flag
The site is proxied through **Cloudflare**, which aggressively caches static files. Astro's built CSS/JS bundles are safe because they get **content-hashed filenames** on every build (e.g. `_astro/index.Bx3kF9.js`) — Cloudflare never has an old copy because the filename itself changes.
`public/site-links-client.js` is the exception. It lives in `public/` so it always deploys to the same URL (`/site-links-client.js`). Cloudflare caches this URL and will keep serving the old version until either:
- The Cloudflare cache is **purged** (Caching → Purge Everything in the dashboard), or
- The script URL is **versioned** so Cloudflare treats it as a new file.
The URL is versioned in `src/layouts/Base.astro`:
```html
<script src="/site-links-client.js?v=2" defer></script>
```
**Every time you change `site-links-client.js`**, bump this number (`?v=2``?v=3`, etc.) and redeploy. Cloudflare will fetch the latest file immediately without needing a cache purge.
| Change type | Cache action needed |
|---|---|
| CSS / Astro component change | None — hashed filename handles it |
| `site-links-client.js` change | Bump `?v=N` in `Base.astro` and redeploy |
| Emergency full reset | Cloudflare dashboard → Caching → Purge Everything |
## i18n — Adding or Updating Translations
All page copy lives in `src/i18n/translations.ts`. The English (`en`) entry is the base — every other language only needs to override the keys it wants to change; missing keys fall back to English automatically.
Supported languages: `en`, `zh-cn`, `zh-tw`, `es`, `vi`, `pt`, `de`, `fr`, `hi`, `ar`, `ru`, `id`, `ur`, `ja`, `ko`, `ms`.
To add a new language:
1. Add the locale code to the `languages` array at the top of `translations.ts`.
2. Add its label to `languageLabels` and `languageNames`.
3. Add a translation object under `translations` (only the keys you want to override are required).
4. Create the page route: copy `src/pages/[lang]/index.astro` if it doesn't exist.
## Design Source ## Design Source
Figma: [Talk Pro — Home Page Desktop](https://www.figma.com/design/Gb8WMJ2RLlcZ0bigoiOQx9/Talk-Pro?node-id=9505-537&m=dev) Figma: [Talk Pro — Home Page Desktop](https://www.figma.com/design/Gb8WMJ2RLlcZ0bigoiOQx9/Talk-Pro?node-id=9505-537&m=dev)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 716 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 473 KiB

View File

@@ -1,6 +1,13 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon"> <g clip-path="url(#clip0_9846_29800)">
<rect width="44" height="44" rx="12" fill="var(--fill-0, #D55F31)"/> <path d="M3.79923 9.12891H3.7275C2.80804 9.12891 2.05835 9.88098 2.05835 10.7981V18.0653C2.05835 18.986 2.80804 19.7356 3.7275 19.7356H3.80045C4.71991 19.7356 5.4696 18.9835 5.4696 18.0653V10.798C5.46838 9.88098 4.71753 9.12891 3.79923 9.12891Z" fill="white"/>
<path id="Vector" d="M23.8954 10H15.6259C14.728 10 14 10.8028 14 11.793V32.207C14 33.1972 14.728 34 15.6259 34H28.3741C29.272 34 30 33.1972 30 32.207V16.7328C30 16.1165 29.7778 15.5252 29.3827 15.0895L25.3848 10.6807C24.9897 10.245 24.4536 10 23.8947 10H23.8954Z" fill="var(--fill-0, white)"/> <path d="M6.26831 20.8732C6.26831 21.7173 6.95819 22.4048 7.80233 22.4048H9.4416V26.3302C9.4416 27.252 10.1937 28.0017 11.1108 28.0017H11.1825C12.1032 28.0017 12.854 27.2509 12.854 26.3302V22.4048H15.1449V26.3302C15.1449 27.252 15.8994 28.0017 16.8165 28.0017H16.887C17.8077 28.0017 18.5573 27.2509 18.5573 26.3302V22.4048H20.1978C21.0407 22.4048 21.7306 21.7173 21.7306 20.8732V9.39844H6.26831V20.8732Z" fill="white"/>
<path d="M17.8506 2.43795L19.1527 0.428069C19.2364 0.301301 19.1993 0.127973 19.0714 0.045438C18.9447 -0.0382608 18.7713 -0.00356184 18.6887 0.126753L17.3389 2.20603C16.3262 1.79114 15.1951 1.558 14.0006 1.558C12.8049 1.558 11.6762 1.79114 10.6611 2.20603L9.3136 0.126753C9.23112 -0.00356184 9.05651 -0.0382608 8.92858 0.045438C8.80065 0.127917 8.76357 0.301301 8.84727 0.428069L10.1505 2.43795C7.80103 3.58939 6.2168 5.75951 6.2168 8.24886C6.2168 8.4019 6.22639 8.55256 6.23952 8.70199H21.7628C21.7759 8.55256 21.7843 8.4019 21.7843 8.24886C21.7843 5.75951 20.1989 3.58939 17.8506 2.43795ZM10.4016 6.03688C9.98912 6.03688 9.65432 5.70447 9.65432 5.2908C9.65432 4.87713 9.98912 4.54588 10.4016 4.54588C10.8165 4.54588 11.1489 4.87707 11.1489 5.2908C11.1489 5.70453 10.8141 6.03688 10.4016 6.03688ZM17.5983 6.03688C17.1858 6.03688 16.851 5.70447 16.851 5.2908C16.851 4.87713 17.1858 4.54588 17.5983 4.54588C18.012 4.54588 18.3444 4.87707 18.3444 5.2908C18.3444 5.70447 18.012 6.03688 17.5983 6.03688Z" fill="white"/>
<path d="M24.2714 9.12891H24.2021C23.2826 9.12891 22.5305 9.88098 22.5305 10.7981V18.0653C22.5305 18.986 23.2838 19.7356 24.2021 19.7356H24.2726C25.1932 19.7356 25.9418 18.9835 25.9418 18.0653V10.798C25.9418 9.88098 25.1909 9.12891 24.2714 9.12891Z" fill="white"/>
</g> </g>
<defs>
<clipPath id="clip0_9846_29800">
<rect width="28" height="28" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 559 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 KiB

After

Width:  |  Height:  |  Size: 880 KiB

Binary file not shown.

310
public/site-links-client.js Normal file
View File

@@ -0,0 +1,310 @@
/**
* Mirrors talkpro static site: /api/site-links, APK default, in-app + App Store modals.
* Strings come from #site-links-i18n (JSON) rendered by DownloadCTA.astro.
*/
(function () {
const DEFAULT_APK = 'https://talkspro.xyz/download';
let apkDownloadUrl = DEFAULT_APK;
function getApkDownloadUrl() {
return apkDownloadUrl;
}
function getI18n() {
const el = document.getElementById('site-links-i18n');
if (!el || !el.textContent.trim()) return {};
try {
return JSON.parse(el.textContent);
} catch {
return {};
}
}
function detectInAppBrowser() {
const ua = navigator.userAgent || '';
if (/MicroMessenger/i.test(ua)) return 'wechat';
try {
if (typeof window.WeixinJSBridge !== 'undefined') return 'wechat';
} catch {
/* ignore */
}
if (/QQ\//i.test(ua) && /MQQBrowser/i.test(ua)) return 'qq';
if (/Weibo/i.test(ua)) return 'weibo';
return '';
}
function isIOSDevice() {
const ua = navigator.userAgent || '';
if (/iPhone|iPad|iPod/i.test(ua)) return true;
if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return true;
return false;
}
function isAndroidDevice() {
const ua = navigator.userAgent || '';
return /Android/i.test(ua) && !isIOSDevice();
}
const MODAL_WRAP =
'position:fixed;inset:0;z-index:10050;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;';
const MODAL_BACK = 'position:absolute;inset:0;background:rgba(0,0,0,.45);cursor:pointer;';
const MODAL_PANEL =
'position:relative;max-width:420px;width:100%;margin:auto;padding:20px 22px;border-radius:12px;background:#fff;color:#1a1a1a;font:15px/1.45 system-ui,-apple-system,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.18);';
const MODAL_TITLE = 'margin:0 0 10px;font-size:18px;font-weight:700;line-height:1.25;';
const MODAL_BODY = 'margin:0 0 18px;font-size:14px;color:#444;';
const MODAL_ACTIONS = 'display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;';
const BTN =
'cursor:pointer;border-radius:8px;padding:10px 14px;font-size:14px;font-weight:600;border:1px solid #ccc;background:#f5f5f5;color:#1a1a1a;';
const BTN_PRIMARY = 'border-color:#1a6cff;background:#1a6cff;color:#fff;';
const BTN_ORANGE = 'cursor:pointer;border-radius:14px;padding:10px 14px;font-size:14px;font-weight:600;border:1px solid #f28a4b;background:#f28a4b;color:#fff;';
function initInAppBrowserModal() {
if (document.getElementById('inapp-browser-modal')) return;
const wrap = document.createElement('div');
wrap.id = 'inapp-browser-modal';
wrap.setAttribute('role', 'dialog');
wrap.setAttribute('aria-modal', 'true');
wrap.setAttribute('aria-hidden', 'true');
wrap.style.cssText = MODAL_WRAP + 'display:none;';
wrap.innerHTML =
'<div data-inapp-close style="' +
MODAL_BACK +
'"></div>' +
'<div style="' +
MODAL_PANEL +
'">' +
'<h3 id="inapp-modal-title" style="' +
MODAL_TITLE +
'"></h3>' +
'<p id="inapp-modal-body" style="' +
MODAL_BODY +
'"></p>' +
'<div style="' +
MODAL_ACTIONS +
'">' +
'<button type="button" id="inapp-copy-apk" style="' +
BTN +
BTN_PRIMARY +
'"></button>' +
'<button type="button" id="inapp-try-chrome" style="' +
BTN +
'" hidden></button>' +
'<button type="button" id="inapp-got-it" data-inapp-close style="' +
BTN +
'"></button>' +
'</div></div>';
document.body.appendChild(wrap);
const close = () => {
wrap.style.display = 'none';
wrap.setAttribute('aria-hidden', 'true');
};
wrap.querySelectorAll('[data-inapp-close]').forEach((el) => {
el.addEventListener('click', close);
});
const i18n = () => getI18n();
document.getElementById('inapp-copy-apk').addEventListener('click', async () => {
const btn = document.getElementById('inapp-copy-apk');
const dict = i18n();
const done = dict.inapp_browser_copied || 'Copied';
try {
await navigator.clipboard.writeText(getApkDownloadUrl());
const prev = btn.textContent;
btn.textContent = done;
setTimeout(() => {
btn.textContent = dict.inapp_browser_copy || prev;
}, 1600);
} catch {
window.prompt(dict.inapp_browser_copy || 'Copy', getApkDownloadUrl());
}
});
document.getElementById('inapp-try-chrome').addEventListener('click', () => {
const enc = encodeURIComponent(getApkDownloadUrl());
const intent =
getApkDownloadUrl().replace(/^https:/i, 'intent:') +
'#Intent;' +
'scheme=https;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;' +
'package=com.android.chrome;S.browser_fallback_url=' +
enc +
';end';
window.location.href = intent;
});
}
function openInAppBrowserModal() {
const modal = document.getElementById('inapp-browser-modal');
if (!modal) return;
const dict = getI18n();
const tryChrome = modal.querySelector('#inapp-try-chrome');
const showChrome = isAndroidDevice() && !!detectInAppBrowser();
if (tryChrome) {
tryChrome.hidden = !showChrome;
tryChrome.setAttribute('aria-hidden', showChrome ? 'false' : 'true');
}
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
const titleEl = modal.querySelector('#inapp-modal-title');
const bodyEl = modal.querySelector('#inapp-modal-body');
if (titleEl) titleEl.textContent = dict.inapp_browser_title || '';
if (bodyEl) {
const iosWechat = isIOSDevice() && detectInAppBrowser() === 'wechat';
const bodyKey = iosWechat ? 'inapp_browser_body_ios' : 'inapp_browser_body';
bodyEl.innerHTML =
dict[bodyKey] || dict.inapp_browser_body || '';
}
const copyBtn = modal.querySelector('#inapp-copy-apk');
const gotBtn = modal.querySelector('#inapp-got-it');
if (copyBtn) copyBtn.textContent = dict.inapp_browser_copy || '';
if (tryChrome) tryChrome.textContent = dict.inapp_browser_try_chrome || '';
if (gotBtn) gotBtn.textContent = dict.inapp_browser_got_it || 'OK';
}
function initAppStoreSoonModal() {
if (document.getElementById('app-store-soon-modal')) return;
const wrap = document.createElement('div');
wrap.id = 'app-store-soon-modal';
wrap.setAttribute('role', 'dialog');
wrap.setAttribute('aria-modal', 'true');
wrap.setAttribute('aria-hidden', 'true');
wrap.style.cssText = MODAL_WRAP + 'display:none;';
wrap.innerHTML =
'<div data-app-soon-close style="' +
MODAL_BACK +
'"></div>' +
'<div style="' +
MODAL_PANEL +
'">' +
'<h3 id="app-soon-title" style="' +
MODAL_TITLE +
'"></h3>' +
'<p id="app-soon-body" style="' +
MODAL_BODY +
'"></p>' +
'<div style="' +
MODAL_ACTIONS +
'">' +
'<button type="button" data-app-soon-close style="' +
BTN_ORANGE +
'"></button>' +
'</div></div>';
document.body.appendChild(wrap);
const close = () => {
wrap.style.display = 'none';
wrap.setAttribute('aria-hidden', 'true');
};
wrap.querySelectorAll('[data-app-soon-close]').forEach((el) => {
el.addEventListener('click', close);
});
}
function openAppStoreSoonModal() {
const modal = document.getElementById('app-store-soon-modal');
if (!modal) return;
const dict = getI18n();
const tEl = modal.querySelector('#app-soon-title');
const bEl = modal.querySelector('#app-soon-body');
const ok = modal.querySelector('button[data-app-soon-close]');
if (tEl) tEl.textContent = dict.app_store_soon_title || '';
if (bEl) bEl.innerHTML = dict.app_store_soon_body || '';
if (ok) ok.textContent = dict.app_store_soon_ok || 'OK';
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
}
function bindBrowserDownloadLinks() {
document.querySelectorAll('a.store-badge--browser').forEach((a) => {
const openForInApp = (e) => {
if (!detectInAppBrowser()) return;
e.preventDefault();
e.stopPropagation();
openInAppBrowserModal();
};
a.addEventListener('click', openForInApp);
a.addEventListener(
'touchend',
(e) => {
if (!detectInAppBrowser()) return;
e.preventDefault();
openInAppBrowserModal();
},
{ passive: false }
);
});
}
function bindAppStoreAppleBadges() {
document.querySelectorAll('a.store-badge--apple[data-app-soon]').forEach((a) => {
const stopAndOpen = (e) => {
e.preventDefault();
e.stopPropagation();
openAppStoreSoonModal();
};
a.addEventListener('click', stopAndOpen);
a.addEventListener(
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
openAppStoreSoonModal();
},
{ passive: false }
);
});
}
function loadSiteLinksThenBind() {
if (window.location.protocol === 'file:') {
bindBrowserDownloadLinks();
bindAppStoreAppleBadges();
return;
}
fetch('/api/site-links', { credentials: 'same-origin' })
.then((r) => {
if (!r.ok) throw new Error('bad status');
return r.json();
})
.then((j) => {
const apk = String(j.apk_download_url || '').trim();
const ios = String(j.app_store_url || '').trim();
if (apk.startsWith('https://')) apkDownloadUrl = apk;
document.querySelectorAll('a.store-badge--browser').forEach((a) => {
if (apk.startsWith('https://')) a.setAttribute('href', apk);
});
document.querySelectorAll('a.store-badge--apple').forEach((a) => {
if (ios.startsWith('https://')) {
a.setAttribute('href', ios);
a.removeAttribute('data-app-soon');
a.setAttribute('rel', 'noopener noreferrer');
a.setAttribute('target', '_blank');
}
});
const meta = document.getElementById('site-links-meta');
if (meta) {
const dict = getI18n();
const u = String(j.updated_at || '').trim();
if (u) {
meta.textContent =
(dict.linksMetaPrefix || 'Store links file updated: ') +
u +
(dict.linksMetaSuffix || ' (GMT+8)');
meta.hidden = false;
} else {
meta.hidden = true;
}
}
})
.catch(() => {
/* keep defaults */
})
.finally(() => {
bindBrowserDownloadLinks();
bindAppStoreAppleBadges();
});
}
initInAppBrowserModal();
initAppStoreSoonModal();
loadSiteLinksThenBind();
})();

71
scripts/deploy-talkpro.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Build Astro dist/ and sync to the Talk Pro marketing host (talkpro.info).
# Used by Gitea Actions and for manual deploys from this repo.
#
# Required (CI): TALKPRO_SSH_PRIVATE_KEY — PEM contents (Gitea secret)
# Optional: TALKPRO_SSH_KEY_FILE — path to PEM (local)
# Defaults match .env.deploy.example / Gitea workflow job env:
# TALKPRO_HOST=13.214.179.69 TALKPRO_USER=ubuntu TALKPRO_REMOTE_ROOT=/home/ubuntu/talkpro
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
HOST="${TALKPRO_HOST:-13.214.179.69}"
USER="${TALKPRO_USER:-ubuntu}"
REMOTE_ROOT="${TALKPRO_REMOTE_ROOT:-/home/ubuntu/talkpro}"
[[ -n "$HOST" ]] || HOST=13.214.179.69
[[ -n "$USER" ]] || USER=ubuntu
[[ -n "$REMOTE_ROOT" ]] || REMOTE_ROOT=/home/ubuntu/talkpro
KEY_FILE="${TALKPRO_SSH_KEY_FILE:-}"
TMP_KEY=""
cleanup() {
[[ -n "$TMP_KEY" && -f "$TMP_KEY" ]] && rm -f "$TMP_KEY"
}
trap cleanup EXIT
if [[ -n "${TALKPRO_SSH_PRIVATE_KEY:-}" ]]; then
TMP_KEY="$(mktemp)"
chmod 600 "$TMP_KEY"
printf '%s\n' "$TALKPRO_SSH_PRIVATE_KEY" | tr -d '\r' > "$TMP_KEY"
KEY_FILE="$TMP_KEY"
elif [[ -z "$KEY_FILE" || ! -f "$KEY_FILE" ]]; then
echo "ERROR: Set TALKPRO_SSH_PRIVATE_KEY (Gitea secret / env) or TALKPRO_SSH_KEY_FILE (local path)" >&2
exit 1
fi
if [[ ! -s "$KEY_FILE" ]]; then
echo "ERROR: SSH key file is empty — check TALKPRO_SSH_PRIVATE_KEY secret in Gitea" >&2
exit 1
fi
chmod 600 "$KEY_FILE"
SSH_OPTS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
RSYNC_SSH="ssh -i \"$KEY_FILE\" -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
REMOTE="${USER}@${HOST}"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
command -v ssh >/dev/null || { echo "ERROR: ssh not found (install openssh-client)" >&2; exit 127; }
ssh "${SSH_OPTS[@]}" "$REMOTE" "mkdir -p ${REMOTE_ROOT}"
if command -v rsync >/dev/null 2>&1; then
echo "Uploading with rsync..."
rsync -avz --delete \
-e "$RSYNC_SSH" \
dist/ \
"${REMOTE}:${REMOTE_ROOT}/"
else
command -v tar >/dev/null || { echo "ERROR: need rsync or tar on the runner" >&2; exit 127; }
echo "Uploading with tar over ssh (rsync not available on runner)..."
ssh "${SSH_OPTS[@]}" "$REMOTE" "mkdir -p ${REMOTE_ROOT} && rm -rf ${REMOTE_ROOT:?}/*"
tar -C dist -cf - . | ssh "${SSH_OPTS[@]}" "$REMOTE" "tar -C ${REMOTE_ROOT} -xf -"
fi
echo "Deployed dist/ → ${REMOTE}:${REMOTE_ROOT}/"

5
src/assets.ts Normal file
View File

@@ -0,0 +1,5 @@
const assetVersion = '20260518-1'
export function assetPath(path: string) {
return `${path}?v=${assetVersion}`
}

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -7,10 +8,12 @@ export interface Props {
const { t } = Astro.props const { t } = Astro.props
const slides = [ const slides = [
"/assets/preview-phone.png", assetPath("/assets/preview-phone.png"),
"/assets/preview-phone.png", assetPath("/assets/preview-phone.png"),
"/assets/preview-phone.png", assetPath("/assets/preview-phone.png"),
] ]
const arrowLeft = assetPath("/assets/preview-arrow-left.svg")
const arrowRight = assetPath("/assets/preview-arrow-right.svg")
--- ---
<section class="app-preview"> <section class="app-preview">
@@ -31,7 +34,7 @@ const slides = [
<div class="app-preview__control-wrap"> <div class="app-preview__control-wrap">
<div class="app-preview__control-inner"> <div class="app-preview__control-inner">
<button id="btn-prev" class="app-preview__button"> <button id="btn-prev" class="app-preview__button">
<img alt={t.previous} class="app-preview__button-icon" src="/assets/preview-arrow-left.svg" /> <img alt={t.previous} class="app-preview__button-icon" src={arrowLeft} />
</button> </button>
</div> </div>
</div> </div>
@@ -43,7 +46,7 @@ const slides = [
<div class="app-preview__control-wrap"> <div class="app-preview__control-wrap">
<div class="app-preview__control-inner"> <div class="app-preview__control-inner">
<button id="btn-next" class="app-preview__button"> <button id="btn-next" class="app-preview__button">
<img alt={t.next} class="app-preview__button-icon app-preview__button-icon--next" src="/assets/preview-arrow-right.svg" /> <img alt={t.next} class="app-preview__button-icon app-preview__button-icon--next" src={arrowRight} />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -6,14 +7,14 @@ export interface Props {
} }
const { t } = Astro.props const { t } = Astro.props
const halftone = "/assets/core-halftone-bg.png"; const halftone = assetPath("/assets/core-halftone-bg.png");
const icons = [ const icons = [
"/assets/core-icon-private.png", assetPath("/assets/core-icon-private.png"),
"/assets/core-icon-groups.png", assetPath("/assets/core-icon-groups.png"),
"/assets/core-icon-channels.png", assetPath("/assets/core-icon-channels.png"),
"/assets/core-icon-voice.png", assetPath("/assets/core-icon-voice.png"),
"/assets/core-icon-video.png", assetPath("/assets/core-icon-video.png"),
"/assets/core-icon-media.png", assetPath("/assets/core-icon-media.png"),
] ]
--- ---

View File

@@ -1,16 +1,20 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
t: Translations['download'] t: Translations['download']
siteLinks: Translations['siteLinks']
} }
const { t } = Astro.props const { t, siteLinks } = Astro.props
const bgPattern = "/assets/cta-bg-pattern.svg"; const bgPattern = assetPath("/assets/cta-bg-pattern.svg");
const talkproLogo = "/assets/cta-talkpro-logo.svg"; const talkproLogo = assetPath("/assets/cta-talkpro-logo.svg");
const androidIcon = "/assets/cta-android-icon.svg"; const androidIcon = assetPath("/assets/cta-android-icon.svg");
const appleIcon = "/assets/cta-apple-icon.svg"; const appleIcon = assetPath("/assets/cta-apple-icon.svg");
const phoneArt = "/assets/cta-phone-art.png"; const phoneArt = assetPath("/assets/cta-phone-art.png");
const defaultApkHref = "https://talkspro.xyz/download";
const siteLinksJson = JSON.stringify(siteLinks);
--- ---
<section id="download" class="download-cta"> <section id="download" class="download-cta">
@@ -35,14 +39,25 @@ const phoneArt = "/assets/cta-phone-art.png";
</div> </div>
<div class="store-badges"> <div class="store-badges">
<div class="store-badge store-badge--android"> <a
<img alt={t.androidAlt} class="store-badge__icon" src={androidIcon} /> class="store-badge store-badge--android store-badge--browser"
href={defaultApkHref}
rel="noopener noreferrer"
target="_blank"
>
<div class="store-badge__icon-frame">
<img alt={t.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div>
<div class="store-badge__copy"> <div class="store-badge__copy">
<p class="store-badge__platform">{t.android}</p> <p class="store-badge__platform">{t.android}</p>
<p class="store-badge__label">{t.androidCta}</p> <p class="store-badge__label">{t.androidCta}</p>
</div> </div>
</div> </a>
<div class="store-badge store-badge--ios"> <a
class="store-badge store-badge--ios store-badge--apple"
href="#"
data-app-soon="1"
>
<div class="store-badge__icon-frame"> <div class="store-badge__icon-frame">
<img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} /> <img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div> </div>
@@ -50,8 +65,9 @@ const phoneArt = "/assets/cta-phone-art.png";
<p class="store-badge__platform">{t.ios}</p> <p class="store-badge__platform">{t.ios}</p>
<p class="store-badge__label">{t.iosCta}</p> <p class="store-badge__label">{t.iosCta}</p>
</div> </div>
</a>
</div> </div>
</div>
</div> </div>
<div class="download-cta__phone"> <div class="download-cta__phone">
@@ -61,3 +77,5 @@ const phoneArt = "/assets/cta-phone-art.png";
</div> </div>
</div> </div>
</section> </section>
<script type="application/json" id="site-links-i18n" set:html={siteLinksJson} />

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -6,16 +7,26 @@ export interface Props {
} }
const { t } = Astro.props const { t } = Astro.props
const images = ["/assets/exp-card-1.png", "/assets/exp-card-2.png", "/assets/exp-card-3.png"] const images = [
assetPath("/assets/exp-card-1.png"),
assetPath("/assets/exp-card-2.png"),
assetPath("/assets/exp-card-3.png"),
]
const imageClasses = ['experience-card__image--one', 'experience-card__image--two', 'experience-card__image--three'] const imageClasses = ['experience-card__image--one', 'experience-card__image--two', 'experience-card__image--three']
--- ---
<section id="experience" class="experience"> <section id="experience" class="experience">
<div class="experience__inner"> <div class="experience__inner">
<div class="experience__heading">
<p class="experience__title"> <p class="experience__title">
{t.title} {t.title}
</p> </p>
<p class="experience__caption">
{t.caption}
</p>
</div>
<div class="experience__body"> <div class="experience__body">
<div class="experience__grid"> <div class="experience__grid">
{t.cards.map((card, index) => ( {t.cards.map((card, index) => (
@@ -36,10 +47,6 @@ const imageClasses = ['experience-card__image--one', 'experience-card__image--tw
</div> </div>
))} ))}
</div> </div>
<p class="experience__caption">
{t.caption}
</p>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -6,7 +7,7 @@ export interface Props {
} }
const { t } = Astro.props const { t } = Astro.props
const logoFull = "/assets/footer-logo.png"; const logoFull = assetPath("/assets/footer-logo.png");
--- ---
<footer class="site-footer"> <footer class="site-footer">

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import { getLocalePath, languageLabels, languageNames, languages, type Lang, type Translations } from '../i18n/translations' import { getLocalePath, languageLabels, languageNames, languages, type Lang, type Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -7,9 +8,9 @@ export interface Props {
} }
const { lang, t } = Astro.props const { lang, t } = Astro.props
const logoIcon = "/assets/header-logo-icon.png"; const logoIcon = assetPath("/assets/header-logo-icon.png");
const logoWordmark = "/assets/header-logo-wordmark.svg"; const logoWordmark = assetPath("/assets/header-logo-wordmark.svg");
const globeIcon = "/assets/header-globe.svg"; const globeIcon = assetPath("/assets/header-globe.svg");
const navItems = [ const navItems = [
{ href: '#hero', label: t.nav.home }, { href: '#hero', label: t.nav.home },
{ href: '#features', label: t.nav.features }, { href: '#features', label: t.nav.features },

View File

@@ -1,13 +1,18 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
t: Translations['hero'] t: Translations['hero']
download: Translations['download']
} }
const { t } = Astro.props const { t, download } = Astro.props
const heroBg = "/assets/hero-bg.png"; const heroBg = assetPath("/assets/hero-bg.png");
const phoneMockup = "/assets/hero-phone.png"; const phoneMockup = assetPath("/assets/hero-phone.png");
const androidIcon = assetPath("/assets/cta-android-icon.svg");
const appleIcon = assetPath("/assets/cta-apple-icon.svg");
const defaultApkHref = "https://talkspro.xyz/download";
--- ---
<section id="hero" class="hero"> <section id="hero" class="hero">
@@ -36,11 +41,32 @@ const phoneMockup = "/assets/hero-phone.png";
{t.description} {t.description}
</p> </p>
<div class="hero__actions"> <div class="hero__actions">
<a href="#download" class="hero__button hero__button--primary"> <a
<span class="hero__button-label">{t.download}</span> href={defaultApkHref}
class="store-badge store-badge--android store-badge--browser hero__store-badge"
rel="noopener noreferrer"
target="_blank"
>
<div class="store-badge__icon-frame">
<img alt={download.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div>
<div class="store-badge__copy">
<p class="store-badge__platform">{download.android}</p>
<p class="store-badge__label">{download.androidCta}</p>
</div>
</a> </a>
<a href="#features" class="hero__button hero__button--secondary"> <a
<span class="hero__button-label">{t.explore}</span> href="#"
class="store-badge store-badge--ios store-badge--apple hero__store-badge"
data-app-soon="1"
>
<div class="store-badge__icon-frame">
<img alt={download.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div>
<div class="store-badge__copy">
<p class="store-badge__platform">{download.ios}</p>
<p class="store-badge__label">{download.iosCta}</p>
</div>
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -6,6 +7,9 @@ export interface Props {
} }
const { t } = Astro.props const { t } = Astro.props
const trustIconSprite = assetPath("/assets/trust-icon-sprite.png")
const trustIconImprovement = assetPath("/assets/trust-icon-improvement.png")
const trustDivider = assetPath("/assets/trust-divider.svg")
const iconClasses = [ const iconClasses = [
'trust-card__icon--one', 'trust-card__icon--one',
'trust-card__icon--two', 'trust-card__icon--two',
@@ -35,7 +39,7 @@ const iconClasses = [
<img <img
alt="" alt=""
class={`trust-card__icon ${iconClasses[index]}`} class={`trust-card__icon ${iconClasses[index]}`}
src={index === 3 ? "/assets/trust-icon-improvement.png" : "/assets/trust-icon-sprite.png"} src={index === 3 ? trustIconImprovement : trustIconSprite}
/> />
</div> </div>
</div> </div>
@@ -47,7 +51,7 @@ const iconClasses = [
{index < t.cards.length - 1 && ( {index < t.cards.length - 1 && (
<div class="trust__divider"> <div class="trust__divider">
<div class="trust__divider-frame"> <div class="trust__divider-frame">
<img alt="" class="trust__divider-image" src="/assets/trust-divider.svg" /> <img alt="" class="trust__divider-image" src={trustDivider} />
</div> </div>
</div> </div>
)} )}

View File

@@ -1,4 +1,5 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations' import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
@@ -6,14 +7,15 @@ export interface Props {
} }
const { t } = Astro.props const { t } = Astro.props
const underline = "/assets/why-underline.svg"; const underline = assetPath("/assets/why-underline.svg");
const icons = [ const icons = [
"/assets/why-icon-simple.svg", assetPath("/assets/why-icon-simple.svg"),
"/assets/why-icon-familiar.svg", assetPath("/assets/why-icon-familiar.svg"),
"/assets/why-icon-connected.svg", assetPath("/assets/why-icon-connected.svg"),
"/assets/why-icon-modern.svg", assetPath("/assets/why-icon-modern.svg"),
] ]
const iconClasses = ['why-card__icon--square', 'why-card__icon--familiar', 'why-card__icon--square', 'why-card__icon--modern'] const illustrationVideo = assetPath("/assets/why-illustration.mp4")
const iconClasses = ['why-card__icon--simple', 'why-card__icon--familiar', 'why-card__icon--connected', 'why-card__icon--modern']
--- ---
<section class="why"> <section class="why">
@@ -49,7 +51,7 @@ const iconClasses = ['why-card__icon--square', 'why-card__icon--familiar', 'why-
playsinline playsinline
preload="metadata" preload="metadata"
> >
<source src="/assets/why-illustration.mp4" type="video/mp4" /> <source src={illustrationVideo} type="video/mp4" />
</video> </video>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -27,10 +27,49 @@ const {
</head> </head>
<body class="bg-surface font-sans overflow-x-hidden"> <body class="bg-surface font-sans overflow-x-hidden">
<slot /> <slot />
<script src="/site-links-client.js?v=2" defer></script>
<script> <script>
(() => { (() => {
const header = document.getElementById('site-header'); const header = document.getElementById('site-header');
const getOffset = () => header ? header.offsetHeight : 0; const getOffset = () => header ? header.offsetHeight : 0;
let activeScrollAnimation = 0;
const easeInOutCubic = (t: number) => (
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
);
const animateScrollTo = (targetTop: number) => {
const startTop = window.scrollY;
const distance = targetTop - startTop;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
window.scrollTo(0, targetTop);
return;
}
if (activeScrollAnimation) {
cancelAnimationFrame(activeScrollAnimation);
}
const duration = Math.min(1300, Math.max(650, Math.abs(distance) * 0.7));
const startTime = performance.now();
const step = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeInOutCubic(progress);
window.scrollTo(0, startTop + distance * easedProgress);
if (progress < 1) {
activeScrollAnimation = requestAnimationFrame(step);
} else {
activeScrollAnimation = 0;
}
};
activeScrollAnimation = requestAnimationFrame(step);
};
document.querySelectorAll('a[href^="#"]').forEach(link => { document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', e => { link.addEventListener('click', e => {
@@ -40,7 +79,8 @@ const {
if (!target) return; if (!target) return;
e.preventDefault(); e.preventDefault();
const top = target.getBoundingClientRect().top + window.scrollY - getOffset(); const top = target.getBoundingClientRect().top + window.scrollY - getOffset();
window.scrollTo({ top, behavior: 'smooth' }); animateScrollTo(top);
history.pushState(null, '', href);
}); });
}); });

View File

@@ -7,7 +7,6 @@ import CoreSystem from '../../components/CoreSystem.astro'
import Experience from '../../components/Experience.astro' import Experience from '../../components/Experience.astro'
import UseCases from '../../components/UseCases.astro' import UseCases from '../../components/UseCases.astro'
import Trust from '../../components/Trust.astro' import Trust from '../../components/Trust.astro'
import AppPreview from '../../components/AppPreview.astro'
import DownloadCTA from '../../components/DownloadCTA.astro' import DownloadCTA from '../../components/DownloadCTA.astro'
import Footer from '../../components/Footer.astro' import Footer from '../../components/Footer.astro'
import { defaultLang, getTranslations, isLang, languages } from '../../i18n/translations' import { defaultLang, getTranslations, isLang, languages } from '../../i18n/translations'
@@ -25,13 +24,13 @@ const t = getTranslations(lang)
<Base lang={lang} title={t.meta.title} description={t.meta.description}> <Base lang={lang} title={t.meta.title} description={t.meta.description}>
<Header lang={lang} t={t.header} /> <Header lang={lang} t={t.header} />
<Hero t={t.hero} /> <Hero t={t.hero} download={t.download} />
<WhyTalkPro t={t.why} /> <WhyTalkPro t={t.why} />
<CoreSystem t={t.core} /> <CoreSystem t={t.core} />
<Experience t={t.experience} /> <Experience t={t.experience} />
<UseCases t={t.useCases} /> <UseCases t={t.useCases} />
<Trust t={t.trust} /> <Trust t={t.trust} />
<AppPreview t={t.preview} /> <!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} /> <DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} /> <Footer t={t.footer} />
</Base> </Base>

View File

@@ -7,7 +7,6 @@ import CoreSystem from '../components/CoreSystem.astro'
import Experience from '../components/Experience.astro' import Experience from '../components/Experience.astro'
import UseCases from '../components/UseCases.astro' import UseCases from '../components/UseCases.astro'
import Trust from '../components/Trust.astro' import Trust from '../components/Trust.astro'
import AppPreview from '../components/AppPreview.astro'
import DownloadCTA from '../components/DownloadCTA.astro' import DownloadCTA from '../components/DownloadCTA.astro'
import Footer from '../components/Footer.astro' import Footer from '../components/Footer.astro'
import { defaultLang, getTranslations } from '../i18n/translations' import { defaultLang, getTranslations } from '../i18n/translations'
@@ -18,13 +17,13 @@ const t = getTranslations(lang)
<Base lang={lang} title={t.meta.title} description={t.meta.description}> <Base lang={lang} title={t.meta.title} description={t.meta.description}>
<Header lang={lang} t={t.header} /> <Header lang={lang} t={t.header} />
<Hero t={t.hero} /> <Hero t={t.hero} download={t.download} />
<WhyTalkPro t={t.why} /> <WhyTalkPro t={t.why} />
<CoreSystem t={t.core} /> <CoreSystem t={t.core} />
<Experience t={t.experience} /> <Experience t={t.experience} />
<UseCases t={t.useCases} /> <UseCases t={t.useCases} />
<Trust t={t.trust} /> <Trust t={t.trust} />
<AppPreview t={t.preview} /> <!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} /> <DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} /> <Footer t={t.footer} />
</Base> </Base>

View File

@@ -28,9 +28,9 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
max-width: 1280px; max-width: 1280px;
gap: 32px; gap: 0px;
margin: 0 auto; margin: 0 auto;
padding: 48px 16px; padding: 60px 16px;
} }
.download-cta__content { .download-cta__content {
@@ -60,7 +60,7 @@
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
gap: 20px; gap: 16px;
max-width: 100%; max-width: 100%;
} }
@@ -72,6 +72,7 @@
font-size: var(--download-heading-title-size); font-size: var(--download-heading-title-size);
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
letter-spacing: var(--ls-32);
color: #1a1a1a; color: #1a1a1a;
white-space: nowrap; white-space: nowrap;
} }
@@ -98,6 +99,7 @@
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-16);
color: #7a726d; color: #7a726d;
text-align: center; text-align: center;
} }
@@ -127,14 +129,29 @@
border-radius: 20px; border-radius: 20px;
} }
a.store-badge {
box-sizing: border-box;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.download-cta__links-meta {
font-size: 13px;
color: #7a726d;
margin: 14px auto 0;
max-width: min(1180px, calc(100% - 40px));
text-align: center;
}
.store-badge--android { .store-badge--android {
background: #f28a4b; background: #f28a4b;
border: 1px solid #c5834e; border: 1px solid #c5834e;
} }
.store-badge--ios { .store-badge--ios {
background: #383838; background: #121212F0;
border: 1px solid #141414; border: 1px solid #2C2C2C;
} }
.store-badge__icon { .store-badge__icon {
@@ -151,7 +168,7 @@
flex-shrink: 0; flex-shrink: 0;
width: 44px; width: 44px;
height: 44px; height: 44px;
background: #151515; background: #323232;
border-radius: 12px; border-radius: 12px;
} }
@@ -161,6 +178,16 @@
height: 27px; height: 27px;
} }
.store-badge--android .store-badge__icon-frame {
background: #d55f31;
}
.store-badge__android-icon {
display: block;
width: 28px;
height: 28px;
}
.store-badge__copy { .store-badge__copy {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -173,10 +200,10 @@
.store-badge__platform { .store-badge__platform {
margin: 0; margin: 0;
font-size: 11px; font-size: 13px;
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
letter-spacing: 0.05px; letter-spacing: var(--ls-13);
} }
.store-badge--android .store-badge__platform { .store-badge--android .store-badge__platform {
@@ -184,7 +211,7 @@
} }
.store-badge--ios .store-badge__platform { .store-badge--ios .store-badge__platform {
color: #ccc; color: #949494;
} }
.store-badge__label { .store-badge__label {
@@ -192,6 +219,7 @@
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-15);
color: #fff; color: #fff;
} }
@@ -200,9 +228,17 @@
display: block; display: block;
order: 1; order: 1;
flex-shrink: 0; flex-shrink: 0;
width: 240px;
height: 292px;
}
@media (min-width: 578px) {
.download-cta__phone {
width: min(418px, calc(100vw - 32px)); width: min(418px, calc(100vw - 32px));
height: auto;
aspect-ratio: 418 / 510; aspect-ratio: 418 / 510;
} }
}
.download-cta__phone-crop { .download-cta__phone-crop {
position: absolute; position: absolute;
@@ -242,8 +278,13 @@
--download-heading-title-size: 40px; --download-heading-title-size: 40px;
} }
.download-cta__title {
letter-spacing: var(--ls-40);
}
.download-cta__description { .download-cta__description {
font-size: 18px; font-size: 18px;
letter-spacing: var(--ls-18);
} }
.store-badges { .store-badges {
@@ -253,14 +294,14 @@
} }
} }
@media (min-width: 1023px) { @media (min-width: 1024px) {
.download-cta { .download-cta {
height: 600px; height: 600px;
} }
.download-cta__inner { .download-cta__inner {
flex-direction: row; flex-direction: row;
gap: 0; gap: 16px;
padding: 0 24px; padding: 0 24px;
} }
@@ -286,6 +327,10 @@
--download-heading-title-size: 48px; --download-heading-title-size: 48px;
} }
.download-cta__title {
letter-spacing: var(--ls-48);
}
.download-cta__phone { .download-cta__phone {
order: 2; order: 2;
width: 418px; width: 418px;

View File

@@ -5,7 +5,7 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: 60px; gap: 60px;
padding: 64px 0; padding: 60px 0;
overflow: hidden; overflow: hidden;
} }
@@ -38,9 +38,10 @@
.features__title { .features__title {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 32px; font-size: 28px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a; color: #1a1a1a;
text-align: center; text-align: center;
} }
@@ -48,9 +49,10 @@
.features__description { .features__description {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
text-align: center; text-align: center;
} }
@@ -70,8 +72,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 16px; gap: 24px;
padding: 32px; padding: 36px;
background: rgba(255, 255, 255, 0.78); background: rgba(255, 255, 255, 0.78);
border-radius: 30px; border-radius: 30px;
} }
@@ -105,9 +107,10 @@
.feature-card__title { .feature-card__title {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 24px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-20);
color: #2e2a28; color: #2e2a28;
} }
@@ -117,35 +120,78 @@
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
} }
@media (min-width: 768px) { @media (min-width: 440px) {
.features__header,
.features__grid {
padding-left: 20px;
padding-right: 20px;
}
.features__title { .features__title {
font-size: 40px; font-size: 32px;
letter-spacing: var(--ls-32);
}
}
@media (min-width: 576px) {
.features__header,
.features__grid {
padding-left: 24px;
padding-right: 24px;
}
.features__title {
font-size: 36px;
letter-spacing: var(--ls-36);
}
}
@media (min-width: 768px) {
.features__header,
.features__grid {
padding-left: 36px;
padding-right: 36px;
}
.features__title {
font-size: 42px;
letter-spacing: var(--ls-42);
} }
.features__grid { .features__grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.feature-card__title {
font-size: 24px;
letter-spacing: var(--ls-24);
}
} }
@media (min-width: 1023px) { @media (min-width: 1024px) {
.features {
padding: 120px 0;
}
.features__header { .features__header {
padding: 0 clamp(24px, 14vw, 180px); padding: 0 clamp(36px, 14vw, 180px);
}
.features__title {
font-size: 48px;
} }
.features__grid { .features__grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
padding: 0 24px; padding: 0 36px;
}
}
@media (min-width: 1200px) {
.features__title {
font-size: 48px;
letter-spacing: var(--ls-48);
}
.features__description {
font-size: 18px;
letter-spacing: var(--ls-18);
} }
} }
@@ -154,3 +200,10 @@
padding: 0; padding: 0;
} }
} }
@media (min-width: 1376px) {
.features {
padding-top: 120px;
padding-bottom: 120px;
}
}

View File

@@ -4,7 +4,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
padding: 64px 0; padding: 60px 0;
background: #fef0eb; background: #fef0eb;
} }
@@ -58,6 +58,7 @@
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-14);
color: #7a726d; color: #7a726d;
} }
@@ -80,6 +81,7 @@
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d; color: #7a726d;
} }
@@ -101,9 +103,9 @@
} }
} }
@media (min-width: 1023px) { @media (min-width: 1024px) {
.site-footer { .site-footer {
padding: 120px 0; padding: 60px 0;
} }
.site-footer__inner { .site-footer__inner {

View File

@@ -8,3 +8,72 @@
@import './features.css'; @import './features.css';
@import './trust.css'; @import './trust.css';
@import './preview.css'; @import './preview.css';
body {
font-family: "Inter", sans-serif;
--ls-12: 0.01px;
--ls-13: -0.04px;
--ls-14: -0.09px;
--ls-15: -0.13px;
--ls-16: -0.18px;
--ls-17: -0.22px;
--ls-18: -0.26px;
--ls-19: -0.30px;
--ls-20: -0.33px;
--ls-21: -0.37px;
--ls-22: -0.40px;
--ls-23: -0.44px;
--ls-24: -0.47px;
--ls-25: -0.50px;
--ls-26: -0.53px;
--ls-27: -0.56px;
--ls-28: -0.59px;
--ls-29: -0.61px;
--ls-30: -0.64px;
--ls-31: -0.67px;
--ls-32: -0.69px;
--ls-33: -0.72px;
--ls-34: -0.74px;
--ls-35: -0.77px;
--ls-36: -0.79px;
--ls-37: -0.81px;
--ls-38: -0.84px;
--ls-39: -0.86px;
--ls-40: -0.89px;
--ls-41: -0.91px;
--ls-42: -0.93px;
--ls-43: -0.95px;
--ls-44: -0.98px;
--ls-45: -1.00px;
--ls-46: -1.02px;
--ls-47: -1.05px;
--ls-48: -1.07px;
--ls-49: -1.09px;
--ls-50: -1.11px;
--ls-51: -1.14px;
--ls-52: -1.16px;
--ls-53: -1.18px;
--ls-54: -1.20px;
--ls-55: -1.23px;
--ls-56: -1.25px;
--ls-57: -1.27px;
--ls-58: -1.29px;
--ls-59: -1.32px;
--ls-60: -1.34px;
--ls-61: -1.36px;
--ls-62: -1.38px;
--ls-63: -1.40px;
--ls-64: -1.43px;
--ls-65: -1.45px;
--ls-66: -1.47px;
--ls-67: -1.49px;
--ls-68: -1.52px;
--ls-69: -1.54px;
--ls-70: -1.56px;
--ls-71: -1.58px;
--ls-72: -1.61px;
}
:target {
scroll-margin-top: 72px;
}

View File

@@ -86,6 +86,7 @@
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d; color: #7a726d;
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
@@ -133,6 +134,7 @@
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 14px; line-height: 14px;
letter-spacing: var(--ls-14);
color: #2e2a28; color: #2e2a28;
white-space: nowrap; white-space: nowrap;
} }
@@ -143,7 +145,8 @@
right: 0; right: 0;
z-index: 60; z-index: 60;
width: 240px; width: 240px;
overflow: hidden; max-height: min(520px, calc(100vh - 96px));
overflow-y: auto;
background: #fff; background: #fff;
border: 1px solid #e3d9d1; border: 1px solid #e3d9d1;
border-radius: 18px; border-radius: 18px;
@@ -156,10 +159,11 @@
.language-switcher__option { .language-switcher__option {
display: block; display: block;
padding: 18px 24px; padding: 14px 24px;
font-size: 18px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d; color: #7a726d;
text-decoration: none; text-decoration: none;
transition: transition:
@@ -197,6 +201,7 @@
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #fff; color: #fff;
white-space: nowrap; white-space: nowrap;
} }
@@ -228,6 +233,8 @@
} }
.mobile-nav { .mobile-nav {
max-height: calc(100vh - 72px);
overflow-y: auto;
background: #fff; background: #fff;
border-top: 1px solid #e3d9d1; border-top: 1px solid #e3d9d1;
} }
@@ -249,9 +256,15 @@
padding: 16px 0; padding: 16px 0;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
letter-spacing: var(--ls-16);
color: #2e2a28; color: #2e2a28;
text-decoration: none; text-decoration: none;
border-bottom: 1px solid #e3d9d1; border-bottom: 1px solid #e3d9d1;
transition: color 160ms ease;
}
.mobile-nav__link.is-active {
color: #f28a4b;
} }
.mobile-nav__link--last { .mobile-nav__link--last {
@@ -259,17 +272,22 @@
} }
.mobile-nav__languages { .mobile-nav__languages {
display: flex; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: center; align-items: center;
gap: 16px; gap: 12px 16px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid #e3d9d1; border-top: 1px solid #e3d9d1;
} }
.mobile-nav__language-link { .mobile-nav__language-link {
font-size: 15px; min-width: 0;
font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 1.25;
letter-spacing: var(--ls-14);
color: #2e2a28; color: #2e2a28;
text-align: center;
text-decoration: none; text-decoration: none;
} }
@@ -277,6 +295,18 @@
color: #f28a4b; color: #f28a4b;
} }
@media (max-width: 500px) {
.mobile-nav__languages {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 501px) and (max-width: 1024px) {
.mobile-nav__languages {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
.mobile-nav__download-item { .mobile-nav__download-item {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -290,6 +320,7 @@
padding: 14px 24px; padding: 14px 24px;
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
letter-spacing: var(--ls-15);
color: #fff; color: #fff;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
@@ -302,7 +333,7 @@
background: #e07a3b; background: #e07a3b;
} }
@media (min-width: 1023px) { @media (min-width: 1024px) {
.site-header__bar { .site-header__bar {
padding: 0 24px; padding: 0 24px;
} }

View File

@@ -96,6 +96,7 @@
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #0d0d0d; color: #0d0d0d;
} }
@@ -110,9 +111,10 @@
.hero__title { .hero__title {
width: 100%; width: 100%;
font-size: 40px; font-size: 36px;
font-weight: 700; font-weight: 700;
line-height: 1.1; line-height: 1.1;
letter-spacing: var(--ls-36);
color: #2e2a28; color: #2e2a28;
} }
@@ -124,9 +126,10 @@
.hero__description { .hero__description {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
} }
@@ -136,52 +139,21 @@
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
flex-shrink: 0; flex-shrink: 0;
width: 100%;
gap: 14px; gap: 14px;
overflow: clip; overflow: clip;
} }
.hero__button { .hero__store-badge {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 14px 24px;
overflow: clip;
border-radius: 17px;
text-decoration: none; text-decoration: none;
transition: background-color 160ms ease; transition:
transform 160ms ease,
filter 160ms ease;
} }
.hero__button--primary { .hero__store-badge:hover {
background: #f28a4b; filter: brightness(0.96);
} transform: translateY(-1px);
.hero__button--primary:hover {
background: #e07a3b;
}
.hero__button--secondary {
background: rgba(255, 255, 255, 0.55);
border: 1px solid #e3d9d1;
}
.hero__button--secondary:hover {
background: #fff;
}
.hero__button-label {
font-size: 15px;
font-weight: 700;
line-height: normal;
white-space: nowrap;
}
.hero__button--primary .hero__button-label {
color: #fff;
}
.hero__button--secondary .hero__button-label {
color: #2e2a28;
} }
.hero__tags { .hero__tags {
@@ -208,31 +180,93 @@
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d; color: #7a726d;
white-space: nowrap; white-space: nowrap;
} }
@media (max-width: 433px) { @media (max-width: 433px) {
.hero__store-badge {
min-width: 0;
}
}
@media (max-width: 1024px) {
.hero__actions {
flex-wrap: nowrap;
}
.hero__store-badge {
flex: 1 1 calc((100% - 14px) / 2);
width: calc((100% - 14px) / 2);
min-width: 0;
padding-right: 10px;
padding-left: 10px;
}
.hero__store-badge .store-badge__copy,
.hero__store-badge .store-badge__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
@media (max-width: 540px) {
.hero__actions { .hero__actions {
flex-direction: column; flex-direction: column;
}
.hero__store-badge {
flex-basis: auto;
width: min(100%, calc(100vw - 32px));
}
.hero__tags {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.hero__button {
width: min(100%, calc(100vw - 32px));
padding-right: 14px;
padding-left: 14px;
} }
.hero__button-label { @media (min-width: 440px) {
font-size: clamp(13px, 3.5vw, 15px); .hero__title {
font-size: 42px;
letter-spacing: var(--ls-42);
}
}
@media (min-width: 576px) {
.hero__title {
font-size: 48px;
letter-spacing: var(--ls-48);
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.hero__title { .hero__title {
font-size: 56px; font-size: 56px;
letter-spacing: var(--ls-56);
}
.hero__phone-column {
padding-top: 60px;
}
.hero__phone-frame {
width: min(580px, 100%);
height: auto;
aspect-ratio: 116 / 191;
}
.hero__phone-crop {
position: relative;
inset: auto;
overflow: visible;
}
.hero__phone {
position: static;
width: 100%;
height: auto;
} }
} }
@@ -247,27 +281,59 @@
} }
.hero__phone-column { .hero__phone-column {
align-items: center; position: relative;
justify-content: flex-start;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 100%; height: 100%;
padding-top: 60px; overflow: hidden;
} }
.hero__phone-frame { .hero__phone-frame {
width: 100%; position: absolute;
height: auto; top: 60px;
aspect-ratio: 673 / 1108; left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.hero__phone-crop {
position: relative;
inset: auto;
height: 100%;
overflow: visible;
}
.hero__phone {
position: static;
width: auto;
height: 100%;
}
.hero__actions {
flex-wrap: nowrap;
}
.hero__store-badge {
flex: 0 1 auto;
min-width: 0;
} }
.hero__content { .hero__content {
width: 660px; width: clamp(560px, 44vw, 600px);
padding: 80px 0; padding: 0;
}
} }
@media (min-width: 1200px) {
.hero__title { .hero__title {
font-size: 72px; font-size: 64px;
letter-spacing: var(--ls-64);
}
.hero__description {
font-size: 18px;
letter-spacing: var(--ls-18);
} }
} }
@@ -275,4 +341,42 @@
.hero__inner { .hero__inner {
padding: 0; padding: 0;
} }
.hero__phone-column {
align-items: flex-start;
padding-top: 60px;
}
.hero__phone-frame {
position: static;
top: auto;
right: auto;
bottom: auto;
overflow: visible;
flex: 1 0 0;
width: auto;
max-width: calc(955px * 116 / 191);
height: 955px;
aspect-ratio: 116 / 191;
}
.hero__phone-crop {
height: auto;
}
.hero__phone {
width: 100%;
height: 100%;
}
.hero__content {
width: clamp(560px, 52vw, 660px);
}
}
@media (min-width: 1376px) {
.hero__title {
font-size: 72px;
letter-spacing: var(--ls-72);
}
} }

View File

@@ -33,9 +33,10 @@
.app-preview__description { .app-preview__description {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
text-align: center; text-align: center;
} }
@@ -205,7 +206,7 @@
} }
} }
@media (min-width: 1023px) { @media (min-width: 1024px) {
.app-preview { .app-preview {
padding: 120px clamp(32px, 6vw, 130px) 0; padding: 120px clamp(32px, 6vw, 130px) 0;
} }
@@ -214,6 +215,11 @@
font-size: 48px; font-size: 48px;
} }
.app-preview__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
.app-preview__side-phone, .app-preview__side-phone,
.app-preview__control-wrap { .app-preview__control-wrap {
display: flex; display: flex;

View File

@@ -3,7 +3,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
padding: 64px 16px; padding: 60px 16px;
background: #fff; background: #fff;
} }
@@ -18,12 +18,21 @@
margin: 0 auto; margin: 0 auto;
} }
.experience__heading {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 24px;
}
.experience__title { .experience__title {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 32px; font-size: 28px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a; color: #1a1a1a;
text-align: center; text-align: center;
} }
@@ -47,10 +56,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-self: center;
width: 100%; width: 100%;
max-width: 320px; gap: 36px;
gap: 28px;
padding: 0 0 36px; padding: 0 0 36px;
overflow: hidden; overflow: hidden;
background: linear-gradient(to bottom, #fef0eb, #fff); background: linear-gradient(to bottom, #fef0eb, #fff);
@@ -85,21 +92,21 @@
} }
.experience-card__image--one { .experience-card__image--one {
top: -58.58%; top: -86.5%;
left: 0; left: 0;
width: 100%; width: 100%;
height: 298.5%; height: 298.5%;
} }
.experience-card__image--two { .experience-card__image--two {
top: -335.6%; top: -137.86%;
left: -5.95%; left: -3.13%;
width: 137.79%; width: 130.62%;
height: 411.28%; height: 389.91%;
} }
.experience-card__image--three { .experience-card__image--three {
top: -99.23%; top: -115.66%;
left: 0; left: 0;
width: 100%; width: 100%;
height: 298.5%; height: 298.5%;
@@ -123,7 +130,7 @@
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
letter-spacing: -0.47px; letter-spacing: var(--ls-24);
color: #0d0d0d; color: #0d0d0d;
} }
@@ -133,16 +140,17 @@
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
letter-spacing: -0.18px; letter-spacing: var(--ls-16);
color: #7a726d; color: #7a726d;
} }
.experience__caption { .experience__caption {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
text-align: center; text-align: center;
} }
@@ -152,7 +160,7 @@
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
padding: 64px 16px; padding: 60px 16px;
background: #fef0eb; background: #fef0eb;
} }
@@ -208,6 +216,7 @@
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-14);
color: #f08458; color: #f08458;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
@@ -216,9 +225,10 @@
.use-cases__title { .use-cases__title {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 32px; font-size: 28px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a; color: #1a1a1a;
text-align: left; text-align: left;
} }
@@ -226,9 +236,10 @@
.use-cases__description { .use-cases__description {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
text-align: left; text-align: left;
} }
@@ -291,6 +302,7 @@
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
letter-spacing: var(--ls-18);
color: #fff; color: #fff;
} }
@@ -300,10 +312,11 @@
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
} }
@media (max-width: 639px) { @media (max-width: 578px) {
.use-cases__rows { .use-cases__rows {
gap: 24px; gap: 24px;
overflow: visible; overflow: visible;
@@ -323,10 +336,55 @@
.use-case-row__title { .use-case-row__title {
font-size: 20px; font-size: 20px;
letter-spacing: var(--ls-20);
} }
} }
@media (min-width: 640px) { @media (min-width: 440px) {
.experience,
.use-cases {
padding-left: 20px;
padding-right: 20px;
}
.experience__title,
.use-cases__title {
font-size: 32px;
letter-spacing: var(--ls-32);
}
}
@media (min-width: 576px) {
.experience,
.use-cases {
padding-left: 24px;
padding-right: 24px;
}
.experience__title,
.use-cases__title {
font-size: 36px;
letter-spacing: var(--ls-36);
}
}
@media (min-width: 578px) {
.experience__grid {
grid-template-columns: repeat(2, minmax(0, 320px));
}
.experience-card {
max-width: 320px;
justify-self: center;
}
.experience-card:nth-child(3):last-child {
grid-column: 1 / -1;
justify-self: center;
}
}
@media (min-width: 578px) {
.use-case-row { .use-case-row {
grid-template-columns: minmax(220px, 300px) minmax(280px, 1fr); grid-template-columns: minmax(220px, 300px) minmax(280px, 1fr);
height: 120px; height: 120px;
@@ -339,28 +397,25 @@
.use-case-row__title { .use-case-row__title {
font-size: 20px; font-size: 20px;
letter-spacing: var(--ls-20);
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.experience,
.use-cases {
padding-left: 36px;
padding-right: 36px;
}
.experience__title, .experience__title,
.use-cases__title { .use-cases__title {
font-size: 40px; font-size: 42px;
letter-spacing: var(--ls-42);
} }
} }
@media (min-width: 626px) { @media (min-width: 1200px) {
.experience__grid {
grid-template-columns: repeat(2, minmax(0, 320px));
}
.experience-card:nth-child(3):last-child {
grid-column: 1 / -1;
justify-self: center;
}
}
@media (min-width: 920px) {
.experience__grid { .experience__grid {
grid-template-columns: repeat(3, minmax(0, 320px)); grid-template-columns: repeat(3, minmax(0, 320px));
} }
@@ -370,28 +425,38 @@
} }
} }
@media (min-width: 1023px) { @media (min-width: 576px) {
.experience {
padding: 120px 16px;
}
.experience-card {
height: 477px;
}
.experience-card__title { .experience-card__title {
white-space: nowrap; white-space: nowrap;
} }
}
@media (min-width: 1024px) {
.experience {
padding-top: 60px;
padding-bottom: 60px;
padding-left: 36px;
padding-right: 36px;
}
}
@media (min-width: 1200px) {
.experience__title, .experience__title,
.use-cases__title { .use-cases__title {
font-size: 48px; font-size: 48px;
letter-spacing: var(--ls-48);
}
.experience__caption,
.use-cases__description {
font-size: 18px;
letter-spacing: var(--ls-18);
} }
} }
@media (min-width: 1295px) { @media (min-width: 1201px) {
.use-cases { .use-cases {
padding: 120px 64px; padding: 60px 64px;
} }
.use-cases__inner { .use-cases__inner {
@@ -401,6 +466,20 @@
} }
} }
@media (min-width: 1376px) {
.experience {
padding-top: 120px;
padding-bottom: 120px;
padding-left: 0;
padding-right: 0;
}
.use-cases {
padding-top: 120px;
padding-bottom: 120px;
}
}
@media (min-width: 1440px) { @media (min-width: 1440px) {
.use-cases { .use-cases {
padding-right: 130px; padding-right: 130px;

View File

@@ -4,7 +4,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
padding: 64px 16px; padding: 60px 16px;
background: #fff; background: #fff;
} }
@@ -30,18 +30,20 @@
.trust__title { .trust__title {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 32px; font-size: 28px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a; color: #1a1a1a;
} }
.trust__description { .trust__description {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
} }
@@ -126,6 +128,7 @@
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
line-height: 22px; line-height: 22px;
letter-spacing: var(--ls-16);
color: #0d0d0d; color: #0d0d0d;
} }
@@ -135,12 +138,13 @@
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
} }
.trust__divider { .trust__divider {
position: relative; position: relative;
display: none; display: none; /* shown only in desktop flex row via 1023px breakpoint */
flex-shrink: 0; flex-shrink: 0;
width: 0; width: 0;
height: 118px; height: 118px;
@@ -158,7 +162,7 @@
height: 100%; height: 100%;
} }
@media (max-width: 1022px) { @media (max-width: 1023px) {
.trust-card__copy { .trust-card__copy {
align-items: center; align-items: center;
text-align: center; text-align: center;
@@ -170,44 +174,155 @@
} }
} }
@media (min-width: 768px) { @media (min-width: 440px) {
.trust {
padding-left: 20px;
padding-right: 20px;
}
.trust__title { .trust__title {
font-size: 40px; font-size: 32px;
letter-spacing: var(--ls-32);
}
}
@media (min-width: 576px) {
.trust {
padding-left: 24px;
padding-right: 24px;
}
.trust__title {
font-size: 36px;
letter-spacing: var(--ls-36);
}
}
@media (min-width: 768px) {
.trust {
padding-left: 36px;
padding-right: 36px;
}
.trust__title {
font-size: 42px;
letter-spacing: var(--ls-42);
} }
.trust__grid { .trust__grid {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); position: relative;
} grid-template-columns: repeat(2, minmax(0, 1fr));
.trust__divider {
display: block;
align-self: center;
}
.trust__divider:nth-of-type(4) {
display: none;
} }
} }
@media (min-width: 1023px) { @media (min-width: 768px) and (max-width: 1023px) {
.trust__grid > .trust-card:nth-child(1),
.trust__grid > .trust-card:nth-child(5) {
position: relative;
}
.trust__grid > .trust-card:nth-child(1)::after,
.trust__grid > .trust-card:nth-child(5)::after {
content: "";
position: absolute;
top: 50%;
right: -16px;
transform: translateY(-50%);
width: 0;
height: 118px;
border-left: 1px solid rgba(240, 132, 88, 0.5);
}
}
@media (min-width: 1024px) {
.trust { .trust {
padding: 120px; padding-top: 60px;
} padding-bottom: 60px;
padding-left: 28px;
.trust__title { padding-right: 28px;
font-size: 48px;
} }
.trust__grid { .trust__grid {
display: flex; display: flex;
gap: 24px; gap: 14px;
} }
.trust-card { .trust-card {
flex: 1; flex: 1;
padding: 16px 8px;
}
.trust-card__icon-frame {
width: 112px;
height: 112px;
}
.trust-card__copy {
align-items: center;
text-align: center;
}
.trust-card__title {
font-size: 15px;
line-height: 20px;
letter-spacing: var(--ls-15);
text-align: center;
}
.trust-card__description {
font-size: 14px;
line-height: 1.45;
letter-spacing: var(--ls-14);
text-align: center;
} }
.trust__divider { .trust__divider {
display: block; display: block;
} }
} }
@media (min-width: 1200px) {
.trust__title {
font-size: 48px;
letter-spacing: var(--ls-48);
}
.trust__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
.trust__grid {
gap: 24px;
}
.trust-card {
padding: 24px;
}
.trust-card__icon-frame {
width: 128px;
height: 128px;
}
.trust-card__title {
font-size: 16px;
line-height: 22px;
letter-spacing: var(--ls-16);
}
.trust-card__description {
font-size: 15px;
line-height: 1.5;
letter-spacing: var(--ls-15);
}
}
@media (min-width: 1376px) {
.trust {
padding-top: 120px;
padding-bottom: 120px;
padding-left: 120px;
padding-right: 120px;
}
}

View File

@@ -4,7 +4,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
padding: 64px 16px; padding: 60px 16px;
background: #fff; background: #fff;
} }
@@ -47,8 +47,9 @@
.why__title { .why__title {
width: 100%; width: 100%;
font-size: 32px; font-size: 28px;
font-weight: 700; font-weight: 700;
letter-spacing: var(--ls-28);
color: #1a1a1a; color: #1a1a1a;
} }
@@ -79,12 +80,20 @@
.why__description { .why__description {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 18px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d; color: #7a726d;
} }
@media (min-width: 1200px) {
.why__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
}
.why__illustration { .why__illustration {
position: relative; position: relative;
display: block; display: block;
@@ -117,8 +126,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
min-width: 0; min-width: 0;
gap: 24px; gap: 20px;
padding: 36px; padding: 20px 16px;
overflow: clip; overflow: clip;
background: #fef0eb; background: #fef0eb;
border: 1px solid #e8e4de; border: 1px solid #e8e4de;
@@ -130,8 +139,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
width: 80px; width: 60px;
height: 80px; height: 60px;
overflow: clip; overflow: clip;
background: #f08458; background: #f08458;
border-radius: 9999px; border-radius: 9999px;
@@ -141,19 +150,24 @@
display: block; display: block;
} }
.why-card__icon--square { .why-card__icon--simple {
width: 44px; width: 33.001px;
height: 44px; height: 32.973px;
} }
.why-card__icon--familiar { .why-card__icon--familiar {
width: 38px; width: 28.5px;
height: 40px; height: 30px;
}
.why-card__icon--connected {
width: 33px;
height: 32.936px;
} }
.why-card__icon--modern { .why-card__icon--modern {
width: 24px; width: 18.125px;
height: 44px; height: 33px;
} }
.why-card__copy { .why-card__copy {
@@ -169,9 +183,10 @@
.why-card__title { .why-card__title {
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 24px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 20px; line-height: 23px;
letter-spacing: var(--ls-20);
color: #0d0d0d; color: #0d0d0d;
} }
@@ -181,28 +196,97 @@
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
letter-spacing: var(--ls-16);
color: #7a726d; color: #7a726d;
} }
@media (min-width: 768px) { @media (min-width: 440px) {
.why {
padding-left: 20px;
padding-right: 20px;
}
.why-card {
gap: 24px;
}
.why-card__icon-frame {
width: 80px;
height: 80px;
}
.why-card__icon--simple {
width: 44.001px;
height: 43.964px;
}
.why-card__icon--familiar {
width: 38px;
height: 40px;
}
.why-card__icon--connected {
width: 44px;
height: 43.914px;
}
.why-card__icon--modern {
width: 24.168px;
height: 44px;
}
.why__title { .why__title {
font-size: 40px; font-size: 32px;
letter-spacing: var(--ls-32);
}
}
@media (min-width: 576px) {
.why {
padding-left: 24px;
padding-right: 24px;
}
.why__title {
font-size: 36px;
letter-spacing: var(--ls-36);
}
}
@media (min-width: 768px) {
.why {
padding-left: 36px;
padding-right: 36px;
}
.why__title {
font-size: 42px;
letter-spacing: var(--ls-42);
}
.why-card {
padding: 36px;
min-height: 152px;
}
.why-card__title {
font-size: 24px;
letter-spacing: var(--ls-24);
}
}
@media (min-width: 1024px) {
.why {
padding-top: 60px;
padding-bottom: 60px;
padding-left: 36px;
padding-right: 36px;
} }
.why__grid { .why__grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.why-card {
min-height: 152px;
}
}
@media (min-width: 1023px) {
.why {
padding: 120px 24px;
}
.why__intro { .why__intro {
flex-direction: row; flex-direction: row;
} }
@@ -214,16 +298,20 @@
.why__illustration { .why__illustration {
order: 2; order: 2;
} }
}
@media (min-width: 1200px) {
.why__title { .why__title {
font-size: 48px; font-size: 48px;
letter-spacing: var(--ls-48);
}
} }
} @media (min-width: 1376px) {
@media (min-width: 1280px) {
.why { .why {
padding-right: 0; padding-top: 120px;
padding-bottom: 120px;
padding-left: 0; padding-left: 0;
padding-right: 0;
} }
} }