Compare commits

..

46 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
a8474fd208 fix(feat): Rearrange the element to fix the responsive layout 2026-05-14 17:58:14 +08:00
SeekingGamer
c587df063b fix(feat): Adjust button and cards position when screen size change 2026-05-14 12:03:42 +08:00
SeekingGamer
c7a205e40c fix(feat): Add and Rearrange the img container after screen change 2026-05-14 11:20:49 +08:00
SeekingGamer
92bd81aed4 Merge finn-staging into main 2026-05-13 15:09:28 +08:00
SeekingGamer
33b1f7f9dd fix(style): Change the css styling for different screen size 2026-05-13 14:57:52 +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
e5bcb7bad2 Merge pull request 'main' (#1) from main into finn-staging
Reviewed-on: #1
2026-05-13 05:51:12 +00:00
SeekingGamer
dbda554d28 fix(style): Seperate CSS style to src\styles 2026-05-13 13:46:21 +08:00
SeekingGamer
93049e9044 style: Seperate and Reformat inline style into css seperate files 2026-05-13 11:48:22 +08:00
TerryM
a6bd0ca864 feat: enhance header and mobile navigation with smooth scrolling and active link highlighting 2026-05-12 22:39:36 +08:00
TerryM
dbaad19d0b fix: resolve tsconfig extends path for Vite/esbuild compatibility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:46:00 +08:00
TerryM
4134aec1f8 fix: responsive layout for 360px minimum viewport width
- Store badges: w-full on mobile, sm:w-[260px] on larger screens
- AppPreview phone: w-full with aspect-ratio on mobile
- UseCases rows: stack vertically on mobile (flex-col sm:flex-row)
- Footer/DownloadCTA: responsive padding and container widths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:35:29 +08:00
TerryM
a23dcf0a95 refactor: update layout and styling for multiple components to enhance responsiveness and visual consistency 2026-05-12 18:30:55 +08:00
TerryM
b0329e3863 fix: ensure layout works at 360px minimum viewport width
- Store badges: w-full on mobile, sm:w-[260px] on larger screens
- AppPreview phone: w-full with aspect-ratio on mobile, fixed size on lg
- UseCases rows: stack vertically on mobile (flex-col sm:flex-row)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 17:45:23 +08:00
44 changed files with 6837 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

View File

@@ -1,4 +1,4 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 1920 923" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg preserveAspectRatio="xMidYMid slice" width="1920" height="923" overflow="visible" style="display: block;" viewBox="0 0 1920 923" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Layer_1" clip-path="url(#clip0_0_4)"> <g id="Layer_1" clip-path="url(#clip0_0_4)">
<path id="Vector" d="M12.1851 -29.997L0.00488281 -17.7969L12.1851 -5.59676L24.3653 -17.7969L12.1851 -29.997Z" fill="var(--fill-0, #FFF0E8)"/> <path id="Vector" d="M12.1851 -29.997L0.00488281 -17.7969L12.1851 -5.59676L24.3653 -17.7969L12.1851 -29.997Z" fill="var(--fill-0, #FFF0E8)"/>
<path id="Vector_2" d="M102.45 -30.0001L90.2725 -17.7969L102.45 -5.59361L114.627 -17.7969L102.45 -30.0001Z" fill="var(--fill-0, #FFF0E8)"/> <path id="Vector_2" d="M102.45 -30.0001L90.2725 -17.7969L102.45 -5.59361L114.627 -17.7969L102.45 -30.0001Z" fill="var(--fill-0, #FFF0E8)"/>

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 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,65 +1,63 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations'
export interface Props {
t: Translations['preview']
}
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="bg-[#fef0eb] w-full flex flex-col gap-[60px] items-center pt-[120px] px-[130px] overflow-hidden"> <section class="app-preview">
<!-- Heading --> <div class="app-preview__header">
<div class="flex flex-col gap-[40px] items-start overflow-clip w-[940px]"> <p class="app-preview__title">
<p class="font-bold text-[#1a1a1a] text-[48px] text-center tracking-[-1.16px] leading-[1.2] w-full"> {t.title}
A Familiar App Experience, Reimagined with a Modern Look
</p> </p>
<p class="font-normal text-[#7a726d] text-[18px] text-center tracking-[-0.33px] leading-[1.5] w-full"> <p class="app-preview__description">
TalkPro keeps the communication experience familiar while refining the visual layer with updated icons, colors, spacing, and interface details. {t.description}
</p> </p>
</div> </div>
<!-- Carousel --> <div class="app-preview__carousel" id="carousel">
<div class="flex gap-[20px] items-end justify-center shrink-0" id="carousel"> <div class="app-preview__side-phone">
<img id="phone-left" alt="" class="app-preview__phone-image" src={slides[slides.length - 1]} />
<!-- Left phone -->
<div class="relative h-[396px] w-[336px] opacity-20 shrink-0 overflow-hidden pointer-events-none transition-opacity duration-300">
<img id="phone-left" alt="" class="absolute h-[175.34%] left-0 max-w-none top-[-0.03%] w-full transition-opacity duration-300" src={slides[slides.length - 1]} />
</div> </div>
<!-- Prev button --> <div class="app-preview__control-wrap">
<div class="flex items-end self-stretch shrink-0"> <div class="app-preview__control-inner">
<div class="flex items-center justify-center h-full"> <button id="btn-prev" class="app-preview__button">
<button id="btn-prev" class="bg-[#f08458] flex items-center justify-center rounded-[9999px] size-[48px] hover:bg-[#e07a3b] active:scale-95 transition-all shrink-0 cursor-pointer"> <img alt={t.previous} class="app-preview__button-icon" src={arrowLeft} />
<img alt="Previous" class="block size-[24px]" src="/assets/preview-arrow-left.svg" />
</button> </button>
</div> </div>
</div> </div>
<!-- Center phone --> <div class="app-preview__center-phone">
<div class="relative h-[542px] w-[459px] shrink-0 overflow-hidden pointer-events-none"> <img id="phone-center" alt={t.phoneAlt} class="app-preview__phone-image" src={slides[0]} />
<img id="phone-center" alt="TalkPro app preview" class="absolute h-[175.34%] left-0 max-w-none top-[-0.03%] w-full transition-opacity duration-300" src={slides[0]} />
</div> </div>
<!-- Next button --> <div class="app-preview__control-wrap">
<div class="flex items-end self-stretch shrink-0"> <div class="app-preview__control-inner">
<div class="flex items-center justify-center h-full"> <button id="btn-next" class="app-preview__button">
<button id="btn-next" class="bg-[#f08458] flex items-center justify-center rounded-[9999px] size-[48px] hover:bg-[#e07a3b] active:scale-95 transition-all shrink-0 cursor-pointer"> <img alt={t.next} class="app-preview__button-icon app-preview__button-icon--next" src={arrowRight} />
<img alt="Next" class="block size-[24px] rotate-180" src="/assets/preview-arrow-right.svg" />
</button> </button>
</div> </div>
</div> </div>
<!-- Right phone --> <div class="app-preview__side-phone">
<div class="relative h-[396px] w-[336px] opacity-20 shrink-0 overflow-hidden pointer-events-none transition-opacity duration-300"> <img id="phone-right" alt="" class="app-preview__phone-image" src={slides[1]} />
<img id="phone-right" alt="" class="absolute h-[175.34%] left-0 max-w-none top-[-0.03%] w-full transition-opacity duration-300" src={slides[1]} />
</div> </div>
</div> </div>
</section> </section>
<script> <script>
const slides: string[] = (window as any).__carouselSlides || [];
// Read slides injected via data attribute
const carousel = document.getElementById('carousel')!; const carousel = document.getElementById('carousel')!;
const allSlides: string[] = JSON.parse(carousel.dataset.slides || '[]'); const allSlides: string[] = JSON.parse(carousel.dataset.slides || '[]');
@@ -74,18 +72,18 @@ const slides = [
function mod(n: number, m: number) { return ((n % m) + m) % m; } function mod(n: number, m: number) { return ((n % m) + m) % m; }
function transition(next: number) { function transition(next: number) {
[leftImg, centerImg, rightImg].forEach(img => img.style.opacity = '0'); [leftImg, centerImg, rightImg].forEach(img => img && (img.style.opacity = '0'));
setTimeout(() => { setTimeout(() => {
current = mod(next, allSlides.length); current = mod(next, allSlides.length);
leftImg.src = allSlides[mod(current - 1, allSlides.length)]; if (leftImg) leftImg.src = allSlides[mod(current - 1, allSlides.length)];
centerImg.src = allSlides[current]; if (centerImg) centerImg.src = allSlides[current];
rightImg.src = allSlides[mod(current + 1, allSlides.length)]; if (rightImg) rightImg.src = allSlides[mod(current + 1, allSlides.length)];
[leftImg, centerImg, rightImg].forEach(img => img.style.opacity = '1'); [leftImg, centerImg, rightImg].forEach(img => img && (img.style.opacity = '1'));
}, 200); }, 200);
} }
btnPrev.addEventListener('click', () => transition(current - 1)); if (btnPrev) btnPrev.addEventListener('click', () => transition(current - 1));
btnNext.addEventListener('click', () => transition(current + 1)); if (btnNext) btnNext.addEventListener('click', () => transition(current + 1));
</script> </script>
<script define:vars={{ slides }}> <script define:vars={{ slides }}>

View File

@@ -1,66 +1,47 @@
--- ---
const halftone = "/assets/core-halftone-bg.png"; import { assetPath } from '../assets'
const iconPrivate = "/assets/core-icon-private.png"; import type { Translations } from '../i18n/translations'
const iconGroups = "/assets/core-icon-groups.png";
const iconChannels = "/assets/core-icon-channels.png";
const iconVoice = "/assets/core-icon-voice.png";
const iconVideo = "/assets/core-icon-video.png";
const iconMedia = "/assets/core-icon-media.png";
const cards = [ export interface Props {
{ img: iconPrivate, title: 'Private Messaging', desc: 'Stay connected through fast and familiar one-on-one conversations.' }, t: Translations['core']
{ img: iconGroups, title: 'Group Chats', desc: 'Create spaces for friends, teams, communities, and shared discussions.' }, }
{ img: iconChannels, title: 'Channels', desc: 'Follow updates, announcements, and content from the people or communities you care about.' },
{ img: iconVoice, title: 'Voice Calls', desc: 'Talk in real time whenever messages are not enough.' }, const { t } = Astro.props
{ img: iconVideo, title: 'Video Calls', desc: 'Connect face-to-face with a simple and reliable video call experience.' }, const halftone = assetPath("/assets/core-halftone-bg.png");
{ img: iconMedia, title: 'Media Sharing', desc: 'Share photos, videos, files, and updates in your conversations.' }, const icons = [
assetPath("/assets/core-icon-private.png"),
assetPath("/assets/core-icon-groups.png"),
assetPath("/assets/core-icon-channels.png"),
assetPath("/assets/core-icon-voice.png"),
assetPath("/assets/core-icon-video.png"),
assetPath("/assets/core-icon-media.png"),
] ]
--- ---
<section id="features" class="relative w-full flex flex-col gap-[60px] items-center py-[120px] overflow-hidden"> <section id="features" class="features">
<!-- Background halftone --> <img alt="" class="features__bg" src={halftone} />
<img alt="" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[1319px] w-[2363px] object-cover pointer-events-none opacity-20" src={halftone} />
<!-- Section header --> <div class="features__header">
<div class="relative flex flex-col gap-[24px] items-center justify-end overflow-clip px-[180px] w-[1280px]"> <div class="section-eyebrow">
<div class="bg-white border border-[#fbbfa3] flex items-center justify-center px-[24px] py-[12px] rounded-[9999px] shrink-0"> <span class="section-eyebrow__text">{t.eyebrow}</span>
<span class="font-bold text-[#f08458] text-[14px] text-center tracking-[-0.04px] whitespace-nowrap leading-normal">CORE FEATURES</span>
</div> </div>
<p class="font-bold text-[#1a1a1a] text-[48px] text-center tracking-[-1.16px] leading-[1.2] w-full">Everything You Need to Communicate</p> <p class="features__title">{t.title}</p>
<p class="font-normal text-[#7a726d] text-[18px] text-center tracking-[-0.33px] leading-[1.5] w-full"> <p class="features__description">
Different identities, different conversations, and different privacy levels should not be forced into one flat interface. TalkPro lets them exist in order inside one platform. {t.description}
</p> </p>
</div> </div>
<!-- Card grid --> <div class="features__grid">
<div class="relative flex flex-col gap-[22px] items-start w-[1280px]"> {t.cards.map((card, index) => (
<!-- Row 1 --> <div class="feature-card">
<div class="flex gap-[22px] items-start w-full"> <div class="feature-card__icon-frame">
{cards.slice(0, 3).map(card => ( <img alt={card.title} class="feature-card__icon" src={icons[index]} />
<div class="bg-[rgba(255,255,255,0.78)] flex flex-1 flex-col gap-[16px] items-center min-w-0 p-[32px] rounded-[30px] self-stretch">
<div class="relative shrink-0 size-[160px] overflow-hidden">
<img alt={card.title} class="absolute inset-0 max-w-none size-full object-contain" src={card.img} />
</div> </div>
<div class="flex flex-col gap-[12px] items-start text-center w-full"> <div class="feature-card__copy">
<p class="font-bold text-[#2e2a28] text-[24px] tracking-[-0.5px] leading-normal w-full">{card.title}</p> <p class="feature-card__title">{card.title}</p>
<p class="font-normal text-[#7a726d] text-[15px] tracking-[-0.13px] leading-[1.5] w-full">{card.desc}</p> <p class="feature-card__description">{card.desc}</p>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<!-- Row 2 -->
<div class="flex gap-[22px] items-center w-full">
{cards.slice(3).map(card => (
<div class="bg-[rgba(255,255,255,0.78)] flex flex-1 flex-col gap-[16px] items-center min-w-0 p-[32px] rounded-[30px] self-stretch">
<div class="relative shrink-0 size-[160px] overflow-hidden">
<img alt={card.title} class="absolute inset-0 max-w-none size-full object-contain" src={card.img} />
</div>
<div class="flex flex-col gap-[12px] items-start text-center w-full">
<p class="font-bold text-[#2e2a28] text-[24px] tracking-[-0.5px] leading-normal w-full">{card.title}</p>
<p class="font-normal text-[#7a726d] text-[15px] tracking-[-0.13px] leading-[1.5] w-full">{card.desc}</p>
</div>
</div>
))}
</div>
</div>
</section> </section>

View File

@@ -1,60 +1,81 @@
--- ---
const bgPattern = "/assets/cta-bg-pattern.svg"; import { assetPath } from '../assets'
const talkproLogo = "/assets/cta-talkpro-logo.svg"; import type { Translations } from '../i18n/translations'
const androidIcon = "/assets/cta-android-icon.svg";
const appleIcon = "/assets/cta-apple-icon.svg"; export interface Props {
const phoneArt = "/assets/cta-phone-art.png"; t: Translations['download']
siteLinks: Translations['siteLinks']
}
const { t, siteLinks } = Astro.props
const bgPattern = assetPath("/assets/cta-bg-pattern.svg");
const talkproLogo = assetPath("/assets/cta-talkpro-logo.svg");
const androidIcon = assetPath("/assets/cta-android-icon.svg");
const appleIcon = assetPath("/assets/cta-apple-icon.svg");
const phoneArt = assetPath("/assets/cta-phone-art.png");
const defaultApkHref = "https://talkspro.xyz/download";
const siteLinksJson = JSON.stringify(siteLinks);
--- ---
<section id="download" class="relative bg-white border-t border-[#eec8b8] w-full flex items-center justify-center overflow-hidden h-[600px]"> <section id="download" class="download-cta">
<!-- Background pattern --> <img alt="" class="download-cta__pattern" src={bgPattern} />
<img alt="" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[923px] w-full max-w-[1920px] pointer-events-none" src={bgPattern} />
<div class="relative flex items-center w-[1280px]"> <div class="download-cta__inner">
<!-- Left: download panel --> <div class="download-cta__content">
<div class="flex flex-1 flex-col gap-[36px] items-start justify-center min-w-0 overflow-clip"> <div class="section-eyebrow">
<div class="bg-white border border-[#fbbfa3] flex items-center justify-center px-[24px] py-[12px] rounded-[9999px] shrink-0"> <span class="section-eyebrow__text">{t.eyebrow}</span>
<span class="font-bold text-[#f08458] text-[14px] text-center tracking-[-0.04px] whitespace-nowrap leading-normal">DOWNLOAD</span>
</div> </div>
<div class="flex flex-col gap-[16px] items-start w-full"> <div class="download-cta__copy">
<div class="flex gap-[20px] items-start shrink-0"> <div class="download-cta__heading">
<p class="font-bold text-[#1a1a1a] text-[48px] tracking-[-1.16px] leading-[1.2] whitespace-nowrap">Download</p> <p class="download-cta__title">{t.title}</p>
<div class="relative shrink-0 h-[72px] w-[188px]"> <div class="download-cta__logo-frame">
<img alt="TalkPro" class="absolute inset-0 block max-w-none size-full" src={talkproLogo} /> <img alt={t.logoAlt} class="download-cta__logo" src={talkproLogo} />
</div> </div>
</div> </div>
<p class="font-normal text-[#7a726d] text-[18px] tracking-[-0.33px] leading-[1.5] max-w-[542px]"> <p class="download-cta__description">
Download TalkPro and experience a cleaner, simpler, and more modern way to stay connected. {t.description}
</p> </p>
</div> </div>
<!-- Store badges --> <div class="store-badges">
<div class="flex gap-[16px] items-center overflow-clip shrink-0"> <a
<div class="bg-[#f28a4b] border border-[#c5834e] flex gap-[8px] h-[70px] items-center overflow-clip px-[16px] py-[12px] rounded-[20px] shrink-0 w-[260px]"> class="store-badge store-badge--android store-badge--browser"
<img alt="Android" class="block shrink-0 size-[44px]" src={androidIcon} /> href={defaultApkHref}
<div class="flex flex-col gap-[3px] items-start overflow-clip shrink-0 whitespace-nowrap"> rel="noopener noreferrer"
<p class="font-semibold text-[#ffd6bc] text-[11px] tracking-[0.05px] leading-normal">ANDROID</p> target="_blank"
<p class="font-semibold text-white text-[15px] tracking-[-0.13px] leading-normal">APK Coming Soon</p> >
<div class="store-badge__icon-frame">
<img alt={t.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div> </div>
<div class="store-badge__copy">
<p class="store-badge__platform">{t.android}</p>
<p class="store-badge__label">{t.androidCta}</p>
</div> </div>
<div class="bg-[#383838] border border-[#141414] flex gap-[8px] h-[70px] items-center overflow-clip px-[16px] py-[12px] rounded-[20px] shrink-0 w-[260px]"> </a>
<div class="bg-[#151515] flex items-center justify-center rounded-[12px] shrink-0 size-[44px]"> <a
<img alt="Apple" class="block h-[27px] w-[22px]" src={appleIcon} /> class="store-badge store-badge--ios store-badge--apple"
</div> href="#"
<div class="flex flex-col gap-[3px] items-start overflow-clip shrink-0 whitespace-nowrap"> data-app-soon="1"
<p class="font-semibold text-[#ccc] text-[11px] tracking-[0.05px] leading-normal">IOS</p> >
<p class="font-semibold text-white text-[15px] tracking-[-0.13px] leading-normal">Coming on App Store</p> <div class="store-badge__icon-frame">
</div> <img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div> </div>
<div class="store-badge__copy">
<p class="store-badge__platform">{t.ios}</p>
<p class="store-badge__label">{t.iosCta}</p>
</div> </div>
</a>
</div> </div>
<!-- Right: phone art --> </div>
<div class="relative shrink-0 h-[510px] w-[418px]">
<div class="absolute inset-0 overflow-hidden pointer-events-none"> <div class="download-cta__phone">
<img alt="TalkPro on phone" class="absolute h-[136.1%] left-[3.8%] max-w-none top-[-18.05%] w-[92.43%]" src={phoneArt} /> <div class="download-cta__phone-crop">
<img alt={t.phoneAlt} class="download-cta__phone-image" src={phoneArt} />
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<script type="application/json" id="site-links-i18n" set:html={siteLinksJson} />

View File

@@ -1,65 +1,52 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations'
export interface Props {
t: Translations['experience']
}
const { t } = Astro.props
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']
--- ---
<section id="experience" class="bg-white w-full flex flex-col items-center justify-center py-[120px]"> <section id="experience" class="experience">
<div class="flex flex-col gap-[40px] items-center justify-center w-[1008px]"> <div class="experience__inner">
<p class="font-bold text-[#1a1a1a] text-[48px] text-center tracking-[-1.16px] leading-[1.2] w-full"> <div class="experience__heading">
A Cleaner, More Comfortable Messaging Experience <p class="experience__title">
{t.title}
</p> </p>
<div class="flex flex-col items-start w-full"> <p class="experience__caption">
<!-- 3 cards row --> {t.caption}
<div class="flex gap-[24px] h-[477px] items-center w-full shrink-0">
<!-- Card 1: Clear Interface -->
<div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] h-full items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] shrink-0 w-[320px]">
<div class="relative h-[232px] shrink-0 w-[320px]">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="Clear interface screenshot" class="absolute h-[298.5%] left-0 max-w-none top-[-58.58%] w-full" src="/assets/exp-card-1.png" />
</div>
</div>
<div class="flex flex-col gap-[16px] items-start overflow-clip text-center w-full">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[1.4] w-full">Clear Interface</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">A clean layout that makes conversations easy to follow.</p>
</div>
</div>
<!-- Card 2: Smooth Navigation -->
<div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] h-full items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] shrink-0 w-[320px]">
<div class="bg-[#ffeddf] flex flex-col items-start shrink-0 w-[320px]">
<div class="relative h-[232px] shrink-0 w-full">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="Smooth navigation screenshot" class="absolute h-[411.28%] left-[-5.95%] max-w-none top-[-335.6%] w-[137.79%]" src="/assets/exp-card-2.png" />
</div>
</div>
</div>
<div class="flex flex-col gap-[16px] items-start overflow-clip text-center w-full">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[1.4] w-full">Smooth Navigation</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">Move between chats, groups, and channels with familiar controls.</p>
</div>
</div>
<!-- Card 3: Refined Visual Design -->
<div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] h-full items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] shrink-0 w-[320px]">
<div class="bg-[#ffeddf] flex flex-col h-[232px] items-start overflow-clip shrink-0 w-[320px]">
<div class="flex-1 min-h-0 relative w-full">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="Refined visual design screenshot" class="absolute h-[298.5%] left-0 max-w-none top-[-99.23%] w-full" src="/assets/exp-card-3.png" />
</div>
</div>
</div>
<div class="flex flex-col gap-[16px] items-start overflow-clip text-center w-full">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[1.4] w-full">Refined Visual Design</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">Modern icons, colors, and interface details create a more polished experience.</p>
</div>
</div>
</div>
<!-- Footer caption -->
<p class="font-normal text-[#7a726d] text-[18px] text-center tracking-[-0.33px] leading-[1.5] w-full mt-0">
TalkPro is designed with clarity in mind. Every screen is built to help users focus on their conversations, reduce distractions, and move naturally between chats, groups, channels, and calls.
</p> </p>
</div> </div>
<div class="experience__body">
<div class="experience__grid">
{t.cards.map((card, index) => (
<div class="experience-card">
<div class={`experience-card__media ${index > 0 ? 'experience-card__media--tinted' : ''}`}>
<div class="experience-card__media-crop">
<img
alt={card.alt}
class={`experience-card__image ${imageClasses[index]}`}
src={images[index]}
/>
</div>
</div>
<div class="experience-card__copy">
<p class="experience-card__title">{card.title}</p>
<p class="experience-card__description">{card.desc}</p>
</div>
</div>
))}
</div>
</div>
</div> </div>
</section> </section>

View File

@@ -1,65 +1,34 @@
--- ---
const logoFull = "/assets/footer-logo.png"; import { assetPath } from '../assets'
const androidIcon = "/assets/footer-android-icon.svg"; import type { Translations } from '../i18n/translations'
const appleIcon = "/assets/footer-apple-icon.svg";
export interface Props {
t: Translations['footer']
}
const { t } = Astro.props
const logoFull = assetPath("/assets/footer-logo.png");
--- ---
<footer class="bg-[#fef0eb] w-full flex flex-col items-center justify-center py-[120px]"> <footer class="site-footer">
<div class="flex flex-col gap-[36px] items-start w-[1280px]"> <div class="site-footer__inner">
<!-- Top row --> <div class="site-footer__top">
<div class="flex items-start justify-between w-full"> <div class="site-footer__brand">
<!-- Brand col --> <div class="site-footer__logo-frame">
<div class="flex flex-col gap-[24px] items-start overflow-clip shrink-0"> <img alt={t.logoAlt} class="site-footer__logo" src={logoFull} />
<div class="relative h-[64px] w-[220px]">
<img alt="TalkPro" class="absolute inset-0 max-w-none object-cover size-full pointer-events-none" src={logoFull} />
</div> </div>
<p class="font-normal text-[#7a726d] text-[14px] tracking-[-0.09px] leading-[1.5] max-w-[320px]"> <p class="site-footer__description">
TalkPro is a modern communication app designed for messaging, group conversations, channels, voice calls, and video calls. {t.description}
</p> </p>
<!-- Store badges -->
<div class="flex gap-[16px] items-center overflow-clip shrink-0">
<div class="bg-[#f28a4b] border border-[#c5834e] flex gap-[8px] h-[70px] items-center overflow-clip px-[16px] py-[12px] rounded-[20px] shrink-0 w-[260px]">
<img alt="Android" class="block shrink-0 size-[44px]" src={androidIcon} />
<div class="flex flex-col gap-[3px] items-start overflow-clip shrink-0 whitespace-nowrap">
<p class="font-semibold text-[#ffd6bc] text-[11px] tracking-[0.05px] leading-normal">ANDROID</p>
<p class="font-semibold text-white text-[15px] tracking-[-0.13px] leading-normal">APK Coming Soon</p>
</div>
</div>
<div class="bg-[#383838] border border-[#141414] flex gap-[8px] h-[70px] items-center overflow-clip px-[16px] py-[12px] rounded-[20px] shrink-0 w-[260px]">
<div class="bg-[#151515] flex items-center justify-center rounded-[12px] shrink-0 size-[44px]">
<img alt="Apple" class="block h-[27px] w-[22px]" src={appleIcon} />
</div>
<div class="flex flex-col gap-[3px] items-start overflow-clip shrink-0 whitespace-nowrap">
<p class="font-semibold text-[#ccc] text-[11px] tracking-[0.05px] leading-normal">IOS</p>
<p class="font-semibold text-white text-[15px] tracking-[-0.13px] leading-normal">Coming on App Store</p>
</div>
</div>
</div> </div>
</div> </div>
<!-- Link cols --> <div class="site-footer__divider"></div>
<div class="flex gap-[32px] items-center leading-normal shrink-0">
<div class="flex flex-col gap-[24px] items-start overflow-clip w-[160px]">
<a href="#download" class="font-normal text-[#4a4a4a] text-[14px] whitespace-nowrap hover:text-[#f28a4b] transition-colors">Download</a>
<a href="#features" class="font-normal text-[#4a4a4a] text-[14px] whitespace-nowrap hover:text-[#f28a4b] transition-colors">Features</a>
<a href="#" class="font-normal text-[#4a4a4a] text-[14px] whitespace-nowrap hover:text-[#f28a4b] transition-colors">About</a>
</div>
<div class="flex flex-col gap-[24px] items-start overflow-clip w-[160px]">
<p class="font-medium text-[#4a4a4a] text-[14px] whitespace-nowrap">Contact</p>
<p class="font-normal text-[#4a4a4a] text-[14px] whitespace-nowrap">email@hotmail.com</p>
<p class="font-normal text-[#4a4a4a] text-[14px] whitespace-nowrap">+01 123 45562334</p>
</div>
</div>
</div>
<!-- Divider --> <div class="site-footer__bottom">
<div class="w-full h-px bg-[#e3d9d1]"></div> <p class="site-footer__legal">{t.copyright}</p>
<div class="site-footer__spacer"></div>
<!-- Bottom row --> <p class="site-footer__legal">{t.legal}</p>
<div class="flex gap-[20px] items-center overflow-clip w-full">
<p class="font-normal text-[#7a726d] text-[14px] tracking-[-0.09px] whitespace-nowrap leading-normal">© 2026 TalkPro. All rights reserved.</p>
<div class="flex-1 min-w-0"></div>
<p class="font-normal text-[#7a726d] text-[14px] tracking-[-0.09px] whitespace-nowrap leading-normal">Terms of Use · Privacy Policy · Support</p>
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -1,41 +1,181 @@
--- ---
const logoIcon = "/assets/header-logo-icon.png"; import { assetPath } from '../assets'
const logoWordmark = "/assets/header-logo-wordmark.svg"; import { getLocalePath, languageLabels, languageNames, languages, type Lang, type Translations } from '../i18n/translations'
const globeIcon = "/assets/header-globe.svg";
export interface Props {
lang: Lang
t: Translations['header']
}
const { lang, t } = Astro.props
const logoIcon = assetPath("/assets/header-logo-icon.png");
const logoWordmark = assetPath("/assets/header-logo-wordmark.svg");
const globeIcon = assetPath("/assets/header-globe.svg");
const navItems = [
{ href: '#hero', label: t.nav.home },
{ href: '#features', label: t.nav.features },
{ href: '#experience', label: t.nav.experience },
{ href: '#use-cases', label: t.nav.useCases },
{ href: '#reliability', label: t.nav.reliability },
]
const languageOptions = languages.map(item => ({
lang: item,
href: getLocalePath(item),
label: `${languageLabels[item]} - ${languageNames[item]}`,
}))
--- ---
<header class="bg-white border-b border-[#e3d9d1] w-full flex items-center justify-center h-[72px] sticky top-0 z-50"> <header id="site-header" class="site-header">
<div class="flex items-center w-[1280px]"> <div class="site-header__bar">
<!-- Logo --> <div class="site-header__brand">
<div class="flex flex-1 items-center min-w-0"> <a href={getLocalePath(lang)} class="site-logo">
<a href="/" class="relative h-[42px] w-[143px] shrink-0 block"> <div class="site-logo__icon-frame">
<div class="absolute h-[42px] left-0 top-0 w-[53px] overflow-hidden"> <img alt={t.logoIconAlt} class="site-logo__icon" src={logoIcon} />
<img alt="TalkPro icon" class="absolute h-[126.98%] left-0 max-w-none top-[-13.49%] w-full" src={logoIcon} />
</div> </div>
<div class="absolute inset-[26.97%_0_7.92%_45.45%]"> <div class="site-logo__wordmark-frame">
<img alt="TalkPro" class="absolute block inset-0 max-w-none size-full" src={logoWordmark} /> <img alt={t.logoAlt} class="site-logo__wordmark" src={logoWordmark} />
</div> </div>
</a> </a>
</div> </div>
<!-- Nav --> <nav class="site-nav" aria-label={t.navLabel}>
<nav class="flex items-center gap-[32px] shrink-0"> {navItems.map((item, index) => (
<a href="#" class="font-semibold text-[14px] text-[#f28a4b] tracking-[-0.09px] whitespace-nowrap leading-normal">Home</a> <a href={item.href} class={`site-nav__link ${index === 0 ? 'is-active' : ''}`} data-nav-link>{item.label}</a>
<a href="#features" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Features</a> ))}
<a href="#experience" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Experience</a>
<a href="#use-cases" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Use Cases</a>
<a href="#reliability" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Reliability</a>
</nav> </nav>
<!-- Actions --> <div class="site-header__actions">
<div class="flex flex-1 gap-[12px] items-center justify-end min-w-0"> <div class="language-switcher">
<button class="border border-[rgba(46,42,40,0.3)] flex gap-[8px] h-[43px] items-center justify-center pl-[12px] pr-[16px] rounded-[17px] shrink-0 hover:border-[#f28a4b] transition-colors"> <button
<img alt="Language" class="block size-[26px] object-contain" src={globeIcon} /> id="language-toggle"
<span class="font-semibold text-[14px] text-[#2e2a28] tracking-[-0.09px] whitespace-nowrap leading-[14px]">EN</span> class="language-switcher__button"
type="button"
aria-label={t.languageAlt}
aria-expanded="false"
aria-controls="language-menu"
>
<img alt="" class="language-switcher__icon" src={globeIcon} />
<span class="language-switcher__current">{languageLabels[lang]}</span>
</button> </button>
<a href="#download" class="bg-[#f28a4b] flex items-center justify-center px-[22px] py-[13px] rounded-[17px] shrink-0 hover:bg-[#e07a3b] transition-colors"> <div id="language-menu" class="language-switcher__menu is-hidden" aria-hidden="true">
<span class="font-bold text-[14px] text-white leading-normal whitespace-nowrap">Download</span> {languageOptions.map(option => (
<a
href={option.href}
class={`language-switcher__option ${option.lang === lang ? 'is-active' : ''}`}
aria-current={option.lang === lang ? 'page' : undefined}
>
{option.label}
</a> </a>
))}
</div> </div>
</div> </div>
<a href="#download" class="site-header__download">
<span class="site-header__download-label">{t.download}</span>
</a>
<button
id="menu-toggle"
class="menu-toggle"
type="button"
aria-label={t.openMenu}
aria-expanded="false"
aria-controls="mobile-nav"
data-open-label={t.openMenu}
data-close-label={t.closeMenu}
>
<span id="bar-1" class="menu-toggle__bar"></span>
<span id="bar-2" class="menu-toggle__bar"></span>
<span id="bar-3" class="menu-toggle__bar"></span>
</button>
</div>
</div>
<div id="mobile-nav" class="mobile-nav is-hidden" aria-hidden="true">
<ul class="mobile-nav__list">
{navItems.map((item, index) => (
<li><a href={item.href} class={`mobile-nav__link ${index === navItems.length - 1 ? 'mobile-nav__link--last' : ''}`} data-nav-link>{item.label}</a></li>
))}
<li class="mobile-nav__languages">
{languages.map(item => (
<a href={getLocalePath(item)} class={`mobile-nav__language-link ${item === lang ? 'is-active' : ''}`} aria-current={item === lang ? 'page' : undefined}>
{languageNames[item]}
</a>
))}
</li>
<li class="mobile-nav__download-item">
<a href="#download" class="mobile-nav__download">
{t.download}
</a>
</li>
</ul>
</div>
</header> </header>
<script>
const toggle = document.getElementById('menu-toggle') as HTMLButtonElement;
const nav = document.getElementById('mobile-nav') as HTMLDivElement;
const bar1 = document.getElementById('bar-1') as HTMLSpanElement;
const bar2 = document.getElementById('bar-2') as HTMLSpanElement;
const bar3 = document.getElementById('bar-3') as HTMLSpanElement;
const languageToggle = document.getElementById('language-toggle') as HTMLButtonElement | null;
const languageMenu = document.getElementById('language-menu') as HTMLDivElement | null;
function closeLanguageMenu() {
if (!languageToggle || !languageMenu) return;
languageToggle.setAttribute('aria-expanded', 'false');
languageMenu.classList.add('is-hidden');
languageMenu.setAttribute('aria-hidden', 'true');
}
function openLanguageMenu() {
if (!languageToggle || !languageMenu) return;
languageToggle.setAttribute('aria-expanded', 'true');
languageMenu.classList.remove('is-hidden');
languageMenu.removeAttribute('aria-hidden');
}
function openMenu() {
toggle.setAttribute('aria-expanded', 'true');
toggle.setAttribute('aria-label', toggle.dataset.closeLabel || 'Close menu');
nav.classList.remove('is-hidden');
nav.removeAttribute('aria-hidden');
bar1.style.transform = 'translateY(7px) rotate(45deg)';
bar2.style.opacity = '0';
bar3.style.transform = 'translateY(-7px) rotate(-45deg)';
}
function closeMenu() {
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-label', toggle.dataset.openLabel || 'Open menu');
nav.classList.add('is-hidden');
nav.setAttribute('aria-hidden', 'true');
bar1.style.transform = '';
bar2.style.opacity = '';
bar3.style.transform = '';
}
toggle.addEventListener('click', () => {
toggle.getAttribute('aria-expanded') === 'true' ? closeMenu() : openMenu();
});
languageToggle?.addEventListener('click', event => {
event.stopPropagation();
languageToggle.getAttribute('aria-expanded') === 'true' ? closeLanguageMenu() : openLanguageMenu();
});
languageMenu?.addEventListener('click', event => event.stopPropagation());
document.addEventListener('click', closeLanguageMenu);
document.addEventListener('keydown', event => {
if (event.key === 'Escape') closeLanguageMenu();
});
nav.querySelectorAll('a').forEach(a => a.addEventListener('click', closeMenu));
window.addEventListener('resize', () => {
if (window.innerWidth >= 1024) closeMenu();
if (window.innerWidth < 1024) closeLanguageMenu();
});
</script>

View File

@@ -1,57 +1,82 @@
--- ---
const heroBg = "/assets/hero-bg.png"; import { assetPath } from '../assets'
const phoneMockup = "/assets/hero-phone.png"; import type { Translations } from '../i18n/translations'
export interface Props {
t: Translations['hero']
download: Translations['download']
}
const { t, download } = Astro.props
const heroBg = assetPath("/assets/hero-bg.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 class="relative w-full flex items-start justify-center h-[891px] overflow-hidden"> <section id="hero" class="hero">
<img alt="" class="absolute inset-0 max-w-none object-cover size-full pointer-events-none" src={heroBg} /> <img alt="" class="hero__bg" src={heroBg} />
<div class="relative flex gap-[40px] items-start w-[1280px] h-full"> <div class="hero__inner">
<!-- Phone mockup --> <div class="hero__phone-column">
<div class="flex flex-1 items-center min-w-0 pt-[60px] h-full"> <div class="hero__phone-frame">
<div class="relative w-full" style="aspect-ratio: 673/1108;"> <div class="hero__phone-crop">
<div class="absolute inset-0 overflow-hidden pointer-events-none"> <img alt={t.phoneAlt} class="hero__phone" src={phoneMockup} />
<img alt="TalkPro app on iPhone" class="absolute h-[114.36%] left-[-2.67%] max-w-none top-[-7.18%] w-[105.35%]" src={phoneMockup} />
</div> </div>
</div> </div>
</div> </div>
<!-- Hero copy --> <div class="hero__content">
<div class="flex flex-col gap-[60px] h-full items-start justify-center overflow-clip shrink-0 w-[660px]"> <div class="hero__badge">
<!-- Pill --> <p class="hero__badge-text">{t.badge}</p>
<div class="bg-white border border-[#f08458] flex items-center overflow-clip px-[16px] py-[12px] rounded-[999px] shrink-0">
<p class="font-bold text-[#0d0d0d] text-[14px] tracking-[-0.09px] leading-normal">✦&nbsp; Available on iOS and Android</p>
</div> </div>
<div class="flex flex-col gap-[24px] items-start shrink-0 w-full"> <div class="hero__copy">
<div class="font-bold text-[#2e2a28] text-[72px] tracking-[-1.61px] w-full"> <div class="hero__title">
<p class="leading-[1.1] mb-0">A Modern Way to</p> <p class="hero__title-line">{t.titleLine1}</p>
<p class="leading-[1.1]">Stay Connected</p> <p class="hero__title-line">{t.titleLine2}</p>
</div> </div>
<p class="font-normal text-[#7a726d] text-[18px] tracking-[-0.33px] leading-[1.5] w-full"> <p class="hero__description">
TalkPro is a modern messaging app designed for clear, simple, and reliable communication. From private chats to group conversations, channels, voice calls, and video calls, TalkPro helps people stay connected in one familiar experience. {t.description}
</p> </p>
<div class="flex gap-[14px] items-center overflow-clip shrink-0"> <div class="hero__actions">
<a href="#download" class="bg-[#f28a4b] flex items-center justify-center overflow-clip px-[24px] py-[14px] rounded-[17px] shrink-0 hover:bg-[#e07a3b] transition-colors"> <a
<span class="font-bold text-white text-[15px] tracking-[-0.13px] leading-normal">Download TalkPro ↓</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="bg-[rgba(255,255,255,0.55)] border border-[#e3d9d1] flex items-center justify-center overflow-clip px-[24px] py-[14px] rounded-[17px] shrink-0 hover:bg-white transition-colors"> <a
<span class="font-bold text-[#2e2a28] text-[15px] tracking-[-0.13px] leading-normal">Explore Features →</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>
<!-- Tags --> <div class="hero__tags">
<div class="flex gap-[10px] items-center overflow-clip shrink-0"> {t.tags.map(tag => (
<div class="bg-[rgba(255,255,255,0.68)] border border-white flex items-center overflow-clip px-[14px] py-[9px] rounded-[999px] shrink-0"> <div class="hero__tag">
<span class="font-bold text-[#7a726d] text-[14px] tracking-[-0.09px] whitespace-nowrap leading-normal">Identity Layer</span> <span class="hero__tag-text">{tag}</span>
</div>
<div class="bg-[rgba(255,255,255,0.68)] border border-white flex items-center overflow-clip px-[14px] py-[9px] rounded-[999px] shrink-0">
<span class="font-bold text-[#7a726d] text-[14px] tracking-[-0.09px] whitespace-nowrap leading-normal">AI Native Messaging</span>
</div>
<div class="bg-[rgba(255,255,255,0.68)] border border-white flex items-center overflow-clip px-[14px] py-[9px] rounded-[999px] shrink-0">
<span class="font-bold text-[#7a726d] text-[14px] tracking-[-0.09px] whitespace-nowrap leading-normal">Adaptive Privacy</span>
</div> </div>
))}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,96 +1,62 @@
--- ---
import { assetPath } from '../assets'
import type { Translations } from '../i18n/translations'
export interface Props {
t: Translations['trust']
}
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 = [
'trust-card__icon--one',
'trust-card__icon--two',
'trust-card__icon--three',
'trust-card__icon--four',
]
--- ---
<section id="reliability" class="bg-white w-full flex flex-col items-center justify-center p-[120px]"> <section id="reliability" class="trust">
<div class="flex flex-col gap-[40px] items-start w-[1280px]"> <div class="trust__inner">
<div class="trust__header">
<!-- Header --> <div class="section-eyebrow">
<div class="flex flex-col gap-[24px] items-start overflow-clip w-full"> <span class="section-eyebrow__text">{t.eyebrow}</span>
<div class="bg-white border border-[#fbbfa3] flex items-center justify-center px-[24px] py-[12px] rounded-[9999px] shrink-0">
<span class="font-bold text-[#f08458] text-[14px] text-center tracking-[-0.04px] whitespace-nowrap leading-normal">RELIABILITY</span>
</div> </div>
<p class="font-bold text-[#1a1a1a] text-[48px] tracking-[-1.16px] leading-[1.2] w-full">Built with User Trust in Mind</p> <p class="trust__title">{t.title}</p>
<p class="font-normal text-[#7a726d] text-[18px] tracking-[-0.33px] leading-[1.5] w-full"> <p class="trust__description">
TalkPro is designed with a privacy-conscious approach and a focus on dependable communication. The app keeps the user experience simple while supporting the core communication features people expect from a modern messaging platform. {t.description}
</p> </p>
</div> </div>
<!-- 4-column feature row --> <div class="trust__grid">
<div class="flex gap-[24px] items-center justify-center w-full"> {t.cards.map((card, index) => (
<>
<!-- Card 1: Privacy-Conscious Design --> <div class="trust-card">
<div class="flex flex-1 flex-col gap-[16px] items-center justify-end min-w-0 p-[24px] rounded-[30px]"> <div class="trust-card__icon-frame">
<div class="relative shrink-0 size-[128px]"> <div class="trust-card__icon-crop">
<div class="absolute inset-0 overflow-hidden pointer-events-none"> <img
<img alt="" class="absolute h-[174.55%] left-[-31.48%] max-w-none top-[7.04%] w-[312.73%]" src="/assets/trust-icon-sprite.png" /> alt=""
class={`trust-card__icon ${iconClasses[index]}`}
src={index === 3 ? trustIconImprovement : trustIconSprite}
/>
</div> </div>
</div> </div>
<div class="flex flex-col gap-[8px] items-start w-full"> <div class="trust-card__copy">
<p class="font-semibold text-[#0d0d0d] text-[16px] leading-[22px] w-full">Privacy-Conscious Design</p> <p class="trust-card__title">{card.title}</p>
<p class="font-medium text-[#7a726d] text-[15px] leading-[1.5] w-full">Built with careful consideration for user communication and data handling.</p> <p class="trust-card__description">{card.desc}</p>
</div> </div>
</div> </div>
{index < t.cards.length - 1 && (
<!-- Divider --> <div class="trust__divider">
<div class="relative h-[118px] shrink-0 w-0"> <div class="trust__divider-frame">
<div class="absolute inset-[0_-0.5px]"> <img alt="" class="trust__divider-image" src={trustDivider} />
<img alt="" class="block max-w-none size-full" src="/assets/trust-divider.svg" />
</div> </div>
</div> </div>
)}
<!-- Card 2: Minimal and Focused --> </>
<div class="flex flex-1 flex-col gap-[16px] items-center justify-end min-w-0 p-[24px] rounded-[30px]"> ))}
<div class="relative shrink-0 size-[128px]">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="" class="absolute h-[187.32%] left-[-164.72%] max-w-none top-[3.1%] w-[335.61%]" src="/assets/trust-icon-sprite.png" />
</div>
</div>
<div class="flex flex-col gap-[8px] items-start w-full">
<p class="font-semibold text-[#0d0d0d] text-[16px] leading-[22px] w-full">Minimal and Focused</p>
<p class="font-medium text-[#7a726d] text-[15px] leading-[1.5] w-full">Designed around essential messaging features without unnecessary complexity.</p>
</div>
</div>
<!-- Divider -->
<div class="relative h-[118px] shrink-0 w-0">
<div class="absolute inset-[0_-0.5px]">
<img alt="" class="block max-w-none size-full" src="/assets/trust-divider.svg" />
</div>
</div>
<!-- Card 3: Reliable Experience -->
<div class="flex flex-1 flex-col gap-[16px] items-center justify-end min-w-0 p-[24px] rounded-[30px]">
<div class="relative shrink-0 size-[128px]">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="" class="absolute h-[211.46%] left-[-187.93%] max-w-none top-[-105.62%] w-[378.86%]" src="/assets/trust-icon-sprite.png" />
</div>
</div>
<div class="flex flex-col gap-[8px] items-start w-full">
<p class="font-semibold text-[#0d0d0d] text-[16px] leading-[22px] w-full">Reliable Experience</p>
<p class="font-medium text-[#7a726d] text-[15px] leading-[1.5] w-full">Focused on providing a stable and familiar communication experience.</p>
</div>
</div>
<!-- Divider -->
<div class="relative h-[118px] shrink-0 w-0">
<div class="absolute inset-[0_-0.5px]">
<img alt="" class="block max-w-none size-full" src="/assets/trust-divider.svg" />
</div>
</div>
<!-- Card 4: Continuous Improvement -->
<div class="flex flex-1 flex-col gap-[16px] items-center justify-end min-w-0 p-[24px] rounded-[30px]">
<div class="relative shrink-0 size-[128px]">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="" class="absolute left-[4.14%] max-w-none size-full top-0" src="/assets/trust-icon-improvement.png" />
</div>
</div>
<div class="flex flex-col gap-[8px] items-start w-full">
<p class="font-semibold text-[#0d0d0d] text-[16px] leading-[22px] w-full">Continuous Improvement</p>
<p class="font-medium text-[#7a726d] text-[15px] leading-[1.5] w-full">TalkPro will continue to improve its interface, features, and overall user experience.</p>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,36 +1,35 @@
--- ---
const rows = [ import type { Translations } from '../i18n/translations'
{ title: 'Personal Conversations', desc: 'Message friends, family, and close contacts in a simple private chat experience.' },
{ title: 'Communities', desc: 'Join group conversations and stay active in shared interest spaces.' }, export interface Props {
{ title: 'Teams and Projects', desc: 'Coordinate discussions, updates, and quick decisions in group chats.' }, t: Translations['useCases']
{ title: 'News and Updates', desc: 'Follow channels for announcements, information, and community content.' }, }
]
const { t } = Astro.props
--- ---
<section id="use-cases" class="bg-[#fef0eb] w-full flex items-start justify-center px-[130px] py-[120px]"> <section id="use-cases" class="use-cases">
<div class="flex gap-[40px] items-center w-[1280px]"> <div class="use-cases__inner">
<!-- Left: heading --> <div class="use-cases__copy">
<div class="flex flex-col gap-[24px] items-start overflow-clip shrink-0 w-[540px]"> <div class="section-eyebrow">
<div class="bg-white border border-[#fbbfa3] flex items-center justify-center px-[24px] py-[12px] rounded-[9999px] shrink-0"> <span class="section-eyebrow__text">{t.eyebrow}</span>
<span class="font-bold text-[#f08458] text-[14px] text-center tracking-[-0.04px] whitespace-nowrap leading-normal">USE CASES</span>
</div> </div>
<p class="font-bold text-[#1a1a1a] text-[48px] tracking-[-1.16px] leading-[1.2] w-full"> <p class="use-cases__title">
Made for Personal, Social, and Community Communication {t.title}
</p> </p>
<p class="font-normal text-[#7a726d] text-[18px] tracking-[-0.33px] leading-[1.5] w-full"> <p class="use-cases__description">
With separate spaces for every context, TalkPro keeps your personal, social, and professional communications distinct and organized. {t.description}
</p> </p>
</div> </div>
<!-- Right: rows --> <div class="use-cases__rows">
<div class="flex flex-1 flex-col items-start min-w-0 overflow-clip rounded-[30px] gap-px"> {t.rows.map(row => (
{rows.map(row => ( <div class="use-case-row">
<div class="bg-[#faede8] flex h-[120px] items-center overflow-clip w-full shrink-0"> <div class="use-case-row__title-cell">
<div class="bg-[#f08458] flex h-full items-center px-[36px] py-[24px] shrink-0 w-[300px]"> <p class="use-case-row__title">{row.title}</p>
<p class="flex-1 font-semibold text-[20px] text-white tracking-[-0.6px] leading-normal min-w-0">{row.title}</p>
</div> </div>
<div class="bg-white flex flex-1 h-full items-center min-w-0 px-[36px] py-[24px]"> <div class="use-case-row__description-cell">
<p class="font-medium text-[#7a726d] text-[15px] leading-[1.5] flex-1 min-w-0">{row.desc}</p> <p class="use-case-row__description">{row.desc}</p>
</div> </div>
</div> </div>
))} ))}

View File

@@ -1,89 +1,73 @@
--- ---
const underline = "/assets/why-underline.svg"; import { assetPath } from '../assets'
const iconSimple = "/assets/why-icon-simple.svg"; import type { Translations } from '../i18n/translations'
const iconFamiliar = "/assets/why-icon-familiar.svg";
const iconConn = "/assets/why-icon-connected.svg"; export interface Props {
const iconModern = "/assets/why-icon-modern.svg"; t: Translations['why']
}
const { t } = Astro.props
const underline = assetPath("/assets/why-underline.svg");
const icons = [
assetPath("/assets/why-icon-simple.svg"),
assetPath("/assets/why-icon-familiar.svg"),
assetPath("/assets/why-icon-connected.svg"),
assetPath("/assets/why-icon-modern.svg"),
]
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="bg-white w-full flex flex-col items-center justify-center py-[120px]"> <section class="why">
<div class="flex flex-col gap-[40px] items-start w-[1280px]"> <div class="why__inner">
<div class="why__intro">
<!-- Header row --> <div class="why__copy">
<div class="flex gap-[36px] items-start overflow-clip w-full"> <div class="section-eyebrow">
<!-- Left: pill + heading + description --> <span class="section-eyebrow__text">{t.eyebrow}</span>
<div class="flex flex-1 flex-col gap-[36px] items-start min-w-0">
<div class="bg-white border border-[#fbbfa3] flex items-center justify-center px-[24px] py-[12px] rounded-[9999px] shrink-0">
<span class="font-bold text-[#f08458] text-[14px] text-center tracking-[-0.04px] whitespace-nowrap leading-normal">WHY TALKPRO</span>
</div> </div>
<div class="flex flex-col gap-[24px] items-start w-full"> <div class="why__text">
<div class="font-bold text-[#1a1a1a] text-[48px] tracking-[-1.16px] w-full"> <div class="why__title">
<p class="leading-[1.2] mb-0">Designed for the Way</p> <p class="why__title-line">{t.titleLine1}</p>
<p class="leading-[1.2]">People Communicate Today</p> <p class="why__title-line">{t.titleLine2}</p>
</div> </div>
<!-- Underline decoration --> <div class="why__underline">
<div class="relative h-0 w-[295.5px] shrink-0"> <div class="why__underline-frame">
<div class="absolute inset-[-0.5px_0]"> <img alt="" class="why__underline-image" src={underline} />
<img alt="" class="block max-w-none size-full" src={underline} />
</div> </div>
</div> </div>
<p class="font-normal text-[#7a726d] text-[18px] tracking-[-0.33px] leading-[1.5] w-full"> <p class="why__description">
Communication today happens across personal conversations, communities, work groups, and content channels. TalkPro brings these essential communication experiences together in a simple and familiar interface, making it easier for users to connect, share, and stay updated. {t.description}
</p> </p>
</div> </div>
</div> </div>
<!-- Right: illustration --> <div class="why__illustration">
<div class="relative shrink-0 size-[480px]"> <video
<img alt="People communicating with TalkPro" class="absolute inset-0 size-full object-contain" src="/assets/why-illustration.png" /> aria-label={t.illustrationAlt}
class="why__illustration-image"
autoplay
loop
muted
playsinline
preload="metadata"
>
<source src={illustrationVideo} type="video/mp4" />
</video>
</div> </div>
</div> </div>
<!-- 2×2 card grid --> <div class="why__grid">
<div class="flex flex-col gap-[24px] items-start overflow-clip w-full"> {t.cards.map((card, index) => (
<!-- Row 1 --> <div class="why-card">
<div class="flex gap-[24px] items-center w-full"> <div class="why-card__icon-frame">
<div class="bg-[#fef0eb] border border-[#e8e4de] flex flex-1 gap-[24px] h-[152px] items-center min-w-0 overflow-clip p-[36px] rounded-[30px]"> <img alt="" class={`why-card__icon ${iconClasses[index]}`} src={icons[index]} />
<div class="bg-[#f08458] aspect-square h-full flex items-center justify-center overflow-clip rounded-[9999px] shrink-0">
<img alt="" class="block size-[44px]" src={iconSimple} />
</div> </div>
<div class="flex flex-1 flex-col gap-[12px] items-start min-w-0 overflow-clip"> <div class="why-card__copy">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[20px] w-full">Simple</p> <p class="why-card__title">{card.title}</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">An easy-to-use experience designed for everyday communication.</p> <p class="why-card__description">{card.desc}</p>
</div> </div>
</div> </div>
<div class="bg-[#fef0eb] border border-[#e8e4de] flex flex-1 gap-[24px] h-[152px] items-center min-w-0 overflow-clip p-[36px] rounded-[30px]"> ))}
<div class="bg-[#f08458] aspect-square h-full flex items-center justify-center overflow-clip rounded-[9999px] shrink-0">
<img alt="" class="block" style="width:38px;height:40px;" src={iconFamiliar} />
</div> </div>
<div class="flex flex-1 flex-col gap-[12px] items-start min-w-0 overflow-clip">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[20px] w-full">Familiar</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">A messaging structure that feels natural from the first use.</p>
</div>
</div>
</div>
<!-- Row 2 -->
<div class="flex gap-[24px] items-center w-full">
<div class="bg-[#fef0eb] border border-[#e8e4de] flex flex-1 gap-[24px] h-[152px] items-center min-w-0 overflow-clip p-[36px] rounded-[30px]">
<div class="bg-[#f08458] aspect-square h-full flex items-center justify-center overflow-clip rounded-[9999px] shrink-0">
<img alt="" class="block size-[44px]" src={iconConn} />
</div>
<div class="flex flex-1 flex-col gap-[12px] items-start min-w-0 overflow-clip">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[20px] w-full">Connected</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">Private chats, groups, channels, voice, and video in one app.</p>
</div>
</div>
<div class="bg-[#fef0eb] border border-[#e8e4de] flex flex-1 gap-[24px] h-[152px] items-center min-w-0 overflow-clip p-[36px] rounded-[30px]">
<div class="bg-[#f08458] aspect-square h-full flex items-center justify-center overflow-clip rounded-[9999px] shrink-0">
<img alt="" class="block" style="width:24px;height:44px;" src={iconModern} />
</div>
<div class="flex flex-1 flex-col gap-[12px] items-start min-w-0 overflow-clip">
<p class="font-semibold text-[#0d0d0d] text-[24px] tracking-[-0.47px] leading-[20px] w-full">Modern</p>
<p class="font-medium text-[#7a726d] text-[16px] tracking-[-0.18px] leading-[1.5] w-full">A refined interface with clean visuals and smooth interaction.</p>
</div>
</div>
</div>
</div>
</div> </div>
</section> </section>

2738
src/i18n/translations.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,22 +3,106 @@ import '../styles/global.css'
export interface Props { export interface Props {
title?: string title?: string
description?: string
lang?: string
} }
const { title = 'Talk Pro — One User. Multiple Worlds.' } = Astro.props
const {
title = 'Talk Pro - One User. Multiple Worlds.',
description = 'Talk Pro is a modern messaging app for private chats, group conversations, channels, voice and video calls.',
lang = 'en',
} = Astro.props
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang={lang}>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Talk Pro is a modern messaging app for private chats, group conversations, channels, voice and video calls." /> <meta name="description" content={description} />
<title>{title}</title> <title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</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>
(() => {
const header = document.getElementById('site-header');
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 => {
link.addEventListener('click', e => {
const href = link.getAttribute('href');
if (!href || href === '#') return;
const target = document.querySelector(href);
if (!target) return;
e.preventDefault();
const top = target.getBoundingClientRect().top + window.scrollY - getOffset();
animateScrollTo(top);
history.pushState(null, '', href);
});
});
const navLinks = document.querySelectorAll('[data-nav-link]');
const sections = Array.from(navLinks)
.map(l => document.querySelector(l.getAttribute('href') ?? ''))
.filter(Boolean) as Element[];
if ('IntersectionObserver' in window && sections.length) {
const observer = new IntersectionObserver(entries => {
const visible = entries
.filter(e => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (!visible) return;
navLinks.forEach(link => {
const active = link.getAttribute('href') === `#${visible.target.id}`;
link.classList.toggle('is-active', active);
});
}, { rootMargin: '-30% 0px -60% 0px', threshold: 0 });
sections.forEach(s => observer.observe(s));
}
})();
</script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,36 @@
---
import Base from '../../layouts/Base.astro'
import Header from '../../components/Header.astro'
import Hero from '../../components/Hero.astro'
import WhyTalkPro from '../../components/WhyTalkPro.astro'
import CoreSystem from '../../components/CoreSystem.astro'
import Experience from '../../components/Experience.astro'
import UseCases from '../../components/UseCases.astro'
import Trust from '../../components/Trust.astro'
import DownloadCTA from '../../components/DownloadCTA.astro'
import Footer from '../../components/Footer.astro'
import { defaultLang, getTranslations, isLang, languages } from '../../i18n/translations'
export function getStaticPaths() {
return languages
.filter(lang => lang !== defaultLang)
.map(lang => ({ params: { lang } }))
}
const currentLang = Astro.params.lang
const lang = isLang(currentLang) ? currentLang : defaultLang
const t = getTranslations(lang)
---
<Base lang={lang} title={t.meta.title} description={t.meta.description}>
<Header lang={lang} t={t.header} />
<Hero t={t.hero} download={t.download} />
<WhyTalkPro t={t.why} />
<CoreSystem t={t.core} />
<Experience t={t.experience} />
<UseCases t={t.useCases} />
<Trust t={t.trust} />
<!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} />
</Base>

View File

@@ -7,20 +7,23 @@ 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'
const lang = defaultLang
const t = getTranslations(lang)
--- ---
<Base> <Base lang={lang} title={t.meta.title} description={t.meta.description}>
<Header /> <Header lang={lang} t={t.header} />
<Hero /> <Hero t={t.hero} download={t.download} />
<WhyTalkPro /> <WhyTalkPro t={t.why} />
<CoreSystem /> <CoreSystem t={t.core} />
<Experience /> <Experience t={t.experience} />
<UseCases /> <UseCases t={t.useCases} />
<Trust /> <Trust t={t.trust} />
<AppPreview /> <!-- AppPreview section disabled per lead request. -->
<DownloadCTA /> <DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer /> <Footer t={t.footer} />
</Base> </Base>

344
src/styles/download.css Normal file
View File

@@ -0,0 +1,344 @@
.download-cta {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 400px;
overflow: hidden;
background: #fff;
border-top: 1px solid #eec8b8;
}
.download-cta__pattern {
position: absolute;
top: 50%;
left: 50%;
width: 1920px;
max-width: none;
height: 923px;
pointer-events: none;
transform: translate(-50%, -50%);
}
.download-cta__inner {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 1280px;
gap: 0px;
margin: 0 auto;
padding: 60px 16px;
}
.download-cta__content {
display: flex;
order: 2;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 0;
gap: 36px;
overflow: clip;
text-align: center;
}
.download-cta__copy {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 16px;
}
.download-cta__heading {
--download-heading-title-size: 32px;
display: flex;
flex-wrap: nowrap;
align-items: center;
flex-shrink: 0;
gap: 16px;
max-width: 100%;
}
.download-cta__title {
position: relative;
top: -4px;
margin: 0;
flex-shrink: 0;
font-size: var(--download-heading-title-size);
font-weight: 700;
line-height: 1.2;
letter-spacing: var(--ls-32);
color: #1a1a1a;
white-space: nowrap;
}
.download-cta__logo-frame {
position: relative;
flex-shrink: 0;
width: calc(var(--download-heading-title-size) * 3.9167);
height: calc(var(--download-heading-title-size) * 1.5);
}
.download-cta__logo {
position: absolute;
inset: 0;
display: block;
width: 100%;
max-width: none;
height: 100%;
}
.download-cta__description {
max-width: 542px;
margin: 0;
font-size: 16px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-16);
color: #7a726d;
text-align: center;
}
.store-badges {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 100%;
gap: 16px;
overflow: clip;
}
.store-badge {
display: flex;
align-items: center;
flex-shrink: 0;
width: 260px;
max-width: 100%;
height: 70px;
gap: 8px;
padding: 12px 16px;
overflow: clip;
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 {
background: #f28a4b;
border: 1px solid #c5834e;
}
.store-badge--ios {
background: #121212F0;
border: 1px solid #2C2C2C;
}
.store-badge__icon {
display: block;
flex-shrink: 0;
width: 44px;
height: 44px;
}
.store-badge__icon-frame {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 44px;
height: 44px;
background: #323232;
border-radius: 12px;
}
.store-badge__apple-icon {
display: block;
width: 22px;
height: 27px;
}
.store-badge--android .store-badge__icon-frame {
background: #d55f31;
}
.store-badge__android-icon {
display: block;
width: 28px;
height: 28px;
}
.store-badge__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-shrink: 0;
gap: 3px;
overflow: clip;
white-space: nowrap;
}
.store-badge__platform {
margin: 0;
font-size: 13px;
font-weight: 600;
line-height: normal;
letter-spacing: var(--ls-13);
}
.store-badge--android .store-badge__platform {
color: #ffd6bc;
}
.store-badge--ios .store-badge__platform {
color: #949494;
}
.store-badge__label {
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: normal;
letter-spacing: var(--ls-15);
color: #fff;
}
.download-cta__phone {
position: relative;
display: block;
order: 1;
flex-shrink: 0;
width: 240px;
height: 292px;
}
@media (min-width: 578px) {
.download-cta__phone {
width: min(418px, calc(100vw - 32px));
height: auto;
aspect-ratio: 418 / 510;
}
}
.download-cta__phone-crop {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.download-cta__phone-image {
position: absolute;
top: -18.05%;
left: 3.8%;
width: 92.43%;
max-width: none;
height: 136.1%;
}
@media (max-width: 397px) {
.download-cta__heading {
--download-heading-title-size: clamp(24px, 8vw, 32px);
gap: clamp(8px, 2.5vw, 12px);
}
.download-cta__title {
line-height: 1;
}
}
@media (max-width: 569px) {
.store-badge {
width: min(100%, calc(100vw - 32px));
}
}
@media (min-width: 570px) {
.download-cta__heading {
--download-heading-title-size: 40px;
}
.download-cta__title {
letter-spacing: var(--ls-40);
}
.download-cta__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
.store-badges {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
}
@media (min-width: 1024px) {
.download-cta {
height: 600px;
}
.download-cta__inner {
flex-direction: row;
gap: 16px;
padding: 0 24px;
}
.download-cta__content {
order: 1;
align-items: flex-start;
text-align: left;
}
.download-cta__copy {
align-items: flex-start;
}
.download-cta__description {
text-align: left;
}
.store-badges {
justify-content: flex-start;
}
.download-cta__heading {
--download-heading-title-size: 48px;
}
.download-cta__title {
letter-spacing: var(--ls-48);
}
.download-cta__phone {
order: 2;
width: 418px;
}
}
@media (min-width: 1280px) {
.download-cta__inner {
padding: 0;
}
}

209
src/styles/features.css Normal file
View File

@@ -0,0 +1,209 @@
.features {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 60px;
padding: 60px 0;
overflow: hidden;
}
.features__bg {
position: absolute;
top: 50%;
left: 50%;
width: 2363px;
height: 1319px;
object-fit: cover;
opacity: 0.2;
pointer-events: none;
transform: translate(-50%, -50%);
}
.features__header {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 100%;
max-width: 1280px;
gap: 24px;
margin: 0 auto;
padding: 0 16px;
overflow: clip;
}
.features__title {
width: 100%;
margin: 0;
font-size: 28px;
font-weight: 700;
line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a;
text-align: center;
}
.features__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
text-align: center;
}
.features__grid {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr);
width: 100%;
max-width: 1280px;
gap: 22px;
margin: 0 auto;
padding: 0 16px;
}
.feature-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
padding: 36px;
background: rgba(255, 255, 255, 0.78);
border-radius: 30px;
}
.feature-card__icon-frame {
position: relative;
flex-shrink: 0;
width: 160px;
height: 160px;
overflow: hidden;
}
.feature-card__icon {
position: absolute;
inset: 0;
width: 100%;
max-width: none;
height: 100%;
object-fit: contain;
}
.feature-card__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 12px;
text-align: center;
}
.feature-card__title {
width: 100%;
margin: 0;
font-size: 20px;
font-weight: 700;
line-height: normal;
letter-spacing: var(--ls-20);
color: #2e2a28;
}
.feature-card__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
}
@media (min-width: 440px) {
.features__header,
.features__grid {
padding-left: 20px;
padding-right: 20px;
}
.features__title {
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 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.feature-card__title {
font-size: 24px;
letter-spacing: var(--ls-24);
}
}
@media (min-width: 1024px) {
.features__header {
padding: 0 clamp(36px, 14vw, 180px);
}
.features__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
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);
}
}
@media (min-width: 1280px) {
.features__grid {
padding: 0;
}
}
@media (min-width: 1376px) {
.features {
padding-top: 120px;
padding-bottom: 120px;
}
}

