commit 63b4959b6bb0eb30ceb9b3822ffcdbab722045d8 Author: inman Date: Thu Jul 23 15:43:23 2026 +0800 Initial project import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/API.md b/API.md new file mode 100644 index 0000000..6077785 --- /dev/null +++ b/API.md @@ -0,0 +1,46 @@ +# Inquiry Notification API + +## Run + +```bash +npm run dev:full +``` + +The API listens on `http://127.0.0.1:5174` by default, and Vite proxies `/api` to it. + +## Configuration + +```bash +INQUIRY_NOTIFICATION_URL="https://notification.example.com/inquiries" +INQUIRY_NOTIFICATION_AUTH_HEADER="Bearer optional-token" +INQUIRY_ALLOWED_ORIGIN="https://www.wanderq.com" +INQUIRY_API_PORT=5174 +INQUIRY_MAX_BODY_BYTES=100000 +``` + +The notification URL must be configured for inquiry submission to succeed. The API does not persist inquiry data locally. + +## Endpoints + +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/api/health` | API health and notification configuration status | +| `POST` | `/api/inquiries` | Validate an inquiry and forward it to the notification webhook | + +### `POST /api/inquiries` + +Required fields: + +```json +{ + "name": "Guest name", + "contact": "guest@example.com", + "interest": "Classic Guizhou landscapes", + "travelMonth": "October 2026", + "guests": "2", + "message": "Additional notes", + "locale": "en" +} +``` + +On success, the API returns `202` with an accepted inquiry receipt. It returns an error when the payload is invalid or the notification webhook is unavailable. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca17256 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# WanderQ Travel + +万趣旅行贵州网站是一个代码维护内容的双语 React 前台。内容修改通过 TypeScript 文件完成,构建后直接发布,不提供管理后台或 CMS 写入接口。 + +## Stack + +- React + TypeScript + Vite frontend +- Code-defined bilingual content in `src/siteContent.ts`, `src/guizhouContent.ts`, and `src/guizhouEnContent.ts` +- CMS-shaped static view model assembled by `src/cms.ts` +- Minimal Node HTTP API for inquiry notification delivery + +## Scripts + +```bash +npm run dev +npm run api +npm run dev:full +npm run lint +npm run build +npm run preview +``` + +`npm run dev:full` starts the Vite frontend and the inquiry API together. The frontend proxies `/api` to `http://127.0.0.1:5174` during local development. + +## Inquiry notification API + +The public form posts to `POST /api/inquiries`. The API does not store inquiries. It validates the request and forwards an `inquiry.submitted` event to the configured notification webhook. If no webhook is configured, or the webhook rejects the event, the API returns an error so the frontend does not report a lost inquiry as successful. + +Configure the deployment with: + +```bash +INQUIRY_NOTIFICATION_URL="https://notification.example.com/inquiries" +INQUIRY_NOTIFICATION_AUTH_HEADER="Bearer optional-token" +INQUIRY_ALLOWED_ORIGIN="https://www.wanderq.com" +INQUIRY_API_PORT=5174 +``` + +The webhook receives JSON in this shape: + +```json +{ + "type": "inquiry.submitted", + "occurredAt": "2026-07-23T00:00:00.000Z", + "data": { + "id": "inq-example", + "receivedAt": "2026-07-23T00:00:00.000Z", + "name": "Guest name", + "contact": "guest@example.com", + "interest": "Classic Guizhou landscapes · USD 3,000-5,000", + "travelMonth": "October 2026", + "guests": "2", + "message": "Additional notes", + "locale": "en" + } +} +``` + +Use `GET /api/health` to verify the API and whether a notification destination is configured. + +## Maintaining content + +Production raster images are hosted in OSS and referenced from `src/guizhouContent.ts`; local `public/` assets are limited to SVG/HTML files and the OSS manifest. Edit the tracked TypeScript content, then run `npm run lint` and `npm run build` before deploying. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..4ee9759 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + 万趣旅行|贵州秘境定制旅行 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..783d081 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2833 @@ +{ + "name": "wanderq-travel", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wanderq-travel", + "version": "0.0.0", + "dependencies": { + "@gsap/react": "^2.1.2", + "@phosphor-icons/react": "^2.1.10", + "gsap": "^3.15.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "playwright": "^1.61.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..094ae47 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "wanderq-travel", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "api": "node server/cms-server.mjs", + "dev:full": "node scripts/dev-full.mjs", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@gsap/react": "^2.1.2", + "@phosphor-icons/react": "^2.1.10", + "gsap": "^3.15.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "playwright": "^1.61.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6461034 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/guizhou-assets/manifest.json b/public/guizhou-assets/manifest.json new file mode 100644 index 0000000..30d267c --- /dev/null +++ b/public/guizhou-assets/manifest.json @@ -0,0 +1,205 @@ +[ + { + "id": "hero-wanfenglin", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/hero-wanfenglin.webp", + "width": 2200, + "height": 1157, + "size_kb": 626.4 + }, + { + "id": "hero-huangguoshu", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/hero-huangguoshu.webp", + "width": 2200, + "height": 1467, + "size_kb": 558.2 + }, + { + "id": "hero-fanjing", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/hero-fanjing.webp", + "width": 2200, + "height": 1236, + "size_kb": 342.3 + }, + { + "id": "hero-xijiang", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/hero-xijiang.webp", + "width": 2200, + "height": 779, + "size_kb": 515.2 + }, + { + "id": "menu-landscape", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/menu-landscape.webp", + "width": 1600, + "height": 1090, + "size_kb": 634.5 + }, + { + "id": "menu-outdoor", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/menu-outdoor.webp", + "width": 1600, + "height": 2400, + "size_kb": 976.5 + }, + { + "id": "menu-culture", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/menu-culture.webp", + "width": 1600, + "height": 2133, + "size_kb": 764.7 + }, + { + "id": "menu-hotels", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/menu-hotels.webp", + "width": 1600, + "height": 1067, + "size_kb": 514.5 + }, + { + "id": "destination-huangguoshu", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/destination-huangguoshu.webp", + "width": 1800, + "height": 1200, + "size_kb": 356.3 + }, + { + "id": "destination-fanjing", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/destination-fanjing.webp", + "width": 1800, + "height": 1196, + "size_kb": 263.2 + }, + { + "id": "destination-liboxqk", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/destination-liboxqk.webp", + "width": 1800, + "height": 1200, + "size_kb": 601.8 + }, + { + "id": "destination-xijiang", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/destination-xijiang.webp", + "width": 1800, + "height": 1200, + "size_kb": 714.7 + }, + { + "id": "destination-zhenyuan", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/destination-zhenyuan.webp", + "width": 1800, + "height": 1200, + "size_kb": 511.4 + }, + { + "id": "destination-wanfenglin", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/destination-wanfenglin.webp", + "width": 1700, + "height": 734, + "size_kb": 381.4 + }, + { + "id": "experience-cave", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/experience-cave.webp", + "width": 1600, + "height": 2404, + "size_kb": 1144.2 + }, + { + "id": "experience-rafting", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/experience-rafting.webp", + "width": 1600, + "height": 1016, + "size_kb": 357.9 + }, + { + "id": "experience-miao-craft", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/experience-miao-craft.webp", + "width": 1600, + "height": 1065, + "size_kb": 217.7 + }, + { + "id": "experience-coffee-village", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/experience-coffee-village.webp", + "width": 1600, + "height": 1065, + "size_kb": 390.6 + }, + { + "id": "hotel-bolian-spa", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/hotel-bolian-spa.webp", + "width": 1600, + "height": 1097, + "size_kb": 449.9 + }, + { + "id": "route-classic", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/route-classic.webp", + "width": 1600, + "height": 1065, + "size_kb": 387.5 + }, + { + "id": "route-outdoor", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/route-outdoor.webp", + "width": 1600, + "height": 1067, + "size_kb": 331.7 + }, + { + "id": "route-mixed", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/route-mixed.webp", + "width": 1600, + "height": 913, + "size_kb": 567.7 + }, + { + "id": "footer-cta", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/footer-cta.webp", + "width": 1519, + "height": 1007, + "size_kb": 451.1 + }, + { + "id": "quote-guests", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/quote-guests.webp", + "width": 1600, + "height": 900, + "size_kb": 481.1 + }, + { + "id": "faq-outdoor", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/faq-outdoor.webp", + "width": 1400, + "height": 2108, + "size_kb": 720.6 + }, + { + "id": "menu-food", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/menu-food.webp", + "width": 1800, + "height": 1198, + "size_kb": 744.0 + }, + { + "id": "menu-transport", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/menu-transport.webp", + "width": 1800, + "height": 1012, + "size_kb": 551.2 + }, + { + "id": "hotel-reference", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/guizhou-assets/hotel-reference.webp", + "width": 1800, + "height": 1200, + "size_kb": 440.5 + }, + { + "id": "wanderq-logo", + "url": "https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/wanderq/wanderq-logo.webp", + "width": 8240, + "height": 8240, + "size_kb": 6228.7 + } +] diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/privacy-policy.html b/public/privacy-policy.html new file mode 100644 index 0000000..539b8f0 --- /dev/null +++ b/public/privacy-policy.html @@ -0,0 +1,157 @@ + + + + + + 隐私政策|万趣旅行 + + + +
+
+ 返回首页 + +
+ +
+

隐私政策

+

万趣旅行重视用户隐私。本页面说明我们在提供贵州定制旅行咨询、线路沟通和地接服务时,可能收集、使用和保护的信息。

+ +

我们可能收集的信息

+
    +
  • 你主动提交的姓名、联系方式、出行日期、人数、预算和旅行偏好。
  • +
  • 你与我们沟通时提供的航班、酒店、饮食禁忌、同行人需求等行程相关信息。
  • +
  • 网站运行所需的基础访问数据,例如浏览器类型、页面访问情况和必要的安全日志。
  • +
+ +

信息用途

+

我们仅将上述信息用于回应咨询、设计旅行方案、安排服务资源、改进网站体验和满足必要的合规要求。

+ +

信息保护

+

我们会采取合理的管理和技术措施保护你的个人信息。未经你的同意,我们不会将你的个人信息出售给第三方。

+ +

联系我们

+

如需查询、更正或删除你提交的信息,请通过网站 联系页面 与我们沟通。

+
+ +
+

Privacy Policy

+

Wanqu Travel values your privacy. This page explains what information we may collect, use, and protect when providing Guizhou custom travel consultation, route communication, and destination-management services.

+ +

Information We May Collect

+
    +
  • Name, contact details, travel dates, group size, budget, and travel preferences that you submit voluntarily.
  • +
  • Trip-related details you share with us, such as flights, hotels, dietary restrictions, and companion needs.
  • +
  • Basic website operation data, such as browser type, page visits, and necessary security logs.
  • +
+ +

How We Use Information

+

We use this information only to respond to inquiries, design travel plans, arrange service resources, improve the website experience, and meet necessary compliance requirements.

+ +

Information Protection

+

We take reasonable administrative and technical measures to protect your personal information. We do not sell your personal information to third parties without your consent.

+ +

Contact Us

+

To request access, correction, or deletion of information you submitted, please contact us through the website contact page.

+
+
+ + + + diff --git a/public/service-agreement.html b/public/service-agreement.html new file mode 100644 index 0000000..00ac08d --- /dev/null +++ b/public/service-agreement.html @@ -0,0 +1,155 @@ + + + + + + 服务协议|万趣旅行 + + + +
+
+ 返回首页 + +
+ +
+

服务协议

+

欢迎使用万趣旅行网站。你通过本网站提交咨询或与我们沟通旅行需求,即表示你理解并接受以下服务说明。

+ +

服务范围

+

万趣旅行提供贵州定制旅行咨询、线路规划建议、在地执行资源沟通和相关地接服务信息。具体服务内容、价格和履约安排以双方最终确认的方案或合同为准。

+ +

用户责任

+
    +
  • 请确保提交的联系人、出行人数、日期、预算和特殊需求真实、准确。
  • +
  • 涉及证件、签证、保险、健康状况和户外活动限制的信息,应在确认方案前主动告知。
  • +
+ +

内容说明

+

网站展示的线路、图片、酒店、活动和价格信息用于咨询参考,可能因季节、天气、资源档期、政策和供应商情况发生变化。

+ +

联系我们

+

如对本协议或服务安排有疑问,请通过 联系页面 与我们沟通。

+
+ +
+

Service Agreement

+

Welcome to the Wanqu Travel website. By submitting an inquiry or communicating travel needs through this website, you acknowledge and accept the following service notes.

+ +

Scope of Service

+

Wanqu Travel provides Guizhou custom travel consultation, route-planning advice, local operating-resource communication, and related destination-management service information. Specific service content, pricing, and fulfillment arrangements are subject to the final plan or contract confirmed by both parties.

+ +

User Responsibilities

+
    +
  • Please ensure that submitted contact details, group size, dates, budget, and special requirements are true and accurate.
  • +
  • Information involving documents, visas, insurance, health conditions, and outdoor-activity limits should be disclosed before confirming the plan.
  • +
+ +

Content Notes

+

Routes, images, hotels, activities, and pricing shown on the website are for consultation reference and may change due to season, weather, availability, policy, and supplier conditions.

+ +

Contact Us

+

If you have questions about this agreement or service arrangements, please contact us through the contact page.