131
src/styles/footer.css Normal file
View File

@@ -0,0 +1,131 @@
.site-footer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 60px 0;
background: #fef0eb;
}
.site-footer__inner {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 1280px;
gap: 36px;
margin: 0 auto;
padding: 0 16px;
}
.site-footer__top {
display: flex;
flex-direction: column;
width: 100%;
gap: 40px;
}
.site-footer__brand {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
min-width: 0;
gap: 24px;
overflow: clip;
}
.site-footer__logo-frame {
position: relative;
width: 220px;
height: 64px;
}
.site-footer__logo {
position: absolute;
inset: 0;
width: 100%;
max-width: none;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.site-footer__description {
max-width: 320px;
margin: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-14);
color: #7a726d;
}
.site-footer__divider {
width: 100%;
height: 1px;
background: #e3d9d1;
}
.site-footer__bottom {
display: flex;
flex-direction: column;
width: 100%;
gap: 12px;
overflow: clip;
}
.site-footer__legal {
margin: 0;
font-size: 14px;
font-weight: 400;
line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d;
}
.site-footer__spacer {
display: none;
}
@media (min-width: 768px) {
.site-footer__bottom {
flex-direction: row;
align-items: center;
gap: 20px;
}
.site-footer__spacer {
display: block;
flex: 1;
min-width: 0;
}
}
@media (min-width: 1024px) {
.site-footer {
padding: 60px 0;
}
.site-footer__inner {
padding: 0 24px;
}
.site-footer__top {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
gap: 0;
}
}
@media (min-width: 1280px) {
.site-footer__inner {
padding: 0;
}
.site-footer__legal {
white-space: nowrap;
}
}

View File

@@ -1 +1,79 @@
/* UnoCSS handles reset and utilities via astro.config.mjs injectReset */ /* UnoCSS handles reset and utilities via astro.config.mjs injectReset */
@import './header.css';
@import './hero.css';
@import './sections.css';
@import './download.css';
@import './footer.css';
@import './why.css';
@import './features.css';
@import './trust.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;
}

354
src/styles/header.css Normal file
View File

@@ -0,0 +1,354 @@
.site-header {
position: sticky;
top: 0;
z-index: 50;
width: 100%;
background: #fff;
border-bottom: 1px solid #e3d9d1;
}
.site-header__bar {
display: flex;
align-items: center;
width: 100%;
max-width: 1280px;
height: 72px;
gap: 24px;
margin: 0 auto;
padding: 0 16px;
}
.site-header__brand,
.site-header__actions {
display: flex;
flex: 1;
min-width: 0;
}
.site-header__brand {
align-items: center;
}
.site-header__actions {
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.site-logo {
position: relative;
display: block;
flex-shrink: 0;
width: 143px;
height: 42px;
}
.site-logo__icon-frame {
position: absolute;
top: 0;
left: 0;
width: 53px;
height: 42px;
overflow: hidden;
}
.site-logo__icon {
position: absolute;
top: -13.49%;
left: 0;
width: 100%;
max-width: none;
height: 126.98%;
}
.site-logo__wordmark-frame {
position: absolute;
inset: 26.97% 0 7.92% 45.45%;
}
.site-logo__wordmark {
position: absolute;
inset: 0;
display: block;
width: 100%;
max-width: none;
height: 100%;
}
.site-nav {
display: none;
align-items: center;
flex-shrink: 0;
gap: 32px;
}
.site-nav__link {
font-size: 14px;
font-weight: 600;
line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d;
text-decoration: none;
white-space: nowrap;
transition: color 160ms ease;
}
.site-nav__link:hover,
.site-nav__link.is-active {
color: #f28a4b;
}
.language-switcher {
position: relative;
display: none;
flex-shrink: 0;
}
.language-switcher__button {
display: flex;
align-items: center;
justify-content: center;
height: 43px;
gap: 8px;
padding: 0 16px 0 12px;
background: #fff;
border: 1px solid rgba(46, 42, 40, 0.3);
border-radius: 17px;
cursor: pointer;
transition: border-color 160ms ease;
}
.language-switcher__button:hover,
.language-switcher__button[aria-expanded="true"] {
border-color: #f28a4b;
}
.language-switcher__icon {
display: block;
width: 26px;
height: 26px;
object-fit: contain;
}
.language-switcher__current {
font-size: 14px;
font-weight: 600;
line-height: 14px;
letter-spacing: var(--ls-14);
color: #2e2a28;
white-space: nowrap;
}
.language-switcher__menu {
position: absolute;
top: 52px;
right: 0;
z-index: 60;
width: 240px;
max-height: min(520px, calc(100vh - 96px));
overflow-y: auto;
background: #fff;
border: 1px solid #e3d9d1;
border-radius: 18px;
box-shadow: 0 16px 40px rgba(46, 42, 40, 0.16);
}
.language-switcher__menu.is-hidden {
display: none;
}
.language-switcher__option {
display: block;
padding: 14px 24px;
font-size: 14px;
font-weight: 600;
line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d;
text-decoration: none;
transition:
background-color 160ms ease,
color 160ms ease;
}
.language-switcher__option:hover {
color: #f28a4b;
background: #fef0eb;
}
.language-switcher__option.is-active {
color: #2e2a28;
background: #f8f3ee;
}
.site-header__download {
display: none;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 13px 22px;
background: #f28a4b;
border-radius: 17px;
text-decoration: none;
transition: background-color 160ms ease;
}
.site-header__download:hover {
background: #e07a3b;
}
.site-header__download-label {
font-size: 14px;
font-weight: 700;
line-height: normal;
letter-spacing: var(--ls-14);
color: #fff;
white-space: nowrap;
}
.menu-toggle {
display: flex;
flex-shrink: 0;
flex-direction: column;
justify-content: center;
width: 40px;
height: 40px;
gap: 5px;
padding: 8px;
background: transparent;
border: 0;
cursor: pointer;
}
.menu-toggle__bar {
display: block;
width: 100%;
height: 2px;
background: #2e2a28;
border-radius: 2px;
transform-origin: center;
transition:
transform 240ms ease,
opacity 240ms ease;
}
.mobile-nav {
max-height: calc(100vh - 72px);
overflow-y: auto;
background: #fff;
border-top: 1px solid #e3d9d1;
}
.mobile-nav.is-hidden {
display: none;
}
.mobile-nav__list {
display: flex;
flex-direction: column;
padding: 16px 24px;
margin: 0;
list-style: none;
}
.mobile-nav__link {
display: block;
padding: 16px 0;
font-size: 16px;
font-weight: 600;
letter-spacing: var(--ls-16);
color: #2e2a28;
text-decoration: none;
border-bottom: 1px solid #e3d9d1;
transition: color 160ms ease;
}
.mobile-nav__link.is-active {
color: #f28a4b;
}
.mobile-nav__link--last {
border-bottom: 0;
}
.mobile-nav__languages {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: center;
gap: 12px 16px;
padding: 16px 0;
border-top: 1px solid #e3d9d1;
}
.mobile-nav__language-link {
min-width: 0;
font-size: 14px;
font-weight: 600;
line-height: 1.25;
letter-spacing: var(--ls-14);
color: #2e2a28;
text-align: center;
text-decoration: none;
}
.mobile-nav__language-link.is-active {
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 {
display: flex;
justify-content: center;
margin: 16px 0 8px;
}
.mobile-nav__download {
display: block;
width: 250px;
max-width: 100%;
padding: 14px 24px;
font-size: 15px;
font-weight: 700;
letter-spacing: var(--ls-15);
color: #fff;
text-align: center;
text-decoration: none;
background: #f28a4b;
border-radius: 17px;
transition: background-color 160ms ease;
}
.mobile-nav__download:hover {
background: #e07a3b;
}
@media (min-width: 1024px) {
.site-header__bar {
padding: 0 24px;
}
.site-nav,
.language-switcher,
.site-header__download {
display: flex;
}
.menu-toggle {
display: none;
}
.mobile-nav {
display: none !important;
}
}

382
src/styles/hero.css Normal file
View File

@@ -0,0 +1,382 @@
.hero {
position: relative;
display: flex;
align-items: flex-start;
justify-content: center;
width: 100%;
min-height: 600px;
overflow: hidden;
}
.hero__bg {
position: absolute;
inset: 0;
width: 100%;
max-width: none;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.hero__inner {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 1280px;
height: 100%;
gap: 40px;
margin: 0 auto;
padding: 0 16px;
}
.hero__phone-column {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding-top: 24px;
pointer-events: none;
}
.hero__phone-frame {
position: relative;
z-index: 2;
width: min(320px, 100%);
height: 527px;
}
.hero__phone-crop {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.hero__phone {
position: absolute;
top: -7.18%;
left: -2.67%;
width: 105.35%;
max-width: none;
height: 114.36%;
}
.hero__content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
flex-shrink: 0;
width: 100%;
height: 100%;
gap: 60px;
padding: 32px 0;
overflow: clip;
}
.hero__badge {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 12px 16px;
overflow: clip;
background: #fff;
border: 1px solid #f08458;
border-radius: 999px;
}
.hero__badge-text {
margin: 0;
font-size: 14px;
font-weight: 700;
line-height: normal;
letter-spacing: var(--ls-14);
color: #0d0d0d;
}
.hero__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-shrink: 0;
width: 100%;
gap: 24px;
}
.hero__title {
width: 100%;
font-size: 36px;
font-weight: 700;
line-height: 1.1;
letter-spacing: var(--ls-36);
color: #2e2a28;
}
.hero__title-line {
margin: 0;
line-height: 1.1;
}
.hero__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
}
.hero__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
flex-shrink: 0;
width: 100%;
gap: 14px;
overflow: clip;
}
.hero__store-badge {
text-decoration: none;
transition:
transform 160ms ease,
filter 160ms ease;
}
.hero__store-badge:hover {
filter: brightness(0.96);
transform: translateY(-1px);
}
.hero__tags {
display: flex;
flex-wrap: wrap;
align-items: center;
flex-shrink: 0;
gap: 10px;
overflow: clip;
}
.hero__tag {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 9px 14px;
overflow: clip;
background: rgba(255, 255, 255, 0.68);
border: 1px solid #fff;
border-radius: 999px;
}
.hero__tag-text {
font-size: 14px;
font-weight: 700;
line-height: normal;
letter-spacing: var(--ls-14);
color: #7a726d;
white-space: nowrap;
}
@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 {
flex-direction: column;
}
.hero__store-badge {
flex-basis: auto;
width: min(100%, calc(100vw - 32px));
}
.hero__tags {
justify-content: center;
width: 100%;
}
}
@media (min-width: 440px) {
.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) {
.hero__title {
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;
}
}
@media (min-width: 1024px) {
.hero {
height: 891px;
}
.hero__inner {
flex-direction: row;
padding: 0 24px;
}
.hero__phone-column {
position: relative;
flex: 1;
min-width: 0;
height: 100%;
overflow: hidden;
}
.hero__phone-frame {
position: absolute;
top: 60px;
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 {
width: clamp(560px, 44vw, 600px);
padding: 0;
}
}
@media (min-width: 1200px) {
.hero__title {
font-size: 64px;
letter-spacing: var(--ls-64);
}
.hero__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
}
@media (min-width: 1280px) {
.hero__inner {
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);
}
}