+
+
+ + + + diff --git a/public/social-icons/facebook.svg b/public/social-icons/facebook.svg new file mode 100644 index 0000000..9c2d5a8 --- /dev/null +++ b/public/social-icons/facebook.svg @@ -0,0 +1 @@ +Facebook \ No newline at end of file diff --git a/public/social-icons/instagram.svg b/public/social-icons/instagram.svg new file mode 100644 index 0000000..6781d37 --- /dev/null +++ b/public/social-icons/instagram.svg @@ -0,0 +1 @@ +Instagram \ No newline at end of file diff --git a/public/social-icons/klook.svg b/public/social-icons/klook.svg new file mode 100644 index 0000000..e64846f --- /dev/null +++ b/public/social-icons/klook.svg @@ -0,0 +1 @@ +Klook \ No newline at end of file diff --git a/public/social-icons/tripadvisor.svg b/public/social-icons/tripadvisor.svg new file mode 100644 index 0000000..fbdbbdb --- /dev/null +++ b/public/social-icons/tripadvisor.svg @@ -0,0 +1 @@ +Tripadvisor \ No newline at end of file diff --git a/public/social-icons/tripdotcom.svg b/public/social-icons/tripdotcom.svg new file mode 100644 index 0000000..e1e7503 --- /dev/null +++ b/public/social-icons/tripdotcom.svg @@ -0,0 +1 @@ +Trip.com \ No newline at end of file diff --git a/public/social-icons/wechat.svg b/public/social-icons/wechat.svg new file mode 100644 index 0000000..520f3e8 --- /dev/null +++ b/public/social-icons/wechat.svg @@ -0,0 +1 @@ +WeChat \ No newline at end of file diff --git a/public/social-icons/x.svg b/public/social-icons/x.svg new file mode 100644 index 0000000..c732f26 --- /dev/null +++ b/public/social-icons/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/scripts/dev-full.mjs b/scripts/dev-full.mjs new file mode 100644 index 0000000..61d9a94 --- /dev/null +++ b/scripts/dev-full.mjs @@ -0,0 +1,22 @@ +import { spawn } from 'node:child_process'; + +const commands = [ + ['node', ['server/cms-server.mjs']], + ['npm', ['run', 'dev', '--', '--host', '127.0.0.1']], +]; + +const children = commands.map(([cmd, args]) => { + const child = spawn(cmd, args, { stdio: 'inherit', shell: false }); + child.on('exit', (code) => { + if (code && code !== 0) { + children.forEach((item) => item.pid !== child.pid && item.kill()); + process.exit(code); + } + }); + return child; +}); + +process.on('SIGINT', () => { + children.forEach((child) => child.kill('SIGINT')); + process.exit(0); +}); diff --git a/server/cms-server.mjs b/server/cms-server.mjs new file mode 100644 index 0000000..be9b18a --- /dev/null +++ b/server/cms-server.mjs @@ -0,0 +1,174 @@ +import crypto from 'node:crypto'; +import http from 'node:http'; + +const port = Number(process.env.INQUIRY_API_PORT || 5174); +const maxBodyBytes = Number(process.env.INQUIRY_MAX_BODY_BYTES || 100_000); +const notificationUrl = String(process.env.INQUIRY_NOTIFICATION_URL || '').trim(); +const notificationAuthHeader = String(process.env.INQUIRY_NOTIFICATION_AUTH_HEADER || '').trim(); +const allowedOrigin = String(process.env.INQUIRY_ALLOWED_ORIGIN || '*').trim() || '*'; + +const server = http.createServer(async (request, response) => { + const url = new URL(request.url || '/', `http://${request.headers.host || '127.0.0.1'}`); + + if (request.method === 'OPTIONS') { + send(response, 204, null); + return; + } + + try { + if (url.pathname === '/api/health' && request.method === 'GET') { + send(response, 200, { + ok: true, + service: 'travel-inquiry-api', + uptime: Math.round(process.uptime()), + notification: { + configured: Boolean(notificationUrl), + provider: notificationUrl ? 'webhook' : 'none', + }, + }); + return; + } + + if (url.pathname === '/api/inquiries' && request.method === 'POST') { + const inquiry = normalizeInquiry(await readBody(request)); + await notifyInquiry(inquiry); + send(response, 202, { + id: inquiry.id, + receivedAt: inquiry.receivedAt, + status: 'accepted', + notification: 'delivered', + }); + return; + } + + send(response, 404, { error: 'Not found' }); + } catch (error) { + const status = error instanceof HttpError ? error.status : 500; + if (status >= 500) console.error(error); + send(response, status, { error: error instanceof Error ? error.message : 'Server error' }); + } +}); + +server.listen(port, '127.0.0.1', () => { + console.log(`Travel inquiry API running at http://127.0.0.1:${port}`); + console.log(`Notification webhook: ${notificationUrl ? 'configured' : 'not configured'}`); +}); + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); + +async function shutdown() { + server.close(); + process.exit(0); +} + +async function notifyInquiry(inquiry) { + if (!notificationUrl) { + throw new HttpError(503, 'Inquiry notification is not configured'); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (notificationAuthHeader) headers.Authorization = notificationAuthHeader; + + try { + const response = await fetch(notificationUrl, { + method: 'POST', + headers, + body: JSON.stringify({ + type: 'inquiry.submitted', + occurredAt: inquiry.receivedAt, + data: inquiry, + }), + signal: controller.signal, + }); + + if (!response.ok) { + throw new HttpError(502, 'Inquiry notification service rejected the request'); + } + } catch (error) { + if (error instanceof HttpError) throw error; + throw new HttpError(502, 'Inquiry notification service is unavailable'); + } finally { + clearTimeout(timeout); + } +} + +function normalizeInquiry(body) { + if (!isObject(body)) { + throw new HttpError(400, 'Inquiry must be an object'); + } + + const name = field(body.name, 160); + const contact = field(body.contact, 240); + if (!name || !contact) { + throw new HttpError(400, 'Inquiry name and contact are required'); + } + + return { + id: field(body.id, 120) || makeId('inq'), + receivedAt: new Date().toISOString(), + name, + contact, + interest: field(body.interest, 500), + travelMonth: field(body.travelMonth, 120), + guests: field(body.guests, 120), + message: field(body.message, 6_000), + locale: body.locale === 'en' ? 'en' : 'zh', + }; +} + +function field(value, maxLength) { + return String(value || '').trim().slice(0, maxLength); +} + +function isObject(value) { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)); +} + +async function readBody(request) { + const chunks = []; + let size = 0; + for await (const chunk of request) { + size += chunk.length; + if (size > maxBodyBytes) { + throw new HttpError(413, 'Request body is too large'); + } + chunks.push(chunk); + } + + const body = Buffer.concat(chunks).toString('utf8').trim(); + if (!body) return {}; + + try { + return JSON.parse(body); + } catch { + throw new HttpError(400, 'Request body must be valid JSON'); + } +} + +function send(response, status, body) { + response.writeHead(status, { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json; charset=utf-8', + }); + response.end(body === null ? '' : JSON.stringify(body)); +} + +function makeId(prefix) { + return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`; +} + +class HttpError extends Error { + constructor(status, message) { + super(message); + this.status = status; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..53ef508 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,11 @@ +import { useState } from 'react'; +import { cloneDefaultCms } from './cms'; +import TravelSite from './TravelSite'; + +function App() { + const [content] = useState(() => cloneDefaultCms()); + + return ; +} + +export default App; diff --git a/src/TravelSite.css b/src/TravelSite.css new file mode 100644 index 0000000..cd537b0 --- /dev/null +++ b/src/TravelSite.css @@ -0,0 +1,6866 @@ +.oc-site { + --oc-bg: #f4f8ef; + --oc-panel: #e8f0e2; + --oc-panel-2: #dde8d5; + --oc-ink: #243328; + --oc-muted: #5e6c61; + --oc-clay: #48a850; + --oc-clay-dark: #2f7436; + --oc-orange: #48a850; + --oc-action-bg: #3f7647; + --oc-action-bg-hover: #35673d; + --oc-action-shadow: 0 10px 22px rgba(45, 92, 53, 0.16); + --oc-action-shadow-hover: 0 14px 28px rgba(45, 92, 53, 0.2); + --oc-logo-green: #49a64d; + --oc-footer: #183222; + --oc-footer-soft: #d9ead3; + --oc-line: #c4d4bc; + min-height: 100vh; + background: var(--oc-bg); + color: var(--oc-ink); + font-family: 'Assistant', Arial, sans-serif; +} + +.oc-site a { + color: inherit; + text-decoration: none; +} + +.oc-site h1, +.oc-site h2, +.oc-site h3 { + font-family: 'Tenor Sans', Georgia, serif; + font-weight: 400; + letter-spacing: 0; + margin: 0; +} + +.oc-site p { + margin: 0; +} + +.oc-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +.oc-container { + width: min(1116px, calc(100% - 120px)); + margin: 0 auto; +} + +.oc-container-wide { + width: min(1360px, calc(100% - 88px)); + margin: 0 auto; +} + +.oc-container-narrow { + width: min(875px, calc(100% - 120px)); + margin: 0 auto; +} + +.oc-header-wrap { + position: sticky; + top: 0; + z-index: 30; + background: rgba(244, 248, 239, 0.97); +} + +.oc-header { + height: 91px; + display: grid; + grid-template-columns: minmax(184px, 1fr) auto minmax(184px, 1fr); + align-items: center; + gap: 18px; + padding: 0 20px 0 22px; + color: #26372a; +} + +.oc-logo { + display: flex; + align-items: center; + justify-self: start; +} + +.oc-logo img { + width: 168px; + max-height: 54px; + height: auto; + object-fit: contain; +} + +.oc-nav { + position: relative; + display: flex; + justify-self: center; + justify-content: center; + align-items: center; + gap: clamp(12px, 1.8vw, 29px); + height: 100%; +} + +.oc-nav a { + display: flex; + align-items: center; + height: 100%; + color: #26372a; + font-size: 14px; + font-weight: 400; + line-height: 21px; + letter-spacing: 0.2px; + text-transform: uppercase; + white-space: nowrap; + will-change: transform, color; +} + +.oc-nav a:hover { + color: var(--oc-orange); +} + +.oc-nav-indicator { + position: absolute; + left: 0; + bottom: 22px; + width: 0; + height: 2px; + background: #48a850; + border-radius: 999px; + pointer-events: none; + opacity: 0; + will-change: transform, width, opacity; +} + +.oc-header-actions { + display: flex; + align-items: center; + justify-self: end; + gap: 18px; +} + +.oc-icon-action { + width: 48px; + min-height: 44px; + border: 0; + background: transparent; + color: #26372a; + cursor: pointer; + display: grid; + justify-items: center; + gap: 1px; + padding: 0; + will-change: transform; +} + +.oc-icon-action span { + font-size: 12px; + line-height: 1; +} + +.oc-language-action { + position: relative; + width: 92px; + height: 38px; + min-height: 38px; + display: flex; + align-items: center; + justify-content: center; + align-self: center; + padding: 3px; + border: 1px solid rgba(38, 55, 42, 0.18); + border-radius: 999px; + background: rgba(255, 255, 255, 0.72); + box-shadow: 0 10px 24px rgba(24, 50, 34, 0.08); + overflow: hidden; +} + +.oc-language-track { + position: relative; + display: grid; + grid-template-columns: repeat(2, 42px); + width: 84px; + height: 32px; + align-items: center; +} + +.oc-language-thumb { + position: absolute; + left: 0; + top: 0; + width: 42px; + height: 32px; + border-radius: 999px; + background: var(--oc-action-bg); + box-shadow: 0 8px 18px rgba(45, 92, 53, 0.22); + will-change: transform; +} + +.oc-language-action[data-current='en'] .oc-language-thumb { + transform: translateX(42px); +} + +.oc-language-option { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + height: 32px; + color: #26372a; + font-size: 12px; + font-weight: 800; + line-height: 1; + will-change: transform, opacity, color; +} + +.oc-language-option.is-active { + color: #fff; +} + +html[data-oc-language='zh'] .oc-site * { + letter-spacing: 0 !important; + text-transform: none !important; +} + +.oc-orange-btn, +.oc-clay-btn, +.oc-outline-btn { + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + min-height: 45px; + padding: 0 20px; + font-size: 14px; + font-weight: 600; + line-height: 18px; + text-transform: uppercase; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.22s ease, border-color 0.22s ease, box-shadow 0.22s ease, color 0.22s ease, transform 0.22s ease; + will-change: transform; +} + +.oc-orange-btn, +.oc-clay-btn { + min-width: 128px; + height: 45px; + border-radius: 2px; + background: var(--oc-action-bg); + color: #fff; + box-shadow: var(--oc-action-shadow); +} + +.oc-site :is(.oc-orange-btn, .oc-clay-btn, .oc-mega-cta) { + color: #fff; +} + +.oc-orange-btn:is(:hover, :focus-visible), +.oc-clay-btn:is(:hover, :focus-visible) { + background: var(--oc-action-bg-hover); + color: #fff; + box-shadow: var(--oc-action-shadow-hover); +} + +.oc-site :is(.oc-orange-btn, .oc-clay-btn, .oc-mega-cta):is(:hover, :focus-visible) { + color: #fff; +} + +.oc-orange-btn:focus-visible, +.oc-clay-btn:focus-visible, +.oc-mega-cta:focus-visible { + outline: 3px solid rgba(63, 118, 71, 0.38); + outline-offset: 3px; +} + +.oc-orange-btn:active, +.oc-clay-btn:active { + transform: translateY(1px); +} + +.oc-outline-btn { + min-height: 35px; + border: 1px solid var(--oc-clay); + color: var(--oc-clay-dark); + background: transparent; + box-shadow: none; +} + +a.oc-special-menu-card, +a.oc-info-band, +a.oc-experience-band, +a.oc-home-safari, +a.oc-safari-card, +a.oc-archive-card, +a.oc-food-card, +a.oc-transport-card, +a.oc-page-card, +a.oc-country, +a.oc-special-card, +a.oc-season-card, +a.oc-blog-card, +a.oc-feature-panel, +a.oc-route-service-panel, +.oc-poi-related-grid a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +.oc-card-action, +.oc-food-detail-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-transform: uppercase; +} + +:is( + .oc-special-menu-card, + .oc-info-band, + .oc-experience-band, + .oc-home-safari, + .oc-safari-card, + .oc-archive-card, + .oc-food-card, + .oc-transport-card, + .oc-page-card, + .oc-country, + .oc-special-card, + .oc-season-card, + .oc-blog-card, + .oc-feature-panel, + .oc-route-service-panel, + .oc-poi-related-grid a +) { + box-shadow: 0 14px 34px rgba(24, 50, 34, 0.12); + transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease, background-color 0.22s ease; + will-change: transform; +} + +:is( + .oc-route-action-card, + .oc-poi-fact-card, + .oc-poi-highlight-card, + .oc-includes div, + .oc-contact-card +) { + box-shadow: 0 10px 26px rgba(24, 50, 34, 0.08); + transition: box-shadow 0.22s ease, border-color 0.22s ease, background-color 0.22s ease; +} + +:is( + .oc-special-menu-card, + .oc-info-band, + .oc-experience-band, + .oc-home-safari, + .oc-safari-card, + .oc-archive-card, + .oc-food-card, + .oc-transport-card, + .oc-page-card, + .oc-country, + .oc-special-card, + .oc-season-card, + .oc-blog-card, + .oc-feature-panel, + .oc-route-service-panel, + .oc-poi-related-grid a +):hover { + box-shadow: 0 24px 54px rgba(24, 50, 34, 0.2); + transform: translateY(-5px); +} + +:is( + .oc-route-action-card, + .oc-poi-fact-card, + .oc-poi-highlight-card, + .oc-includes div, + .oc-contact-card +):hover { + box-shadow: 0 18px 42px rgba(24, 50, 34, 0.13); +} + +:is( + .oc-special-menu-card, + .oc-info-band, + .oc-experience-band, + .oc-home-safari, + .oc-safari-card, + .oc-archive-card, + .oc-food-card, + .oc-transport-card, + .oc-page-card, + .oc-country, + .oc-special-card, + .oc-season-card, + .oc-blog-card, + .oc-feature-panel, + .oc-route-service-panel, + .oc-poi-related-grid a +):active { + box-shadow: 0 18px 38px rgba(24, 50, 34, 0.18); + transform: translateY(-2px) scale(0.995); +} + +:is( + .oc-special-menu-card, + .oc-info-band, + .oc-experience-band, + .oc-home-safari, + .oc-safari-card, + .oc-archive-card, + .oc-food-card, + .oc-transport-card, + .oc-page-card, + .oc-country, + .oc-special-card, + .oc-season-card, + .oc-blog-card, + .oc-feature-panel, + .oc-route-service-panel, + .oc-poi-related-grid a +):focus-visible { + outline: 3px solid rgba(72, 168, 80, 0.55); + outline-offset: 4px; +} + +:is(.oc-card-action, .oc-food-detail-link, .oc-special-menu-button) { + transition: color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, background-color 0.2s ease; +} + +:is( + .oc-special-menu-card, + .oc-info-band, + .oc-home-safari, + .oc-safari-card, + .oc-archive-card, + .oc-food-card, + .oc-transport-card, + .oc-page-card, + .oc-country, + .oc-special-card, + .oc-season-card, + .oc-blog-card, + .oc-feature-panel, + .oc-route-service-panel, + .oc-poi-related-grid a +):hover :is(.oc-card-action, .oc-food-detail-link) { + transform: translateX(4px); +} + +.oc-home-safari-image, +.oc-info-band-image, +.oc-experience-band > img, +.oc-special-menu-image img, +.oc-safari-card-image img, +.oc-archive-card-image > img, +.oc-food-card-image img, +.oc-transport-card > img, +.oc-page-card > img, +.oc-country > img, +.oc-special-card > img, +.oc-route-service-panel > img, +.oc-poi-related-grid img, +.oc-feature-panel > img { + transition: transform 0.32s ease, filter 0.32s ease; +} + +.oc-home-safari:hover .oc-home-safari-image, +.oc-info-band:hover .oc-info-band-image, +.oc-experience-band:hover > img, +.oc-special-menu-card:hover .oc-special-menu-image img, +.oc-safari-card:hover .oc-safari-card-image img, +.oc-archive-card:hover .oc-archive-card-image > img, +.oc-food-card:hover .oc-food-card-image img, +.oc-transport-card:hover > img, +.oc-page-card:hover > img, +.oc-country:hover > img, +.oc-special-card:hover > img, +.oc-route-service-panel:hover > img, +.oc-poi-related-grid a:hover img, +.oc-feature-panel:hover > img { + filter: saturate(1.04); + transform: scale(1.035); +} + +.oc-site .oc-header-cta { + min-width: 128px; + height: 45px; + padding: 0 20px; + border-radius: 2px; + background: var(--oc-action-bg); + color: #fff; + box-shadow: var(--oc-action-shadow); + transition: background-color 0.22s ease, box-shadow 0.22s ease, transform 0.22s ease; + will-change: transform; +} + +.oc-site .oc-header-cta:hover { + background: var(--oc-action-bg-hover); + color: #fff; + box-shadow: var(--oc-action-shadow-hover); +} + +.oc-mega { + position: absolute; + left: 0; + right: 0; + top: 91px; + overflow: hidden; + background: #edf5e8; + border-top: 0; + transform-origin: 50% 0; +} + +.oc-mega-inner { + width: 100%; + height: 360px; + display: flex; + gap: 0; + margin: 0; + padding: 0; + position: relative; +} + +.oc-mega-image { + width: clamp(340px, 30vw, 500px); + max-width: 40%; + height: 360px; + flex: 0 0 clamp(340px, 30vw, 500px); + overflow: hidden; + will-change: transform, opacity; +} + +.oc-mega-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-mega-groups { + display: flex; + flex: 1 1 auto; + gap: 0; + min-width: 0; +} + +.oc-mega-group { + width: auto; + max-width: 280px; + height: 360px; + flex: 1 1 180px; + padding: 0 18px; + min-width: 0; + will-change: transform, opacity; +} + +.oc-mega-group h5 { + width: 100%; + margin: 24px 0 12px; + color: #48a850; + font-family: 'Tenor Sans', Georgia, serif; + font-size: clamp(16px, 1.15vw, 18px); + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.5px; +} + +.oc-mega-group a { + width: 100%; + display: block; + margin: 0 0 5px; + padding: 0; + border-bottom: 0; + color: var(--oc-ink); + font-size: clamp(13px, 0.98vw, 15px); + font-weight: 400; + line-height: 1.65; + text-transform: none; + overflow-wrap: anywhere; +} + +.oc-menu-option { + position: relative; + will-change: transform, color, opacity; +} + +.oc-mega-option::before, +.oc-full-menu-link::before { + content: ''; + position: absolute; + left: -12px; + top: 50%; + width: 5px; + height: 5px; + border-radius: 999px; + background: currentColor; + opacity: 0; + transform: translate(-5px, -50%) scale(0.45); + transition: opacity 0.18s ease, transform 0.18s ease; +} + +.oc-mega-option:is(:hover, :focus-visible, .is-hovered) { + color: #2f8f43; +} + +.oc-full-menu-link:is(:hover, :focus-visible, .is-hovered) { + border-bottom-color: rgba(236, 247, 231, 0.58); + color: #fff; +} + +.oc-mega-option:is(:hover, :focus-visible, .is-hovered)::before, +.oc-full-menu-link:is(:hover, :focus-visible, .is-hovered)::before { + opacity: 0.82; + transform: translate(0, -50%) scale(1); +} + +.oc-special-menu-card:is(:hover, :focus-visible, .is-hovered) .oc-special-menu-button { + background: var(--oc-action-bg-hover); + color: #fff; + box-shadow: var(--oc-action-shadow-hover); +} + +.oc-mega-cta svg, +.oc-special-menu-button { + transition: color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; +} + +.oc-mega-cta:is(:hover, :focus-visible, .is-hovered) svg { + transform: translateX(3px); +} + +.oc-mega-group a svg { + display: none; +} + +.oc-mega-cta { + position: absolute; + right: 30px; + bottom: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-width: 128px; + height: 45px; + padding: 0 18px 0 20px; + border-radius: 2px; + background: var(--oc-action-bg); + color: #fff; + box-shadow: var(--oc-action-shadow); + font-size: 14px; + font-weight: 600; + line-height: 18px; + text-transform: uppercase; +} + +.oc-mega-cta svg { + width: 20px; + height: 20px; +} + +.oc-mega-cta:is(:hover, :focus-visible, .is-hovered) { + background: var(--oc-action-bg-hover); + color: #fff; + box-shadow: var(--oc-action-shadow-hover); +} + +.oc-mega.is-specials { + height: 320px; +} + +.oc-special-menu { + height: 320px; + display: flex; + padding: 24px 22px 72px; +} + +.oc-special-menu-card { + max-width: 50%; + flex: 0 0 50%; + display: flex; + min-width: 0; + will-change: transform, opacity; +} + +.oc-special-menu-image { + width: 30%; + max-width: 170px; + height: 190px; + position: relative; + flex: 0 0 30%; + overflow: hidden; + border-radius: 6px; +} + +.oc-special-menu-image span { + position: absolute; + top: 0; + right: 8px; + z-index: 1; + background: #9fc97f; + color: #fff; + padding: 5px; + font-size: 12px; + font-weight: 600; + line-height: 18px; +} + +.oc-special-menu-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-special-menu-content { + width: 50.125%; + margin-left: 16px; + min-width: 0; +} + +.oc-special-menu-content h5 { + margin: 6px 0 0; + padding: 0 0 8px; + border-bottom: 1px solid rgba(36, 51, 40, 0.65); + color: var(--oc-ink); + font-family: 'Tenor Sans', Georgia, serif; + font-size: 17px; + font-weight: 400; + line-height: 1.42; +} + +.oc-special-menu-content p { + margin: 0; + color: var(--oc-ink); + font-size: 15px; + line-height: 1.42; +} + +.oc-special-menu-date { + min-height: 58px; + padding: 8px 0; + border-bottom: 1px solid rgba(36, 51, 40, 0.65); + color: #48a850 !important; + font-size: 16px !important; + font-weight: 600; + line-height: 1.42 !important; +} + +.oc-special-menu-terms { + min-height: 58px; + padding-top: 12px; +} + +.oc-special-menu-button { + width: 100%; + height: 45px; + display: grid; + place-items: center; + border: 0; + border-radius: 2px; + background: var(--oc-action-bg); + color: #fff; + box-shadow: var(--oc-action-shadow); + font-size: 14px; + font-weight: 600; + line-height: 18px; + text-transform: uppercase; +} + +.oc-modal { + position: fixed; + inset: 0; + z-index: 60; + overflow-y: auto; + overscroll-behavior: contain; + background: rgba(21, 43, 30, 0.97); + color: #f6fff2; + transform-origin: 50% 42%; +} + +.oc-close { + position: absolute; + top: 28px; + right: 34px; + min-width: 44px; + min-height: 44px; + border: 0; + background: transparent; + color: #f6fff2; + display: inline-flex; + align-items: center; + gap: 10px; + cursor: pointer; + font-size: 13px; + text-transform: uppercase; +} + +.oc-full-menu-inner { + width: min(1360px, calc(100% - 80px)); + min-height: 100%; + margin: 0 auto; + padding: 104px 0 64px; + display: grid; + grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); + gap: 56px; + align-items: start; +} + +.oc-full-menu-intro { + max-width: 300px; + justify-self: start; + text-align: left; +} + +.oc-menu-logo { + width: 190px; + filter: brightness(0) invert(1) opacity(0.82); + display: block; + margin: 0 0 34px; +} + +.oc-full-menu h2, +.oc-search h2 { + font-size: 42px; + color: #dcebd5; + margin-bottom: 22px; +} + +.oc-full-menu p { + color: #ccdfc7; + font-size: 17px; + line-height: 1.65; + margin-bottom: 28px; +} + +.oc-full-menu-grid { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 26px; + align-items: start; +} + +.oc-full-menu-grid > div { + min-width: 0; +} + +.oc-full-menu-title { + display: inline-flex; + min-height: 44px; + align-items: center; + margin: 0 0 14px; + color: #73ae56; + font-family: 'Tenor Sans', Georgia, serif; + font-size: 18px; + font-weight: 400; + line-height: 1.2; + will-change: transform, color; +} + +.oc-full-menu-link { + display: block; + border-bottom: 1px solid rgba(220, 235, 213, 0.14); + padding: 7px 0; + color: #ecf7e7; + font-size: 12px; + text-transform: uppercase; + overflow-wrap: anywhere; + will-change: transform, color; +} + +.oc-search { + display: grid; + place-items: center; +} + +.oc-search-panel { + width: min(760px, calc(100% - 60px)); + text-align: center; +} + +.oc-search-panel label { + display: grid; + gap: 14px; + color: #d9ead3; +} + +.oc-search-panel input { + height: 62px; + border: 1px solid rgba(220, 235, 213, 0.35); + background: rgba(255, 255, 255, 0.04); + color: #fff; + padding: 0 24px; + text-align: center; +} + +.oc-search-links { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 14px; + margin-top: 26px; +} + +.oc-search-links a { + color: #dcebd5; + border-bottom: 1px solid currentColor; + font-size: 13px; + text-transform: uppercase; +} + +.oc-home-hero { + position: relative; + height: 80vh; + min-height: 720px; + background-position: center; + background-size: cover; + display: grid; + align-items: end; + text-align: center; + color: #fff; +} + +.oc-home-hero::after, +.oc-image-hero::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.22)); + pointer-events: none; +} + +.oc-awards { + position: absolute; + top: 62px; + left: 52px; + z-index: 1; + display: flex; + gap: 25px; + align-items: center; +} + +.oc-awards img { + width: 82px; + height: 82px; + object-fit: contain; +} + +.oc-hero-dots { + position: absolute; + top: 76px; + left: 54px; + z-index: 1; + display: flex; + align-items: center; + gap: 10px; +} + +.oc-hero-dots button { + width: 10px; + height: 10px; + border: 1px solid rgba(255, 255, 255, 0.86); + border-radius: 999px; + background: transparent; + cursor: pointer; +} + +.oc-hero-dots button.is-active { + width: 30px; + background: #fff; +} + +.oc-trip { + position: absolute; + top: 74px; + right: 64px; + z-index: 1; + width: 205px; + background: #fff; +} + +.oc-hero-copy { + position: relative; + z-index: 1; + padding: 0 24px 88px; +} + +.oc-hero-copy p { + font-size: 16px; + line-height: 28.8px; + letter-spacing: 1px; + margin-bottom: 19px; + opacity: 0.9; +} + +.oc-hero-copy h1 { + font-size: 50px; + line-height: 60px; + text-shadow: 0 3px 16px rgba(0, 0, 0, 0.22); + margin-bottom: 26px; +} + +.oc-hero-copy .oc-orange-btn { + min-width: 168px; + height: 45px; + font-size: 16px; +} + +.oc-plan-panel { + min-height: 275px; + display: grid; + grid-template-columns: 280px repeat(3, 235px); + gap: 45px; + align-items: start; + position: relative; + padding: 40px 0 70px; +} + +.oc-plan-title { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: 20px; +} + +.oc-curly { + font-family: Georgia, serif; + font-size: 148px; + line-height: 0.78; + transform: rotate(180deg); + color: var(--oc-ink); +} + +.oc-plan-title h2 { + font-size: 40px; + line-height: 48px; +} + +.oc-step { + display: block; +} + +.oc-step b { + display: inline; + width: auto; + height: auto; + border-radius: 0; + background: transparent; + color: var(--oc-ink); + font-size: 26px; + font-weight: 600; + line-height: 39px; + letter-spacing: 1px; + margin-right: 8px; +} + +.oc-step h3 { + display: inline; + font-family: 'Assistant', Arial, sans-serif; + font-size: 26px; + font-weight: 600; + line-height: 39px; + letter-spacing: 1px; + text-transform: uppercase; +} + +.oc-step p { + max-width: 235px; + color: var(--oc-ink); + font-size: 17px; + line-height: 25.5px; + margin-top: 10px; +} + +.oc-plan-panel .oc-outline-btn { + position: absolute; + left: 50%; + bottom: 12px; + transform: translateX(-50%); + height: 30px; +} + +.oc-home-plan-row { + min-height: auto; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: clamp(28px, 5vw, 72px); + padding: 58px 0 62px; + background: var(--oc-bg); +} + +.oc-home-plan-row .oc-step { + border-top: 1px solid rgba(36, 51, 40, 0.18); + padding-top: 18px; +} + +.oc-home-plan-row .oc-step p { + max-width: 315px; +} + +.oc-plan-steps-row { + min-height: auto; + position: relative; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: clamp(28px, 5vw, 72px); + padding: 78px 0 72px; + color: #f4f8ef; + isolation: isolate; +} + +.oc-plan-steps-row::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + z-index: -1; + width: 100vw; + transform: translateX(-50%); + background: var(--oc-footer); + pointer-events: none; +} + +.oc-plan-steps-row .oc-step { + border-top: 1px solid rgba(244, 248, 239, 0.28); + padding-top: 18px; +} + +.oc-plan-steps-row .oc-step b, +.oc-plan-steps-row .oc-step h3 { + color: #f7fbf4; +} + +.oc-plan-steps-row .oc-step p { + max-width: 315px; + color: rgba(244, 248, 239, 0.78); +} + +.oc-home-section { + padding: 0; +} + +.oc-home-section[id], +.oc-home-social-strip, +.oc-about-page section[id] { + scroll-margin-top: 118px; +} + +.oc-journey { + margin-top: 120px; + min-height: 912px; +} + +.oc-home-social-strip { + margin-top: -54px; + margin-bottom: 96px; + border-radius: 6px; + background: var(--oc-panel); + padding: 24px; + box-shadow: 0 14px 34px rgba(24, 50, 34, 0.1); +} + +.oc-home-social-strip .oc-social-channel-list { + gap: 10px; +} + +.oc-home-social-strip .oc-social-channel { + min-height: 58px; + justify-content: flex-start; + border: 1px solid rgba(72, 168, 80, 0.14); + border-radius: 5px; + background: rgba(255, 255, 255, 0.55); + padding: 10px 12px; + transition: opacity 180ms ease, transform 180ms ease, border-color 0.22s ease, background-color 0.22s ease; +} + +.oc-home-social-strip .oc-social-channel:hover, +.oc-home-social-strip .oc-social-channel:focus-visible { + border-color: rgba(72, 168, 80, 0.42); + background: #fff; +} + +.oc-home-social-strip .oc-social-channel img { + width: 32px; + height: 32px; +} + +.oc-home-social-strip .oc-social-channel strong { + font-size: 13px; +} + +.oc-home-social-strip .oc-social-channel span { + font-size: 11px; +} + +.oc-home-experiences { + margin-top: 60px; + min-height: 0; +} + +.oc-home-safaris-section { + margin-top: 120px; + min-height: 0; +} + +.oc-section-title { + text-align: center; + margin: 0 auto 30px; + max-width: 1120px; +} + +.oc-section-title h2 { + color: var(--oc-clay); + font-size: 48px; + line-height: 62.4px; +} + +.oc-section-title p { + max-width: 800px; + color: #59685c; + font-size: 16px; + line-height: 24px; + margin: 24px auto 0; +} + +.oc-info-band { + min-height: 250px; + background: var(--oc-panel); + margin-bottom: 30px; + display: grid; + grid-template-columns: minmax(0, 560px) minmax(0, 1fr); + align-items: stretch; + padding: 0; + border-radius: 4px; + overflow: hidden; +} + +.oc-info-band-image { + width: 100%; + height: 250px; + object-fit: cover; +} + +.oc-info-band-copy { + align-self: center; + padding: 26px 46px 26px 64px; +} + +.oc-info-band h3 { + font-size: 32px; + line-height: 38.4px; + color: #334632; + margin-bottom: 18px; +} + +.oc-info-band p { + font-size: 16px; + line-height: 24px; + color: var(--oc-ink); + margin-bottom: 16px; +} + +.oc-info-band .oc-card-action { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--oc-clay); + font-size: 10px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-journey .oc-section-title { + margin-bottom: 40px; +} + +.oc-journey .oc-info-band { + margin-bottom: 30px; +} + +.oc-journey .oc-info-band:first-of-type { + margin-bottom: 30px; +} + +.oc-journey .oc-info-band:last-of-type { + margin-bottom: 0; +} + +.oc-experience-stack { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.oc-experience-band { + min-height: 520px; + display: grid; + grid-template-rows: 300px minmax(220px, 1fr); + text-align: center; + color: #fff; + overflow: hidden; + border-radius: 10px; +} + +.oc-experience-band.tan { + background: #9dbf73; +} + +.oc-experience-band.clay { + background: #48a850; +} + +.oc-experience-band.charcoal { + background: #193523; +} + +.oc-experience-band img { + width: 100%; + height: 300px; + object-fit: cover; +} + +.oc-experience-band-content { + display: grid; + align-content: center; + justify-items: center; + gap: 24px; + padding: 28px 28px 30px; +} + +.oc-experience-band span, +.oc-experience-band small { + color: #f4fbef; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 1.5px; + text-transform: uppercase; +} + +.oc-experience-band h3 { + max-width: 320px; + color: #fff; + font-size: 36px; + line-height: 43.2px; +} + +.oc-experience-band small { + display: inline-flex; + align-items: center; + gap: 16px; +} + +.oc-home-safaris { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 32px; +} + +.oc-home-safari { + position: relative; + min-height: 560px; + display: flex; + flex-direction: column; + background: var(--oc-panel); + border-radius: 4px; + padding: 0; + overflow: hidden; +} + +.oc-home-safari-image { + width: 100%; + height: 200px; + object-fit: cover; +} + +.oc-home-safari-body { + flex: 1; + display: flex; + flex-direction: column; + padding: 24px 20px 27px; +} + +.oc-home-safari::after { + content: 'English'; + position: absolute; + top: 13px; + right: 18px; + background: #9fbf79; + color: #fff; + padding: 6px 10px; + font-size: 10px; +} + +html[data-oc-language='zh'] .oc-home-safari::after { + content: '英语'; +} + +.oc-home-safari span { + color: #6a766b; + font-size: 12px; + font-weight: 700; +} + +.oc-home-safari h3 { + margin: 18px 0 12px; + font-family: 'Tenor Sans', Georgia, serif; + color: #223127; + font-size: 18px; + font-weight: 600; + line-height: 25.2px; +} + +.oc-home-safari b { + display: block; + border-top: 1px solid #c4d4bc; + border-bottom: 1px solid #c4d4bc; + color: #243328; + font-size: 11px; + padding: 9px 0; +} + +.oc-home-safari p { + color: #5e6c61; + font-size: 11px; + line-height: 1.5; + margin: 12px 0 18px; +} + +.oc-home-safari .oc-card-action { + display: flex; + align-items: center; + justify-content: center; + height: 31px; + margin-top: auto; + border: 1px solid var(--oc-clay); + color: var(--oc-clay-dark); + font-size: 10px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-center { + text-align: center; + margin-top: 28px; +} + +.oc-quote { + margin-top: 120px; + margin-bottom: 120px; + position: relative; + min-height: 380px; + width: min(1116px, calc(100% - 120px)); + border: 0; + border-radius: 5px; + background: #3f8f43; + color: #fff; + display: grid; + grid-template-columns: 448px minmax(0, 1fr); + align-content: stretch; + justify-items: stretch; + padding: 0; + overflow: hidden; +} + +.oc-quote::after { + content: ''; + position: absolute; + right: 32px; + bottom: 28px; + width: 156px; + height: 1px; + background: rgba(247, 251, 244, 0.34); +} + +.oc-quote-media { + position: relative; + min-height: 380px; +} + +.oc-quote-media::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(90deg, rgba(24, 50, 34, 0.08), rgba(24, 50, 34, 0.42)); +} + +.oc-quote img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.oc-quote-copy { + position: relative; + display: grid; + align-content: center; + justify-items: start; + padding: 64px 58px 56px; +} + +.oc-quote-copy::before { + display: none; +} + +.oc-quote blockquote { + position: relative; + z-index: 1; + margin: 0; +} + +.oc-quote p { + max-width: 590px; + color: #f4f8ef; + font-family: 'STXingkai', 'Xingkai SC', 'HanziPen SC', 'Kaiti SC', 'KaiTi', 'STKaiti', cursive; + font-size: 26px; + font-weight: 400; + line-height: 1.86; + text-indent: 2em; + text-wrap: pretty; +} + +.oc-quote span { + position: relative; + z-index: 1; + justify-self: end; + margin-top: 28px; + font-family: 'STXingkai', 'Xingkai SC', 'HanziPen SC', 'Kaiti SC', 'KaiTi', 'STKaiti', cursive; + font-size: 22px; + color: #d9ead3; +} + +.oc-live { + width: min(100%, 1440px); + text-align: left; + min-height: 0; + padding-bottom: 26px; + overflow: hidden; +} + +.oc-live h2 { + color: var(--oc-clay); + font-size: 48px; + line-height: 62.4px; + margin: 0 0 28px; + text-align: center; +} + +.oc-live-carousel { + overflow: hidden; + width: 100%; + -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%); + mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%); +} + +.oc-live-track { + display: flex; + width: max-content; + animation: oc-live-scroll 44s linear infinite; +} + +.oc-live-carousel:hover .oc-live-track { + animation-play-state: paused; +} + +.oc-live-set { + display: flex; + flex: 0 0 auto; + gap: 14px; + padding-right: 14px; +} + +.oc-live-slide { + flex: 0 0 292px; + width: 292px; + margin: 0; + border-radius: 6px; + overflow: hidden; + background: var(--oc-panel); + box-shadow: 0 14px 28px rgba(24, 50, 34, 0.12); +} + +.oc-live-slide img { + width: 100%; + height: 214px; + object-fit: cover; + display: block; +} + +@keyframes oc-live-scroll { + from { + transform: translateX(0); + } + + to { + transform: translateX(-50%); + } +} + +@media (prefers-reduced-motion: reduce) { + .oc-live-carousel { + overflow-x: auto; + -webkit-mask-image: none; + mask-image: none; + } + + .oc-live-track { + animation: none; + } + + .oc-live-set[aria-hidden='true'] { + display: none; + } +} + +.oc-breadcrumb { + position: relative; + z-index: 2; + padding: 28px 18px; + color: #5b675c; + font-size: 12px; +} + +.oc-breadcrumb.is-light { + position: absolute; + left: 0; + right: 0; + color: #fff; +} + +.oc-page-safaris, +.oc-experiences-page, +.oc-food-page, +.oc-transport-page, +.oc-collective-page, +.oc-specials-page, +.oc-destinations-page { + position: relative; +} + +.oc-page-safaris, +.oc-experiences-page, +.oc-food-page, +.oc-transport-page, +.oc-collective-page { + --oc-module-hero-band-height: clamp(382px, 30vw, 468px); + background-color: var(--oc-bg); + overflow-x: hidden; +} + +.oc-page-safaris::before, +.oc-experiences-page::before, +.oc-food-page::before, +.oc-transport-page::before, +.oc-collective-page::before, +.oc-page-safaris::after, +.oc-experiences-page::after, +.oc-food-page::after, +.oc-transport-page::after, +.oc-collective-page::after { + content: ''; + position: absolute; + inset: 0 0 auto; + height: var(--oc-module-hero-band-height); + pointer-events: none; +} + +.oc-page-safaris::before, +.oc-experiences-page::before, +.oc-food-page::before, +.oc-transport-page::before, +.oc-collective-page::before { + z-index: 0; + background-image: var(--oc-module-hero-image); + background-size: cover; + background-position: center bottom; +} + +.oc-page-safaris::after, +.oc-experiences-page::after, +.oc-food-page::after, +.oc-transport-page::after, +.oc-collective-page::after { + z-index: 0; + background: + linear-gradient(180deg, rgba(244, 248, 239, 0.72) 0%, rgba(244, 248, 239, 0.58) 43%, rgba(244, 248, 239, 0.42) 68%, var(--oc-bg) 100%), + rgba(244, 248, 239, 0.04); +} + +.oc-page-safaris .oc-breadcrumb, +.oc-experiences-page .oc-breadcrumb, +.oc-food-page .oc-breadcrumb, +.oc-transport-page .oc-breadcrumb, +.oc-collective-page .oc-breadcrumb, +.oc-specials-page .oc-breadcrumb { + position: absolute; + inset: 0 auto auto 0; + width: auto; + padding: 24px 20px; + color: #243328; + text-shadow: 0 1px 18px rgba(244, 248, 239, 0.95); +} + +.oc-list-intro { + min-height: 60vh; + display: grid; + align-content: center; + justify-items: center; + text-align: center; + padding: 30px 24px; +} + +.oc-list-intro h1 { + color: #48a850; + font-size: 55px; + line-height: 1.5; + margin: 0; +} + +.oc-list-intro h2 { + color: #56645b; + font-size: 26px; + margin-bottom: 18px; +} + +.oc-list-intro p { + max-width: 670px; + color: #243328; + font-size: 22px; + line-height: 1.5; +} + +.oc-route-summary-copy span, +.oc-route-cta span { + color: var(--oc-clay); + font-size: 11px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-safaris-layout { + position: relative; + z-index: 1; + width: min(1800px, 100%); + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 28px; + align-items: start; + padding: 0 24px 96px; +} + +.oc-filters { + position: sticky; + top: 91px; + z-index: 24; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: start; + justify-self: center; + width: 100%; + max-width: 100%; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +.oc-filter-groups { + display: flex; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + width: 100%; + max-width: 100%; + gap: 4px; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-x: contain; + scrollbar-width: none; +} + +.oc-filter-groups::-webkit-scrollbar { + display: none; +} + +.oc-filter-mobile-accordion { + display: none; +} + +.oc-filter-mobile-panel { + display: none; +} + +.oc-filter-group { + display: grid; + grid-template-columns: minmax(0, 1fr); + justify-items: center; + align-items: start; + width: max-content; + max-width: 100%; + min-width: max-content; + flex: 0 0 auto; + gap: 6px; + border: 1px solid rgba(196, 212, 188, 0.52); + border-radius: 13px; + padding: 6px; + margin: 0; + background: rgba(244, 248, 239, 0.44); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72); + backdrop-filter: blur(16px) saturate(1.08); + -webkit-backdrop-filter: blur(16px) saturate(1.08); +} + +.oc-filter-label { + width: auto; + height: 22px; + padding: 0 2px; + margin: 0; + color: #243328; + font-size: 12px; + font-weight: 700; + line-height: 24px; + text-align: center; +} + +.oc-filter-options { + display: flex; + flex-wrap: nowrap; + justify-content: center; + gap: 4px; + width: max-content; + max-width: 100%; + min-width: max-content; + overflow: visible; + padding: 1px 3px 3px 1px; +} + +.oc-filter-options label { + position: relative; + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 29px; + margin: 0; + padding: 0 7px; + border: 1px solid transparent; + border-radius: 999px; + background: rgba(226, 239, 220, 0.68); + color: #5e6c61; + font-size: 11px; + line-height: 1; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; +} + +.oc-filter-options label:hover, +.oc-filter-options label.is-active { + border-color: rgba(72, 168, 80, 0.32); + background: #dbead4; + color: #243328; +} + +.oc-filter-group input { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.oc-filter-options label span { + width: 7px; + height: 7px; + border: 1px solid #93a58f; + border-radius: 999px; + flex: 0 0 7px; + transition: background-color 0.18s ease, border-color 0.18s ease; +} + +.oc-filter-options label span.checked { + background: #48a850; + border-color: #48a850; + box-shadow: 0 0 0 3px rgba(72, 168, 80, 0.16); +} + +.oc-filter-empty { + grid-column: 1 / -1; + width: 100%; + min-height: 180px; + display: grid; + place-items: center; + border: 1px solid rgb(88 112 86 / 0.26); + border-radius: 10px; + color: #5e6c61; + font-size: 16px; + text-align: center; +} + +.oc-safari-grid .oc-filter-empty { + flex: 1 1 100%; +} + +.is-collective .oc-filter-empty { + border-color: rgb(88 112 86 / 0.26); + color: #5e6c61; +} + +.oc-safari-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + gap: 16px; +} + +.oc-safari-card { + flex: 0 0 min(30.64%, 434px); + width: min(30.64%, 434px); + max-width: 434px; + overflow: hidden; + border-radius: 10px; + background: #183222; + color: #fff; +} + +.oc-safari-card-image { + position: relative; + height: 300px; +} + +.oc-safari-card-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-safari-card-image span { + position: absolute; + left: 0; + top: 18px; + background: rgba(24, 50, 34, 0.78); + padding: 10px 12px; + color: #fff; + font-size: 12px; +} + +.oc-safari-card-body { + min-height: 305px; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr) auto auto; + gap: 11px; + padding: 18px 18px 16px; +} + +.oc-safari-card-body > p:first-child { + color: #bcd6a9; + font-size: 10px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-safari-card-body h3 { + font-family: 'Assistant', Arial, sans-serif; + color: #fff; + font-size: 24px; + font-weight: 700; + line-height: 1.15; + margin: 0; +} + +.oc-safari-card-meta, +.oc-safari-card-tags { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 7px; +} + +.oc-safari-card-meta span, +.oc-safari-card-tags span { + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 4px; + color: #f3fbeb; + font-size: 11px; + line-height: 1.2; + white-space: nowrap; + padding: 6px 7px; +} + +.oc-safari-card-tags span { + background: rgba(255, 255, 255, 0.08); +} + +.oc-safari-card-summary { + color: #d8ead2; + font-size: 14px; + line-height: 1.55; +} + +.oc-safari-card-body > div:last-child { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; +} + +.oc-safari-card-body b, +.oc-safari-card-body .oc-card-action { + color: #fff; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-safari-card-body .oc-card-action { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.oc-menu-page-hero { + min-height: clamp(260px, 17vw, 326px); + position: relative; + z-index: 1; + display: grid; + place-items: center; + padding: 54px 24px 28px; + background: transparent; + text-align: center; +} + +.oc-menu-page-hero::before, +.oc-menu-page-hero::after { + content: none; +} + +.oc-menu-page-hero-content { + display: grid; + justify-items: center; + gap: 14px; + width: min(650px, 100%); + position: relative; + z-index: 1; +} + +.oc-menu-page-hero-brand { + display: inline-flex; + align-items: center; + justify-content: center; + gap: clamp(10px, 1.25vw, 18px); + width: auto; + max-width: calc(100vw - 48px); + margin-inline: auto; +} + +.oc-menu-page-hero-logo { + width: clamp(145px, 16vw, 230px); + height: auto; + display: block; + flex: 0 1 auto; + min-width: 0; + filter: + drop-shadow(0 1px 0 rgba(255, 255, 255, 0.8)) + drop-shadow(0 18px 36px rgba(24, 50, 34, 0.18)); +} + +.oc-menu-page-hero-tag { + flex: 0 0 auto; + color: var(--oc-logo-green); + font-family: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', Arial, sans-serif; + font-size: clamp(32px, 3.1vw, 44px); + font-weight: 700; + line-height: 1; + white-space: nowrap; + text-shadow: 0 1px 18px rgba(244, 248, 239, 0.9); +} + +.oc-menu-page-hero-tag-mark { + flex: 0 0 2px; + width: 2px; + height: clamp(30px, 3vw, 42px); + background: var(--oc-logo-green); + border-radius: 999px; +} + +.oc-menu-page-hero p { + width: min(600px, 100%); + max-width: 600px; + margin-inline: auto; + color: #203326; + font-size: clamp(16px, 1.45vw, 18px); + line-height: 1.52; + text-align: left; + text-indent: 2em; + text-wrap: pretty; + text-shadow: 0 1px 22px rgba(244, 248, 239, 0.96); + transform: translateX(clamp(-28px, -1.8vw, -8px)); +} + +.oc-archive-layout { + position: relative; + z-index: 1; + width: min(100%, 1800px); + max-width: 1800px; + margin: 0 auto; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 28px; + align-items: start; + padding-left: 24px; + padding-right: 24px; + padding-bottom: 150px; +} + +.oc-archive-filters { + top: 91px; +} + +.oc-page-safaris .oc-safaris-layout, +.oc-experiences-page .oc-archive-layout, +.oc-collective-page .oc-archive-layout { + margin-top: clamp(-44px, -2.4vw, -26px); +} + +.oc-archive-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; +} + +.oc-archive-card { + display: grid; + grid-template-rows: 208px minmax(0, 1fr); + overflow: hidden; + border-radius: 13px; + background: #243328; + color: #fff; + height: 475px; +} + +.oc-archive-card-image { + position: relative; + height: 208px; + overflow: hidden; +} + +.oc-archive-card-image > img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-archive-card-image > span { + position: absolute; + left: 0; + top: 0; + z-index: 1; + background: rgba(24, 50, 34, 0.75); + color: #fff; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.04em; + padding: 8px 13px; + text-transform: uppercase; +} + +.oc-archive-card-body { + min-height: 0; + min-width: 0; + padding: 18px 16px 21px; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr) auto; + align-content: stretch; + gap: 10px; +} + +.oc-archive-card-body h3 { + min-width: 0; + color: #fff; + font-family: 'Tenor Sans', Georgia, serif; + font-size: 19px; + font-weight: 400; + line-height: 1.25; + display: -webkit-box; + overflow-wrap: anywhere; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; +} + +.oc-archive-card-body b { + min-width: 0; + color: rgba(237, 247, 232, 0.76); + font-size: 12px; + font-weight: 700; + line-height: 1.35; + display: -webkit-box; + overflow-wrap: anywhere; + overflow: hidden; + text-transform: none; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; +} + +.oc-archive-card-body p { + min-width: 0; + color: rgba(255, 255, 255, 0.9); + font-size: 13px; + line-height: 1.45; + display: -webkit-box; + overflow-wrap: anywhere; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + line-clamp: 5; +} + +.oc-archive-card-body .oc-card-action { + align-self: end; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 45px; + border: 1px solid currentColor; + border-radius: 5px; + color: #fff; + font-size: 13px; + font-weight: 600; + line-height: 1.2; + text-transform: uppercase; +} + +.oc-food-page, +.oc-transport-page { + background: #f4f8ef; +} + +.oc-food-intro, +.oc-transport-intro { + display: grid; + grid-template-columns: minmax(240px, 0.42fr) minmax(0, 0.58fr); + gap: 48px; + padding: 80px 0 36px; + border-bottom: 1px solid rgba(36, 51, 40, 0.12); +} + +.oc-food-intro h2, +.oc-transport-intro h2 { + color: var(--oc-ink); + font-size: clamp(30px, 3vw, 52px); + line-height: 1.05; +} + +.oc-food-intro p, +.oc-transport-intro p { + color: var(--oc-muted); + font-size: 18px; + line-height: 1.75; +} + +.oc-food-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px; + padding: 0; +} + +.oc-food-archive { + display: grid; + gap: 28px; + padding: 0 0 72px; +} + +.oc-food-card { + display: grid; + grid-template-rows: 230px 1fr; + min-width: 0; + background: #fbfff7; + border: 1px solid rgba(36, 51, 40, 0.12); +} + +.oc-food-card-image { + position: relative; + overflow: hidden; +} + +.oc-food-card-image img, +.oc-transport-card > img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-food-card-image span { + position: absolute; + left: 16px; + bottom: 16px; + padding: 7px 10px; + background: rgba(244, 248, 239, 0.9); + color: var(--oc-ink); + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.oc-food-card-body { + display: grid; + gap: 14px; + padding: 24px; +} + +.oc-food-card-body > p:first-child, +.oc-transport-card span { + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.oc-food-card-body h2, +.oc-transport-card h2 { + color: var(--oc-ink); + font-size: 34px; + line-height: 1; +} + +.oc-food-card-body > p:last-child, +.oc-transport-card p, +.oc-transport-card dd { + color: var(--oc-muted); + font-size: 15px; + line-height: 1.7; +} + +.oc-food-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.oc-food-tags span { + padding: 8px 10px; + background: #e2efdc; + color: #52634f; + font-size: 13px; + line-height: 1.2; +} + +.oc-menu-bottom-prompt { + display: grid; + justify-items: center; + gap: 14px; + box-sizing: border-box; + margin-bottom: 77px; + padding: 30px 24px 28px; + text-align: center; +} + +.oc-menu-bottom-prompt h2 { + color: var(--oc-ink); + font-size: clamp(32px, 3vw, 38px); + line-height: 1.18; + text-wrap: balance; +} + +.oc-menu-bottom-prompt-body { + display: grid; + justify-items: center; + gap: 12px; + min-width: 0; +} + +.oc-menu-bottom-prompt p { + max-width: 760px; + color: var(--oc-muted); + font-size: 15px; + line-height: 1.72; + text-wrap: pretty; +} + +.oc-menu-bottom-prompt .oc-orange-btn { + justify-self: center; + margin-top: 2px; + white-space: nowrap; +} + +.oc-transport-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 24px; + padding: 0 0 72px; +} + +.oc-transport-card { + display: grid; + grid-template-columns: 210px minmax(0, 1fr); + min-width: 0; + background: #fbfff7; + border: 1px solid rgba(36, 51, 40, 0.12); +} + +.oc-transport-card > img { + min-height: 100%; +} + +.oc-transport-card > div { + display: grid; + gap: 14px; + padding: 24px; +} + +.oc-transport-card dl { + display: grid; + gap: 10px; + margin: 0; +} + +.oc-transport-card dl div { + display: grid; + grid-template-columns: 54px minmax(0, 1fr); + gap: 12px; +} + +.oc-transport-card dt { + color: var(--oc-clay-dark); + font-size: 13px; + font-weight: 700; +} + +.oc-transport-card dd { + margin: 0; +} + +.oc-collective-page { + background: var(--oc-bg); + color: var(--oc-ink); +} + +.oc-archive-layout.is-collective { + padding-bottom: 105px; +} + +.oc-archive-layout.has-bottom-prompt, +.oc-archive-layout.is-collective.has-bottom-prompt { + padding-bottom: 72px; +} + +.is-collective .oc-archive-grid { + gap: 18px; +} + +.is-collective .oc-filter-label, +.is-collective .oc-filter-options label { + color: #243328; +} + +.is-collective .oc-filter-group { + border-color: rgba(36, 51, 40, 0.16); +} + +.is-collective .oc-filter-options label span { + border-color: #93a58f; +} + +@media (min-width: 1600px) { + .oc-archive-card { + height: 450px; + } +} + +.oc-footer-legal { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + gap: clamp(18px, 2.4vw, 30px); + width: min(1116px, calc(100% - 80px)); + margin: 0 auto; + padding: 22px 0 26px; + border-top: 1px solid rgba(74, 110, 70, 0.24); + color: #6a756d; + font-size: 13px; +} + +.oc-footer-legal-logo { + flex: 0 0 auto; +} + +.oc-footer-legal-logo img { + width: 92px; + height: auto; + object-fit: contain; +} + +.oc-footer-legal-meta { + display: flex; + flex: 1 1 auto; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: 8px 24px; + min-width: 0; +} + +.oc-footer-company-line, +.oc-footer-legal-links { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: 8px; + margin: 0; + line-height: 1.6; + white-space: nowrap; +} + +.oc-footer-legal .oc-footer-email { + color: var(--oc-clay); + font-weight: 700; +} + +.oc-footer-legal a { + color: inherit; + text-decoration: none; + transition: color 0.18s ease; +} + +.oc-footer-legal a:hover, +.oc-footer-legal a:focus-visible { + color: var(--oc-clay); +} + +.oc-image-hero { + min-height: 660px; + position: relative; + overflow: hidden; + background-size: cover; + background-position: center; + display: grid; + align-items: end; + justify-items: start; + padding: 0 0 84px 154px; + color: #fff; +} + +.oc-image-hero-media { + position: absolute; + inset: 0; + z-index: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-image-hero.is-center { + align-items: center; + justify-items: center; + padding: 96px 24px 40px; + text-align: center; +} + +.oc-image-hero.is-center h1 { + font-size: clamp(48px, 5.4vw, 74px); +} + +.oc-image-hero.is-center p { + font-size: 22px; + max-width: 620px; + margin-left: auto; + margin-right: auto; +} + +.oc-image-hero-content { + position: relative; + z-index: 1; + max-width: 760px; +} + +.oc-image-hero h1 { + font-size: clamp(56px, 7vw, 96px); + line-height: 1.02; + margin-bottom: 18px; +} + +.oc-image-hero p { + font-family: 'Tenor Sans', Georgia, serif; + font-size: 32px; + line-height: 1.25; + margin-bottom: 16px; +} + +.oc-image-hero-content > span { + display: inline-block; + font-size: 20px; + margin-top: 2px; +} + +.oc-destinations-page .oc-image-hero { + min-height: 60vh; + align-items: center; + justify-items: center; + padding: 30px 0; + text-align: center; +} + +.oc-destinations-page .oc-image-hero-content { + width: min(1120px, calc(100% - 48px)); + max-width: none; + transform: translateY(10px); +} + +.oc-destinations-page .oc-image-hero h1 { + width: 100%; + font-size: 51px; + line-height: 1.5; + margin: 0; +} + +.oc-destinations-page .oc-hero-actions { + justify-content: center; + margin-top: 53px; +} + +.oc-destinations-page .oc-hero-actions .oc-orange-btn { + width: auto; + min-width: 128px; + height: 45px; + min-height: 45px; + padding: 0 20px; + border: 0; + border-radius: 2px; + background: var(--oc-action-bg); + color: #fff; + box-shadow: var(--oc-action-shadow); + font-size: 14px; + font-weight: 600; + line-height: 18px; + text-transform: uppercase; +} + +.oc-hero-actions { + display: flex; + align-items: center; + gap: 24px; + margin-top: 25px; +} + +.oc-video-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: #fff; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-tabs { + min-height: 72px; + background: #183222; + color: #dcebd5; + display: flex; + align-items: center; + justify-content: center; + gap: 54px; + padding: 0 24px; + position: sticky; + top: 84px; + z-index: 20; +} + +.oc-tabs a { + display: inline-flex; + align-items: center; + gap: 9px; + flex: 0 0 auto; + min-height: 44px; + padding: 0 2px; + font-size: 12px; + font-weight: 800; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; +} + +.oc-poi-page :is(section, article, aside)[id], +.oc-route-detail-page :is(section, a)[id] { + scroll-margin-top: 172px; +} + +.oc-detail-icon-slot { + width: 45px; + height: 45px; + display: inline-grid; + place-items: center; + flex: 0 0 auto; + border: 1px solid #b5c7ad; + border-radius: 50%; +} + +.oc-detail-icon-slot.is-tab { + width: 24px; + height: 24px; + border-color: rgb(220 235 213 / 0.5); +} + +.oc-detail-icon-slot.is-live { + width: 34px; + height: 34px; + border-color: rgb(255 255 255 / 0.56); +} + +.oc-detail-icon-slot img { + width: 58%; + height: 58%; + object-fit: contain; +} + +.oc-tabs .oc-detail-icon-slot img { + filter: brightness(0) saturate(100%) invert(91%) sepia(12%) saturate(259%) hue-rotate(54deg) brightness(104%) contrast(90%); + opacity: 0.96; +} + +.oc-detail-icon-placeholder { + width: 42%; + height: 42%; + display: block; + border: 1px solid currentColor; + color: inherit; + opacity: 0.52; + transform: rotate(45deg); +} + +.oc-tabs .oc-detail-icon-placeholder, +.oc-live-stats .oc-detail-icon-placeholder { + color: #fff; +} + +.oc-detail-strip { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 17px; + padding: 85px 0 66px; +} + +.oc-gallery-trigger { + display: block; + min-width: 0; + padding: 0; + border: 0; + background: transparent; + color: inherit; + cursor: zoom-in; +} + +.oc-gallery-trigger img { + transition: transform 220ms ease, filter 220ms ease; +} + +.oc-gallery-trigger:hover img { + filter: brightness(1.04); + transform: scale(1.018); +} + +.oc-detail-strip .oc-gallery-trigger, +.oc-detail-strip img { + width: 100%; + height: 194px; + overflow: hidden; +} + +.oc-detail-strip .oc-gallery-trigger img, +.oc-detail-strip > img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-detail-overview { + display: grid; + grid-template-columns: 1fr 330px; + gap: 118px; + padding: 14px 0 88px; +} + +.oc-detail-gallery { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} + +.oc-detail-gallery img { + width: 100%; + height: 245px; + object-fit: cover; +} + +.oc-detail-gallery img:first-child { + grid-column: span 2; + height: 360px; +} + +.oc-detail-copy h2, +.oc-itinerary h2, +.oc-rates h2, +.oc-highlights h2, +.oc-article h2, +.oc-season h2, +.oc-region-grid h2, +.oc-gallery-heading h2, +.oc-reviews h2 { + color: var(--oc-clay); + font-size: 44px; + line-height: 1.12; + margin-bottom: 26px; +} + +.oc-detail-copy p, +.oc-article p, +.oc-season p, +.oc-split-cta p, +.oc-country p, +.oc-itinerary-stop p { + color: #4f6054; + font-size: 17px; + line-height: 1.7; + margin-bottom: 20px; +} + +.oc-price-card { + margin-top: 0; + border: 1px solid var(--oc-line); + padding: 26px; + display: grid; + gap: 10px; + align-self: start; + background: rgba(244, 248, 239, 0.88); +} + +.oc-price-card span { + margin: -26px -26px 12px; + padding: 13px 20px; + background: #527a45; + color: #fff; + font-size: 13px; + font-weight: 800; +} + +.oc-price-card strong { + color: #243328; + font-size: 28px; + font-weight: 700; +} + +.oc-price-card em { + color: #6a766b; + font-style: normal; + margin-bottom: 12px; +} + +.oc-price-card label { + color: #6d7b70; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-price-card select { + min-height: 44px; + border: 1px solid var(--oc-line); + background: transparent; + color: #4d5e51; + padding: 0 12px; +} + +.oc-price-card small { + color: #5e6c61; + font-size: 12px; + line-height: 1.55; + margin: 4px 0 10px; + text-transform: uppercase; +} + +.oc-mini-route { + color: var(--oc-clay-dark) !important; + font-size: 14px !important; +} + +.oc-route-summary { + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 76px; + align-items: start; + padding: 16px 0 48px; +} + +.oc-route-summary-copy h2, +.oc-route-cta h2 { + color: var(--oc-clay); + font-size: 46px; + line-height: 1.1; + margin: 10px 0 24px; +} + +.oc-route-summary-copy p, +.oc-route-cta p { + color: #4f6054; + font-size: 18px; + line-height: 1.68; + margin-bottom: 20px; +} + +.oc-route-tags, +.oc-route-action-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.oc-route-tags span, +.oc-route-action-meta b { + border: 1px solid rgba(74, 110, 70, 0.18); + border-radius: 4px; + color: #4d5e51; + font-size: 12px; + font-weight: 800; + padding: 8px 10px; +} + +.oc-route-decision-list { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 0 0 18px; +} + +.oc-route-decision-list b { + border-left: 3px solid #7fbf6b; + background: rgba(224, 238, 218, 0.66); + color: #334632; + font-size: 13px; + font-weight: 800; + line-height: 1.35; + padding: 9px 11px; +} + +.oc-route-action-card { + border: 1px solid var(--oc-line); + border-radius: 8px; + background: rgba(244, 248, 239, 0.9); + display: grid; + gap: 14px; + padding: 26px; +} + +.oc-route-action-card > span { + margin: -26px -26px 4px; + padding: 13px 20px; + background: #527a45; + color: #fff; + font-size: 13px; + font-weight: 800; +} + +.oc-route-action-card strong { + color: #243328; + font-size: 30px; + line-height: 1.1; +} + +.oc-route-action-card em { + color: #6a766b; + font-style: normal; +} + +.oc-route-action-card small { + color: #5e6c61; + font-size: 13px; + line-height: 1.55; +} + +.oc-route-section-head { + display: grid; + grid-template-columns: minmax(220px, 0.42fr) minmax(0, 0.58fr); + gap: 42px; + align-items: end; + margin-bottom: 24px; +} + +.oc-route-section-head span { + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.oc-route-section-head h2, +.oc-route-service-panel h2 { + color: var(--oc-clay); + font-size: clamp(34px, 4vw, 52px); + line-height: 1.05; +} + +.oc-route-section-head p { + color: #4f6054; + font-size: 17px; + line-height: 1.65; +} + +.oc-includes ul { + display: grid; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} + +.oc-includes li { + position: relative; + color: #4f6054; + font-size: 16px; + line-height: 1.55; + padding-left: 20px; +} + +.oc-includes li::before { + content: ''; + position: absolute; + left: 0; + top: 0.72em; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--oc-clay); +} + +.oc-route-flow { + padding: 8px 0 82px; +} + +.oc-route-flow-layout { + display: grid; + grid-template-columns: minmax(280px, 0.42fr) minmax(0, 0.58fr); + gap: 42px; + align-items: start; +} + +.oc-route-flow figure { + position: sticky; + top: 116px; + overflow: hidden; + border-radius: 8px; + background: #183222; + color: #eff8eb; + margin: 0; +} + +.oc-route-flow figure img { + width: 100%; + height: 440px; + object-fit: cover; +} + +.oc-route-flow figcaption { + border-top: 1px solid rgba(255, 255, 255, 0.16); + color: #e4f3dc; + font-size: 15px; + line-height: 1.55; + padding: 18px 20px; +} + +.oc-route-day-list { + display: grid; + gap: 10px; +} + +.oc-route-day { + display: grid; + grid-template-columns: 86px minmax(0, 1fr); + gap: 16px; + border-top: 1px solid rgba(74, 110, 70, 0.18); + padding: 16px 0; +} + +.oc-route-day > span { + align-self: start; + width: fit-content; + border-radius: 4px; + background: #e5f2de; + color: var(--oc-clay); + font-size: 12px; + font-weight: 800; + line-height: 1; + padding: 9px 10px; + text-transform: uppercase; +} + +.oc-route-day h3 { + color: #40382f; + font-size: 22px; + line-height: 1.2; + margin: 2px 0 8px; +} + +.oc-route-day p { + color: #4f6054; + font-size: 14px; + line-height: 1.55; + margin-bottom: 8px; +} + +.oc-route-day div div { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.oc-route-day b { + border: 1px solid rgba(74, 110, 70, 0.2); + border-radius: 4px; + color: #536459; + font-size: 11px; + padding: 6px 8px; +} + +.oc-route-service-stack { + display: grid; + gap: 28px; + padding: 0 0 82px; +} + +.oc-route-service-panel { + display: grid; + grid-template-columns: minmax(280px, 0.38fr) minmax(0, 0.62fr); + min-height: 390px; + overflow: hidden; + border-radius: 8px; + background: #e7f1e0; + cursor: default; + will-change: auto; +} + +.oc-route-service-panel:hover, +.oc-route-service-panel:active { + box-shadow: 0 14px 34px rgba(24, 50, 34, 0.12); + transform: none; +} + +.oc-route-service-panel:hover > img { + filter: none; + transform: none; +} + +.oc-route-service-panel:nth-child(even) { + grid-template-columns: minmax(0, 0.62fr) minmax(280px, 0.38fr); +} + +.oc-route-service-panel:nth-child(even) > img { + order: 2; +} + +.oc-route-service-panel > img { + width: 100%; + height: 100%; + min-height: 390px; + object-fit: cover; +} + +.oc-route-service-panel > div { + display: grid; + align-content: start; + gap: 16px; + padding: 34px; +} + +.oc-route-service-panel span { + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.oc-route-service-panel h2 { + font-size: 34px; +} + +.oc-route-service-panel h3 { + color: #26372a; + font-size: 25px; + line-height: 1.18; +} + +.oc-route-service-panel p { + color: #4f6054; + font-size: 16px; + line-height: 1.65; +} + +.oc-route-service-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.oc-route-service-tags b { + border: 1px solid rgba(74, 110, 70, 0.18); + border-radius: 4px; + color: #4d5e51; + font-size: 12px; + padding: 7px 9px; +} + +.oc-route-service-panel ul { + display: grid; + gap: 10px; + margin: 0; + padding: 0; + list-style: none; +} + +.oc-route-service-panel li { + position: relative; + color: #3f5145; + font-size: 15px; + line-height: 1.55; + padding-left: 18px; +} + +.oc-route-service-panel li::before { + content: ''; + position: absolute; + left: 0; + top: 0.68em; + width: 6px; + height: 6px; + border-radius: 50%; + background: #7fbf6b; +} + +.oc-route-cta { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 34px; + align-items: center; + border-top: 1px solid var(--oc-line); + border-bottom: 1px solid var(--oc-line); + margin-bottom: 90px; + padding: 34px 0; +} + +.oc-route-cta h2 { + font-size: 38px; + margin-bottom: 14px; +} + +.oc-poi-page { + --oc-poi-detail-width: 1360px; + --oc-poi-detail-gutter: 88px; + background: #f4f8ef; + padding-bottom: 96px; +} + +.oc-poi-page > .oc-breadcrumb { + width: auto; + margin: 0; + padding: 28px 20px 0; +} + +.oc-poi-lead { + display: grid; + grid-template-columns: minmax(0, 0.82fr) minmax(460px, 1.18fr); + gap: clamp(34px, 5vw, 76px); + align-items: center; + padding: 42px 0 48px; +} + +.oc-poi-intro { + display: grid; + align-content: center; + gap: 22px; + min-height: 430px; +} + +.oc-poi-summary-copy > span, +.oc-poi-content-head > span { + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-transform: uppercase; +} + +.oc-poi-intro h1 { + max-width: 720px; + color: var(--oc-ink); + font-size: clamp(46px, 5.6vw, 86px); + line-height: 0.98; + text-wrap: balance; +} + +.oc-poi-intro p { + max-width: 58ch; + color: #4d5d51; + font-size: clamp(18px, 1.55vw, 22px); + line-height: 1.78; + text-align: justify; + text-indent: 2em; +} + +.oc-poi-location-tags { + display: flex; + flex-wrap: wrap; + gap: 9px; +} + +.oc-poi-location-tags b { + border: 1px solid rgba(74, 110, 70, 0.2); + border-radius: 4px; + color: #536459; + font-size: 12px; + line-height: 1.2; + padding: 8px 10px; +} + +.oc-poi-album { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-auto-rows: 136px; + gap: 10px; + min-height: 428px; +} + +.oc-poi-album-item { + position: relative; + width: 100%; + height: 100%; + grid-column: span 2; + overflow: hidden; + border: 0; + border-radius: 6px; + padding: 0; + background: #dfe9d8; + cursor: zoom-in; +} + +.oc-poi-album-item.is-featured { + grid-column: span 4; + grid-row: span 2; +} + +.oc-poi-album-item:nth-child(n + 4) { + grid-column: span 3; +} + +.oc-poi-album-item img, +.oc-poi-hero-image img, +.oc-poi-content-block img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.oc-poi-album-item img { + transition: transform 0.32s ease, filter 0.32s ease; +} + +.oc-poi-album-item:hover img { + filter: saturate(1.04); + transform: scale(1.035); +} + +.oc-poi-album-item > span { + position: absolute; + left: 18px; + bottom: 18px; + padding: 9px 12px; + border-radius: 4px; + background: rgb(24 50 34 / 0.82); + color: #f4f8ef; + font-size: 12px; + font-weight: 800; + line-height: 1; +} + +.oc-poi-hero-image.oc-container-wide { + width: min(var(--oc-poi-detail-width), calc(100% - var(--oc-poi-detail-gutter))); + overflow: hidden; + border-radius: 8px; + aspect-ratio: 2.1 / 1; +} + +.oc-poi-hero-image img { + height: 100%; +} + +.oc-gallery-lightbox { + position: fixed; + inset: 0; + z-index: 1200; + display: grid; + place-items: center; + padding: 42px; + color: #f4f8ef; +} + +.oc-gallery-lightbox-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgb(7 18 12 / 0.88); + cursor: zoom-out; +} + +.oc-gallery-lightbox figure { + position: relative; + z-index: 1; + display: grid; + gap: 14px; + justify-items: center; + max-width: min(1120px, 88vw); + max-height: 86vh; + margin: 0; +} + +.oc-gallery-lightbox img, +.oc-gallery-lightbox video { + max-width: min(1120px, 88vw); + max-height: 78vh; + border-radius: 4px; + object-fit: contain; + box-shadow: 0 22px 70px rgb(0 0 0 / 0.42); +} + +.oc-gallery-lightbox figcaption { + color: rgb(244 248 239 / 0.82); + font-size: 13px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-gallery-lightbox-close, +.oc-gallery-lightbox-nav { + position: absolute; + z-index: 2; + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 1px solid rgb(244 248 239 / 0.44); + border-radius: 50%; + background: rgb(24 50 34 / 0.72); + color: #f4f8ef; + cursor: pointer; +} + +.oc-gallery-lightbox-close { + top: 24px; + right: 24px; +} + +.oc-gallery-lightbox-nav { + top: 50%; + transform: translateY(-50%); +} + +.oc-gallery-lightbox-nav.is-prev { + left: 28px; +} + +.oc-gallery-lightbox-nav.is-next { + right: 28px; +} + +body.oc-lightbox-open { + overflow: hidden; +} + +.oc-poi-summary.oc-container-narrow { + width: min(var(--oc-poi-detail-width), calc(100% - var(--oc-poi-detail-gutter))); + display: grid; + justify-items: center; + gap: 34px; + padding: 76px 0 46px; + text-align: center; +} + +.oc-poi-summary-copy { + width: 100%; + display: grid; + justify-items: center; + gap: 18px; +} + +.oc-poi-summary h2, +.oc-poi-content-head h2 { + max-width: 820px; + color: var(--oc-ink); + font-size: clamp(34px, 3.8vw, 56px); + line-height: 1.08; + text-wrap: balance; +} + +.oc-poi-summary p { + width: 100%; + max-width: none; + color: #4f6054; + font-size: 18px; + line-height: 1.82; + text-align: justify; + text-indent: 2em; +} + +.oc-poi-facts-centered { + width: min(100%, 940px); + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.oc-poi-fact-card { + display: grid; + align-content: start; + gap: 10px; + min-height: 132px; + padding: 20px; + border: 1px solid rgba(74, 110, 70, 0.18); + border-radius: 7px; + background: rgba(251, 255, 247, 0.94); + text-align: left; +} + +.oc-poi-fact-card span { + color: #6d7b70; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-poi-fact-card strong { + color: #243328; + font-family: var(--oc-heading); + font-size: clamp(20px, 2vw, 27px); + font-weight: 400; + line-height: 1.22; +} + +.oc-poi-content.oc-container { + width: min(var(--oc-poi-detail-width), calc(100% - var(--oc-poi-detail-gutter))); + padding: 30px 0 12px; +} + +.oc-poi-content-head { + display: grid; + justify-items: center; + gap: 12px; + margin-bottom: 42px; + text-align: center; +} + +.oc-poi-content-stack { + width: 100%; + display: grid; + gap: 64px; + margin: 0 auto; +} + +.oc-poi-content-block { + display: grid; + justify-items: center; + gap: 22px; + padding-bottom: 58px; + border-bottom: 1px solid rgba(74, 110, 70, 0.14); + text-align: center; +} + +.oc-poi-content-block:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.oc-poi-content-block figure { + width: 100%; + height: auto; + aspect-ratio: 2.1 / 1; + margin: 0; + overflow: hidden; + border-radius: 8px; + background: #dfe9d8; +} + +.oc-poi-content-copy { + width: 100%; + display: grid; + justify-items: center; + gap: 13px; + max-width: none; +} + +.oc-poi-content-copy span { + color: #527a45; + font-size: 13px; + font-weight: 800; + letter-spacing: 0.12em; +} + +.oc-poi-content-copy h3 { + color: #243328; + font-size: clamp(30px, 3vw, 44px); + line-height: 1.08; + text-wrap: balance; +} + +.oc-poi-content-copy p { + width: 100%; + max-width: none; + color: #4f6054; + font-size: 18px; + line-height: 1.78; + text-align: justify; + text-indent: 2em; +} + +.oc-poi-content-block.is-image-only { + gap: 0; +} + +.oc-poi-content-block.is-text-only { + padding-top: 6px; +} + +.oc-food-detail-link { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--oc-clay-dark); + font-size: 13px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-highlights { + padding: 55px 0 82px; +} + +.oc-highlight-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 22px; +} + +.oc-highlight-card { + height: 238px; + background-size: cover; + background-position: center; + color: #fff; + position: relative; + overflow: hidden; + display: grid; + place-items: center; + text-align: center; +} + +.oc-highlight-card::before { + content: ''; + position: absolute; + inset: 0; + background: rgba(24, 50, 34, 0.32); +} + +.oc-highlight-card span { + position: relative; + z-index: 1; + font-family: 'Tenor Sans', Georgia, serif; + font-size: 21px; +} + +.oc-itinerary { + padding: 35px 0 78px; +} + +.oc-breakdown-feature { + display: grid; + grid-template-columns: 1fr 420px; + gap: 80px; + align-items: center; + border-top: 1px solid var(--oc-line); + padding: 38px 0; +} + +.oc-breakdown-feature .oc-itinerary-stop { + border-top: 0; + padding: 0; +} + +.oc-breakdown-feature img { + width: 100%; + height: 310px; + object-fit: cover; +} + +.oc-itinerary-stop { + border-top: 1px solid var(--oc-line); + padding: 38px 0; +} + +.oc-itinerary-stop h3 { + color: #36483a; + font-size: 34px; + margin-bottom: 4px; +} + +.oc-itinerary-stop span { + color: var(--oc-clay); + display: block; + font-weight: 800; + margin-bottom: 16px; + text-transform: uppercase; +} + +.oc-itinerary-stop a { + color: var(--oc-clay); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-rates { + padding-bottom: 95px; +} + +.oc-rates table { + width: 100%; + border-collapse: collapse; + margin-bottom: 42px; +} + +.oc-rates th, +.oc-rates td { + border: 1px solid var(--oc-line); + padding: 19px 22px; + text-align: left; +} + +.oc-rates th { + background: #183222; + color: #fff; + font-size: 14px; + text-transform: uppercase; +} + +.oc-includes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} + +.oc-includes div { + background: var(--oc-panel); + padding: 30px; +} + +.oc-includes h3, +.oc-article h3 { + font-family: 'Tenor Sans', Georgia, serif; + font-size: 29px; + font-weight: 400; + margin: 0 0 14px; +} + +.oc-includes p { + color: #536459; + font-size: 16px; + line-height: 1.6; +} + +.oc-route-map { + padding: 35px 0 95px; +} + +.oc-map-card { + position: relative; + min-height: 470px; + border: 1px solid #bfd0b8; + overflow: hidden; + background: + radial-gradient(circle at 26% 70%, rgba(72, 168, 80, 0.28) 0 12%, transparent 13%), + radial-gradient(circle at 68% 34%, rgba(72, 168, 80, 0.25) 0 13%, transparent 14%), + linear-gradient(150deg, transparent 0 46%, rgba(106, 172, 112, 0.35) 47% 50%, transparent 51%), + linear-gradient(28deg, transparent 0 35%, rgba(106, 172, 112, 0.35) 36% 39%, transparent 40%), + #e7f1e0; +} + +.oc-map-card::before { + content: ''; + position: absolute; + inset: 60px 90px; + border-top: 6px solid rgba(72, 168, 80, 0.5); + border-right: 6px solid rgba(72, 168, 80, 0.5); + transform: skew(-18deg) rotate(-4deg); +} + +.oc-map-card b { + position: absolute; + color: rgba(38, 55, 42, 0.45); + font-family: 'Tenor Sans', Georgia, serif; + font-size: 34px; +} + +.oc-map-card b:nth-of-type(1) { + left: 250px; + bottom: 90px; +} + +.oc-map-card b:nth-of-type(2) { + right: 285px; + top: 120px; +} + +.oc-map-card b:nth-of-type(3) { + right: 350px; + bottom: 110px; +} + +.oc-map-dot { + position: absolute; + z-index: 1; + background: #243328; + color: #fff; + padding: 8px 12px; + border-radius: 2px; + font-size: 12px; + line-height: 1.25; +} + +.oc-map-dot::before { + content: ''; + position: absolute; + left: -10px; + top: 50%; + width: 14px; + height: 14px; + border-radius: 50%; + background: #48a850; + border: 3px solid #fff; + transform: translateY(-50%); +} + +.oc-map-dot.one { + left: 240px; + bottom: 150px; +} + +.oc-map-dot.two { + left: 548px; + top: 190px; +} + +.oc-map-dot.three { + right: 280px; + top: 105px; +} + +.oc-wildlife { + display: grid; + grid-template-columns: 0.9fr 1.1fr; + gap: 80px; + align-items: center; + padding: 28px 0 105px; +} + +.oc-wildlife h2 { + color: var(--oc-clay); + font-size: 43px; + margin-bottom: 22px; +} + +.oc-wildlife p { + color: #4f6054; + font-size: 17px; + line-height: 1.7; +} + +.oc-wildlife img { + width: 100%; + height: 360px; + object-fit: cover; +} + +.oc-destination-intro { + padding: 94px 0 64px; + text-align: center; +} + +.oc-destination-intro h2 { + color: var(--oc-clay); + font-size: 48px; + margin-bottom: 30px; +} + +.oc-destination-intro p { + max-width: 980px; + color: #536459; + font-size: 18px; + line-height: 1.7; + margin: 0 auto 20px; +} + +.oc-destination-intro img { + width: min(880px, 100%); + margin: 45px auto 0; +} + +.oc-country-list { + display: grid; + gap: 84px; + padding: 46px 0 104px; +} + +.oc-country { + display: grid; + grid-template-columns: 1fr 0.9fr; + gap: 68px; + align-items: center; +} + +.oc-country.flip { + grid-template-columns: 0.9fr 1fr; +} + +.oc-country.flip img { + order: 2; +} + +.oc-country img, +.oc-split-cta img { + width: 100%; + height: 470px; + object-fit: cover; +} + +.oc-country h2, +.oc-split-cta h2 { + color: var(--oc-clay); + font-size: 52px; + margin-bottom: 22px; +} + +.oc-country .oc-card-action, +.oc-region-grid .oc-card-action { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--oc-clay); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-split-cta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 65px; + align-items: center; + padding: 54px 0 116px; +} + +.oc-article { + padding: 100px 0 70px; +} + +.oc-article h3 { + color: #36483a; + margin-top: 35px; +} + +.oc-about-page { + background: #f4f8ef; + padding: 0 0 96px; +} + +.oc-about-split { + display: grid; + align-items: stretch; + overflow: hidden; +} + +.oc-about-split.oc-container:not(.oc-about-social), +.oc-about-split.oc-container.is-reversed:not(.oc-about-social) { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0; + height: 560px; + margin: 0; + padding: 0; + width: 100%; +} + +.oc-about-split + .oc-about-split { + border-top: 0; +} + +.oc-about-split:not(.oc-about-social) .oc-about-copy { + align-self: center; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: clamp(22px, 2.4vw, 32px); + height: 100%; + justify-content: center; + min-height: 0; + padding: clamp(42px, 4.8vw, 72px) clamp(44px, 4.5vw, 64px); + width: 100%; +} + +.oc-about-split:not(.is-reversed):not(.oc-about-social) .oc-about-copy { + justify-self: end; + margin-right: 0; +} + +.oc-about-split.is-reversed:not(.oc-about-social) .oc-about-copy { + justify-self: start; + margin-left: 0; +} + +.oc-about-copy span { + display: block; + color: var(--oc-orange); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + margin-bottom: 18px; + text-transform: uppercase; +} + +.oc-about-copy h1, +.oc-about-copy h2 { + color: #203425; + font-size: clamp(38px, 5vw, 68px); + line-height: 0.98; + margin: 0; + text-wrap: balance; +} + +.oc-about-copy h2 { + font-size: clamp(32px, 4vw, 52px); +} + +.oc-about-copy p, +.oc-about-copy li { + color: #536252; + font-size: 17px; + line-height: 1.72; + margin: 0; + text-wrap: pretty; +} + +.oc-about-body { + display: grid; + gap: 18px; +} + +.oc-about-body p { + text-align: justify; + text-indent: 2em; +} + +.oc-about-body p strong { + color: #26372a; + font-weight: 800; +} + +.oc-about-copy p + p { + margin-top: 0; +} + +.oc-about-points { + display: grid; + gap: 14px; + list-style: none; + margin: 0; + padding: 0; +} + +.oc-about-points li { + border-left: 2px solid rgba(220, 107, 47, 0.55); + padding-left: 18px; + text-align: justify; +} + +.oc-about-points li strong { + color: #26372a; +} + +.oc-about-media { + margin: 0; + width: 100%; +} + +.oc-about-split:not(.oc-about-social) .oc-about-media { + height: 100%; + min-height: 0; +} + +.oc-about-media img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; +} + +.oc-module-page .oc-about-copy { + gap: clamp(18px, 2vw, 26px); +} + +.oc-module-page .oc-about-copy h1, +.oc-module-page .oc-about-copy h2 { + font-size: clamp(36px, 4.5vw, 62px); +} + +.oc-module-page .oc-about-points { + gap: 12px; +} + +.oc-module-page .oc-about-points li { + font-size: 16px; + line-height: 1.62; +} + +.oc-module-gallery { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0; +} + +.oc-module-gallery .oc-gallery-trigger { + border: 0; + cursor: zoom-in; + display: block; + height: 100%; + margin: 0; + min-height: 0; + overflow: hidden; + padding: 0; +} + +.oc-module-gallery .oc-gallery-trigger img { + height: 100%; + margin: 0; + width: 100%; +} + +.oc-module-faq-list { + display: grid; + gap: 10px; +} + +.oc-module-faq-list details { + border-top: 1px solid rgba(51, 71, 55, 0.18); + padding: 0; +} + +.oc-module-faq-list summary { + color: #26372a; + cursor: pointer; + font-size: 17px; + font-weight: 800; + line-height: 1.35; + list-style: none; + padding: 12px 0; +} + +.oc-module-faq-list summary::-webkit-details-marker { + display: none; +} + +.oc-module-faq-list p { + color: #536252; + font-size: 15px; + line-height: 1.62; + margin: 0; + padding: 0 0 12px; + text-align: justify; + text-indent: 2em; +} + +.oc-about-social { + grid-template-columns: 1fr; + align-items: start; + gap: 36px; + padding-top: 72px; + text-align: center; +} + +.oc-about-social .oc-about-copy { + justify-self: center; + max-width: 840px; + text-align: center; +} + +.oc-about-social .oc-about-copy p { + color: #5b6a59; + font-size: 17px; + line-height: 1.6; + margin-top: 12px; +} + +.oc-social-channel-list { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: clamp(16px, 2.4vw, 28px); +} + +.oc-social-channel { + display: flex; + gap: 13px; + align-items: center; + color: inherit; + justify-content: center; + min-width: 0; + text-decoration: none; + transition: opacity 180ms ease, transform 180ms ease; +} + +.oc-social-channel:hover { + opacity: 0.76; + transform: translateY(-2px); +} + +.oc-social-channel:focus-visible { + outline: 2px solid rgba(220, 107, 47, 0.72); + outline-offset: 7px; +} + +.oc-social-channel img { + flex: 0 0 auto; + width: 42px; + height: 42px; + display: block; + object-fit: contain; +} + +.oc-social-channel strong, +.oc-social-channel span { + display: block; +} + +.oc-social-channel strong { + color: #203425; + font-size: 16px; + line-height: 1.2; + margin-bottom: 3px; +} + +.oc-social-channel span { + color: #667565; + font-size: 13px; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.oc-season { + padding: 35px 0 90px; + text-align: center; +} + +.oc-season-row, +.oc-months { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 24px; +} + +.oc-season-row button { + height: 42px; + border: 1px solid var(--oc-line); + background: transparent; + color: var(--oc-clay-dark); + padding: 0 23px; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-months span { + width: 54px; + height: 38px; + display: grid; + place-items: center; + border-bottom: 1px solid var(--oc-line); + font-weight: 800; + color: #536459; +} + +.oc-season > p { + max-width: 840px; + margin: 0 auto; +} + +.oc-region-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + padding-bottom: 95px; +} + +.oc-region-grid h2 { + grid-column: 1 / -1; + margin-top: 34px; + margin-bottom: 6px; +} + +.oc-region-grid article { + background: var(--oc-panel); + padding: 32px; +} + +.oc-region-grid h3 { + color: #40382f; + font-size: 26px; + margin-bottom: 10px; +} + +.oc-region-grid p { + color: #6a766b; + text-transform: uppercase; + font-size: 12px; + font-weight: 800; + margin-bottom: 22px; +} + +.oc-live-stats { + display: flex; + gap: 36px; + margin-bottom: 28px; + color: #fff; +} + +.oc-live-stat { + display: inline-flex; + align-items: center; + gap: 11px; +} + +.oc-live-stat-copy { + font-size: 15px; + line-height: 1.45; +} + +.oc-amenities { + min-height: 150px; + display: grid; + grid-template-columns: repeat(8, 1fr) auto; + gap: 24px; + align-items: center; + padding: 30px 0; + border-bottom: 1px solid var(--oc-line); +} + +.oc-amenities div { + display: grid; + justify-items: center; + gap: 10px; + text-align: center; +} + +.oc-amenities p { + color: #5e6c61; + font-size: 12px; + line-height: 1.2; +} + +.oc-gallery-block { + padding: 75px 0 60px; +} + +.oc-gallery-heading { + display: grid; + grid-template-columns: 105px 1fr; + gap: 32px; + align-items: center; + margin-bottom: 32px; +} + +.oc-gallery-heading img { + width: 105px; +} + +.oc-gallery-heading p { + color: #5e6c61; + font-size: 18px; + margin-bottom: 12px; +} + +.oc-masonry { + display: grid; + grid-template-columns: 1.2fr 0.8fr 0.8fr; + gap: 18px; +} + +.oc-masonry img { + width: 100%; + height: 285px; + object-fit: cover; +} + +.oc-masonry img:first-child { + grid-row: span 2; + height: 588px; +} + +.oc-activity-section { + padding: 42px 0 80px; +} + +.oc-activity-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.oc-activity-card { + min-height: 312px; + background-size: cover; + background-position: center; + position: relative; + display: grid; + place-items: end start; + padding: 25px; + color: #fff; +} + +.oc-activity-card::before { + content: ''; + position: absolute; + inset: 0; + background: rgba(24, 50, 34, 0.32); +} + +.oc-activity-card span { + position: relative; + z-index: 1; + font-family: 'Tenor Sans', Georgia, serif; + font-size: 26px; +} + +.oc-reviews { + padding: 10px 0 96px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} + +.oc-reviews h2 { + grid-column: 1 / -1; +} + +.oc-reviews article { + background: var(--oc-panel); + padding: 34px; +} + +.oc-reviews h3 { + font-size: 25px; + margin-bottom: 16px; +} + +.oc-reviews p { + color: #536459; + font-size: 16px; + line-height: 1.65; + margin-bottom: 18px; +} + +.oc-reviews span { + color: var(--oc-clay-dark); + font-weight: 800; +} + +.oc-specials-intro { + min-height: 0; + display: block; + padding: 64px 0 0; + background: #f4f8ef; +} + +.oc-specials-intro h1 { + width: min(1120px, calc(100% - 48px)); + margin: 0 auto 30px; + font-size: 55px; + line-height: 1.5; +} + +.oc-specials-intro h2 { + width: min(1120px, calc(100% - 48px)); + margin: 0 auto 30px; + color: #48a850; + font-size: 33px; + line-height: 1.3; +} + +.oc-specials-intro p { + width: min(1120px, calc(100% - 48px)); + max-width: none; + margin: 0 auto; + color: #243328; + font-size: 16px; + line-height: 1.5; +} + +.oc-specials-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 36px; + padding: 64px 0 92px; +} + +.oc-special-card { + background: var(--oc-panel); + display: grid; + grid-template-columns: 0.8fr 1fr; + min-height: 305px; +} + +.oc-special-card img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.oc-special-card div { + padding: 33px 30px; +} + +.oc-special-card span, +.oc-special-card h3 { + color: var(--oc-clay-dark); + font-family: 'Assistant', Arial, sans-serif; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-special-card h2 { + color: #26372a; + font-size: 29px; + margin: 8px 0 19px; +} + +.oc-special-card h3 { + margin: 0 0 18px; + line-height: 1.45; +} + +.oc-special-card p { + color: #5e6c61; + font-size: 15px; + line-height: 1.55; + margin-bottom: 24px; +} + +.oc-special-card .oc-card-action { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-contact { + min-height: 720px; + padding: 135px 0 120px; +} + +.oc-contact h1 { + color: var(--oc-clay); + text-align: center; + font-size: 66px; + margin-bottom: 78px; +} + +.oc-contact-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 44px; +} + +.oc-contact-grid article { + border-top: 1px solid var(--oc-line); + padding-top: 24px; +} + +.oc-contact-grid h2 { + color: #26372a; + font-size: 25px; + line-height: 1.2; + margin-bottom: 22px; +} + +.oc-contact-grid p { + color: #5e6c61; + font-size: 15px; + line-height: 1.65; + margin-bottom: 16px; +} + +.oc-plan-inquiry-hero { + position: relative; + min-height: calc(100dvh - 91px); + display: grid; + align-items: center; + padding: clamp(24px, 4vw, 46px) 0; + background-position: center; + background-size: cover; + isolation: isolate; + overflow: hidden; +} + +.oc-plan-inquiry-hero::before { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background: + linear-gradient(180deg, rgba(24, 50, 34, 0) 56%, rgba(24, 50, 34, 0.74) 84%, var(--oc-footer) 100%), + linear-gradient(90deg, rgba(14, 32, 22, 0.78), rgba(14, 32, 22, 0.42) 52%, rgba(14, 32, 22, 0.68)), + linear-gradient(180deg, rgba(14, 32, 22, 0.18), rgba(14, 32, 22, 0.64)); +} + +.oc-plan-inquiry-hero::after { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background-image: repeating-linear-gradient(90deg, rgba(244, 248, 239, 0.04) 0 1px, transparent 1px 5px); + mix-blend-mode: soft-light; + opacity: 0.34; +} + +.oc-plan-hero-grid { + position: relative; + z-index: 1; + width: min(1280px, calc(100% - 64px)); + margin-left: max(32px, calc((100vw - 1360px) / 2)); + margin-right: auto; + display: grid; + grid-template-columns: minmax(760px, 0.9fr) minmax(260px, 0.32fr); + gap: clamp(26px, 4vw, 54px); + align-items: center; +} + +.oc-plan-glass-card { + position: relative; + display: grid; + grid-template-columns: minmax(260px, 0.72fr) minmax(420px, 1.28fr); + gap: clamp(22px, 3vw, 38px); + align-items: start; + padding: clamp(22px, 3vw, 34px); + border: 1px solid rgba(244, 248, 239, 0.42); + border-radius: 8px; + background: rgba(244, 248, 239, 0.2); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.36), + inset 0 -1px 0 rgba(24, 50, 34, 0.16), + 0 28px 70px rgba(10, 28, 18, 0.32); + backdrop-filter: blur(24px) saturate(1.14); + -webkit-backdrop-filter: blur(24px) saturate(1.14); +} + +.oc-plan-glass-copy { + display: grid; + gap: 16px; + color: #f7fbf4; + padding-top: 4px; +} + +.oc-plan-glass-copy > span { + color: rgba(247, 251, 244, 0.82); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.oc-plan-glass-copy h1 { + max-width: 440px; + color: #fffdf7; + font-size: clamp(36px, 4.5vw, 54px); + line-height: 1.03; + text-wrap: balance; + text-shadow: 0 18px 44px rgba(8, 24, 15, 0.34); +} + +.oc-plan-glass-copy p { + max-width: 440px; + color: rgba(247, 251, 244, 0.84); + font-size: 15px; + line-height: 1.62; +} + +.oc-plan-contact-email { + display: grid; + gap: 4px; + width: fit-content; + margin-top: 2px; + padding-top: 12px; + border-top: 1px solid rgba(247, 251, 244, 0.28); + color: #f7fbf4; + text-decoration: none; +} + +.oc-plan-contact-email span { + color: rgba(247, 251, 244, 0.76); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.oc-plan-contact-email strong { + color: #fffdf7; + font-size: 15px; + line-height: 1.4; +} + +.oc-plan-contact-email:hover strong, +.oc-plan-contact-email:focus-visible strong { + color: #bce7ae; +} + +.oc-plan-glass-notes { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 6px; +} + +.oc-plan-glass-notes span { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border: 1px solid rgba(247, 251, 244, 0.34); + background: rgba(247, 251, 244, 0.13); + color: rgba(247, 251, 244, 0.88); + font-size: 13px; + font-weight: 800; +} + +.oc-plan-glass-card .oc-inquiry-form { + border: 0; + background: transparent; + gap: 10px; + padding: 0; +} + +.oc-plan-glass-card .oc-inquiry-form span { + font-size: 12px; + color: rgba(247, 251, 244, 0.9); +} + +.oc-plan-glass-card .oc-inquiry-form input, +.oc-plan-glass-card .oc-inquiry-form select, +.oc-plan-glass-card .oc-inquiry-form textarea { + border-color: rgba(247, 251, 244, 0.32); + background: rgba(247, 251, 244, 0.78); + color: #203425; + min-height: 44px; + padding: 8px 10px; + font-size: 13px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.42); + transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; +} + +.oc-plan-glass-card .oc-inquiry-form textarea { + height: 86px; + min-height: 86px; +} + +.oc-plan-glass-card .oc-inquiry-form input:focus, +.oc-plan-glass-card .oc-inquiry-form select:focus, +.oc-plan-glass-card .oc-inquiry-form textarea:focus { + outline: 2px solid rgba(247, 251, 244, 0.52); + outline-offset: 2px; + border-color: rgba(247, 251, 244, 0.72); + background: rgba(255, 255, 255, 0.92); +} + +.oc-plan-glass-card .oc-form-footer { + gap: 12px; + border-top-color: rgba(247, 251, 244, 0.28); + padding-top: 12px; +} + +.oc-plan-glass-card .oc-form-footer p, +.oc-plan-glass-card .oc-form-status { + font-size: 13px; + color: rgba(247, 251, 244, 0.82); +} + +.oc-plan-glass-card .oc-form-footer .oc-orange-btn { + min-height: 45px; + height: 45px; +} + +.oc-plan-glass-card .oc-form-status { + color: #f5fff2; +} + +.oc-plan-glass-card .oc-form-status.is-error { + color: #ffe2d9; +} + +.oc-plan-side-steps { + display: grid; + gap: 22px; + color: #f7fbf4; +} + +.oc-plan-side-steps .oc-step { + position: relative; + padding: 0 0 24px 24px; + border-left: 1px solid rgba(247, 251, 244, 0.26); +} + +.oc-plan-side-steps .oc-step:last-child { + padding-bottom: 0; +} + +.oc-plan-side-steps .oc-step::before { + content: ''; + position: absolute; + left: -5px; + top: 14px; + width: 9px; + height: 9px; + border: 1px solid rgba(247, 251, 244, 0.7); + background: rgba(72, 168, 80, 0.78); +} + +.oc-plan-side-steps .oc-step b, +.oc-plan-side-steps .oc-step h3 { + color: #f7fbf4; +} + +.oc-plan-side-steps .oc-step b, +.oc-plan-side-steps .oc-step h3 { + font-size: 23px; + line-height: 1.25; +} + +.oc-plan-side-steps .oc-step p { + max-width: 280px; + color: rgba(247, 251, 244, 0.76); + font-size: 14px; + line-height: 1.6; + margin-top: 8px; +} + +.oc-contact-layout { + display: grid; + grid-template-columns: minmax(280px, 0.72fr) minmax(420px, 1.28fr); + gap: 44px; + align-items: start; +} + +.oc-contact-card { + border-top: 1px solid var(--oc-line); + padding-top: 24px; +} + +.oc-contact-card span { + display: block; + color: var(--oc-orange); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 18px; +} + +.oc-contact-card h2 { + color: #26372a; + font-size: 30px; + line-height: 1.18; + margin-bottom: 22px; +} + +.oc-contact-card p { + color: #5e6c61; + font-size: 15px; + line-height: 1.7; + margin-bottom: 16px; +} + +.oc-inquiry-form { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + border: 1px solid rgba(38, 55, 42, 0.14); + background: #fbfff7; + padding: 28px; +} + +.oc-inquiry-form label { + display: grid; + gap: 8px; +} + +.oc-inquiry-form label.is-wide, +.oc-inquiry-form .is-wide { + grid-column: 1 / -1; +} + +.oc-inquiry-form span { + color: #26372a; + font-size: 13px; + font-weight: 800; +} + +.oc-inquiry-form input, +.oc-inquiry-form select, +.oc-inquiry-form textarea { + width: 100%; + border: 1px solid rgba(38, 55, 42, 0.2); + border-radius: 0; + background: #fff; + color: #26372a; + font: inherit; + min-height: 44px; + padding: 10px 12px; +} + +.oc-inquiry-form textarea { + resize: vertical; +} + +.oc-form-footer { + display: flex; + gap: 18px; + align-items: center; + justify-content: space-between; + border-top: 1px solid rgba(38, 55, 42, 0.14); + padding-top: 18px; +} + +.oc-form-footer p, +.oc-form-status { + color: #5e6c61; + font-size: 14px; + line-height: 1.55; + margin: 0; +} + +.oc-form-status { + color: #2f6d4b; + font-weight: 800; +} + +.oc-form-status.is-error { + color: #a43d30; +} + +.oc-inquiry-success-layer { + position: fixed; + inset: 0; + z-index: 90; + display: grid; + place-items: center; + padding: 24px; + background: rgba(15, 33, 22, 0.58); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.oc-inquiry-success-dialog { + position: relative; + width: min(430px, 100%); + display: grid; + gap: 14px; + justify-items: start; + padding: 30px; + border: 1px solid rgba(38, 55, 42, 0.12); + border-radius: 8px; + background: #fbfff7; + color: #26372a; + box-shadow: 0 28px 80px rgba(8, 24, 15, 0.34); +} + +.oc-inquiry-success-close { + position: absolute; + top: 14px; + right: 14px; + width: 36px; + height: 36px; + display: grid; + place-items: center; + border: 1px solid rgba(38, 55, 42, 0.12); + background: #fff; + color: #26372a; + cursor: pointer; +} + +.oc-inquiry-success-icon { + color: #2f6d4b; +} + +.oc-inquiry-success-dialog span { + color: var(--oc-orange); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.oc-inquiry-success-dialog h2 { + color: #26372a; + font-size: 30px; + line-height: 1.12; + margin: 0; +} + +.oc-inquiry-success-dialog p { + color: #5e6c61; + font-size: 15px; + line-height: 1.68; + margin: 0 0 4px; +} + +.oc-inquiry-success-dialog .oc-orange-btn { + width: 100%; + margin-top: 4px; +} + +.oc-editorial-hero { + min-height: 490px; +} + +.oc-content-grid { + display: grid; + grid-template-columns: 0.86fr 1.14fr; + gap: 56px; + align-items: start; + padding: 20px 0 92px; +} + +.oc-feature-panel { + position: sticky; + top: 124px; + background: var(--oc-panel); + padding: 42px; + border-radius: 5px; +} + +.oc-feature-panel > img { + width: 100%; + height: 240px; + object-fit: cover; + margin: 0 0 28px; +} + +.oc-feature-panel span, +.oc-page-card span, +.oc-seasons span, +.oc-blog-grid span, +.oc-trade-panel span { + display: inline-block; + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 16px; +} + +.oc-page-card .oc-card-action { + display: inline-flex; + align-items: center; + gap: 8px; + margin: auto 0 0; + letter-spacing: 0; +} + +.oc-feature-panel h2, +.oc-page-card h2, +.oc-seasons h2, +.oc-blog-grid h2 { + color: #26372a; + font-size: 34px; + line-height: 1.18; + margin-bottom: 18px; +} + +.oc-feature-panel p, +.oc-page-card p, +.oc-seasons p, +.oc-blog-grid p { + color: #5e6c61; + font-size: 16px; + line-height: 1.65; + margin-bottom: 22px; +} + +.oc-feature-panel .oc-card-action, +.oc-seasons .oc-card-action, +.oc-blog-grid .oc-card-action { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--oc-clay-dark); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.oc-faq-list { + display: grid; + gap: 14px; +} + +.oc-faq-list details { + border: 1px solid var(--oc-line); + background: rgba(232, 240, 226, 0.55); + border-radius: 5px; + padding: 0 24px; +} + +.oc-faq-list summary { + cursor: pointer; + color: #243328; + font-family: 'Tenor Sans', Georgia, serif; + font-size: 22px; + line-height: 1.3; + list-style: none; + padding: 24px 0; +} + +.oc-faq-list summary::-webkit-details-marker { + display: none; +} + +.oc-faq-list summary::after { + content: '+'; + float: right; + color: var(--oc-clay); +} + +.oc-faq-list details[open] summary::after { + content: '-'; +} + +.oc-faq-list p { + border-top: 1px solid var(--oc-line); + color: #5e6c61; + font-size: 16px; + line-height: 1.65; + padding: 18px 0 24px; +} + +.oc-page-card-grid, +.oc-blog-grid, +.oc-seasons { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 28px; + padding: 0 0 105px; +} + +.oc-page-card, +.oc-blog-card, +.oc-season-card { + display: grid; + align-content: start; + background: var(--oc-panel); + border-radius: 5px; + min-height: 250px; + padding: 32px; +} + +.oc-page-card > img { + width: calc(100% + 64px); + height: 210px; + object-fit: cover; + margin: -32px -32px 28px; +} + +.oc-season-card { + position: relative; + overflow: hidden; + background-color: #183222; + background-size: cover; + background-position: center; + color: #eff8eb; + display: grid; + align-content: end; +} + +.oc-season-card::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(21, 43, 30, 0.18), rgba(21, 43, 30, 0.76)); +} + +.oc-season-card > * { + position: relative; + z-index: 1; +} + +.oc-seasons h2 { + color: #fff; +} + +.oc-seasons p { + color: #dcebd5; +} + +.oc-trade-panel { + position: relative; + overflow: hidden; + min-height: 610px; + display: grid; + place-items: center; + padding: 110px 24px; + text-align: center; + background-color: #183222; + background-position: center; + background-size: cover; + color: #fff; +} + +.oc-trade-panel::before { + content: ''; + position: absolute; + inset: 0; + background: rgba(21, 43, 30, 0.64); +} + +.oc-trade-panel > div { + position: relative; + z-index: 1; + width: min(650px, 100%); +} + +.oc-trade-panel h1 { + font-size: clamp(46px, 5vw, 70px); + line-height: 1.05; + margin-bottom: 22px; +} + +.oc-trade-panel p { + color: #eef8e8; + font-size: 18px; + line-height: 1.62; + margin-bottom: 28px; +} + +.oc-trade-actions { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 14px; +} + +.oc-trade-images { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; + padding: 82px 0 76px; +} + +.oc-trade-images img { + width: 100%; + height: 430px; + object-fit: cover; +} + +.oc-trade-panel.oc-trade-direct { + background: #f4f8ef; + color: #203425; + min-height: auto; + padding: clamp(70px, 8vw, 118px) 0 clamp(90px, 9vw, 130px); + place-items: stretch; + text-align: left; +} + +.oc-trade-panel.oc-trade-direct::before { + display: none; +} + +.oc-trade-panel.oc-trade-direct > div { + margin: 0 auto; + width: min(880px, 100%); +} + +.oc-trade-panel.oc-trade-direct h1 { + color: #203425; + margin-bottom: 24px; +} + +.oc-trade-panel.oc-trade-direct p { + color: #526250; + font-size: 18px; + line-height: 1.72; + margin: 0 0 34px; + max-width: 780px; +} + +.oc-trade-support-list { + display: grid; + gap: 16px; + list-style: none; + margin: 0 0 38px; + padding: 0; +} + +.oc-trade-support-list li { + border-left: 2px solid rgba(220, 107, 47, 0.62); + color: #536252; + font-size: 17px; + line-height: 1.7; + padding-left: 18px; +} + +.oc-trade-email { + display: inline-flex; + align-items: center; + color: var(--oc-clay-dark); + font-size: clamp(24px, 3vw, 38px); + line-height: 1.15; + text-decoration: none; +} + +.oc-trade-email:hover { + color: #203425; +} + +@media (max-width: 1320px) and (min-width: 1181px) { + .oc-header { + grid-template-columns: minmax(164px, 1fr) auto minmax(164px, 1fr); + } + + .oc-nav { + gap: clamp(10px, 1.15vw, 18px); + } + + .oc-header-actions { + gap: 12px; + } + + .oc-language-action { + width: 84px; + } + + .oc-language-track { + grid-template-columns: repeat(2, 38px); + width: 76px; + } + + .oc-language-thumb { + width: 38px; + } + + .oc-language-action[data-current='en'] .oc-language-thumb { + transform: translateX(38px); + } + + .oc-site .oc-header-cta { + min-width: 116px; + padding: 0 14px; + } +} + +@media (max-width: 1180px) { + .oc-nav { + display: none; + } + + .oc-header { + grid-template-columns: 164px 1fr; + } + + .oc-header-actions { + justify-content: flex-end; + } + + .oc-mega { + display: none; + } + + .oc-full-menu-inner { + grid-template-columns: 1fr; + gap: 42px; + } + + .oc-full-menu-intro { + max-width: 540px; + } + + .oc-full-menu-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .oc-safari-grid, + .oc-archive-grid, + .oc-activity-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-archive-layout, + .oc-archive-layout.is-collective { + grid-template-columns: minmax(0, 1fr); + gap: 28px; + } + + .is-collective .oc-archive-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-safari-card { + flex-basis: auto; + width: 100%; + max-width: none; + } + + .oc-amenities { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (max-width: 820px) { + .oc-footer-legal { + flex-wrap: wrap; + } + + .oc-footer-legal-meta, + .oc-footer-company-line, + .oc-footer-legal-links { + flex-wrap: wrap; + white-space: normal; + } + + .oc-poi-page { + --oc-poi-detail-gutter: 36px; + } + + .oc-container, + .oc-container-wide, + .oc-container-narrow { + width: min(100% - 36px, 700px); + } + + .oc-header { + height: 76px; + grid-template-columns: 118px 1fr; + padding: 0 14px; + gap: 12px; + } + + .oc-logo img { + width: 112px; + max-height: 38px; + } + + .oc-header-actions { + gap: 8px; + } + + .oc-icon-action { + width: 38px; + } + + .oc-language-action { + width: 70px; + height: 32px; + min-height: 32px; + padding: 2px; + } + + .oc-language-track { + grid-template-columns: repeat(2, 32px); + width: 64px; + height: 28px; + } + + .oc-language-thumb { + width: 32px; + height: 28px; + } + + .oc-language-action[data-current='en'] .oc-language-thumb { + transform: translateX(32px); + } + + .oc-language-option { + height: 28px; + font-size: 11px; + } + + .oc-header-cta { + display: none; + } + + .oc-home-hero, + .oc-image-hero { + min-height: 560px; + } + + .oc-awards, + .oc-trip { + display: none; + } + + .oc-hero-dots { + top: 70px; + left: 24px; + } + + .oc-plan-panel, + .oc-detail-overview, + .oc-detail-strip, + .oc-highlight-grid, + .oc-breakdown-feature, + .oc-route-summary, + .oc-poi-fact-grid, + .oc-poi-body-block, + .oc-poi-related-grid, + .oc-wildlife, + .oc-safaris-layout, + .oc-archive-layout, + .oc-country, + .oc-country.flip, + .oc-split-cta, + .oc-includes, + .oc-specials-grid, + .oc-contact-grid, + .oc-contact-layout, + .oc-reviews, + .oc-content-grid, + .oc-page-card-grid, + .oc-blog-grid, + .oc-seasons, + .oc-trade-images, + .oc-plan-hero-grid, + .oc-plan-glass-card, + .oc-full-menu-inner { + grid-template-columns: 1fr; + } + + .oc-full-menu-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-plan-inquiry-hero { + min-height: auto; + padding: 48px 0 58px; + } + + .oc-contact { + min-height: auto; + padding: 64px 0 82px; + } + + .oc-contact h1 { + margin-bottom: 52px; + } + + .oc-poi-page > .oc-breadcrumb { + width: auto; + margin: 0; + padding: 22px 20px 0; + } + + .oc-poi-lead { + grid-template-columns: 1fr; + gap: 28px; + padding: 32px 0 36px; + } + + .oc-poi-intro { + min-height: auto; + } + + .oc-poi-intro h1 { + font-size: clamp(40px, 12vw, 64px); + } + + .oc-poi-album { + grid-auto-rows: 132px; + min-height: auto; + } + + .oc-poi-hero-image img { + height: 100%; + } + + .oc-poi-summary { + padding: 56px 0 34px; + } + + .oc-poi-facts-centered { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-poi-content { + padding-top: 22px; + } + + .oc-poi-content-stack { + gap: 42px; + } + + .oc-poi-content-block { + padding-bottom: 42px; + } + + .oc-poi-content-block figure { + height: auto; + aspect-ratio: 2.1 / 1; + } + + .oc-plan-hero-grid { + width: min(100% - 40px, 760px); + margin: 0 auto; + gap: 26px; + } + + .oc-plan-glass-card { + width: min(100% - 40px, 760px); + margin: 0 auto; + gap: 28px; + } + + .oc-plan-glass-copy h1 { + max-width: 100%; + } + + .oc-inquiry-form { + grid-template-columns: 1fr; + padding: 22px; + } + + .oc-plan-glass-card .oc-inquiry-form { + padding: 0; + } + + .oc-plan-side-steps { + grid-template-columns: 1fr; + gap: 16px; + } + + .oc-form-footer { + align-items: flex-start; + flex-direction: column; + } + + .oc-country.flip img { + order: 0; + } + + .oc-plan-panel { + padding-top: 30px; + } + + .oc-detail-strip, + .oc-highlight-grid, + .oc-wildlife { + gap: 18px; + } + + .oc-detail-strip img, + .oc-highlight-card, + .oc-wildlife img { + height: 240px; + } + + .oc-map-card { + min-height: 340px; + } + + .oc-map-card b { + font-size: 22px; + } + + .oc-map-card b:nth-of-type(1) { + left: 70px; + bottom: 70px; + } + + .oc-map-card b:nth-of-type(2) { + right: 80px; + top: 80px; + } + + .oc-map-card b:nth-of-type(3) { + right: 90px; + bottom: 80px; + } + + .oc-map-dot.one { + left: 55px; + bottom: 118px; + } + + .oc-map-dot.two { + left: 150px; + top: 135px; + } + + .oc-map-dot.three { + right: 45px; + top: 70px; + } + + .oc-plan-panel .oc-outline-btn { + position: static; + transform: none; + justify-self: start; + } + + .oc-info-band { + grid-template-columns: 1fr; + padding: 0; + } + + .oc-info-band-image { + height: 240px; + } + + .oc-info-band-copy { + padding: 28px; + } + + .oc-home-social-strip .oc-social-channel-list { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .oc-about-page { + padding: 28px 0 64px; + } + + .oc-about-split, + .oc-about-split.is-reversed { + grid-template-columns: 1fr; + gap: 30px; + padding: 42px 0; + } + + .oc-about-split.oc-container:not(.oc-about-social), + .oc-about-split.oc-container.is-reversed:not(.oc-about-social) { + grid-template-columns: 1fr; + height: auto; + margin: 0 auto; + padding: 42px 0; + width: min(1116px, calc(100% - 36px)); + } + + .oc-about-split:not(.oc-about-social) .oc-about-copy, + .oc-about-split:not(.is-reversed):not(.oc-about-social) .oc-about-copy, + .oc-about-split.is-reversed:not(.oc-about-social) .oc-about-copy { + display: block; + height: auto; + justify-self: stretch; + margin-left: 0; + margin-right: 0; + padding: 0; + width: 100%; + } + + .oc-about-split.is-reversed .oc-about-media { + order: 2; + } + + .oc-about-split.is-reversed .oc-about-copy { + order: 1; + } + + .oc-about-split:not(.oc-about-social) .oc-about-media, + .oc-about-split:not(.is-reversed):not(.oc-about-social) .oc-about-media, + .oc-about-split.is-reversed:not(.oc-about-social) .oc-about-media { + margin-left: 0; + margin-right: 0; + width: 100%; + } + + .oc-about-media img { + aspect-ratio: 16 / 9; + height: auto; + } + + .oc-module-gallery { + height: 260px; + } + + .oc-module-gallery .oc-gallery-trigger img { + aspect-ratio: auto; + height: 100%; + } + + .oc-social-channel-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-quote { + width: min(100% - 36px, 700px); + grid-template-columns: 1fr; + padding: 0; + } + + .oc-quote::after { + display: none; + } + + .oc-quote-media { + min-height: 300px; + } + + .oc-quote img { + height: 300px; + } + + .oc-quote-copy { + padding: 40px 30px; + } + + .oc-quote p { + font-size: 24px; + line-height: 1.78; + } + + .oc-list-intro { + min-height: 440px; + } + + .oc-route-summary { + gap: 28px; + } + + .oc-poi-gallery { + grid-auto-rows: 150px; + padding: 48px 0 36px; + } + + .oc-poi-gallery .oc-gallery-trigger:first-child, + .oc-poi-gallery img:first-child { + grid-column: span 12; + grid-row: span 2; + } + + .oc-poi-gallery .oc-gallery-trigger:not(:first-child), + .oc-poi-gallery img:not(:first-child) { + grid-column: span 6; + } + + .oc-poi-fact-grid, + .oc-poi-body-stack { + gap: 18px; + } + + .oc-poi-body-block:nth-child(even) img { + order: 0; + } + + .oc-route-day { + grid-template-columns: 72px minmax(0, 1fr); + gap: 12px; + padding: 14px 0; + } + + .oc-route-cta { + grid-template-columns: 1fr; + align-items: start; + } + + .oc-safaris-layout { + gap: 36px; + } + + .oc-filters { + position: sticky; + top: 76px; + grid-template-columns: 1fr; + gap: 6px; + padding: 6px 0; + border-radius: 0; + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + .oc-filter-groups { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(0, 1fr); + justify-content: stretch; + align-items: stretch; + gap: 4px; + overflow: visible; + padding: 0; + } + + .oc-filter-group { + width: 100%; + max-width: none; + min-width: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + .oc-filter-label, + .oc-filter-options { + display: none; + } + + .oc-filter-mobile-accordion { + display: grid; + min-width: 0; + width: 100%; + margin: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + } + + .oc-filter-mobile-toggle { + position: relative; + width: 100%; + min-height: 42px; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: center; + gap: 3px; + border: 1px solid rgba(88, 112, 86, 0.16); + border-radius: 8px; + background: rgba(251, 255, 247, 0.56); + color: #243328; + padding: 7px 7px 6px; + text-align: center; + cursor: pointer; + } + + .oc-filter-mobile-accordion.is-open .oc-filter-mobile-toggle { + border-color: rgba(72, 168, 80, 0.32); + background: rgba(226, 239, 220, 0.42); + } + + .oc-filter-mobile-toggle span { + min-width: 0; + color: #435448; + font-size: 10px; + font-weight: 800; + line-height: 1.1; + } + + .oc-filter-mobile-toggle strong { + min-width: 0; + color: #243328; + font-size: 11px; + font-weight: 800; + line-height: 1.1; + text-align: center; + } + + .oc-filter-mobile-toggle::after { + content: ''; + position: absolute; + top: 8px; + right: 7px; + width: 5px; + height: 5px; + border-right: 1.3px solid #526353; + border-bottom: 1.3px solid #526353; + transform: rotate(45deg); + transition: transform 0.18s ease; + } + + .oc-filter-mobile-toggle strong { + padding-right: 0; + } + + .oc-filter-mobile-accordion.is-open .oc-filter-mobile-toggle::after { + transform: rotate(225deg); + } + + .oc-filter-mobile-panel { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(74px, 1fr)); + gap: 6px; + padding: 4px 0 0; + } + + .oc-filter-mobile-panel button { + min-height: 36px; + min-width: 0; + border: 1px solid rgba(72, 168, 80, 0.18); + border-radius: 8px; + background: rgba(251, 255, 247, 0.62); + color: #435448; + padding: 6px 8px; + font-size: 12px; + font-weight: 700; + line-height: 1.2; + } + + .oc-filter-mobile-panel button.is-active { + border-color: rgba(72, 168, 80, 0.42); + background: rgba(219, 234, 212, 0.72); + color: #243328; + } + + .oc-feature-panel { + position: static; + } + + .oc-archive-layout { + gap: 36px; + } + + .oc-archive-layout.is-collective { + grid-template-columns: 1fr; + } + + .oc-tabs { + top: 76px; + justify-content: flex-start; + overflow-x: auto; + gap: 28px; + } + + .oc-image-hero { + padding: 0 22px 64px; + } + + .oc-masonry, + .oc-experience-stack, + .oc-home-safaris { + grid-template-columns: 1fr; + } + + .oc-experience-band { + min-height: 520px; + } + + .oc-home-safari-image { + height: 240px; + } + + .oc-live-slide { + flex-basis: 246px; + width: 246px; + } + + .oc-live-slide img { + height: 184px; + } + + .oc-masonry img:first-child, + .oc-masonry img { + height: 260px; + grid-row: auto; + } + +} + +@media (max-width: 540px) { + .oc-home-hero { + min-height: 515px; + } + + .oc-footer-legal { + width: min(100% - 32px, 700px); + flex-direction: column; + gap: 12px; + padding: 18px 0 22px; + text-align: center; + } + + .oc-footer-legal-logo img { + width: 84px; + } + + .oc-footer-legal-meta { + gap: 4px 12px; + } + + .oc-hero-copy h1, + .oc-list-intro h1, + .oc-plan-glass-copy h1, + .oc-contact h1 { + font-size: 38px; + } + + .oc-contact { + padding: 48px 0 70px; + } + + .oc-contact h1 { + margin-bottom: 40px; + } + + .oc-plan-glass-card { + width: calc(100% - 28px); + padding: 22px; + } + + .oc-plan-glass-copy p { + font-size: 16px; + line-height: 1.62; + } + + .oc-full-menu-inner { + width: calc(100% - 36px); + padding-top: 82px; + } + + .oc-full-menu-grid { + grid-template-columns: 1fr; + } + + .oc-section-title h2, + .oc-image-hero h1 { + font-size: 34px; + } + + .oc-image-hero p, + .oc-country h2, + .oc-split-cta h2 { + font-size: 28px; + } + + .oc-gallery-lightbox { + padding: 24px 14px; + } + + .oc-gallery-lightbox img, + .oc-gallery-lightbox video { + max-width: 94vw; + max-height: 72vh; + } + + .oc-gallery-lightbox-close { + top: 14px; + right: 14px; + } + + .oc-gallery-lightbox-nav { + top: auto; + bottom: 18px; + transform: none; + } + + .oc-gallery-lightbox-nav.is-prev { + left: calc(50% - 54px); + } + + .oc-gallery-lightbox-nav.is-next { + right: calc(50% - 54px); + } + + .oc-poi-page { + padding-bottom: 72px; + } + + .oc-poi-page > .oc-breadcrumb { + width: auto; + margin: 0; + padding: 18px 20px 0; + } + + .oc-poi-lead { + padding: 24px 0 30px; + } + + .oc-poi-intro { + gap: 16px; + } + + .oc-poi-intro h1 { + font-size: 38px; + line-height: 1.04; + } + + .oc-poi-intro p, + .oc-poi-summary p, + .oc-poi-content-copy p { + font-size: 16px; + line-height: 1.74; + } + + .oc-poi-album { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-auto-rows: 118px; + gap: 8px; + } + + .oc-poi-album-item, + .oc-poi-album-item:nth-child(n + 4) { + grid-column: span 1; + grid-row: auto; + } + + .oc-poi-album-item.is-featured { + grid-column: span 2; + grid-row: span 2; + } + + .oc-poi-hero-image { + border-radius: 6px; + } + + .oc-poi-hero-image img { + height: 100%; + } + + .oc-poi-summary { + padding: 46px 0 28px; + } + + .oc-poi-summary h2, + .oc-poi-content-head h2 { + font-size: 32px; + } + + .oc-poi-facts-centered { + grid-template-columns: 1fr; + } + + .oc-poi-content-head { + margin-bottom: 30px; + } + + .oc-poi-content-block figure { + height: auto; + aspect-ratio: 2.1 / 1; + } + + .oc-poi-content-copy h3 { + font-size: 28px; + } + + .oc-safari-grid, + .oc-archive-grid, + .oc-activity-grid, + .oc-region-grid, + .oc-poi-fact-grid, + .oc-poi-body-block, + .oc-poi-related-grid { + grid-template-columns: 1fr; + } + + .oc-poi-gallery { + grid-auto-rows: 132px; + gap: 10px; + } + + .oc-poi-gallery img:first-child, + .oc-poi-gallery img:not(:first-child) { + grid-column: span 12; + } + + .oc-poi-body-block { + padding: 22px 0; + } + + .oc-poi-body-block img { + min-height: 230px; + } + + .oc-route-summary-copy h2, + .oc-poi-overview h2, + .oc-poi-body-block h3 { + font-size: 34px; + } + + .oc-route-cta h2 { + font-size: 30px; + } + + .is-collective .oc-archive-grid { + grid-template-columns: 1fr; + } + + .oc-special-card { + grid-template-columns: 1fr; + } + + .oc-special-card img { + height: 220px; + } + + .oc-journey, + .oc-home-experiences, + .oc-home-safaris-section { + margin-top: 80px; + } + + .oc-home-social-strip { + margin-top: -34px; + margin-bottom: 76px; + padding: 18px; + } + + .oc-home-social-strip .oc-social-channel-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-info-band-image, + .oc-home-safari-image { + height: 210px; + } + + .oc-live-slide { + flex-basis: 214px; + width: 214px; + } + + .oc-live-slide img { + height: 162px; + } + + .oc-about-copy h1, + .oc-about-copy h2 { + font-size: 34px; + line-height: 1.08; + } + + .oc-about-copy p, + .oc-about-copy li { + font-size: 15px; + } + + .oc-social-channel { + gap: 12px; + } + + .oc-social-channel img { + width: 38px; + height: 38px; + } + + .oc-quote img { + height: 240px; + } + + .oc-quote-media { + min-height: 240px; + } + + .oc-quote p { + font-size: 21px; + line-height: 1.72; + } + + .oc-quote span { + font-size: 19px; + } +} + +@media (max-width: 980px) { + .oc-food-intro, + .oc-transport-intro { + grid-template-columns: 1fr; + } + + .oc-food-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oc-transport-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .oc-about-page { + padding-bottom: 54px; + } + + .oc-about-split.oc-container.has-media:not(.oc-about-social), + .oc-about-split.oc-container.is-reversed.has-media:not(.oc-about-social) { + grid-template-columns: 112px minmax(0, 1fr); + gap: 14px; + align-items: start; + width: min(100% - 36px, 1116px); + padding: 28px 0; + } + + .oc-about-split.has-media:not(.oc-about-social) .oc-about-media, + .oc-about-split.is-reversed.has-media:not(.oc-about-social) .oc-about-media { + grid-column: 1; + grid-row: 1; + order: 0; + width: 112px; + min-height: 148px; + } + + .oc-about-split.has-media:not(.oc-about-social) .oc-about-copy, + .oc-about-split.is-reversed.has-media:not(.oc-about-social) .oc-about-copy { + grid-column: 2; + grid-row: 1; + min-width: 0; + } + + .oc-about-split.has-media:not(.oc-about-social) .oc-about-media img { + width: 112px; + height: 100%; + min-height: 148px; + aspect-ratio: auto; + object-fit: cover; + } + + .oc-module-page .oc-about-copy h1, + .oc-module-page .oc-about-copy h2, + .oc-about-copy h1, + .oc-about-copy h2 { + font-size: 24px; + line-height: 1.12; + } + + .oc-about-body { + gap: 10px; + } + + .oc-about-copy p, + .oc-about-copy li, + .oc-module-page .oc-about-points li { + font-size: 12px; + line-height: 1.48; + } + + .oc-about-body p { + text-indent: 0; + } + + .oc-about-points { + gap: 8px; + } + + .oc-about-points li { + padding-left: 10px; + } + + .oc-module-faq-list { + gap: 8px; + margin-top: 12px; + } + + .oc-module-faq-list details { + padding: 11px 12px; + } + + .oc-module-faq-list summary { + font-size: 12px; + line-height: 1.35; + } + + .oc-module-faq-list p { + font-size: 12px; + line-height: 1.45; + } + + .oc-food-intro, + .oc-transport-intro { + gap: 22px; + padding: 54px 0 24px; + } + + .oc-food-archive, + .oc-transport-grid { + grid-template-columns: 1fr; + gap: 18px; + padding: 0 0 54px; + } + + .oc-archive-layout.has-bottom-prompt, + .oc-archive-layout.is-collective.has-bottom-prompt { + padding-bottom: 54px; + } + + .oc-food-grid { + grid-template-columns: 1fr; + padding: 0; + } + + .oc-food-card { + grid-template-rows: 205px 1fr; + } + + .oc-transport-card { + grid-template-columns: 1fr; + } + + .oc-transport-card > img { + height: 205px; + } + + .oc-section-title { + margin-bottom: 20px; + } + + .oc-section-title h2 { + font-size: 30px; + line-height: 1.18; + } + + .oc-section-title p { + margin-top: 12px; + font-size: 13px; + line-height: 1.55; + } + + .oc-experience-band, + .oc-info-band, + .oc-home-safari, + .oc-safari-card, + .oc-archive-card, + .oc-transport-card, + .oc-route-service-panel, + .oc-route-service-panel:nth-child(even) { + display: grid; + grid-template-columns: 112px minmax(0, 1fr); + grid-template-rows: none; + min-height: 0; + height: auto; + text-align: left; + } + + .oc-experience-band img, + .oc-info-band-image, + .oc-home-safari-image, + .oc-safari-card-image, + .oc-archive-card-image, + .oc-transport-card > img, + .oc-route-service-panel > img, + .oc-route-service-panel:nth-child(even) > img { + width: 100%; + height: 100%; + min-height: 148px; + object-fit: cover; + } + + .oc-route-service-panel:nth-child(even) > img { + order: 0; + } + + .oc-experience-band-content, + .oc-info-band-copy, + .oc-home-safari-body, + .oc-safari-card-body, + .oc-archive-card-body, + .oc-transport-card > div, + .oc-route-service-panel > div { + min-width: 0; + padding: 12px 13px; + } + + .oc-experience-band-content { + align-content: center; + justify-items: start; + gap: 8px; + } + + .oc-experience-band span, + .oc-experience-band small, + .oc-home-safari span, + .oc-safari-card-body b, + .oc-safari-card-body .oc-card-action, + .oc-archive-card-body b, + .oc-archive-card-body .oc-card-action, + .oc-transport-card span, + .oc-food-detail-link, + .oc-route-service-panel span, + .oc-route-service-panel small { + font-size: 10px; + line-height: 1.25; + letter-spacing: 0; + } + + .oc-experience-band h3, + .oc-info-band h3, + .oc-home-safari h3, + .oc-safari-card-body h3, + .oc-archive-card-body h3, + .oc-transport-card h2, + .oc-route-service-panel h2, + .oc-route-service-panel h3 { + margin: 0 0 7px; + font-size: 17px; + line-height: 1.18; + } + + .oc-home-safari p, + .oc-info-band p, + .oc-safari-card-summary, + .oc-archive-card-body p, + .oc-transport-card p, + .oc-transport-card dd, + .oc-route-service-panel p, + .oc-route-service-panel li { + font-size: 12px; + line-height: 1.45; + } + + .oc-info-band p, + .oc-home-safari p, + .oc-safari-card-summary, + .oc-archive-card-body p, + .oc-transport-card p, + .oc-route-service-panel p { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + } + + .oc-safari-card-body { + grid-template-rows: auto auto minmax(0, 1fr) auto; + gap: 7px; + min-height: 0; + } + + .oc-safari-card-tags { + display: none; + } + + .oc-safari-card-meta { + gap: 5px; + } + + .oc-safari-card-meta span { + padding: 4px 5px; + font-size: 9px; + } + + .oc-home-safari::after { + top: 8px; + right: auto; + left: 8px; + padding: 4px 6px; + font-size: 9px; + } + + .oc-home-safari b { + margin: 0; + padding: 6px 0; + font-size: 10px; + } + + .oc-home-safari .oc-card-action { + justify-content: flex-start; + height: auto; + min-height: 0; + margin-top: 8px; + border: 0; + } + + .oc-archive-card-image > span, + .oc-safari-card-image span { + top: 8px; + left: 0; + padding: 5px 7px; + font-size: 9px; + } + + .oc-transport-card dl { + gap: 6px; + } + + .oc-transport-card dl div { + grid-template-columns: 38px minmax(0, 1fr); + gap: 7px; + } + + .oc-transport-card dt { + font-size: 11px; + } + + .oc-route-service-panel ul { + margin-top: 8px; + gap: 5px; + } + + .oc-route-service-panel li { + padding-left: 12px; + } + + .oc-menu-bottom-prompt { + margin-bottom: 60px; + padding: 24px 0; + } +} + +/* Route product detail flow layout */ +.oc-route-section-head p { + grid-column: 2; +} + +@media (max-width: 1180px) { + .oc-route-flow-layout, + .oc-route-service-panel, + .oc-route-service-panel:nth-child(even) { + grid-template-columns: minmax(250px, 0.42fr) minmax(0, 0.58fr); + } + + .oc-route-service-panel:nth-child(even) { + grid-template-columns: minmax(0, 0.58fr) minmax(250px, 0.42fr); + } +} + +@media (max-width: 820px) { + .oc-route-section-head, + .oc-route-flow-layout, + .oc-route-service-panel, + .oc-route-service-panel:nth-child(even) { + grid-template-columns: 1fr; + } + + .oc-route-section-head p { + grid-column: auto; + } + + .oc-route-flow figure { + position: static; + } + + .oc-route-flow figure img, + .oc-route-service-panel > img { + height: 300px; + min-height: 0; + } + + .oc-route-day { + grid-template-columns: 72px minmax(0, 1fr); + gap: 12px; + padding: 14px 0; + } + + .oc-route-service-panel:nth-child(even) > img { + order: 0; + } + + .oc-route-service-panel > div { + padding: 28px; + } +} + +@media (max-width: 540px) { + .oc-route-section-head h2, + .oc-route-service-panel h2 { + font-size: 32px; + } + + .oc-route-service-panel h3 { + font-size: 22px; + } + + .oc-route-flow figure img, + .oc-route-service-panel > img { + height: 230px; + } + + .oc-route-day { + grid-template-columns: 62px minmax(0, 1fr); + gap: 10px; + padding: 12px 0; + } + + .oc-route-day h3 { + font-size: 18px; + margin-bottom: 6px; + } + + .oc-route-day p { + font-size: 13px; + line-height: 1.45; + margin-bottom: 6px; + } + + .oc-route-day > span { + font-size: 11px; + padding: 7px 8px; + } + + .oc-route-day div div { + gap: 6px; + } + + .oc-route-day b { + font-size: 10px; + padding: 5px 7px; + } +} + +@media (max-width: 640px) { + .oc-route-service-panel, + .oc-route-service-panel:nth-child(even) { + grid-template-columns: 112px minmax(0, 1fr); + } + + .oc-route-service-panel > img, + .oc-route-service-panel:nth-child(even) > img { + width: 100%; + height: 100%; + min-height: 210px; + object-fit: cover; + order: 0; + } + + .oc-route-service-panel > div { + min-width: 0; + padding: 13px; + } + + .oc-route-service-panel h2, + .oc-route-service-panel h3 { + font-size: 17px; + line-height: 1.18; + } + + .oc-route-service-panel p, + .oc-route-service-panel li, + .oc-route-service-panel small { + font-size: 12px; + line-height: 1.45; + } + + .oc-route-service-tags, + .oc-route-service-panel ul { + gap: 6px; + } +} diff --git a/src/TravelSite.tsx b/src/TravelSite.tsx new file mode 100644 index 0000000..643e8c9 --- /dev/null +++ b/src/TravelSite.tsx @@ -0,0 +1,4234 @@ +import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useMemo, useRef, useState, type CSSProperties, type FormEvent, type ReactElement, type ReactNode } from 'react'; +import { gsap } from 'gsap'; +import { useGSAP } from '@gsap/react'; +import { + ArrowLeft, + ArrowRight, + CheckCircle, + List, + MagnifyingGlass, + PlayCircle, + X, +} from '@phosphor-icons/react'; +import { + defaultSiteContent, + normalizeSiteContent, + siteText, + type SiteLink, + type SiteMegaMenu, + type SiteContent, +} from './siteContent'; +import { + guizhouCollectiveArchiveCards, + guizhouExperienceArchiveCards, + guizhouFoodCards, + guizhouImages, + guizhouSafariCards, + guizhouTransportCards, +} from './guizhouContent'; +import { + experienceEnglish, + foodEnglish, + hotelEnglish, + safariEnglish, + transportEnglish, +} from './guizhouEnContent'; +import { pickText, translateUi, type SiteLanguage } from './i18n'; +import { createInquiry } from './api'; +import type { CmsContent, Destination, DetailContentBlock, Product, ProductRoutePlan } from './cms'; +import './TravelSite.css'; + +gsap.registerPlugin(useGSAP); + +function asset(source: string) { + return source; +} + +function applySiteLanguage(_root: Element, _language: SiteLanguage) { + void _root; + void _language; + // Visible copy is rendered from bilingual CMS fields; no DOM translation table is needed. +} + +const bg = (source: string) => ({ backgroundImage: `url(${asset(source)})` }); +const moduleHeroBg = (source: string) => ({ '--oc-module-hero-image': `url(${asset(source)})` }) as CSSProperties; +const languageStorageKey = 'wanqu-travel-language'; +const videoSourcePattern = /\.(mp4|webm|ogv|ogg|mov)(?:[?#].*)?$/i; + +function isVideoSource(source: string) { + return videoSourcePattern.test(source); +} + +type TravelSiteRuntime = { + content: SiteContent; + cmsContent?: CmsContent; + language: SiteLanguage; +}; + +const TravelSiteContext = createContext({ + content: defaultSiteContent, + language: 'zh', +}); + +function useTravelSite() { + return useContext(TravelSiteContext); +} + +function oc(value: { zh: string; en: string }, language: SiteLanguage) { + return siteText(value, language); +} + +function enabledSiteLinks(links: SiteLink[]) { + return links.filter((linkItem) => linkItem.enabled !== false); +} + +function resolveMegaMenu(content: SiteContent, navId: string): SiteMegaMenu | undefined { + return content.megaMenus.find((menu) => menu.navId === navId && menu.enabled !== false); +} + +const img = guizhouImages; + +const wanquSocialChannels = [ + { name: 'Instagram', handle: 'sally111565', href: 'https://www.instagram.com/sally111565/', iconSrc: '/social-icons/instagram.svg' }, + { name: 'Facebook', handle: 'Wanqu Travel', href: 'https://www.facebook.com/profile.php?id=61590298506474', iconSrc: '/social-icons/facebook.svg' }, + { name: 'X', handle: 'SallyXu8175', href: 'https://x.com/SallyXu8175', iconSrc: '/social-icons/x.svg' }, +]; + +function localizeSocialChannel(channel: (typeof wanquSocialChannels)[number], language: SiteLanguage) { + if (language === 'zh') return channel; + return channel; +} + +type MegaMenu = { + image: string; + variant?: 'standard' | 'specials' | 'collective'; + accent?: string; + groups: Array<{ title: string; links: Array<{ label: string; href: string }> }>; + feature?: Array<{ label: string; image: string; href: string }>; + specialCards?: Array<{ + title: string; + minimum: string; + date: string; + terms: string; + image: string; + href: string; + }>; + cta?: { label: string; href: string }; +}; + +type DetailIconSize = 'tab' | 'amenity' | 'live'; + +type FilterSelections = Record; +type SafariRouteHighlight = { title: string; copy: string; image: string }; +type SafariRouteDay = { day: string; title: string; place: string; copy: string; meta: string[] }; +type RouteDetailPillarId = 'route-flow' | 'route-stay' | 'route-food' | 'route-transport'; +type RouteDetailPillar = { + id: RouteDetailPillarId; + label: string; + title: string; + summary: string; + image: string; + tags: string[]; + items: string[]; + href?: string; + actionLabel?: string; +}; +type SafariRoutePlans = Partial>>; +type SafariCardItem = { + slug: string; + aliases?: string[]; + href: string; + title: string; + subtitle: string; + nights: string; + duration: string; + price: string; + image: string; + gallery: string[]; + theme: string; + pace: string; + fit: string; + intensity: string; + service: string; + season: string; + category: string; + focus: string; + destinations: string[]; + summary: string; + routeLine: string; + decisionTags: string[]; + bestFor: string[]; + highlights: SafariRouteHighlight[]; + itinerary: SafariRouteDay[]; + inclusions: string[]; + exclusions: string[]; + notes: string[]; + routePlans?: SafariRoutePlans; +}; +type ArchiveCardItem = { title: string; meta: string; type?: string; image: string; copy: string; href?: string }; +type FoodCardItem = { id: string; city: string; english: string; region: string; image: string; dishes: string[]; copy: string }; +type TransportCardItem = { id: string; seats: string; english: string; image: string; bestFor: string; luggage: string; copy: string }; +type PoiKind = 'experience' | 'hotel' | 'food' | 'transport'; +type PoiFact = { label: string; value: string }; +type PoiHighlight = { title?: string; copy?: string; image?: string }; +type PoiRelatedItem = { title: string; kicker: string; copy: string; image: string; href: string }; +type GalleryLightboxItem = { source: string; alt: string; caption?: string }; +// Rendered view model generated from small CMS records; do not expose every field as a separate admin-controlled page-builder setting. +type PoiDetailContent = { + kind: PoiKind; + title: string; + subtitle: string; + heroMeta: string; + image: string; + gallery: string[]; + actionLabel: string; + overviewEyebrow: string; + overviewTitle: string; + overview: string[]; + facts: PoiFact[]; + locationTitle: string; + locationCopy: string; + locationTags: string[]; + bestForTitle: string; + bestFor: string[]; + highlights: PoiHighlight[]; + planningTitle: string; + planningNotes: string[]; + trustTitle: string; + trustPoints: string[]; + relatedTitle: string; + relatedItems: PoiRelatedItem[]; +}; + +const detailTabIconAssetIds: Record = { + Gallery: 'detail-tab-gallery', + Description: 'detail-tab-description', + Activities: 'detail-tab-activities', + Reviews: 'detail-tab-reviews', + Location: 'detail-tab-location', + Itineraries: 'detail-tab-itineraries', + Overview: 'detail-tab-overview', + Rates: 'detail-tab-rates', + Includes: 'detail-tab-includes', + Wildlife: 'detail-tab-wildlife', +}; + +const detailIconAssets: Record = {}; + +const safariCards: SafariCardItem[] = guizhouSafariCards; + +const experienceArchiveCards: ArchiveCardItem[] = guizhouExperienceArchiveCards; + +const collectiveArchiveCards: ArchiveCardItem[] = guizhouCollectiveArchiveCards; + +const foodCards: FoodCardItem[] = guizhouFoodCards; + +const transportCards: TransportCardItem[] = guizhouTransportCards; + +function localizeSafariCard(safari: SafariCardItem, language: SiteLanguage): SafariCardItem { + if (language === 'zh') return safari; + const override = safariEnglish[safari.slug as keyof typeof safariEnglish]; + if (!override) return safari; + return { + ...safari, + title: override.title, + subtitle: override.subtitle, + nights: override.nights, + duration: override.duration, + price: override.price, + theme: override.theme, + pace: override.pace, + fit: override.fit, + intensity: override.intensity, + service: override.service, + season: override.season, + category: override.category, + focus: override.focus, + summary: override.summary, + routeLine: override.routeLine, + decisionTags: [...override.decisionTags], + bestFor: [...override.bestFor], + highlights: override.highlights.map((highlight) => ({ + title: highlight.title, + copy: highlight.copy, + image: img[highlight.imageKey as keyof typeof img], + })), + itinerary: override.itinerary.map((day) => ({ + day: day.day, + title: day.title, + place: day.place, + copy: day.copy, + meta: [...day.meta], + })), + inclusions: [...override.inclusions], + exclusions: [...override.exclusions], + notes: [...override.notes], + }; +} + +function localizeExperienceCard(card: ArchiveCardItem, language: SiteLanguage): ArchiveCardItem { + if (language === 'zh') return card; + const slug = archiveCardSlug(card); + const override = experienceEnglish[slug as keyof typeof experienceEnglish]; + return override + ? { ...card, title: override.title, meta: override.meta, type: override.type, copy: override.copy } + : { ...card, title: translateUi(card.title, language), meta: translateUi(card.meta, language), type: card.type ? translateUi(card.type, language) : card.type, copy: translateUi(card.copy, language) }; +} + +function localizeHotelCard(card: ArchiveCardItem, language: SiteLanguage): ArchiveCardItem { + if (language === 'zh') return card; + const slug = archiveCardSlug(card); + const override = hotelEnglish[slug as keyof typeof hotelEnglish]; + return override + ? { ...card, title: override.title, meta: override.meta, copy: override.copy } + : { ...card, title: translateUi(card.title, language), meta: translateUi(card.meta, language), copy: translateUi(card.copy, language) }; +} + +function localizeFoodCard(card: FoodCardItem, language: SiteLanguage): FoodCardItem { + if (language === 'zh') return card; + const override = foodEnglish[card.id as keyof typeof foodEnglish]; + return override + ? { ...card, city: override.city, english: override.title, region: override.region, dishes: [...override.dishes], copy: override.copy } + : { ...card, city: translateUi(card.city, language), region: translateUi(card.region, language), dishes: translateListFallback(card.dishes, language), copy: translateUi(card.copy, language) }; +} + +function localizeTransportCard(card: TransportCardItem, language: SiteLanguage): TransportCardItem { + if (language === 'zh') return card; + const override = transportEnglish[card.id as keyof typeof transportEnglish]; + return override + ? { ...card, seats: override.seats, english: override.vehicle, bestFor: override.bestFor, luggage: override.luggage, copy: override.copy } + : { ...card, seats: translateUi(card.seats, language), english: translateUi(card.english, language), bestFor: translateUi(card.bestFor, language), luggage: translateUi(card.luggage, language), copy: translateUi(card.copy, language) }; +} + +function translateListFallback(items: string[], language: SiteLanguage) { + return items.map((item) => translateUi(item, language)); +} + +function localizeRelatedItem(item: PoiRelatedItem, language: SiteLanguage): PoiRelatedItem { + if (language === 'zh') return item; + const slug = slugFromHref(item.href); + const foodSlug = slug.replace(/^food-/, ''); + const vehicleSlug = slug.replace(/^transport-/, ''); + const experience = experienceEnglish[slug as keyof typeof experienceEnglish]; + const hotel = hotelEnglish[slug as keyof typeof hotelEnglish]; + const food = foodEnglish[foodSlug as keyof typeof foodEnglish]; + const vehicle = transportEnglish[vehicleSlug as keyof typeof transportEnglish]; + + if (experience) return { ...item, title: experience.title, kicker: experience.meta, copy: experience.copy }; + if (hotel) return { ...item, title: hotel.title, kicker: hotel.meta, copy: hotel.copy }; + if (food) return { ...item, title: food.title, kicker: food.region, copy: food.copy }; + if (vehicle) return { ...item, title: vehicle.title, kicker: vehicle.bestFor, copy: vehicle.copy }; + + return { ...item, title: translateUi(item.title, language), kicker: translateUi(item.kicker, language), copy: translateUi(item.copy, language) }; +} + +function localizePoiDetail(detail: PoiDetailContent, language: SiteLanguage, slug: string): PoiDetailContent { + if (language === 'zh') return detail; + + const gallery = detail.gallery.length ? detail.gallery : [detail.image]; + const baseHighlights = (leadTitle: string, leadCopy: string) => [ + { title: leadTitle, copy: leadCopy, image: gallery[0] || detail.image }, + { title: 'Route Fit', copy: 'We place this stop where drive time, meals, hotels, and the day rhythm make sense.', image: gallery[1] || detail.image }, + { title: 'Guest Comfort', copy: 'Pace, language needs, food preferences, and activity level are checked before confirmation.', image: gallery[2] || detail.image }, + { title: 'Backup Plan', copy: 'Weather, availability, crowds, or road conditions can trigger a lighter replacement.', image: gallery[3] || detail.image }, + ]; + const relatedItems = detail.relatedItems.map((item) => localizeRelatedItem(item, language)); + + if (detail.kind === 'experience') { + const experience = experienceEnglish[slug as keyof typeof experienceEnglish]; + const title = experience?.title || translateUi(detail.title, language); + const meta = experience?.meta || 'Guizhou'; + return { + ...detail, + title, + subtitle: experience?.copy || translateUi(detail.subtitle, language), + heroMeta: experience ? `${experience.type} · ${experience.meta}` : translateUi(detail.heroMeta, language), + actionLabel: 'Ask About This Experience', + overviewEyebrow: 'Guizhou Experience', + overviewTitle: 'What to Know Before Adding This Experience', + overview: [ + experience?.copy || translateUi(detail.overview[0] || detail.subtitle, language), + 'We confirm route fit, guide availability, weather, activity level, and language support before making it part of a private plan.', + ], + facts: [ + { label: 'Type', value: experience?.type || 'Experience' }, + { label: 'Area', value: meta }, + { label: 'Best Use', value: 'Private route add-on' }, + { label: 'Confirmation', value: 'Checked by date, weather, host or guide availability' }, + ], + locationTitle: 'Location and Route Fit', + locationCopy: 'This experience is planned together with driving time, hotels, meals, weather windows, and local operating conditions.', + locationTags: [meta, experience?.type || 'Experience', 'Private route add-on'], + bestForTitle: 'Best For', + bestFor: ['Travelers who want Guizhou beyond quick sightseeing', 'Small private groups that value context and flexibility', 'Guests who need clear pacing, safety, and language notes'], + highlights: baseHighlights('On-site Conditions First', 'We check access, timing, activity level, and local operating conditions before confirming.'), + planningTitle: 'Planning Notes', + planningNotes: ['Confirm date, weather, physical ability, and guest expectations before locking the day.', 'Keep buffer time for transfers, meals, rest, and last-minute changes.', 'For inbound guests, we can prepare bilingual notes and on-site interpretation.'], + trustTitle: 'Operating Support', + trustPoints: ['Local condition check', 'Route fit', 'Bilingual briefing', 'Backup option'], + relatedTitle: 'Pair With', + relatedItems, + }; + } + + if (detail.kind === 'hotel') { + const hotel = hotelEnglish[slug as keyof typeof hotelEnglish]; + const title = hotel?.title || translateUi(detail.title, language); + return { + ...detail, + title, + subtitle: hotel?.copy || translateUi(detail.subtitle, language), + heroMeta: hotel ? `${hotel.meta} · Stay` : translateUi(detail.heroMeta, language), + actionLabel: 'Ask About This Hotel', + overviewEyebrow: 'Guizhou Hotels', + overviewTitle: 'Stay Quality, Location, and Route Fit', + overview: [ + hotel?.copy || translateUi(detail.overview[0] || detail.subtitle, language), + 'We compare stays by location, rest quality, breakfast, room type, communication needs, budget, and the next day transfer.', + ], + facts: [ + { label: 'Area', value: hotel?.meta || 'Guizhou' }, + { label: 'Best Use', value: 'Matched to route pace and comfort needs' }, + { label: 'Quote', value: 'Confirmed by room type, date, and group size' }, + ], + locationTitle: 'Location and Connections', + locationCopy: 'Hotel value depends on whether it reduces backtracking, improves rest, and connects smoothly with attractions, meals, and departure time.', + locationTags: [hotel?.meta || 'Guizhou', 'Route-matched stay', 'Backup options compared'], + bestForTitle: 'Best For', + bestFor: ['Guests who care about rest quality and smooth transfers', 'Families, business guests, or inbound small groups', 'Routes where hotel location affects the next day'], + highlights: baseHighlights('Location Value', 'We check whether the hotel area genuinely supports that night and the next transfer.'), + planningTitle: 'Booking Notes', + planningNotes: ['Final quotes depend on room type, season, cancellation terms, breakfast, and group size.', 'Hotel choice is confirmed together with vehicle dispatch, attraction timing, and meal nodes.', 'Tell us early about bed type, quiet rooms, views, or family facilities.'], + trustTitle: 'Comparison Criteria', + trustPoints: ['Location', 'Comfort', 'Breakfast', 'Communication', 'Backup stay'], + relatedTitle: 'Related Stays', + relatedItems, + }; + } + + if (detail.kind === 'food') { + const food = foodEnglish[slug as keyof typeof foodEnglish]; + const title = food?.title || translateUi(detail.title, language); + const dishes = food ? [...food.dishes] : []; + return { + ...detail, + title, + subtitle: food?.copy || translateUi(detail.subtitle, language), + heroMeta: food ? `${food.region} · Food stop` : translateUi(detail.heroMeta, language), + actionLabel: 'Add This Food Stop', + overviewEyebrow: 'Guizhou Food', + overviewTitle: 'Local Flavor, Signature Dishes, and Taste Boundaries', + overview: [ + food?.copy || translateUi(detail.overview[0] || detail.subtitle, language), + 'Guizhou meals should be matched to spice tolerance, sour-soup comfort, dietary needs, allergies, driving time, and the day rhythm.', + ], + facts: [ + { label: 'Area', value: food?.region || 'Guizhou' }, + { label: 'Signature', value: dishes.slice(0, 3).join(' / ') || 'Local food' }, + { label: 'Best Use', value: 'Meal node matched to route timing' }, + { label: 'Taste Check', value: 'Spice, sour flavors, allergies, and restrictions confirmed first' }, + ], + locationTitle: 'Meal Location and Day Rhythm', + locationCopy: 'Food stops are planned with drive time, attraction timing, hotel location, and guest taste preferences instead of chasing a detour.', + locationTags: [food?.region || 'Guizhou', 'Food node', 'Taste check first'], + bestForTitle: 'Best For', + bestFor: ['Guests who want local flavor without avoidable surprises', 'Families or inbound groups with varied taste needs', 'Routes that treat food as part of the travel experience'], + highlights: dishes.slice(0, 4).map((dish, index) => ({ + title: dish, + copy: index === 0 ? 'A good representative flavor to start with in this area.' : 'Added according to guest taste, spice tolerance, and time available.', + image: gallery[index] || detail.image, + })), + planningTitle: 'Meal Notes', + planningNotes: ['Confirm spice level, sour soup, fish, beef, lamb, offal, vegetarian, halal, and allergies in advance.', 'Outdoor or long-drive days should prioritize clean, efficient, reliable meals.', 'Village meals need etiquette and taste expectations explained before arrival.'], + trustTitle: 'Food Standards', + trustPoints: ['Cleanliness', 'Taste fit', 'Route fit', 'Serving speed', 'Backup restaurant'], + relatedTitle: 'Other Food Areas', + relatedItems, + }; + } + + const vehicle = transportEnglish[slug as keyof typeof transportEnglish]; + const title = vehicle?.title || translateUi(detail.title, language); + return { + ...detail, + title, + subtitle: vehicle?.copy || translateUi(detail.subtitle, language), + heroMeta: vehicle ? `${vehicle.vehicle} · ${vehicle.bestFor}` : translateUi(detail.heroMeta, language), + actionLabel: 'Ask for Vehicle Plan', + overviewEyebrow: 'Guizhou Transport', + overviewTitle: 'Match Vehicle, Luggage, and Mountain Transfers First', + overview: [ + vehicle?.copy || translateUi(detail.overview[0] || detail.subtitle, language), + 'Transport planning covers group size, luggage, mountain roads, parking, driver experience, guide language, and backup dispatch.', + ], + facts: [ + { label: 'Vehicle', value: vehicle?.title || title }, + { label: 'Best For', value: vehicle?.bestFor || 'Private Guizhou transfers' }, + { label: 'Luggage', value: vehicle?.luggage || 'Checked by route' }, + { label: 'Quote', value: 'Confirmed by route, dates, vehicle tier, and guide language' }, + ], + locationTitle: 'Dispatch and Roads', + locationCopy: 'Vehicles are matched to airport or rail arrival, hotel location, scenic parking, mountain-road limits, luggage, and group meeting points.', + locationTags: [vehicle?.seats || 'Vehicle', vehicle?.vehicle || 'Transport', 'Guizhou dispatch'], + bestForTitle: 'Best For', + bestFor: [vehicle?.bestFor || 'Private groups', 'Groups that need luggage, driver, and parking checked early', 'Multi-day Guizhou routes with mountain transfers'], + highlights: baseHighlights('Space for People and Luggage', 'We check seats, luggage, and comfort before confirming a vehicle.'), + planningTitle: 'Vehicle Notes', + planningNotes: ['Confirm guests, luggage size, child seats, camera gear, and English-speaking guide needs first.', 'Multi-day routes need driver rest, hotel parking, scenic transfers, and road limits planned together.', 'Peak dates and large groups should keep backup or split-vehicle options.'], + trustTitle: 'Transport Support', + trustPoints: ['Vehicle fit', 'Driver communication', 'Parking check', 'Road-limit review', 'Backup plan'], + relatedTitle: 'Other Vehicle Choices', + relatedItems, + }; +} + +function normalizeFilterValue(value: string) { + return value.trim().toLowerCase(); +} + +function selectedFilterValues(filters: FilterSelections, title: string) { + return filters[title] || []; +} + +function nextFilterSelections(filters: FilterSelections, title: string, item: string) { + const next = { ...filters }; + if (item === 'All' || item === '全部') { + delete next[title]; + return next; + } + + const current = selectedFilterValues(filters, title); + const selected = current.includes(item) ? current.filter((value) => value !== item) : [...current, item]; + if (selected.length) { + next[title] = selected; + } else { + delete next[title]; + } + return next; +} + +function setSingleFilterSelection(filters: FilterSelections, title: string, item: string) { + const next = { ...filters }; + if (item === 'All' || item === '全部') { + delete next[title]; + } else { + next[title] = [item]; + } + return next; +} + +function matchesAnySelected(selected: string[], matcher: (value: string) => boolean) { + return selected.length === 0 || selected.some(matcher); +} + +function safariDurationRange(safari: SafariCardItem) { + const [start, end] = (safari.duration.match(/\d+/g) || []).map(Number); + const min = start || 0; + const max = end || min; + return { min, max }; +} + +function safariMatchesDurationFilter(safari: SafariCardItem, value: string) { + const { min, max } = safariDurationRange(safari); + if (value === '1日游') return max <= 1; + if (value === '3天以内') return max <= 3; + if (value === '3-5天') return min >= 3 && min <= 5; + if (value === '5天以上') return max >= 5; + return safari.duration === value || safari.nights.includes(value); +} + +function safariMatchesFilters(safari: SafariCardItem, filters: FilterSelections) { + const categoryFilters = selectedFilterValues(filters, '线路分类'); + const focusFilters = selectedFilterValues(filters, '游玩重点'); + const paceFilters = selectedFilterValues(filters, '旅行节奏'); + const durationFilters = selectedFilterValues(filters, '天数'); + const destinations = safari.destinations.map(normalizeFilterValue); + + return ( + matchesAnySelected(categoryFilters, (value) => safari.category === value) && + matchesAnySelected(focusFilters, (value) => safari.focus === value) && + matchesAnySelected(paceFilters, (value) => safari.pace === value) && + matchesAnySelected(durationFilters, (value) => safariMatchesDurationFilter(safari, value)) && + destinations.length > 0 + ); +} + +function safariFromSlug(slug: string, cmsContent: CmsContent | undefined, language: SiteLanguage) { + const cmsSafariCards = cmsRouteCards(cmsContent, language); + return ( + cmsSafariCards.find((safari) => safari.slug === slug || safari.aliases?.includes(slug)) || + localizeSafariCard(safariCards.find((safari) => safari.slug === slug || safari.aliases?.includes(slug)) || safariCards[0], language) + ); +} + +function slugFromHref(href?: string) { + if (!href) return ''; + const clean = href.split('#')[0].split('?')[0].replace(/\/+$/, ''); + const parts = clean.split('/').filter(Boolean); + return parts[parts.length - 1] || ''; +} + +function archiveCardSlug(card: ArchiveCardItem) { + return slugFromHref(card.href) || slugifyTitle(card.title); +} + +function experienceFromSlug(slug: string) { + return experienceArchiveCards.find((card) => archiveCardSlug(card) === slug || slugifyTitle(card.title) === slug) || experienceArchiveCards[0]; +} + +function foodFromSlug(slug: string) { + const normalizedSlug = slug.replace(/^food-/, ''); + return foodCards.find((card) => card.id === slug || card.id === normalizedSlug || `food-${card.id}` === slug || slugifyTitle(card.city) === slug || slugifyTitle(card.english) === slug) || foodCards[0]; +} + +function transportFromSlug(slug: string) { + const normalizedSlug = slug.replace(/^transport-/, ''); + return transportCards.find((card) => card.id === slug || card.id === normalizedSlug || `transport-${card.id}` === slug) || transportCards[0]; +} + +function transportHref(vehicle: TransportCardItem) { + return `/transport/${vehicle.id}/`; +} + +function uniqueImages(...sources: Array) { + return Array.from(new Set(sources.filter(Boolean) as string[])); +} + +function cmsText(value: { zh: string; en: string } | undefined, language: SiteLanguage = 'zh') { + return value?.[language] || value?.zh || value?.en || ''; +} + +function cmsMediaSource(cmsContent: CmsContent | undefined, mediaId: string) { + if (!cmsContent || !mediaId) return ''; + return cmsContent.media.find((item) => item.id === mediaId)?.url || ''; +} + +function cmsLocalizedList(items: Array<{ zh: string; en: string }> | undefined, language: SiteLanguage) { + return (items || []).map((item) => cmsText(item, language)).filter(Boolean); +} + +function cmsRoutePlanToPillar(plan: ProductRoutePlan | undefined, language: SiteLanguage, fallbackImage: string): Omit | undefined { + if (!plan) return undefined; + const title = cmsText(plan.title, language); + const summary = cmsText(plan.summary, language); + const tags = cmsLocalizedList(plan.tags, language); + const items = cmsLocalizedList(plan.items, language); + const image = plan.imageUrl || fallbackImage; + if (!title && !summary && tags.length === 0 && items.length === 0 && !image) return undefined; + return { + title, + summary, + image, + tags, + items, + href: plan.href, + actionLabel: cmsText(plan.actionLabel, language), + }; +} + +function cmsRouteCards(cmsContent: CmsContent | undefined, language: SiteLanguage): SafariCardItem[] { + if (!cmsContent) return []; + return cmsContent.products + .filter((product) => product.kind === 'route' && product.status === 'published') + .map((product) => cmsRouteProductToSafari(product, cmsContent, language)); +} + +function cmsRouteProductToSafari(product: Product, cmsContent: CmsContent, language: SiteLanguage): SafariCardItem { + const route = product.routeDetails; + const slug = route?.slug || product.id.replace(/^route-/, '') || slugifyTitle(cmsText(product.title, language)); + const image = cmsMediaSource(cmsContent, product.imageId) || route?.galleryUrls?.[0] || img.routeClassic; + const gallery = uniqueImages( + image, + ...(route?.galleryUrls || []), + ...(route?.routeHighlights || []).map((item) => item.imageUrl), + route?.stayPlan?.imageUrl, + route?.foodPlan?.imageUrl, + route?.transportPlan?.imageUrl, + ).slice(0, 6); + const title = cmsText(product.title, language) || slug; + const summary = cmsText(product.summary, language); + const duration = cmsText(product.duration, language); + const pace = cmsText(route?.pace, language) || cmsText(product.difficulty, language); + const category = cmsText(route?.category, language) || cmsText(product.region, language) || translateUi('线路', language); + const focus = cmsText(route?.focus, language) || cmsText(product.subtitle, language) || category; + const intensity = cmsText(route?.intensity, language) || cmsText(product.difficulty, language); + const routeLine = cmsText(route?.routeLine, language) || cmsText(product.subtitle, language) || title; + const fallbackMeta = [duration, pace, intensity].filter(Boolean); + const itinerary = (route?.itinerary || []).map((day) => ({ + day: cmsText(day.day, language), + place: cmsText(day.place, language), + title: cmsText(day.title, language), + copy: cmsText(day.copy, language), + meta: cmsLocalizedList(day.meta, language), + })).filter((day) => day.day || day.place || day.title || day.copy); + + return { + slug, + aliases: [product.id], + href: route?.href || `/safaris/${slug}/`, + title, + subtitle: cmsText(product.subtitle, language) || summary, + nights: cmsText(route?.nights, language) || duration, + duration, + price: cmsText(product.priceLabel, language), + image, + gallery, + theme: cmsText(route?.theme, language) || category, + pace, + fit: cmsText(route?.fit, language), + intensity, + service: cmsText(route?.service, language), + season: cmsText(route?.season, language), + category, + focus, + destinations: route?.destinations?.length ? route.destinations : [category], + summary, + routeLine, + decisionTags: cmsLocalizedList(route?.decisionTags, language).length ? cmsLocalizedList(route?.decisionTags, language) : cmsLocalizedList(product.highlights, language), + bestFor: cmsLocalizedList(route?.bestFor, language), + highlights: (route?.routeHighlights || []).map((highlight) => ({ + title: cmsText(highlight.title, language), + copy: cmsText(highlight.copy, language), + image: highlight.imageUrl || image, + })).filter((highlight) => highlight.title || highlight.copy || highlight.image), + itinerary: itinerary.length ? itinerary : [{ day: 'Day 1', place: routeLine, title, copy: summary, meta: fallbackMeta }], + inclusions: cmsLocalizedList(route?.inclusions, language), + exclusions: cmsLocalizedList(route?.exclusions, language), + notes: cmsLocalizedList(route?.notes, language), + routePlans: { + stay: cmsRoutePlanToPillar(route?.stayPlan, language, image), + food: cmsRoutePlanToPillar(route?.foodPlan, language, image), + transport: cmsRoutePlanToPillar(route?.transportPlan, language, image), + }, + }; +} + +function cmsDetailBlocksToPoiContent( + cmsContent: CmsContent | undefined, + blocks: DetailContentBlock[] | undefined, + fallbackImage: string, + language: SiteLanguage, +) { + if (!blocks?.length) return []; + return blocks + .map((block, index) => { + const image = cmsMediaSource(cmsContent, block.imageId); + const title = cmsText(block.title, language); + const copy = cmsText(block.body, language); + return { + title: title || (copy ? `${pickText('内容', 'Content', language)} ${String(index + 1).padStart(2, '0')}` : ''), + copy, + image: image || fallbackImage || undefined, + }; + }) + .filter((block) => block.title || block.copy || block.image); +} + +function applyProductCmsDetailContent(detail: PoiDetailContent, product: Product | undefined, cmsContent: CmsContent | undefined, language: SiteLanguage) { + if (!product || product.kind === 'route') return detail; + const image = cmsMediaSource(cmsContent, product.imageId) || detail.image; + const contentBlocks = cmsDetailBlocksToPoiContent(cmsContent, product.detailContentBlocks, image, language); + const title = cmsText(product.title, language) || detail.title; + const summary = cmsText(product.summary, language); + + return { + ...detail, + title, + subtitle: cmsText(product.subtitle, language) || summary || detail.subtitle, + image, + gallery: uniqueImages(image, ...contentBlocks.map((block) => block.image), ...detail.gallery).slice(0, 5), + overviewTitle: title || detail.overviewTitle, + overview: summary ? [summary, ...detail.overview.slice(1)] : detail.overview, + highlights: contentBlocks.length > 0 ? contentBlocks : detail.highlights, + }; +} + +function applyDestinationCmsDetailContent(detail: PoiDetailContent, destination: Destination | undefined, cmsContent: CmsContent | undefined, language: SiteLanguage) { + if (!destination) return detail; + const image = cmsMediaSource(cmsContent, destination.imageId) || detail.image; + const contentBlocks = cmsDetailBlocksToPoiContent(cmsContent, destination.detailContentBlocks, image, language); + const title = cmsText(destination.title, language) || detail.title; + const summary = cmsText(destination.summary, language); + + return { + ...detail, + title, + subtitle: cmsText(destination.subtitle, language) || summary || detail.subtitle, + image, + gallery: uniqueImages(image, ...contentBlocks.map((block) => block.image), ...detail.gallery).slice(0, 5), + overviewTitle: title || detail.overviewTitle, + overview: summary ? [summary, ...detail.overview.slice(1)] : detail.overview, + highlights: contentBlocks.length > 0 ? contentBlocks : detail.highlights, + }; +} + +function findCmsProduct( + cmsContent: CmsContent | undefined, + slug: string, + title: string, + kinds: Product['kind'][], +) { + if (!cmsContent) return undefined; + const prefixedSlugMatches = kinds.map((kind) => `${kind}-${slug}`); + return cmsContent.products.find((product) => ( + kinds.includes(product.kind) && + ( + product.id === slug || + prefixedSlugMatches.includes(product.id) || + cmsText(product.title) === title || + cmsText(product.title, 'en') === title || + slugifyTitle(cmsText(product.title)) === slug || + slugifyTitle(cmsText(product.title, 'en')) === slug + ) + )); +} + +function findCmsDestination(cmsContent: CmsContent | undefined, slug: string, title: string) { + if (!cmsContent) return undefined; + return cmsContent.destinations.find((destination) => ( + destination.id === slug || + cmsText(destination.title) === title || + cmsText(destination.title, 'en') === title || + slugifyTitle(cmsText(destination.title)) === slug || + slugifyTitle(cmsText(destination.title, 'en')) === slug + )); +} + +function initialSafariFilters(): FilterSelections { + if (typeof window === 'undefined') return {}; + const params = new URLSearchParams(window.location.search); + const category = params.get('category'); + const focus = params.get('focus'); + return { + ...(category ? { 线路分类: [category] } : {}), + ...(focus ? { 游玩重点: [focus] } : {}), + }; +} + +function experienceMatchesFilters(card: ArchiveCardItem, filters: FilterSelections) { + const typeFilters = selectedFilterValues(filters, '体验类型'); + const locationFilters = selectedFilterValues(filters, '地点'); + const type = normalizeFilterValue(card.type || ''); + const meta = normalizeFilterValue(card.meta); + + return ( + matchesAnySelected(typeFilters, (value) => type === normalizeFilterValue(value)) && + matchesAnySelected(locationFilters, (value) => meta.includes(normalizeFilterValue(value))) + ); +} + +function collectiveMatchesFilters(card: ArchiveCardItem, filters: FilterSelections) { + const locationFilters = selectedFilterValues(filters, '地点'); + const meta = normalizeFilterValue(card.meta); + return matchesAnySelected(locationFilters, (value) => meta.includes(normalizeFilterValue(value))); +} + +const experienceTitleZh: Record = { + 'cave-expedition': '天然溶洞探险', + rafting: '峡谷漂流与亲水', + 'stream-tracing': '溯溪与瀑降', + 'miao-craft': '苗绣与蜡染非遗体验', + 'dong-song': '侗族大歌与鼓楼村寨', + huangguoshu: '黄果树瀑布错峰玩法', + 'libo-xiaoqikong': '荔波樟江·小七孔喀斯特水系', + 'fanjing-mountain': '梵净山天气窗口', + 'xijiang-miao-village': '西江与侗寨夜晚', + wanfenglin: '兴义万峰林锥状喀斯特峰林', + 'wild-coffee': '山野咖啡停靠', + 'hot-spring-recovery': '温泉恢复日', +}; + +const experiencePoiProfiles: Record = { + Outdoor: { + duration: '半日到 1 日', + intensity: '中等到进阶', + booking: '需按天气、水位和向导档期确认', + bestFor: ['有户外兴趣的朋友小团', '能接受根据天气调整体验的人', '想把贵州山野放进路线核心的客人'], + planningNotes: ['出发前会确认鞋服、装备、保险和退出机制。', '强降雨、水位异常或客人体能不匹配时,会切换为轻量备选体验。', '不建议把高强度体验安排在返程当天。'], + trustPoints: ['本地向导与路线评估', '天气与水位复核', '英文说明可提前准备'], + }, + Culture: { + duration: '2-4 小时到 1 天', + intensity: '轻松', + booking: '需预约老师、表演或村寨主理人', + bestFor: ['想理解苗侗文化的境外客人', '亲子家庭和长辈同行', '偏好慢节奏交流和手作体验的人'], + planningNotes: ['会提前说明拍摄礼仪、餐食禁忌和交流方式。', '非遗老师、表演和村寨活动会受节庆和档期影响。', '建议与村寨住宿或长桌宴放在同一段路线里。'], + trustPoints: ['在地主理人沟通', '礼仪与翻译提示', '餐食接受度提前确认'], + }, + Landscape: { + duration: '半日到 1 日', + intensity: '轻松到中等', + booking: '按门票、人流和天气窗口安排', + bestFor: ['第一次来贵州的客人', '喜欢自然景观和摄影的人', '希望景点动线更稳的家庭或小团'], + planningNotes: ['会结合光线、人流、车程和酒店位置安排进入时间。', '雨季、丰水期和节假日会显著影响体验节奏。', '建议和附近餐食、酒店或轻量体验组合。'], + trustPoints: ['错峰进入建议', '摄影时间窗口', '景区换乘和步行强度说明'], + }, + Lifestyle: { + duration: '1-3 小时', + intensity: '轻松', + booking: '按路线顺路程度和营业时间确认', + bestFor: ['想放慢节奏的人', '摄影和生活方式客人', '户外活动后的恢复段落'], + planningNotes: ['更适合做转场中段或午后恢复,不宜作为整天唯一内容。', '会确认是否适合儿童、长辈和不喝咖啡的同行者。', '遇到天气或营业变化时可替换为观景、茶歇或村寨停靠。'], + trustPoints: ['顺路程度判断', '现场营业复核', '替代停靠点准备'], + }, + Wellness: { + duration: '半日到 1 晚', + intensity: '低强度', + booking: '按房态、泡池开放和身体状态确认', + bestFor: ['户外或山地线路后的恢复客人', '亲子家庭与长辈同行', '偏好舒适住宿和慢节奏的人'], + planningNotes: ['建议放在高强度活动之后,不与早起赶路日硬拼。', '会提前确认儿童、长辈和特殊身体状况是否适合。', '可与黄果树、剑河或度假酒店住宿组合。'], + trustPoints: ['恢复日节奏设计', '酒店与泡池开放复核', '身体状态提醒'], + }, +}; + +experiencePoiProfiles.户外 = experiencePoiProfiles.Outdoor; +experiencePoiProfiles.人文 = experiencePoiProfiles.Culture; +experiencePoiProfiles.风光 = experiencePoiProfiles.Landscape; +experiencePoiProfiles.生活方式 = experiencePoiProfiles.Lifestyle; +experiencePoiProfiles.康养 = experiencePoiProfiles.Wellness; + +function experienceProfile(card: ArchiveCardItem) { + return experiencePoiProfiles[card.type || ''] || experiencePoiProfiles.Landscape; +} + +function poiRelatedFromArchive(cards: ArchiveCardItem[], currentTitle: string, baseHref: (card: ArchiveCardItem) => string) { + return cards + .filter((card) => card.title !== currentTitle) + .slice(0, 3) + .map((card) => ({ + title: card.title, + kicker: card.meta, + copy: card.copy, + image: card.image, + href: baseHref(card), + })); +} + +type PoiDetailOverride = Partial>; + +function withPoiOverride(base: PoiDetailContent, slug: string, overrides: Record) { + const override = overrides[slug]; + return override ? { ...base, ...override } : base; +} + +const experienceDetailOverrides: Record = { + 'cave-expedition': { + title: '双河洞 / 十二背后洞穴探险', + subtitle: '以遵义绥阳双河洞和十二背后为核心,区分游客型洞穴、科普型轻探洞和进阶洞穴活动。', + heroMeta: '遵义绥阳 · 洞穴 / 地心 / 户外安全', + gallery: [img.experienceCave, img.routeOutdoor, img.menuOutdoor, img.faqOutdoor], + overviewTitle: '不是所有洞穴都适合探险,先分清体验级别', + overview: [ + '双河洞/十二背后适合作为贵州洞穴主题的核心素材,但实际进入方式要按景区开放、向导、装备、水位和客人体力判断。', + '我们会把游客型洞穴观光、科普型轻探洞和进阶洞穴活动分开说明,避免把“看洞穴”和“探洞”混成同一个产品。', + ], + facts: [ + { label: '位置', value: '遵义绥阳 / 双河洞 / 十二背后' }, + { label: '建议时长', value: '半日到 1 日,进阶活动另议' }, + { label: '活动强度', value: '轻量观光到进阶探洞分级' }, + { label: '装备', value: '头盔、头灯、防滑鞋、备用衣物按项目确认' }, + { label: '关键变量', value: '水位、降雨、向导档期、撤退路线' }, + { label: '适合场景', value: '户外山野 / 亲子科普 / 小众体验' }, + ], + locationTitle: '绥阳洞穴节点', + locationCopy: '洞穴活动要和贵阳/遵义抵离、活动前住宿、活动后洗浴恢复和备用景点一起安排,不适合塞在长车程末尾。', + locationTags: ['双河洞', '十二背后', '分级探洞'], + highlights: [ + { title: '洞穴分级', copy: '游客型洞穴、轻探洞和进阶探洞采用不同装备、向导比例和风险说明。', image: img.experienceCave }, + { title: '水位与降雨复核', copy: '雨季和连续降雨后不硬排进阶活动,必要时改为科普观光或峡谷/古镇备选。', image: img.routeOutdoor }, + { title: '活动后恢复', copy: '洞穴活动后优先安排热食、洗浴和轻松住宿,不和早起赶路日硬拼。', image: img.menuHotels }, + { title: '英文安全说明', copy: '境外客人可提前准备装备、动作、禁忌和撤退机制的英文说明。', image: img.faqOutdoor }, + ], + planningNotes: ['先确认是否接受黑暗、湿滑、狭窄空间。', '儿童和长辈建议选游客型洞穴或科普轻体验。', '进阶探洞必须单独确认保险、向导、装备和天气窗口。'], + trustPoints: ['洞穴等级说明', '天气/水位复核', '向导与装备确认', '替代路线'], + }, + rafting: { + title: '荔波与黔南亲水漂流', + subtitle: '把夏季河谷、漂流和亲水体验放进荔波或黔南路线,而不是单独为漂流绕路。', + heroMeta: '荔波 / 黔南 · 河谷亲水', + gallery: [img.experienceRafting, img.destinationLibo, img.routeOutdoor, img.menuLandscape], + facts: [ + { label: '位置', value: '荔波 / 黔南 / 黔东南河谷' }, + { label: '建议时长', value: '半日' }, + { label: '活动强度', value: '轻松到中等,按水位判断' }, + { label: '季节', value: '夏季更适合,雨季需确认安全' }, + { label: '适合场景', value: '荔波水系 / 亲子 / 轻户外' }, + { label: '替代方案', value: '水森林步道、摄影、村寨停靠' }, + ], + locationCopy: '亲水活动最适合和荔波、小七孔、水森林或黔南村寨放在同一段,不适合为一项活动反向增加车程。', + locationTags: ['荔波', '亲水', '水位判断'], + highlights: [ + { title: '水位先行', copy: '漂流是否执行以当天水位、降雨和景区/项目开放为准。', image: img.experienceRafting }, + { title: '家庭友好版本', copy: '亲子客人可改为轻亲水、森林步道或短时水边体验。', image: img.destinationLibo }, + { title: '活动后热食', copy: '结束后安排热汤、粉面或稳定正餐,比继续赶路更重要。', image: img.menuFood }, + { title: '不硬塞返程日', copy: '水上活动不建议放在赶飞机或长距离返程当天。', image: img.menuTransport }, + ], + }, + 'stream-tracing': { + title: '安顺峡谷溯溪与瀑降', + subtitle: '以峡谷河谷为场景的进阶轻户外,需要绳索、领队、天气和退出路线共同确认。', + heroMeta: '安顺 / 峡谷河谷 · 溯溪 / 瀑降', + gallery: [img.routeOutdoor, img.experienceRafting, img.destinationHuangguoshu, img.faqOutdoor], + facts: [ + { label: '位置', value: '安顺及周边峡谷河谷' }, + { label: '建议时长', value: '半日到 1 日' }, + { label: '活动强度', value: '中等到进阶' }, + { label: '装备', value: '防滑鞋、头盔、绳索系统按活动确认' }, + { label: '适合人群', value: '有基础体力、接受湿身和高度感的客人' }, + { label: '风险变量', value: '降雨、水位、岩面湿滑、客人体力' }, + ], + locationTags: ['安顺峡谷', '溯溪', '瀑降'], + locationCopy: '溯溪/瀑降不适合作为普通观光项目售卖,必须和活动前住宿、装备检查、天气备选和活动后恢复一起设计。', + highlights: [ + { title: '领队与绳索系统', copy: '按体验难度确认领队、保护方式和撤退点。', image: img.routeOutdoor }, + { title: '湿滑与水位', copy: '强降雨后优先降级或取消,安全边界高于打卡完成度。', image: img.experienceRafting }, + { title: '和黄果树错开', copy: '瀑布观光和峡谷活动不建议塞进同一天满负荷执行。', image: img.destinationHuangguoshu }, + { title: '装备说明', copy: '出发前给客人明确鞋服、防水、换洗和个人体能要求。', image: img.faqOutdoor }, + ], + }, + 'miao-craft': { + title: '苗绣与蜡染非遗体验', + subtitle: '围绕苗绣、蜡染、纹样、服饰和村寨礼仪的慢体验,适合与黔东南停留组合。', + heroMeta: '黔东南 / 丹寨 / 安顺 · 非遗手作', + gallery: [img.experienceCraft, img.menuCulture, img.destinationXijiang, img.quoteGuests], + facts: [ + { label: '主题', value: '苗绣 / 蜡染 / 纹样 / 服饰' }, + { label: '建议时长', value: '2-4 小时到 1 日' }, + { label: '强度', value: '轻松,重在交流和动手' }, + { label: '预约', value: '按老师、作坊或村寨主理人档期确认' }, + { label: '适合场景', value: '亲子慢游 / 人文混搭 / 村寨住宿' }, + { label: '语言', value: '适合提前准备英文背景说明' }, + ], + locationTags: ['苗绣', '蜡染', '非遗手作'], + locationCopy: '手作体验最好和村寨停留、餐食和住宿放在同一段,让客人有时间理解纹样、材料和礼仪。', + highlights: [ + { title: '不是快闪打卡', copy: '体验需要留出讲解、上手和作品完成时间,不建议只拍照离开。', image: img.experienceCraft }, + { title: '村寨礼仪', copy: '会提前说明拍照、购买、交流和餐食礼仪,避免冒犯主人。', image: img.menuCulture }, + { title: '亲子友好', copy: '可按年龄调整材料、难度和时长,保留休息与简餐。', image: img.destinationXijiang }, + { title: '翻译支持', copy: '境外客人需要把纹样和工艺背景翻译成易懂语言。', image: img.quoteGuests }, + ], + }, + 'dong-song': { + title: '侗族大歌与鼓楼村寨', + subtitle: '把侗族大歌、鼓楼、风雨桥和村寨餐食做成慢体验,而不是只看一场表演。', + heroMeta: '肇兴 / 侗寨 · 音乐 / 建筑 / 村寨', + gallery: [img.menuCulture, img.destinationXijiang, img.destinationZhenyuan, img.routeMixed], + facts: [ + { label: '主题', value: '侗族大歌 / 鼓楼 / 风雨桥 / 村寨生活' }, + { label: '建议时长', value: '半日到 1 晚' }, + { label: '预约', value: '表演、讲解和主理人需按档期确认' }, + { label: '适合场景', value: '黔东南人文 / 家庭慢游' }, + { label: '餐食', value: '可搭配侗寨家宴或简餐' }, + { label: '注意', value: '不承诺每日有正式演出' }, + ], + locationTags: ['侗寨', '鼓楼', '侗族大歌'], + highlights: [ + { title: '音乐背景先讲清', copy: '先解释侗族大歌的多声部传统,再进入现场体验。', image: img.menuCulture }, + { title: '建筑与公共空间', copy: '鼓楼和风雨桥适合串联村寨故事和日常生活。', image: img.destinationXijiang }, + { title: '夜晚更有节奏', copy: '如果档期合适,夜晚停留比匆忙白天路过更完整。', image: img.destinationZhenyuan }, + { title: '餐食礼仪', copy: '村寨餐食需要提前确认口味、禁忌和交流方式。', image: img.menuFood }, + ], + }, + huangguoshu: { + gallery: [img.destinationHuangguoshu, img.heroHuangguoshu, img.hotelBolian, img.routeClassic], + facts: [ + { label: '位置', value: '安顺 / 黄果树 5A 景区' }, + { label: '建议时长', value: '半日到 1 日' }, + { label: '重点', value: '瀑布水量、人流错峰、光线方向' }, + { label: '搭配', value: '安顺小吃 / 屯堡 / 黄果树住宿' }, + { label: '适合场景', value: '经典必玩 / 瀑布水系短线' }, + { label: '注意', value: '暑期与节假日需要更严格错峰' }, + ], + locationTags: ['黄果树', '安顺', '5A 瀑布'], + highlights: [ + { title: '错峰进入', copy: '按客流和光线安排进入顺序,减少排队和逆光拍摄。', image: img.destinationHuangguoshu }, + { title: '瀑布日不要赶', copy: '瀑布群、步行和换乘会消耗体力,后面适合安排恢复餐或酒店。', image: img.heroHuangguoshu }, + { title: '住宿恢复', copy: '高品质路线可把黄果树区域住宿作为恢复夜。', image: img.hotelBolian }, + { title: '短线效率', copy: '与荔波/贵阳组合时要控制车程,不要一天塞太多自然点。', image: img.routeClassic }, + ], + }, + 'libo-xiaoqikong': { + title: '荔波樟江与小七孔喀斯特水系', + gallery: [img.destinationLibo, img.experienceRafting, img.menuLandscape, img.routeClassic], + facts: [ + { label: '位置', value: '黔南 / 荔波' }, + { label: '世界遗产语境', value: '中国南方喀斯特荔波片区' }, + { label: '建议时长', value: '1 天,慢线可 1-2 晚' }, + { label: '重点', value: '水系、森林步道、桥、瀑布、亲水' }, + { label: '适合场景', value: '经典自然 / 亲子 / 摄影' }, + { label: '注意', value: '雨季与暑期需关注水位和人流' }, + ], + locationTags: ['荔波', '小七孔', '喀斯特水系'], + highlights: [ + { title: '水森林节奏', copy: '用轻步道、桥、水瀑和摄影时间构成一整天,而不是只赶几个点。', image: img.destinationLibo }, + { title: '亲水备选', copy: '水位合适时加入轻亲水,条件不好时改森林步道和摄影。', image: img.experienceRafting }, + { title: '不做当天往返硬线', copy: '荔波自然体验需要留足车程和休息,不建议被赶路压缩。', image: img.menuLandscape }, + { title: '和黄果树区分', copy: '黄果树是瀑布主视觉,荔波更适合水系和森林步行。', image: img.routeClassic }, + ], + }, + 'fanjing-mountain': { + gallery: [img.destinationFanjing, img.heroFanjing, img.destinationZhenyuan, img.menuLandscape], + facts: [ + { label: '位置', value: '铜仁 / 梵净山' }, + { label: '身份', value: '世界自然遗产山地生态目的地' }, + { label: '建议时长', value: '1 天,建议前后有缓冲' }, + { label: '关键变量', value: '天气、云雾、票务、索道、体力' }, + { label: '搭配', value: '镇远古城 / 黔东南村寨' }, + { label: '注意', value: '不建议压在返程当天' }, + ], + locationTags: ['梵净山', '铜仁', '世界自然遗产'], + highlights: [ + { title: '天气窗口', copy: '云雾、降雨和索道情况会决定是否进山或调整顺序。', image: img.destinationFanjing }, + { title: '山地生态', copy: '把森林、生物多样性和山地景观一起讲清楚,而不只追求登顶。', image: img.heroFanjing }, + { title: '镇远恢复夜', copy: '山地日后用镇远古城或周边慢住承接体力恢复。', image: img.destinationZhenyuan }, + { title: '备选方案', copy: '连续雨雾时切换到古镇、人文或低海拔自然节点。', image: img.menuLandscape }, + ], + }, + 'xijiang-miao-village': { + gallery: [img.destinationXijiang, img.heroXijiang, img.experienceCraft, img.menuCulture], + facts: [ + { label: '位置', value: '黔东南 / 西江千户苗寨' }, + { label: '建议时长', value: '半日到 1 晚' }, + { label: '重点', value: '木楼、梯田、夜景、手作、餐食' }, + { label: '适合场景', value: '经典人文 / 亲子 / 摄影' }, + { label: '注意', value: '节假日人流明显,住宿位置要提前选' }, + { label: '餐食', value: '酸汤、长桌宴需提前确认口味' }, + ], + locationTags: ['西江', '苗寨', '夜景慢住'], + highlights: [ + { title: '不要只拍夜景', copy: '白天手作、餐食和村寨步行能让苗寨体验更完整。', image: img.destinationXijiang }, + { title: '住宿位置', copy: '梯田/观景位置和行李搬运会影响入住体验。', image: img.heroXijiang }, + { title: '手作联动', copy: '苗绣、蜡染或银饰体验适合放在西江或周边村寨段。', image: img.experienceCraft }, + { title: '口味说明', copy: '酸汤、辣度和村寨餐礼仪要提前说明。', image: img.menuCulture }, + ], + }, + wanfenglin: { + gallery: [img.destinationWanfenglin, img.heroHome, img.experienceCoffee, img.menuLandscape], + facts: [ + { label: '位置', value: '兴义 / 黔西南' }, + { label: '身份', value: '4A 锥状喀斯特峰林景区' }, + { label: '建议时长', value: '半日到 1 晚' }, + { label: '重点', value: '峰林、田野、骑行、布依村寨、摄影' }, + { label: '适合场景', value: '小众自然 / 轻户外 / 摄影' }, + { label: '咖啡茶歇', value: '按营业状态和顺路程度复核' }, + ], + locationTags: ['兴义', '万峰林', '峰林田野'], + highlights: [ + { title: '峰林和田野一起看', copy: '万峰林不只是观景台,骑行/步行和村寨餐食能把体验做慢。', image: img.destinationWanfenglin }, + { title: '摄影窗口', copy: '清晨、傍晚和季节田野变化比中午打卡更有价值。', image: img.heroHome }, + { title: '山野茶歇', copy: '咖啡或茶歇点出发前复核营业,不把未确认网红点写死。', image: img.experienceCoffee }, + { title: '兴义停留', copy: '可和马岭河、布依风味餐或星空住宿组合。', image: img.menuLandscape }, + ], + }, + 'wild-coffee': { + title: '山野咖啡与村寨茶歇停靠', + gallery: [img.experienceCoffee, img.destinationWanfenglin, img.routeOutdoor, img.footerCta], + facts: [ + { label: '类型', value: '生活方式 / 转场恢复' }, + { label: '建议时长', value: '1-2 小时' }, + { label: '适合场景', value: '户外后恢复 / 摄影 / 自驾感' }, + { label: '关键变量', value: '营业时间、路况、是否顺路' }, + { label: '替代', value: '观景茶歇、村寨停靠、城市咖啡' }, + { label: '注意', value: '不把未复核店名写成固定承诺' }, + ], + locationTags: ['山野咖啡', '茶歇', '顺路复核'], + highlights: [ + { title: '恢复段落', copy: '适合放在户外活动后或长车程中段,让线路喘口气。', image: img.experienceCoffee }, + { title: '和风景绑定', copy: '选择看得到山、田野或村寨生活的停靠点,而不是单纯打卡店名。', image: img.destinationWanfenglin }, + { title: '营业复核', copy: '出发周确认营业、停车和是否适合团队进入。', image: img.routeOutdoor }, + { title: '替代方案', copy: '若店铺临时变化,可切换为茶歇、观景台或城市咖啡。', image: img.footerCta }, + ], + }, + 'hot-spring-recovery': { + gallery: [img.hotelBolian, img.destinationHuangguoshu, img.menuHotels, img.faqOutdoor], + facts: [ + { label: '类型', value: '恢复 / 康养 / 住宿联动' }, + { label: '建议时长', value: '半日到 1 晚' }, + { label: '适合场景', value: '黄果树 / 户外后恢复 / 亲子长辈' }, + { label: '关键变量', value: '酒店房态、泡池开放、身体状态' }, + { label: '注意', value: '高强度活动后更适合,不和赶路日硬拼' }, + { label: '替代', value: '舒适酒店、早休息、轻餐' }, + ], + locationTags: ['黄果树柏联', '剑河温泉', '恢复日'], + highlights: [ + { title: '活动后恢复', copy: '放在探洞、山地或长车程之后,帮助客人恢复体力。', image: img.hotelBolian }, + { title: '瀑布线衔接', copy: '黄果树区域可把温泉住宿和瀑布日结合。', image: img.destinationHuangguoshu }, + { title: '房态与开放', copy: '泡池和房型需按日期确认,不能只按图片承诺。', image: img.menuHotels }, + { title: '身体提醒', copy: '儿童、长辈和特殊身体状况要提前确认是否适合。', image: img.faqOutdoor }, + ], + }, +}; + +const hotelDetailOverrides: Record = { + 'sheraton-guiyang-hotel': { + gallery: [img.routeClassic, img.heroHome, img.menuCulture, img.menuHotels], + heroMeta: '贵阳 / 大南门 · 城市抵离酒店', + overviewTitle: '贵阳首晚或末晚的稳定城市酒店', + overview: [ + '贵阳喜来登贵航酒店适合放在抵达首晚、返程前一晚、会议或市中心短停场景。', + '它的价值不是“度假感”,而是城市交通、甲秀楼/南明河步行、早餐和稳定服务,帮助客人从长途抵达切换到贵州节奏。', + ], + facts: [ + { label: '区域', value: '贵阳 / 大南门 / 市中心' }, + { label: '适合晚数', value: '1 晚抵离或会议节点' }, + { label: '适合场景', value: '接机接站、城市轻游、返程缓冲' }, + { label: '适合客人', value: '商务、家庭、初到贵州小团' }, + { label: '附近搭配', value: '甲秀楼、青云市集、贵阳小吃' }, + { label: '报价', value: '按房型、日期和是否含早确认' }, + ], + locationTags: ['市中心', '首晚/末晚', '甲秀楼'], + locationCopy: '适合把贵阳作为行程起落点;如果第二天去黄果树或黔南,车辆可从市区直接出发。', + highlights: [ + { title: '抵达首晚稳定', copy: '长途抵达后先保证睡眠、早餐和接送动线。', image: img.routeClassic }, + { title: '城市步行节点', copy: '可搭配甲秀楼、南明河或城市小吃,不需要额外长转场。', image: img.heroHome }, + { title: '会议/商务友好', copy: '适合作为企业团、同业踩线或商务客人的贵阳基地。', image: img.menuCulture }, + { title: '非度假型定位', copy: '这里更适合作为城市功能酒店,而不是山地景观住宿。', image: img.menuHotels }, + ], + planningNotes: ['抵达晚班机建议优先城市酒店,不要当晚再进山。', '贵阳首晚要确认早餐、停车和第二天出发时间。', '如客人想要景观或度假感,应放到黄果树、西江、兴义等节点。'], + }, + 'pearl-gallery-hotel-guiyang': { + gallery: [img.menuHotels, img.heroHome, img.quoteGuests, img.routeClassic], + heroMeta: '贵阳 / 观山湖 · 城市精品酒店', + overviewTitle: '更安静的新区城市基地', + overview: [ + '贵阳珍珀酒店适合偏好观山湖新区、城市精品感和较安静住宿环境的客人。', + '它更适合做贵阳城市停留、商务会面或长线前后的舒适过渡,而不是景区型住宿。', + ], + facts: [ + { label: '区域', value: '贵阳 / 观山湖' }, + { label: '适合晚数', value: '1-2 晚城市停留' }, + { label: '适合场景', value: '新区住宿、商务会面、返程缓冲' }, + { label: '适合客人', value: '偏好安静城市环境和房间质感的小团' }, + { label: '交通提醒', value: '去老城小吃需额外安排车程' }, + { label: '报价', value: '按房型与日期确认' }, + ], + locationTags: ['观山湖', '城市精品', '安静基地'], + locationCopy: '适合在贵阳新区停留,和会展、商务、返程缓冲结合;若要老城烟火气,需要安排车辆进入市区。', + highlights: [ + { title: '新区安静度', copy: '比老城中心更适合想休息或商务会面的客人。', image: img.menuHotels }, + { title: '城市过渡夜', copy: '适合长线前后做洗衣、休息和行程整理。', image: img.heroHome }, + { title: '服务沟通', copy: '境外客人需要提前确认英文沟通和早餐偏好。', image: img.quoteGuests }, + { title: '餐食另排', copy: '若想吃贵阳小吃,需要把老城餐食作为单独节点安排。', image: img.routeClassic }, + ], + }, + 'hampton-by-hilton-guiyang-guanshan-lake': { + gallery: [img.menuHotels, img.menuTransport, img.heroHome, img.footerCta], + heroMeta: '贵阳 / 观山湖 · 实用型舒适酒店', + overviewTitle: '简单可靠的过渡夜酒店', + overview: [ + '贵阳观山湖希尔顿欢朋酒店适合过渡夜、商务抵达和预算更可控的中高端舒适住宿。', + '它不是风景型酒店,但适合需要稳定品牌、早餐、车程和基础服务的小团。', + ], + facts: [ + { label: '区域', value: '贵阳 / 观山湖' }, + { label: '适合晚数', value: '1 晚过渡或商务节点' }, + { label: '适合场景', value: '预算可控、品牌稳定、车辆衔接' }, + { label: '适合客人', value: '家庭、商务、同业踩线' }, + { label: '交通', value: '适合和贵阳新区/高铁/车队调度结合' }, + { label: '报价', value: '按日期和房型确认' }, + ], + locationTags: ['观山湖', '过渡夜', '品牌稳定'], + locationCopy: '适合作为贵阳城市过渡节点,重点是车辆衔接和稳定舒适,而不是目的地体验本身。', + highlights: [ + { title: '品牌稳定', copy: '适合对连锁标准、早餐和基础服务有预期的客人。', image: img.menuHotels }, + { title: '车辆衔接', copy: '贵阳新区出发可与第二天长线车辆调度配合。', image: img.menuTransport }, + { title: '预算友好', copy: '比景区度假型住宿更适合作为成本控制节点。', image: img.heroHome }, + { title: '不承担景观任务', copy: '景观和慢住体验应安排在黄果树、西江或万峰林等节点。', image: img.footerCta }, + ], + }, + 'huangguoshu-bolian-resort': { + gallery: [img.hotelBolian, img.destinationHuangguoshu, img.heroHuangguoshu, img.menuHotels], + heroMeta: '黄果树 / 关岭 · 瀑布恢复夜', + overviewTitle: '黄果树瀑布后的恢复住宿', + facts: [ + { label: '区域', value: '黄果树 / 关岭' }, + { label: '适合晚数', value: '1 晚恢复或慢线 2 晚' }, + { label: '适合场景', value: '瀑布错峰、温泉恢复、减少回头路' }, + { label: '适合客人', value: '高品质住宿、亲子长辈、慢旅行小团' }, + { label: '搭配', value: '黄果树瀑布群、安顺餐食' }, + { label: '报价', value: '按房态、房型和日期确认' }, + ], + locationTags: ['黄果树', '温泉恢复', '瀑布慢住'], + highlights: [ + { title: '瀑布错峰', copy: '住在区域内更容易把黄果树进入时间做得从容。', image: img.destinationHuangguoshu }, + { title: '活动后恢复', copy: '瀑布步行或户外后用温泉/舒适住宿承接体力。', image: img.hotelBolian }, + { title: '高品质短线', copy: '适合把自然短线做成慢节奏,而不是贵阳当天往返。', image: img.heroHuangguoshu }, + { title: '房态先行', copy: '节假日和旺季房态变化快,需先确认再承诺。', image: img.menuHotels }, + ], + }, + 'puyu-wild-luxury': { + gallery: [img.footerCta, img.destinationLibo, img.menuLandscape, img.heroFanjing], + heroMeta: '独山 / 黔南 · 山地野奢停留', + overviewTitle: '黔南自然体验旁的安静山地停留', + facts: [ + { label: '区域', value: '独山 / 黔南' }, + { label: '适合晚数', value: '1-2 晚' }, + { label: '适合场景', value: '黔南自然体验、荔波前后、慢节奏休整' }, + { label: '适合客人', value: '想要安静山地氛围和小众住宿的人' }, + { label: '搭配', value: '荔波、黔南村寨、轻户外' }, + { label: '报价', value: '按房态和淡旺季确认' }, + ], + locationTags: ['独山', '黔南', '山地慢住'], + highlights: [ + { title: '安静山地氛围', copy: '更适合想把住宿本身作为休整体验的客人。', image: img.footerCta }, + { title: '黔南自然线', copy: '可和荔波、黔南河谷或轻户外形成顺路组合。', image: img.destinationLibo }, + { title: '不适合赶路团', copy: '如果每天换点,野奢住宿的价值会被车程吃掉。', image: img.menuLandscape }, + { title: '供应商复核', copy: '房态、餐食和道路条件需按日期单独确认。', image: img.heroFanjing }, + ], + }, + 'xijiang-sanchunli-meng': { + gallery: [img.destinationXijiang, img.heroXijiang, img.experienceCraft, img.menuCulture], + heroMeta: '西江千户苗寨 / 梯田 · 村寨慢住', + overviewTitle: '西江夜景之外的村寨慢住', + facts: [ + { label: '区域', value: '西江千户苗寨 / 梯田' }, + { label: '适合晚数', value: '1 晚' }, + { label: '适合场景', value: '苗寨夜景、清晨摄影、手作体验' }, + { label: '适合客人', value: '摄影、人文、亲子慢游' }, + { label: '注意', value: '行李搬运和观景位置需提前确认' }, + { label: '报价', value: '按房型和景区档期确认' }, + ], + locationTags: ['西江', '梯田视野', '苗寨慢住'], + highlights: [ + { title: '夜晚和清晨', copy: '住一晚才能把夜景、清晨和村寨生活串起来。', image: img.destinationXijiang }, + { title: '观景位置', copy: '房间视野、台阶和行李搬运会直接影响体验。', image: img.heroXijiang }, + { title: '手作联动', copy: '可和苗绣、蜡染或村寨讲解放在同一天。', image: img.experienceCraft }, + { title: '节假日人流', copy: '旺季要提前确认车行/步行接驳和入住时间。', image: img.menuCulture }, + ], + }, + 'xingyi-yuntun-starry-sky': { + gallery: [img.destinationWanfenglin, img.heroHome, img.experienceCoffee, img.menuLandscape], + heroMeta: '兴义 / 万峰林 · 峰林星空慢住', + overviewTitle: '万峰林轻户外与星空慢住', + facts: [ + { label: '区域', value: '兴义 / 万峰林' }, + { label: '适合晚数', value: '1-2 晚' }, + { label: '适合场景', value: '峰林摄影、轻徒步、星空住宿' }, + { label: '适合客人', value: '摄影、小众自然、轻户外客人' }, + { label: '搭配', value: '万峰林、马岭河、山野茶歇' }, + { label: '报价', value: '按房态、天气和房型确认' }, + ], + locationTags: ['兴义', '万峰林', '星空慢住'], + highlights: [ + { title: '峰林视野', copy: '住宿价值来自和万峰林田野、清晨/傍晚摄影窗口的衔接。', image: img.destinationWanfenglin }, + { title: '慢节奏', copy: '适合在兴义多留一晚,不适合只当过路酒店。', image: img.heroHome }, + { title: '山野茶歇', copy: '可把咖啡或茶歇放在峰林轻户外之后。', image: img.experienceCoffee }, + { title: '天气变量', copy: '星空和远景视野受天气影响,不能写成固定承诺。', image: img.menuLandscape }, + ], + }, + 'zunyi-wujiangzhai-miaowang': { + gallery: [img.heroXijiang, img.menuCulture, img.destinationZhenyuan, img.routeMixed], + heroMeta: '遵义 / 乌江寨 · 文化度假区住宿', + overviewTitle: '北线文化度假段的轻松住宿', + facts: [ + { label: '区域', value: '遵义 / 乌江寨' }, + { label: '适合晚数', value: '1 晚' }, + { label: '适合场景', value: '度假区演艺、夜景、文化建筑体验' }, + { label: '适合客人', value: '家庭、长辈、希望动线清晰的小团' }, + { label: '搭配', value: '遵义人文、北线中转、乌江寨夜游' }, + { label: '报价', value: '按景区房态和日期确认' }, + ], + locationTags: ['乌江寨', '遵义', '文化度假'], + highlights: [ + { title: '度假区动线', copy: '适合想要吃住游集中、晚上不用再长距离转场的客人。', image: img.heroXijiang }, + { title: '文化建筑', copy: '建筑、演艺和夜景更适合作为轻松文化段,而非深度村寨替代。', image: img.menuCulture }, + { title: '北线中转', copy: '可和遵义、铜仁或黔东南衔接,但需控制车程。', image: img.destinationZhenyuan }, + { title: '家庭友好', copy: '比野外村寨更容易控制餐食、住宿和步行强度。', image: img.routeMixed }, + ], + }, +}; + +const foodDetailOverrides: Record = { + guiyang: { + gallery: [img.routeClassic, img.menuFood, img.heroHome, img.footerCta], + heroMeta: '贵阳 · 小吃 / 夜市 / 抵离餐', + facts: [ + { label: '代表味道', value: '肠旺面 / 丝娃娃 / 恋爱豆腐果 / 怪噜饭' }, + { label: '建议时段', value: '抵达首晚或返程前' }, + { label: '适合场景', value: '贵阳抵离、城市小吃、夜市体验' }, + { label: '口味提醒', value: '酸辣、蘸水、辣椒可调' }, + { label: '用餐位置', value: '老城小吃需单独安排车程或步行' }, + { label: '客群', value: '第一次来贵州、想快速理解本地味道的人' }, + ], + locationTags: ['贵阳', '小吃首站', '抵离餐'], + highlights: [ + { title: '肠旺面', copy: '适合作为贵阳第一碗热食,辣度和内脏接受度先沟通。', image: img.routeClassic }, + { title: '丝娃娃', copy: '更适合多人分享,能用蘸水解释贵州酸辣味。', image: img.menuFood }, + { title: '恋爱豆腐果', copy: '适合夜市或小吃线,不建议替代正式晚餐。', image: img.heroHome }, + { title: '返程前收尾', copy: '离开贵州前用小吃线补齐城市烟火气。', image: img.footerCta }, + ], + }, + zunyi: { + gallery: [img.menuCulture, img.routeClassic, img.menuFood, img.destinationZhenyuan], + facts: [ + { label: '代表味道', value: '遵义羊肉粉 / 豆花面 / 辣椒风味 / 鸡蛋糕' }, + { label: '建议时段', value: '北线转场早餐或午餐' }, + { label: '适合场景', value: '遵义人文 / 北线中转 / 乌江寨' }, + { label: '口味提醒', value: '粉面和辣椒风味较突出' }, + { label: '用餐位置', value: '适合和遵义会议、乌江寨或北线交通组合' }, + { label: '客群', value: '喜欢粉面、红色文化和地方小吃的人' }, + ], + locationTags: ['遵义', '羊肉粉', '北线餐食'], + }, + liupanshui: { + gallery: [img.routeOutdoor, img.experienceCoffee, img.menuFood, img.footerCta], + facts: [ + { label: '代表味道', value: '水城烙锅 / 水城羊肉粉 / 盘州火腿 / 凉都烧烤' }, + { label: '建议时段', value: '夏季避暑线晚餐或户外后热食' }, + { label: '适合场景', value: '西线 / 户外 / 避暑' }, + { label: '口味提醒', value: '烙锅油香和蘸料较重' }, + { label: '用餐位置', value: '适合西线顺路,不建议从经典东南线硬绕' }, + { label: '客群', value: '喜欢山城夜味和热食补给的人' }, + ], + locationTags: ['六盘水', '凉都', '烙锅'], + }, + anshun: { + gallery: [img.destinationHuangguoshu, img.heroHuangguoshu, img.menuFood, img.hotelBolian], + facts: [ + { label: '代表味道', value: '安顺裹卷 / 夺夺粉 / 破酥包 / 屯堡家常菜' }, + { label: '建议时段', value: '黄果树日午餐或恢复餐' }, + { label: '适合场景', value: '黄果树 / 屯堡 / 安顺中转' }, + { label: '口味提醒', value: '小吃可组合,瀑布日避免太重口' }, + { label: '用餐位置', value: '和瀑布进出时间绑定' }, + { label: '客群', value: '想在瀑布线加入地方小吃的人' }, + ], + locationTags: ['安顺', '黄果树餐食', '屯堡'], + }, + bijie: { + gallery: [img.heroFanjing, img.menuLandscape, img.routeOutdoor, img.menuFood], + facts: [ + { label: '代表味道', value: '织金荞凉粉 / 威宁火腿 / 豆干火锅 / 乌蒙土豆' }, + { label: '建议时段', value: '织金洞、百里杜鹃或乌蒙线热食' }, + { label: '适合场景', value: '织金洞 / 百里杜鹃 / 高原自然' }, + { label: '口味提醒', value: '高原杂粮和火腿风味更朴实' }, + { label: '用餐位置', value: '适合西北线,不建议与黔东南同日硬连' }, + { label: '客群', value: '喜欢自然季节线和朴素热食的人' }, + ], + locationTags: ['毕节', '高原风味', '织金洞'], + }, + tongren: { + gallery: [img.destinationFanjing, img.heroFanjing, img.destinationZhenyuan, img.menuFood], + facts: [ + { label: '代表味道', value: '社饭 / 米豆腐 / 锅巴粉 / 江口豆干' }, + { label: '建议时段', value: '梵净山前后轻餐' }, + { label: '适合场景', value: '梵净山 / 镇远 / 铜仁中转' }, + { label: '口味提醒', value: '进山前不宜安排过重餐食' }, + { label: '用餐位置', value: '和梵净山早出发、索道时间绑定' }, + { label: '客群', value: '需要山地日前后温和餐食的人' }, + ], + locationTags: ['铜仁', '梵净山轻餐', '米食'], + }, + qiannan: { + gallery: [img.destinationLibo, img.experienceRafting, img.menuFood, img.menuLandscape], + facts: [ + { label: '代表味道', value: '虾酸牛肉 / 荔波酸肉 / 都匀冲冲糕 / 水族酸汤' }, + { label: '建议时段', value: '荔波水系日午晚餐' }, + { label: '适合场景', value: '荔波 / 黔南 / 亲水' }, + { label: '口味提醒', value: '酸香明显,需确认接受度' }, + { label: '用餐位置', value: '和小七孔、水森林、漂流节点组合' }, + { label: '客群', value: '想尝试酸汤与民族风味的人' }, + ], + locationTags: ['黔南', '荔波', '酸香'], + }, + qiandongnan: { + gallery: [img.destinationXijiang, img.menuCulture, img.experienceCraft, img.menuFood], + facts: [ + { label: '代表味道', value: '凯里酸汤鱼 / 牛瘪火锅 / 侗家腌鱼 / 苗家长桌宴' }, + { label: '建议时段', value: '村寨慢住晚餐' }, + { label: '适合场景', value: '黔东南人文 / 西江 / 肇兴' }, + { label: '口味提醒', value: '酸汤、牛瘪、腌制风味需提前说明' }, + { label: '用餐位置', value: '和村寨主理人、住宿、手作体验组合' }, + { label: '客群', value: '愿意把餐食当成人文体验的人' }, + ], + locationTags: ['黔东南', '酸汤鱼', '长桌宴'], + }, + qianxinan: { + gallery: [img.destinationWanfenglin, img.experienceCoffee, img.menuFood, img.heroHome], + facts: [ + { label: '代表味道', value: '兴义羊肉粉 / 刷把头 / 鸡肉汤圆 / 布依风味' }, + { label: '建议时段', value: '万峰林轻户外后或兴义停留餐' }, + { label: '适合场景', value: '万峰林 / 马岭河 / 轻户外摄影' }, + { label: '口味提醒', value: '粉面和点心适合轻松切入' }, + { label: '用餐位置', value: '和峰林骑行、摄影、山野茶歇组合' }, + { label: '客群', value: '喜欢峰林田野和生活化餐食的人' }, + ], + locationTags: ['黔西南', '兴义', '布依风味'], + }, +}; + +function makeExperiencePoiDetail(card: ArchiveCardItem): PoiDetailContent { + const slug = archiveCardSlug(card); + const profile = experienceProfile(card); + const title = experienceTitleZh[slug] || card.title; + const related = poiRelatedFromArchive(experienceArchiveCards, card.title, (item) => item.href || `/experiences/${archiveCardSlug(item)}/`); + const gallery = uniqueImages(card.image, img.menuOutdoor, img.experienceCave, img.experienceRafting, img.experienceCraft, img.destinationLibo).slice(0, 5); + + const detail: PoiDetailContent = { + kind: 'experience', + title, + subtitle: card.copy, + heroMeta: `${card.type || '体验'} · ${card.meta}`, + image: card.image, + gallery, + actionLabel: '咨询这个体验', + overviewEyebrow: '贵州体验', + overviewTitle: '先看清这项体验的现场感与边界', + overview: [ + card.copy, + '页面保留体验本身的主题、强度、预约条件和现场变量;是否执行、如何降级或替换,则按天气、体力和语言需求再确认。', + ], + facts: [ + { label: '类型', value: card.type || '体验' }, + { label: '位置', value: card.meta }, + { label: '建议时长', value: profile.duration }, + { label: '活动强度', value: profile.intensity }, + { label: '预约方式', value: profile.booking }, + { label: '适合场景', value: card.type === 'Culture' ? '人文手作 / 亲子慢游' : card.type === 'Landscape' ? '山水摄影 / 初访贵州' : '小众体验 / 户外野咖' }, + ], + locationTitle: '位置与动线', + locationCopy: `${card.meta} 不是单独点位,而是需要和车程、酒店、餐食和天气窗口一起判断的路线节点。`, + locationTags: [card.meta, card.type || '体验', '可按偏好组合'], + bestForTitle: '适合这些客人', + bestFor: profile.bestFor, + highlights: [ + { title: '现场条件先行', copy: profile.booking, image: card.image }, + { title: '节奏要留余地', copy: '把体验放在合适的时段里,留出补给、休息和临时调整空间。', image: gallery[1] || card.image }, + { title: '语言与安全说明', copy: '境外客人可提前准备英文说明、注意事项和现场沟通安排。', image: gallery[2] || card.image }, + { title: '备选方案', copy: '天气、人流或身体状态不合适时,提前准备轻量替代点位。', image: gallery[3] || card.image }, + ], + planningTitle: '到访建议', + planningNotes: profile.planningNotes, + trustTitle: '执行保障', + trustPoints: profile.trustPoints, + relatedTitle: '可继续搭配的体验', + relatedItems: related, + }; + + return withPoiOverride(detail, slug, experienceDetailOverrides); +} + +function makeHotelPoiDetail(hotel: ArchiveCardItem): PoiDetailContent { + const related = poiRelatedFromArchive(collectiveArchiveCards, hotel.title, hotelHref); + const gallery = uniqueImages(hotel.image, img.menuHotels, img.hotelBolian, img.destinationXijiang, img.destinationWanfenglin).slice(0, 5); + const isWild = /wild|野奢|星空|梯田|独山|兴义|乌江寨|terraces|Dushan|Xingyi|Wujiangzhai/i.test(`${hotel.title} ${hotel.meta}`); + const slug = archiveCardSlug(hotel); + + const detail: PoiDetailContent = { + kind: 'hotel', + title: hotel.title, + subtitle: hotel.copy, + heroMeta: `${hotel.meta} · ${isWild ? '野奢住宿' : '舒适住宿'}`, + image: hotel.image, + gallery, + actionLabel: '咨询这家酒店', + overviewEyebrow: '贵州酒店', + overviewTitle: '住宿质感、位置和适合场景', + overview: [ + hotel.copy, + '这里更关注房型、早餐、安静度、服务沟通、预算和周边便利,而不是只按星级或品牌排序。', + ], + facts: [ + { label: '所在区域', value: hotel.meta }, + { label: '住宿场景', value: isWild ? '野奢 / 景观 / 慢停留' : '城市 / 中转 / 舒适基础' }, + { label: '建议晚数', value: isWild ? '1-2 晚慢停留' : '1 晚或抵离节点' }, + { label: '适合场景', value: isWild ? '小众体验 / 山水摄影 / 慢旅行' : '城市抵离 / 商务短停 / 家庭小团' }, + { label: '价格', value: '按房型、档期和人数询价' }, + { label: '服务', value: '酒店比选、房型沟通、入住衔接' }, + ], + locationTitle: '位置与衔接', + locationCopy: `${hotel.meta} 的住宿价值要看它能否减少回头路、提高休息质量,并顺路连接景区、餐食和第二天出发时间。`, + locationTags: [hotel.meta, isWild ? '景观停留' : '交通节点', '可配英文司导'], + bestForTitle: '适合这些入住需求', + bestFor: isWild + ? ['希望住宿本身成为体验的一部分', '想在山地、村寨或景观段放慢节奏', '重视照片、夜晚氛围和私密感的小团'] + : ['重视舒适、卫生和交通效率的客人', '抵达首晚或返程前需要稳定酒店的人', '家庭、商务或境外小团的基础住宿需求'], + highlights: [ + { title: '位置价值', copy: '先看区域是否适合当晚需求,再判断房型、价格和服务细节。', image: hotel.image }, + { title: '休息质量', copy: '连续山路、户外和村寨行程后,酒店节奏比单纯星级更重要。', image: gallery[1] || hotel.image }, + { title: '服务沟通', copy: '境外客人可提前确认英文沟通、早餐、房型和行李安排。', image: gallery[2] || hotel.image }, + { title: '替代选择', copy: '同区域会保留可替代酒店,避免房态变化影响入住体验。', image: gallery[3] || hotel.image }, + ], + planningTitle: '预订与入住提醒', + planningNotes: [ + '最终报价受房型、淡旺季、取消政策、是否含早和团队人数影响。', + '酒店会和车辆调度、景区进入时间、餐食节点一起确认。', + '如果客人对床型、安静度、景观或亲子设施有要求,应在方案阶段提前说明。', + ], + trustTitle: '比选维度', + trustPoints: ['位置', '清洁与舒适', '早餐与设施', '服务沟通', '价格与取消政策'], + relatedTitle: '同区域或同风格酒店', + relatedItems: related, + }; + + return withPoiOverride(detail, slug, hotelDetailOverrides); +} + +function makeFoodPoiDetail(food: FoodCardItem): PoiDetailContent { + const related = foodCards + .filter((card) => card.id !== food.id) + .slice(0, 3) + .map((card) => ({ + title: card.city, + kicker: card.region, + copy: card.copy, + image: card.image, + href: `/food/${card.id}/`, + })); + const gallery = uniqueImages(food.image, img.menuFood, img.routeClassic, img.destinationXijiang, img.experienceCoffee).slice(0, 5); + + const detail: PoiDetailContent = { + kind: 'food', + title: `${food.city}美食`, + subtitle: food.copy, + heroMeta: `${food.region} · 美食节点`, + image: food.image, + gallery, + actionLabel: '把美食加入行程', + overviewEyebrow: '贵州美食', + overviewTitle: '地方风味、代表菜和口味边界', + overview: [ + food.copy, + '贵州餐食需要提前确认辣度、酸汤接受度、牛羊肉/内脏/鱼类偏好、儿童餐、素食、清真或过敏信息,再决定餐厅、菜单和停留时间。', + ], + facts: [ + { label: '地市州', value: food.region }, + { label: '代表味道', value: food.dishes.slice(0, 2).join(' / ') }, + { label: '建议时段', value: food.id === 'guiyang' ? '抵达首晚 / 返程前' : '顺路午餐 / 晚餐节点' }, + { label: '口味沟通', value: '辣度、酸汤、禁忌提前确认' }, + { label: '适合场景', value: food.id === 'qiandongnan' ? '村寨餐 / 长桌宴' : food.id === 'anshun' ? '黄果树 / 屯堡餐食' : '城市小吃 / 顺路正餐' }, + ], + locationTitle: '餐食位置与现场节奏', + locationCopy: `${food.region} 的餐食适合和当天车程、景区游玩、酒店位置和客人口味一起确认,避免为了网红餐厅反向绕路。`, + locationTags: [food.region, '餐食节点', '口味可提前沟通'], + bestForTitle: '适合这些用餐安排', + bestFor: ['想理解贵州味道但不想踩雷的客人', '需要照顾儿童、长辈或境外口味的小团', '希望把地方小吃、正餐和村寨餐食放进路线的人'], + highlights: food.dishes.slice(0, 4).map((dish, index) => ({ + title: dish, + copy: index === 0 ? '可作为这一区域的代表味道先尝试。' : '根据客人口味、辣度和当天停留时间判断是否加入菜单。', + image: gallery[index] || food.image, + })), + planningTitle: '用餐提醒', + planningNotes: [ + '提前确认辣度、酸汤、鱼类、牛羊肉、内脏、素食、清真和过敏信息。', + '户外或长车程日优先安排稳定、卫生、出餐效率高的餐食点。', + '村寨宴和特色餐需要说明礼仪、口味和可接受度,避免现场尴尬。', + ], + trustTitle: '餐食安排标准', + trustPoints: ['卫生与舒适度', '口味适配', '路线顺路程度', '出餐效率', '可替代餐厅'], + relatedTitle: '其他地市州美食', + relatedItems: related, + }; + + const override = foodDetailOverrides[food.id]; + const overridden = withPoiOverride(detail, food.id, foodDetailOverrides); + if (override?.gallery && !override.highlights) { + return { + ...overridden, + highlights: food.dishes.slice(0, 4).map((dish, index) => ({ + title: dish, + copy: index === 0 ? '可作为这一区域的代表味道先尝试。' : '根据客人口味、辣度和当天停留时间判断是否加入菜单。', + image: overridden.gallery[index] || food.image, + })), + }; + } + return overridden; +} + +function makeTransportPoiDetail(vehicle: TransportCardItem): PoiDetailContent { + const gallery = uniqueImages(vehicle.image, img.menuTransport, img.routeMixed, img.routeOutdoor, img.footerCta, img.heroHuangguoshu).slice(0, 5); + const related = transportCards + .filter((card) => card.id !== vehicle.id) + .slice(0, 3) + .map((card) => ({ + title: `${card.seats} ${card.english}`, + kicker: card.bestFor, + copy: card.copy, + image: card.image, + href: transportHref(card), + })); + + return { + kind: 'transport', + title: `${vehicle.seats} ${vehicle.english}`, + subtitle: vehicle.copy, + heroMeta: `${vehicle.english} · ${vehicle.bestFor}`, + image: vehicle.image, + gallery, + actionLabel: '咨询这个车型', + overviewEyebrow: '贵州交通', + overviewTitle: '车型、行李和山地转场要先匹配', + overview: [ + vehicle.copy, + '贵州交通服务不只是选一辆车,还要把人数、行李、山路、景区停车、司机经验和跨地市调度一起确认,避免把体验卡在转场上。', + ], + facts: [ + { label: '车型', value: `${vehicle.seats} ${vehicle.english}` }, + { label: '适合', value: vehicle.bestFor }, + { label: '行李', value: vehicle.luggage }, + { label: '调度方式', value: '接送、包车、多日跨地市用车均可按线路确认' }, + { label: '道路变量', value: '山路、景区停车、酒店车位和限行需提前复核' }, + { label: '报价', value: '按日期、线路、车型等级和司导语言确认' }, + ], + locationTitle: '调度与道路', + locationCopy: `${vehicle.seats} 车型适合在贵州全境按路线调度,具体会结合机场/高铁抵离、酒店位置、景区停车、山路限制和团队集合方式确认。`, + locationTags: [vehicle.seats, vehicle.english, '贵州全境调度'], + bestForTitle: '适合这些用车场景', + bestFor: [ + vehicle.bestFor, + '需要把司机、行李和景区停车提前确认的小团或团队', + '希望长距离转场更稳、更从容的贵州多日路线', + ], + highlights: [ + { title: '人数与行李余量', copy: `按 ${vehicle.bestFor} 和 ${vehicle.luggage} 先判断车内空间,避免满员后行李和乘坐舒适度互相挤压。`, image: vehicle.image }, + { title: '山路和景区停车', copy: '黄果树、荔波、黔东南村寨、梵净山等节点会提前确认道路、换乘、停车和上下车位置。', image: gallery[1] || vehicle.image }, + { title: '司机与司导衔接', copy: '境外客人、亲子家庭或团队接待可同步确认司机经验、英文司导、集合方式和每日出发时间。', image: gallery[2] || vehicle.image }, + { title: '备选车辆预案', copy: '大团队或旺季线路会提前评估分车、备用车、补给和临时调整方案,减少现场等待。', image: gallery[3] || vehicle.image }, + ], + planningTitle: '用车确认事项', + planningNotes: [ + '先确认人数、行李尺寸、儿童座椅、摄影器材和是否需要英文司导。', + '跨地市多日路线要把司机休息、酒店停车、景区换乘和道路限制一起规划。', + '旺季、大团队或山地节点需要保留分车和备选车辆预案。', + ], + trustTitle: '交通执行保障', + trustPoints: ['车型匹配', '司机沟通', '停车复核', '道路限制评估', '备用预案'], + relatedTitle: '其他车型选择', + relatedItems: related, + }; +} + +function TravelSite({ content, cmsContent }: { content?: SiteContent; cmsContent?: CmsContent }) { + const [menuOpen, setMenuOpen] = useState(false); + const [searchOpen, setSearchOpen] = useState(false); + const [language, setLanguage] = useState(() => { + const stored = window.localStorage.getItem(languageStorageKey); + if (stored === 'zh' || stored === 'en') return stored; + return cmsContent?.settings.defaultLanguage || 'zh'; + }); + const activeSiteContent = useMemo(() => normalizeSiteContent(content), [content]); + const path = normalizePath(window.location.pathname); + const page = useMemo(() => selectPage(path), [path]); + + useEffect(() => { + window.localStorage.setItem(languageStorageKey, language); + document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en'; + document.documentElement.dataset.ocLanguage = language; + + const root = document.querySelector('.oc-site'); + if (!root) return; + + const translate = () => applySiteLanguage(root, language); + translate(); + + const observer = new MutationObserver(() => translate()); + observer.observe(root, { + attributes: true, + attributeFilter: ['placeholder', 'aria-label', 'alt', 'title'], + characterData: true, + childList: true, + subtree: true, + }); + + return () => observer.disconnect(); + }, [language, path, menuOpen, searchOpen]); + + useEffect(() => { + if (!window.location.hash) return undefined; + let targetId = window.location.hash.slice(1); + try { + targetId = decodeURIComponent(targetId); + } catch { + targetId = window.location.hash.slice(1); + } + const scrollToTarget = () => { + document.getElementById(targetId)?.scrollIntoView({ block: 'start' }); + }; + let timeoutId: number | undefined; + const frame = window.requestAnimationFrame(() => { + scrollToTarget(); + timeoutId = window.setTimeout(scrollToTarget, 80); + }); + return () => { + window.cancelAnimationFrame(frame); + if (timeoutId) window.clearTimeout(timeoutId); + }; + }, [path]); + + return ( + +
+
setLanguage((current) => (current === 'zh' ? 'en' : 'zh'))} + onMenu={() => setMenuOpen(true)} + onSearch={() => setSearchOpen(true)} + /> + {page} + + {menuOpen && setMenuOpen(false)} />} + {searchOpen && setSearchOpen(false)} />} +
+
+ ); +} + +function normalizePath(pathname: string) { + const path = pathname.replace(/\/+$/, '') || '/'; + return path.toLowerCase(); +} + +function slugifyTitle(title: string) { + return title + .toLowerCase() + .replace(/&/g, 'and') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function hotelHref(card: ArchiveCardItem) { + return card.href || `/hotels/${slugifyTitle(card.title)}/`; +} + +const routeStayPlans: Record> = { + 'classic-guizhou': { + title: '城市舒适酒店 + 景区恢复夜 + 村寨慢停留', + summary: '这条线不适合每天都换住宿。贵阳做抵离基础,黄果树或安顺做瀑布恢复,黔东南保留一晚能看夜景、吃晚餐和放慢节奏。', + image: img.menuHotels, + tags: ['贵阳抵离', '瀑布恢复', '村寨夜景'], + items: ['贵阳首晚优先选交通方便、早餐稳定的城市酒店。', '黄果树段可搭配温泉或舒适度更高的恢复住宿。', '西江、肇兴或镇远不只看景,建议保留夜晚与清晨。'], + href: '/hotels/', + actionLabel: '查看住宿', + }, + 'outdoor-coffee': { + title: '户外活动前后用住宿做体力缓冲', + summary: '高活动量线路的住宿不追求频繁换风景,而是服务装备整理、早出发、活动后洗浴恢复和山地咖啡停靠。', + image: img.routeOutdoor, + tags: ['装备整理', '早出发', '恢复夜'], + items: ['贵阳集合日靠近补给和装备确认点。', '洞穴、峡谷或亲水活动后优先保证热水、洗衣和睡眠。', '山野咖啡可作为高强度后的轻恢复住宿节点。'], + href: '/hotels/', + actionLabel: '查看住宿', + }, + 'culture-outdoor-mix': { + title: '村寨住宿和自然节点交替,避免体验断层', + summary: '混搭线要让文化体验和轻户外互相支撑。村寨或古镇停留需要足够时间,酒店选择要兼顾翻译沟通、餐食和第二天转场。', + image: img.destinationXijiang, + tags: ['村寨慢住', '轻户外缓冲', '沟通便利'], + items: ['黔东南段优先保证一晚以上慢停留。', '轻户外前后搭配更易休整的住宿节点。', '境外客人入住点要考虑英文沟通和餐食稳定性。'], + href: '/hotels/', + actionLabel: '查看住宿', + }, + 'family-heritage': { + title: '少换酒店,把午休、儿童餐和卫生舒适度放在前面', + summary: '亲子慢游不是减少内容,而是减少折腾。住宿要靠近体验点,给孩子和长辈保留休息、洗漱、简单餐食和临时调整空间。', + image: img.menuHotels, + tags: ['少换酒店', '亲子友好', '午休空间'], + items: ['连续两晚同一区域比每天搬行李更适合家庭。', '手作、村寨、梯田体验之间要留午休和车程缓冲。', '优先确认早餐、卫生、安静度和亲子便利性。'], + href: '/hotels/', + actionLabel: '查看住宿', + }, + 'karst-waterfall-forest': { + title: '短线住宿围绕瀑布、水森林和返程缓冲布点', + summary: '自然短线的住宿要服务景区进入时间和返程安全。黄果树与荔波两段都需要根据季节、人流和车程安排恢复夜。', + image: img.hotelBolian, + tags: ['短线效率', '景区错峰', '返程缓冲'], + items: ['黄果树段适合安排舒适恢复夜,降低瀑布日疲劳。', '荔波段要靠近小七孔或合适转场点,减少早晚奔波。', '最后一晚通常建议回贵阳或保留返程缓冲。'], + href: '/hotels/', + actionLabel: '查看住宿', + }, + 'fanjing-ancient-towns': { + title: '梵净山天气窗口 + 古镇恢复夜', + summary: '梵净山线路受天气和票务影响大,住宿要为进山日和替代安排服务;镇远或黔东南慢住则承担恢复和文化体验。', + image: img.destinationZhenyuan, + tags: ['天气窗口', '古镇慢住', '山地恢复'], + items: ['梵净山前一晚不宜离进山点过远。', '镇远古城适合放在山地体验后做休整。', '如果遇到雨雾,需要住宿节点支持替代行程。'], + href: '/hotels/', + actionLabel: '查看住宿', + }, +}; + +const routeTransportPlans: Record> = { + 'classic-guizhou': { + title: '跨贵阳、安顺、荔波、黔东南的私家小团转场', + summary: '经典线看似常规,真正影响体验的是山地车程、景区停车、行李和酒店换住顺序。车辆要跟行程节奏一起设计。', + image: img.menuTransport, + tags: ['私家车', '跨地市', '行李管理'], + items: ['2-5 人优先 7 座 MPV,行李和英文司导空间更稳。', '黄果树、荔波、黔东南之间需要预留山路和天气缓冲。', '景区停车和酒店上下车点会提前确认,减少现场绕行。'], + href: '/transport/', + actionLabel: '查看交通', + }, + 'outdoor-coffee': { + title: '按活动风险调度车辆与备用转场', + summary: '户外线交通不是单纯接送。要能携带装备、防水补给、换洗衣物,并根据水位、雨量和客人体力切换活动地点。', + image: img.routeOutdoor, + tags: ['装备空间', '备用路线', '山路调度'], + items: ['车辆需预留装备、防水袋、头灯和换洗物品空间。', '活动点可能临时调整,司机和向导要提前共享备选路线。', '雨季亲水项目后要安排更稳的回程和补给节点。'], + href: '/transport/', + actionLabel: '查看交通', + }, + 'culture-outdoor-mix': { + title: '文化停靠与轻户外之间保持弹性车程', + summary: '混搭线会在村寨、非遗老师、河谷步道和城市节点之间切换。交通设计要给体验留时间,而不是把全天消耗在路上。', + image: img.routeMixed, + tags: ['弹性停靠', '村寨道路', '轻户外接驳'], + items: ['村寨道路、停车点和行李搬运需要提前判断。', '非遗体验老师档期可能影响出发时间,车程要有弹性。', '轻户外节点不宜排在长距离转场后的同一天尾段。'], + href: '/transport/', + actionLabel: '查看交通', + }, + 'family-heritage': { + title: '亲子长辈优先选择舒适车型和短车程组合', + summary: '家庭线路交通的核心是稳定和舒适。要减少连续山路、给午休留空间,并把儿童座椅、行李、餐食和洗手间停靠纳入计划。', + image: img.menuTransport, + tags: ['舒适车型', '短车程', '停靠缓冲'], + items: ['3-5 人家庭建议 7 座 MPV,儿童与行李空间更从容。', '每天车程不宜过满,安排洗手间、简餐和临时停车点。', '孩子或长辈状态变化时,能删减非核心节点。'], + href: '/transport/', + actionLabel: '查看交通', + }, + 'karst-waterfall-forest': { + title: '短线高效率转场,围绕景区错峰进出', + summary: '瀑布与水森林短线要把有限天数用在景观上。交通要提前判断贵阳、黄果树、荔波之间的出发时间和进景区节奏。', + image: img.menuTransport, + tags: ['短线转场', '错峰进入', '贵阳缓冲'], + items: ['黄果树日尽量错峰,车辆和景区换乘时间要提前核对。', '荔波段不建议当天来回,避免自然体验被车程挤压。', '返程前保留贵阳缓冲,降低山路天气风险。'], + href: '/transport/', + actionLabel: '查看交通', + }, + 'fanjing-ancient-towns': { + title: '梵净山票务、索道和天气共同决定交通节奏', + summary: '梵净山和古镇线路需要更强的时间窗口管理。车辆要服务早出发、票务预约、天气替代和镇远/黔东南衔接。', + image: img.destinationFanjing, + tags: ['早出发', '票务窗口', '古镇衔接'], + items: ['梵净山进山日要预留早出发和索道排队时间。', '雨雾天气需要能切换周边自然或文化节点。', '镇远古城段适合放慢车程,给夜景和休整留时间。'], + href: '/transport/', + actionLabel: '查看交通', + }, +}; + +const routeFoodPlans: Record> = { + 'classic-guizhou': { + title: '贵阳夜市、安顺小吃、荔波酸香与黔东南长桌', + summary: '经典线的餐食要跟城市、瀑布、水森林和村寨节奏对应。不是每天吃特色,而是在合适节点安排好入口味和舒适度。', + image: img.menuFood, + tags: ['贵阳小吃', '安顺瀑布餐', '苗侗餐食'], + items: ['贵阳适合做抵达或返程前的小吃和夜市。', '黄果树/安顺段以高效、干净、不过辣的恢复餐为先。', '黔东南长桌宴、酸汤鱼等要提前确认酸辣接受度。'], + href: '/food/', + actionLabel: '查看美食', + }, + 'outdoor-coffee': { + title: '户外补给、热食恢复和山野咖啡', + summary: '高强度户外线的饮食先服务体力,再服务特色。活动日前后要安排热食、补水、轻便餐和山地咖啡恢复。', + image: img.experienceCoffee, + tags: ['活动补给', '热食恢复', '野咖'], + items: ['探洞、溯溪或漂流日前要准备补水和轻便能量食。', '活动后优先安排热汤、粉面或容易接受的恢复餐。', '山野咖啡适合作为轻松半日,不和高强度活动硬塞同一天。'], + href: '/food/', + actionLabel: '查看美食', + }, + 'culture-outdoor-mix': { + title: '村寨主理人餐、酸汤风味和轻户外恢复餐', + summary: '人文混搭线的餐食本身就是体验。需要解释礼仪、口味和食材,同时给不习惯酸辣的境外客人准备替代选择。', + image: img.destinationXijiang, + tags: ['村寨餐食', '酸汤', '替代菜单'], + items: ['村寨家宴或长桌餐要提前说明礼仪和食材。', '酸汤、腌鱼、牛瘪等强地方风味需要确认可接受度。', '轻户外日前后用稳定正餐恢复体力。'], + href: '/food/', + actionLabel: '查看美食', + }, + 'family-heritage': { + title: '儿童餐、长辈口味和村寨餐食的平衡', + summary: '亲子慢游的饮食不追求猎奇。先确认辣度、过敏、儿童餐、清淡选择,再把手作、村寨和城市小吃放进舒适节奏。', + image: img.menuFood, + tags: ['儿童餐', '清淡替代', '村寨体验'], + items: ['提前确认辣度、酸汤、牛羊肉、鱼类和过敏信息。', '村寨餐食以体验为主,不适合作为唯一正餐保障。', '贵阳或区域城市节点可安排孩子更容易接受的餐食。'], + href: '/food/', + actionLabel: '查看美食', + }, + 'karst-waterfall-forest': { + title: '瀑布日轻餐、荔波河鲜酸香与贵阳收尾', + summary: '自然短线的餐食要轻、稳、顺路。瀑布日避免太重口,荔波段可加入河鲜和酸香,返程前用贵阳小吃收尾。', + image: img.destinationLibo, + tags: ['瀑布轻餐', '荔波酸香', '贵阳收尾'], + items: ['黄果树日以高效轻餐为主,避免影响步行体力。', '荔波可结合河鲜、水族酸汤或布依风味。', '返程前在贵阳安排小吃或夜市更从容。'], + href: '/food/', + actionLabel: '查看美食', + }, + 'fanjing-ancient-towns': { + title: '进山前轻食、镇远慢餐和铜仁地方风味', + summary: '梵净山线路要避免进山前吃得过重。镇远古城适合慢餐恢复,铜仁和黔东南节点可加入更温和的地方味道。', + image: img.destinationZhenyuan, + tags: ['进山轻食', '镇远慢餐', '铜仁风味'], + items: ['梵净山进山日前后以轻食和补水为主。', '镇远古城适合安排河边慢餐、夜景和恢复。', '铜仁社饭、米豆腐等地方味道可作为温和切入。'], + href: '/food/', + actionLabel: '查看美食', + }, +}; + +function routePlanFallback(safari: SafariCardItem, kind: 'stay' | 'transport' | 'food'): Omit { + if (kind === 'stay') { + return { + title: '按线路节奏匹配住宿,而不是单独选酒店', + summary: '住宿会跟抵离城市、景区进入时间、车程、预算和舒适度一起判断。', + image: img.menuHotels, + tags: ['住宿匹配', safari.pace, safari.category], + items: ['优先保证抵离和转场节点稳定。', '根据活动强度安排恢复夜。', '方案中会给出同区域备选酒店。'], + href: '/hotels/', + actionLabel: '查看住宿', + }; + } + if (kind === 'transport') { + return { + title: '按人数、行李、山路和活动强度匹配车辆', + summary: '贵州线路跨地市、景区和村寨,交通调度会直接决定体验质量。', + image: img.menuTransport, + tags: ['车型匹配', safari.duration, safari.service], + items: ['根据人数和行李选择 5-50 座车辆。', '提前确认景区停车、酒店车位和山路条件。', '保留天气和体力变化的转场缓冲。'], + href: '/transport/', + actionLabel: '查看交通', + }; + } + return { + title: '把贵州味道放进正确的行程节点', + summary: '餐食会根据口味、禁忌、车程、活动强度和城市/村寨节点来安排。', + image: img.menuFood, + tags: ['口味确认', '餐食节点', safari.focus], + items: ['提前确认辣度、酸汤和过敏禁忌。', '活动日前后安排更稳定的热食或轻餐。', '城市小吃、村寨餐食和地方风味按节奏组合。'], + href: '/food/', + actionLabel: '查看美食', + }; +} + +function routeDetailPillars(safari: SafariCardItem, language: SiteLanguage = 'zh'): RouteDetailPillar[] { + const cmsStaySource = safari.routePlans?.stay; + const cmsFoodSource = safari.routePlans?.food; + const cmsTransportSource = safari.routePlans?.transport; + + if (language === 'en') { + return [ + { + id: 'route-flow', + label: 'Route', + title: safari.routeLine, + summary: `Recommended ${safari.duration}. We reorder each day around arrival city, attraction windows, hotel moves, drive time, and activity intensity.`, + image: safari.image, + tags: [safari.duration, safari.pace, safari.service], + items: safari.itinerary.map((day) => `${day.day} | ${day.place} | ${day.title}`), + }, + { + id: 'route-stay', + label: 'Stay', + ...(cmsStaySource || { + title: 'Match hotels to the route pace, not just brand names', + summary: 'Hotels are selected around arrival/departure cities, attraction timing, recovery needs, budget, room comfort, and next-day transfers.', + image: img.menuHotels, + tags: ['Route-matched stay', safari.pace, 'Backup options'], + items: ['Keep arrival and departure nights stable.', 'Use recovery stays after waterfall, mountain, or outdoor days.', 'Compare backup hotels in the same area before confirming.'], + href: '/hotels/', + actionLabel: 'View Hotels', + }), + }, + { + id: 'route-food', + label: 'Food', + ...(cmsFoodSource || { + title: 'Place Guizhou flavors at the right moments', + summary: 'Meals are planned around taste preferences, spice tolerance, allergies, drive time, activity intensity, and city or village nodes.', + image: img.menuFood, + tags: ['Taste check', 'Meal nodes', safari.focus], + items: ['Confirm spice, sour soup, allergies, and dietary restrictions in advance.', 'Use stable warm meals before or after outdoor days.', 'Mix city snacks, village meals, and local flavors according to the route rhythm.'], + href: '/food/', + actionLabel: 'View Food', + }), + }, + { + id: 'route-transport', + label: 'Transport', + ...(cmsTransportSource || { + title: 'Match vehicles to guests, luggage, mountain roads, and activity level', + summary: 'Guizhou routes cross cities, scenic areas, and villages, so vehicle dispatch directly shapes comfort and reliability.', + image: img.menuTransport, + tags: ['Vehicle fit', safari.duration, safari.service], + items: ['Choose 5-50 seat vehicles by group size and luggage.', 'Check scenic parking, hotel drop-off points, and mountain-road limits early.', 'Keep transfer buffers for weather and guest energy changes.'], + href: '/transport/', + actionLabel: 'View Transport', + }), + }, + ]; + } + + const route: RouteDetailPillar = { + id: 'route-flow', + label: '线路', + title: safari.routeLine, + summary: `推荐 ${safari.duration},以${safari.pace}完成${safari.category}主线。我们会按抵离城市、景区窗口、酒店换住和体力强度重排每天顺序。`, + image: safari.image, + tags: [safari.duration, safari.pace, safari.service], + items: safari.itinerary.map((day) => `${day.day}|${day.place}|${day.title}`), + }; + + const staySource = cmsStaySource || routeStayPlans[safari.slug] || routePlanFallback(safari, 'stay'); + const transportSource = cmsTransportSource || routeTransportPlans[safari.slug] || routePlanFallback(safari, 'transport'); + const foodSource = cmsFoodSource || routeFoodPlans[safari.slug] || routePlanFallback(safari, 'food'); + + return [ + route, + { id: 'route-stay', label: '住宿', ...staySource }, + { id: 'route-food', label: '吃', ...foodSource }, + { id: 'route-transport', label: '交通', ...transportSource }, + ]; +} + +function selectPage(path: string) { + if (path === '/safari-faqs' || path === '/faqs') return ; + if (path.startsWith('/our-guides')) return ; + if (path.startsWith('/meet-the-team')) return ; + if (path.startsWith('/in-the-media')) return ; + if (path.startsWith('/the-ndlovu-club') || path.startsWith('/guest-stories') || path.startsWith('/blog/guest-stories')) return ; + if (path.startsWith('/foundation') || path.startsWith('/abc-foundation') || path.startsWith('/impact-safaris') || path.startsWith('/conservation')) return ; + if (path.startsWith('/our-code-of-ethics') || path.startsWith('/code-of-ethics')) return ; + if (path.startsWith('/travel-seasons') || path.startsWith('/amber-season') || path.startsWith('/diamond-season') || path.startsWith('/emerald-season')) return ; + if (path.startsWith('/blog') || path.startsWith('/our-expert-guides') || path.startsWith('/live-from-camp')) return ; + if (path.startsWith('/new-agents-sign-up') || path.startsWith('/trade-portal')) return ; + if (path.startsWith('/expedition')) return ; + if (path.startsWith('/signature')) return ; + if (path.startsWith('/icon')) return ; + if (path === '/landscapes') return ; + if (path === '/safaris') return ; + if (path.startsWith('/safaris/')) return ; + if (path === '/destinations' || path === '/destination') return ; + if (path.startsWith('/destinations/')) return ; + if (path.startsWith('/destination/')) return ; + if (path === '/experiences') return ; + if (path.startsWith('/experiences/')) return ; + if (path === '/food') return ; + if (path.startsWith('/food/')) return ; + if (path === '/transport') return ; + if (path.startsWith('/transport/')) return ; + if (path.startsWith('/the-collective') || path.startsWith('/collective/')) return ; + if (path === '/hotels') return ; + if (path.startsWith('/hotels/')) return ; + if (path === '/specials' || path.startsWith('/specials/')) return ; + if (path.startsWith('/plan-your-safari')) return ; + if (path.startsWith('/contact')) return ; + if (path.startsWith('/our-story')) return ; + return ; +} + +function Header({ + language, + onLanguageToggle, + onMenu, + onSearch, +}: { + language: SiteLanguage; + onLanguageToggle: () => void; + onMenu: () => void; + onSearch: () => void; +}) { + const [active, setActive] = useState(null); + const { content } = useTravelSite(); + const headerRef = useRef(null); + const navRef = useRef(null); + const navIndicatorRef = useRef(null); + const languageToggleRef = useRef(null); + const hasAnimatedLanguageToggleRef = useRef(false); + const activeMenu = active ? resolveMegaMenu(content, active) : undefined; + + useGSAP( + () => { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + const nav = navRef.current; + const indicator = navIndicatorRef.current; + if (!nav || !indicator) return; + + const navLinks = gsap.utils.toArray('.oc-nav-link'); + const feedbackTargets = gsap.utils.toArray('.oc-icon-action:not(.oc-language-action), .oc-header-cta'); + const cleanups: Array<() => void> = []; + let indicatorVisible = false; + + gsap.set(indicator, { autoAlpha: 0, width: 0, x: 0 }); + gsap.set(feedbackTargets, { transformOrigin: '50% 50%' }); + + const moveIndicator = (link: HTMLAnchorElement) => { + const navRect = nav.getBoundingClientRect(); + const linkRect = link.getBoundingClientRect(); + indicatorVisible = true; + gsap.to(indicator, { + x: linkRect.left - navRect.left, + width: linkRect.width, + autoAlpha: 1, + duration: 0.34, + ease: 'power3.out', + overwrite: true, + }); + gsap.to(link, { y: -2, color: '#2f8f43', duration: 0.22, ease: 'power2.out', overwrite: true }); + }; + + const resetLink = (link: HTMLAnchorElement) => { + gsap.to(link, { y: 0, color: '#26372a', duration: 0.22, ease: 'power2.out', overwrite: true }); + }; + + const hideIndicator = () => { + indicatorVisible = false; + gsap.to(indicator, { autoAlpha: 0, width: 0, duration: 0.2, ease: 'power2.inOut', overwrite: true }); + navLinks.forEach(resetLink); + }; + + navLinks.forEach((link) => { + const enter = () => moveIndicator(link); + const leave = () => resetLink(link); + link.addEventListener('pointerenter', enter); + link.addEventListener('focus', enter); + link.addEventListener('pointerleave', leave); + link.addEventListener('blur', leave); + cleanups.push(() => { + link.removeEventListener('pointerenter', enter); + link.removeEventListener('focus', enter); + link.removeEventListener('pointerleave', leave); + link.removeEventListener('blur', leave); + }); + }); + + feedbackTargets.forEach((target) => { + const enter = () => gsap.to(target, { y: -2, scale: 1.035, duration: 0.2, ease: 'power2.out', overwrite: true }); + const leave = () => gsap.to(target, { y: 0, scale: 1, duration: 0.22, ease: 'power2.out', overwrite: true }); + const down = () => gsap.to(target, { scale: 0.96, duration: 0.1, ease: 'power2.out', overwrite: true }); + const up = () => gsap.to(target, { scale: 1.035, duration: 0.16, ease: 'back.out(2)', overwrite: true }); + target.addEventListener('pointerenter', enter); + target.addEventListener('pointerleave', leave); + target.addEventListener('pointerdown', down); + target.addEventListener('pointerup', up); + cleanups.push(() => { + target.removeEventListener('pointerenter', enter); + target.removeEventListener('pointerleave', leave); + target.removeEventListener('pointerdown', down); + target.removeEventListener('pointerup', up); + }); + }); + + const hideOnResize = () => { + if (indicatorVisible) hideIndicator(); + }; + + nav.addEventListener('pointerleave', hideIndicator); + window.addEventListener('resize', hideOnResize); + cleanups.push(() => { + nav.removeEventListener('pointerleave', hideIndicator); + window.removeEventListener('resize', hideOnResize); + }); + + return () => cleanups.forEach((cleanup) => cleanup()); + }, + { scope: headerRef, dependencies: [content.header.nav, language], revertOnUpdate: true }, + ); + + useGSAP( + () => { + const root = languageToggleRef.current; + if (!root) return undefined; + + const thumb = root.querySelector('.oc-language-thumb'); + const activeOption = root.querySelector(`.oc-language-option[data-language="${language}"]`); + const options = Array.from(root.querySelectorAll('.oc-language-option')); + const inactiveOptions = options.filter((option) => option !== activeOption); + if (!thumb || !activeOption) return undefined; + + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const thumbX = activeOption.offsetLeft - thumb.offsetLeft; + const shouldAnimate = hasAnimatedLanguageToggleRef.current && !reduceMotion; + let switchTl: gsap.core.Timeline | undefined; + hasAnimatedLanguageToggleRef.current = true; + + gsap.set(root, { transformOrigin: '50% 50%' }); + + if (!shouldAnimate) { + gsap.set(thumb, { x: thumbX }); + gsap.set(activeOption, { autoAlpha: 1, y: 0, color: '#fff' }); + gsap.set(inactiveOptions, { autoAlpha: 0.62, y: 0, color: '#26372a' }); + } else { + switchTl = gsap.timeline({ defaults: { overwrite: true } }); + switchTl + .to(thumb, { x: thumbX, duration: 0.34, ease: 'power3.inOut' }, 0) + .to(activeOption, { autoAlpha: 1, y: 0, color: '#fff', duration: 0.2, ease: 'power2.out' }, 0.08) + .to(inactiveOptions, { autoAlpha: 0.62, y: 0, color: '#26372a', duration: 0.18, ease: 'power2.out' }, 0) + .fromTo(root, { scale: 0.985 }, { scale: 1, duration: 0.34, ease: 'back.out(2.4)' }, 0.02); + } + + if (reduceMotion) return undefined; + + const enter = () => gsap.to(root, { y: -2, scale: 1.025, duration: 0.2, ease: 'power2.out', overwrite: true }); + const leave = () => gsap.to(root, { y: 0, scale: 1, duration: 0.22, ease: 'power2.out', overwrite: true }); + const down = () => gsap.to(root, { scale: 0.965, duration: 0.1, ease: 'power2.out', overwrite: true }); + const up = () => gsap.to(root, { scale: 1.025, duration: 0.16, ease: 'back.out(2)', overwrite: true }); + + root.addEventListener('pointerenter', enter); + root.addEventListener('pointerleave', leave); + root.addEventListener('pointerdown', down); + root.addEventListener('pointerup', up); + + return () => { + switchTl?.kill(); + root.removeEventListener('pointerenter', enter); + root.removeEventListener('pointerleave', leave); + root.removeEventListener('pointerdown', down); + root.removeEventListener('pointerup', up); + }; + }, + { scope: headerRef, dependencies: [language], revertOnUpdate: false }, + ); + + return ( +
setActive(null)} data-no-translate> +
+ + {`${translateUi('万趣旅行', + + +
+ + + + + {oc(content.header.ctaLabel, language)} + +
+
+ {activeMenu && } +
+ ); +} + +function GlobeLanguageLabel({ language }: { language: SiteLanguage }) { + return ( +