243
src/styles/preview.css Normal file
View File

@@ -0,0 +1,243 @@
.app-preview {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 60px;
padding: 64px 16px 0;
overflow: hidden;
background: #fef0eb;
}
.app-preview__header {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 940px;
gap: 40px;
margin: 0 auto;
overflow: clip;
}
.app-preview__title {
width: 100%;
margin: 0;
font-size: 32px;
font-weight: 700;
line-height: 1.2;
color: #1a1a1a;
text-align: center;
}
.app-preview__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
text-align: center;
}
.app-preview__carousel {
box-sizing: border-box;
display: flex;
align-items: flex-end;
justify-content: center;
flex-shrink: 0;
width: 100%;
gap: 24px;
padding: 0 16px;
overflow: hidden;
}
.app-preview__side-phone {
position: relative;
display: none;
flex-shrink: 0;
width: 336px;
height: 396px;
overflow: hidden;
opacity: 0.2;
pointer-events: none;
transition: opacity 300ms ease;
}
.app-preview__phone-image {
position: absolute;
top: -0.03%;
left: 0;
width: 100%;
max-width: none;
height: 175.34%;
transition: opacity 300ms ease;
}
.app-preview__control-wrap {
display: flex;
align-items: flex-end;
align-self: stretch;
flex-shrink: 0;
}
.app-preview__control-inner {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.app-preview__button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 48px;
height: 48px;
background: #f08458;
border: 0;
border-radius: 9999px;
cursor: pointer;
transition:
background-color 160ms ease,
transform 160ms ease;
}
.app-preview__button:hover {
background: #e07a3b;
}
.app-preview__button:active {
transform: scale(0.95);
}
.app-preview__button-icon {
display: block;
width: 24px;
height: 24px;
}
.app-preview__button-icon--next {
transform: rotate(180deg);
}
.app-preview__center-phone {
position: relative;
flex-shrink: 0;
width: min(420px, calc(100vw - 168px));
aspect-ratio: 459 / 542;
overflow: hidden;
pointer-events: none;
}
@media (max-width: 594px) {
.app-preview__carousel {
gap: 12px;
}
.app-preview__button {
width: 40px;
height: 40px;
}
.app-preview__button-icon {
width: 20px;
height: 20px;
}
.app-preview__center-phone {
width: min(420px, calc(100vw - 120px));
}
}
@media (max-width: 554px) {
.app-preview__carousel {
gap: 8px;
padding: 0 12px;
}
.app-preview__button {
width: 36px;
height: 36px;
}
.app-preview__button-icon {
width: 18px;
height: 18px;
}
.app-preview__center-phone {
width: min(420px, calc(100vw - 104px));
}
}
@media (max-width: 540px) {
.app-preview__center-phone {
width: min(390px, calc(100vw - 120px));
}
}
@media (min-width: 768px) {
.app-preview__title {
font-size: 40px;
}
.app-preview__carousel {
gap: clamp(10px, 1.4vw, 16px);
padding: 0 clamp(16px, 3vw, 32px);
}
.app-preview__side-phone,
.app-preview__control-wrap {
display: flex;
}
.app-preview__side-phone {
display: block;
width: clamp(128px, 17vw, 190px);
height: auto;
aspect-ratio: 336 / 396;
}
.app-preview__center-phone {
width: clamp(220px, 31vw, 320px);
}
}
@media (min-width: 1024px) {
.app-preview {
padding: 120px clamp(32px, 6vw, 130px) 0;
}
.app-preview__title {
font-size: 48px;
}
.app-preview__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
.app-preview__side-phone,
.app-preview__control-wrap {
display: flex;
}
.app-preview__carousel {
gap: clamp(16px, 1.4vw, 20px);
padding: 0;
}
.app-preview__side-phone {
display: block;
width: clamp(190px, 20vw, 336px);
}
.app-preview__center-phone {
width: clamp(300px, 27vw, 459px);
height: auto;
aspect-ratio: 459 / 542;
}
}

488
src/styles/sections.css Normal file
View File

@@ -0,0 +1,488 @@
.experience {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 60px 16px;
background: #fff;
}
.experience__inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 1008px;
gap: 40px;
margin: 0 auto;
}
.experience__heading {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 24px;
}
.experience__title {
width: 100%;
margin: 0;
font-size: 28px;
font-weight: 700;
line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a;
text-align: center;
}
.experience__body {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.experience__grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
justify-content: center;
width: 100%;
gap: 24px;
}
.experience-card {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 36px;
padding: 0 0 36px;
overflow: hidden;
background: linear-gradient(to bottom, #fef0eb, #fff);
border-radius: 30px 30px 0 0;
}
.experience-card__media {
position: relative;
flex-shrink: 0;
width: 100%;
aspect-ratio: 320 / 232;
overflow: hidden;
}
.experience-card__media--tinted {
display: flex;
flex-direction: column;
align-items: flex-start;
background: #ffeddf;
}
.experience-card__media-crop {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.experience-card__image {
position: absolute;
max-width: none;
}
.experience-card__image--one {
top: -86.5%;
left: 0;
width: 100%;
height: 298.5%;
}
.experience-card__image--two {
top: -137.86%;
left: -3.13%;
width: 130.62%;
height: 389.91%;
}
.experience-card__image--three {
top: -115.66%;
left: 0;
width: 100%;
height: 298.5%;
}
.experience-card__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 16px;
max-width: 248px;
padding: 0;
overflow: clip;
text-align: center;
}
.experience-card__title {
width: 100%;
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 1.4;
letter-spacing: var(--ls-24);
color: #0d0d0d;
}
.experience-card__description {
width: 100%;
margin: 0;
font-size: 16px;
font-weight: 500;
line-height: 1.5;
letter-spacing: var(--ls-16);
color: #7a726d;
}
.experience__caption {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
text-align: center;
}
.use-cases {
display: flex;
align-items: flex-start;
justify-content: center;
width: 100%;
padding: 60px 16px;
background: #fef0eb;
}
.use-cases__inner {
display: grid;
grid-template-columns: minmax(0, 1fr);
width: 100%;
max-width: 1280px;
gap: 32px;
margin: 0 auto;
}
.use-cases__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 24px;
overflow: clip;
}
@media (max-width: 1294px) {
.use-cases__inner {
grid-template-columns: minmax(0, 1128px);
justify-content: center;
}
.use-cases__copy {
justify-self: start;
width: min(580px, 100%);
max-width: 580px;
margin: 0;
}
.use-cases__title,
.use-cases__description {
max-width: 100%;
}
}
.section-eyebrow {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 12px 24px;
background: #fff;
border: 1px solid #fbbfa3;
border-radius: 9999px;
}
.section-eyebrow__text {
font-size: 14px;
font-weight: 700;
line-height: normal;
letter-spacing: var(--ls-14);
color: #f08458;
text-align: center;
white-space: nowrap;
}
.use-cases__title {
width: 100%;
margin: 0;
font-size: 28px;
font-weight: 700;
line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a;
text-align: left;
}
.use-cases__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
text-align: left;
}
@media (max-width: 768px) {
.use-cases__copy {
align-items: flex-start;
justify-self: stretch;
width: 100%;
max-width: none;
text-align: left;
}
.use-cases__title,
.use-cases__description {
text-align: left;
}
}
.use-cases__rows {
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 1128px;
min-width: 0;
margin: 0 auto;
overflow: hidden;
border-radius: 30px;
gap: 1px;
}
.use-case-row {
display: grid;
grid-template-columns: minmax(0, 1fr);
width: 100%;
overflow: hidden;
background: #faede8;
}
.use-case-row__title-cell,
.use-case-row__description-cell {
display: flex;
align-items: center;
min-width: 0;
padding: 16px 24px;
}
.use-case-row__title-cell {
background: #f08458;
}
.use-case-row__description-cell {
background: #fff;
}
.use-case-row__title {
min-width: 0;
margin: 0;
font-size: 18px;
font-weight: 600;
line-height: normal;
letter-spacing: var(--ls-18);
color: #fff;
}
.use-case-row__description {
min-width: 0;
margin: 0;
font-size: 15px;
font-weight: 500;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
}
@media (max-width: 578px) {
.use-cases__rows {
gap: 24px;
overflow: visible;
border-radius: 0;
}
.use-case-row {
overflow: hidden;
background: #fff;
border-radius: 16px;
}
.use-case-row__title-cell,
.use-case-row__description-cell {
padding: 24px;
}
.use-case-row__title {
font-size: 20px;
letter-spacing: var(--ls-20);
}
}
@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 {
grid-template-columns: minmax(220px, 300px) minmax(280px, 1fr);
height: 120px;
}
.use-case-row__title-cell,
.use-case-row__description-cell {
padding: 24px 36px;
}
.use-case-row__title {
font-size: 20px;
letter-spacing: var(--ls-20);
}
}
@media (min-width: 768px) {
.experience,
.use-cases {
padding-left: 36px;
padding-right: 36px;
}
.experience__title,
.use-cases__title {
font-size: 42px;
letter-spacing: var(--ls-42);
}
}
@media (min-width: 1200px) {
.experience__grid {
grid-template-columns: repeat(3, minmax(0, 320px));
}
.experience-card:nth-child(3):last-child {
grid-column: auto;
}
}
@media (min-width: 576px) {
.experience-card__title {
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,
.use-cases__title {
font-size: 48px;
letter-spacing: var(--ls-48);
}
.experience__caption,
.use-cases__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
}
@media (min-width: 1201px) {
.use-cases {
padding: 60px 64px;
}
.use-cases__inner {
grid-template-columns: minmax(420px, 540px) minmax(560px, 1fr);
align-items: center;
gap: 40px;
}
}
@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) {
.use-cases {
padding-right: 130px;
padding-left: 130px;
}
}

328
src/styles/trust.css Normal file
View File

@@ -0,0 +1,328 @@
.trust {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 60px 16px;
background: #fff;
}
.trust__inner {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 1280px;
gap: 40px;
margin: 0 auto;
}
.trust__header {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 24px;
overflow: clip;
}
.trust__title {
width: 100%;
margin: 0;
font-size: 28px;
font-weight: 700;
line-height: 1.2;
letter-spacing: var(--ls-28);
color: #1a1a1a;
}
.trust__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
}
.trust__grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: center;
justify-content: center;
width: 100%;
gap: 32px;
}
.trust-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
min-width: 0;
gap: 16px;
padding: 24px;
border-radius: 30px;
}
.trust-card__icon-frame {
position: relative;
flex-shrink: 0;
width: 128px;
height: 128px;
}
.trust-card__icon-crop {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.trust-card__icon {
position: absolute;
max-width: none;
}
.trust-card__icon--one {
top: 7.04%;
left: -31.48%;
width: 312.73%;
height: 174.55%;
}
.trust-card__icon--two {
top: 3.1%;
left: -164.72%;
width: 335.61%;
height: 187.32%;
}
.trust-card__icon--three {
top: -105.62%;
left: -187.93%;
width: 378.86%;
height: 211.46%;
}
.trust-card__icon--four {
top: 0;
left: 4.14%;
width: 100%;
height: 100%;
}
.trust-card__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 8px;
}
.trust-card__title {
width: 100%;
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 22px;
letter-spacing: var(--ls-16);
color: #0d0d0d;
}
.trust-card__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 500;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
}
.trust__divider {
position: relative;
display: none; /* shown only in desktop flex row via 1023px breakpoint */
flex-shrink: 0;
width: 0;
height: 118px;
}
.trust__divider-frame {
position: absolute;
inset: 0 -0.5px;
}
.trust__divider-image {
display: block;
width: 100%;
max-width: none;
height: 100%;
}
@media (max-width: 1023px) {
.trust-card__copy {
align-items: center;
text-align: center;
}
.trust-card__title,
.trust-card__description {
text-align: center;
}
}
@media (min-width: 440px) {
.trust {
padding-left: 20px;
padding-right: 20px;
}
.trust__title {
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 {
position: relative;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@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 {
padding-top: 60px;
padding-bottom: 60px;
padding-left: 28px;
padding-right: 28px;
}
.trust__grid {
display: flex;
gap: 14px;
}
.trust-card {
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 {
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;
}
}

317
src/styles/why.css Normal file
View File

@@ -0,0 +1,317 @@
.why {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 60px 16px;
background: #fff;
}
.why__inner {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 1280px;
gap: 40px;
margin: 0 auto;
}
.why__intro {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 36px;
overflow: clip;
}
.why__copy {
display: flex;
order: 2;
flex: 1;
flex-direction: column;
align-items: flex-start;
min-width: 0;
gap: 36px;
}
.why__text {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 24px;
}
.why__title {
width: 100%;
font-size: 28px;
font-weight: 700;
letter-spacing: var(--ls-28);
color: #1a1a1a;
}
.why__title-line {
margin: 0;
line-height: 1.2;
}
.why__underline {
position: relative;
flex-shrink: 0;
width: 295.5px;
height: 0;
}
.why__underline-frame {
position: absolute;
inset: -0.5px 0;
}
.why__underline-image {
display: block;
width: 100%;
max-width: none;
height: 100%;
}
.why__description {
width: 100%;
margin: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
letter-spacing: var(--ls-15);
color: #7a726d;
}
@media (min-width: 1200px) {
.why__description {
font-size: 18px;
letter-spacing: var(--ls-18);
}
}
.why__illustration {
position: relative;
display: block;
order: 1;
align-self: center;
flex-shrink: 0;
width: 480px;
max-width: calc(100vw - 32px);
height: 480px;
max-height: calc(100vw - 32px);
}
.why__illustration-image {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.why__grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
width: 100%;
gap: 24px;
}
.why-card {
display: flex;
align-items: center;
min-width: 0;
gap: 20px;
padding: 20px 16px;
overflow: clip;
background: #fef0eb;
border: 1px solid #e8e4de;
border-radius: 30px;
}
.why-card__icon-frame {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 60px;
height: 60px;
overflow: clip;
background: #f08458;
border-radius: 9999px;
}
.why-card__icon {
display: block;
}
.why-card__icon--simple {
width: 33.001px;
height: 32.973px;
}
.why-card__icon--familiar {
width: 28.5px;
height: 30px;
}
.why-card__icon--connected {
width: 33px;
height: 32.936px;
}
.why-card__icon--modern {
width: 18.125px;
height: 33px;
}
.why-card__copy {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
min-width: 0;
gap: 12px;
overflow: clip;
}
.why-card__title {
width: 100%;
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 23px;
letter-spacing: var(--ls-20);
color: #0d0d0d;
}
.why-card__description {
width: 100%;
margin: 0;
font-size: 16px;
font-weight: 500;
line-height: 1.5;
letter-spacing: var(--ls-16);
color: #7a726d;
}
@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 {
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 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.why__intro {
flex-direction: row;
}
.why__copy {
order: 1;
}
.why__illustration {
order: 2;
}
}
@media (min-width: 1200px) {
.why__title {
font-size: 48px;
letter-spacing: var(--ls-48);
}
}
@media (min-width: 1376px) {
.why {
padding-top: 120px;
padding-bottom: 120px;
padding-left: 0;
padding-right: 0;
}
}

View File

@@ -1,7 +1,3 @@
{ {
"extends": "astro/tsconfigs/strict", "extends": "./node_modules/astro/tsconfigs/strict.json"
"compilerOptions": {
"baseUrl": ".",
"paths": {}
}
} }