Compare commits

..

11 Commits

Author SHA1 Message Date
Ahmed Darrazi
8645938a5c feat(website): add evaluation procurement rollout surface
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 52s
2026-05-30 19:30:00 +02:00
acdea41d92 408: add review pack story surfaces and homepage polish (#405)
## Summary
- add the localized review-pack product story routes at `/platform/review-packs` and `/en/platform/review-packs` with shared page composition, evidence/decision framing, audience sections, trust handoff, and footer/use-case/home/platform discovery
- extend `site-copy`, smoke coverage, and Spec Kit artifacts for feature 408 so the public website contract, tests, research, plan, quickstart, and checklist stay aligned
- polish the public presentation with a cleaner review-pack comparison surface, a more opaque navbar to remove homepage logo bleed-through, a higher-contrast secondary CTA, unique homepage feature icons, and less repetitive homepage use-case copy

## Validation
- `corepack pnpm --filter @tenantatlas/website build`
- `corepack pnpm --filter @tenantatlas/website test tests/smoke/public-routes.spec.ts`
- `corepack pnpm --filter @tenantatlas/website test tests/smoke/interaction.spec.ts`
- source/dist claim scans plus manual browser comprehension checks are recorded in `specs/408-review-evidence-decision/checklists/requirements.md`
- current touched website files are free of editor diagnostics; live browser console check on the homepage returned no errors

## Notes
- trust/proof messaging remains intentionally honest; this PR does not add fabricated customer logos, certifications, or unsupported compliance claims
- `origin/website-dev` is the review base for this PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #405
2026-05-29 13:48:21 +00:00
94cff6ca03 feat: add MSP and internal IT use-case pages (#402)
Implements website feature branch `407-msp-mittelstand-use-case-pages` into `website-dev`.

Summary:
- add German and English MSP and internal IT use-case landing pages
- expose localized buyer-path teasers from the homepage and platform page
- extend smoke coverage for the new routes, navigation links, metadata, and conservative claim checks
- include the aligned Spec Kit artifacts under `specs/407-msp-mittelstand-use-case-pages`

Validation:
- `cd apps/website && corepack pnpm build`
- `cd apps/website && corepack pnpm test -- tests/smoke/public-routes.spec.ts tests/smoke/interaction.spec.ts`

Follow-up integration path after merge:

`website-dev` -> `dev`.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #402
2026-05-27 22:02:42 +00:00
09dc9988cb 406: Provider & Policy Domain Public Taxonomy (#401)
## Summary
- add the 406 feature specification for a public provider and policy-domain taxonomy surface
- include plan, research, data model, quickstart, checklist, and public route contract artifacts
- update agent context with the 406 website technology notes

## Notes
- this PR is spec and planning work only
- no runtime website implementation is included yet

## Validation
- reviewed pending git scope before commit
- verified `Agents.md` has no editor diagnostics

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #401
2026-05-26 12:54:23 +00:00
714b910734 405: DACH Trust, Datenschutz & Security Website Surface (#400)
## Summary
- add a dedicated public trust, privacy, and security surface for DACH evaluation
- expand homepage trust discoverability and localized trust handoff copy
- add and update smoke coverage plus Spec Kit artifacts for feature 405

## Validation
- corepack pnpm --dir apps/website build
- WEBSITE_PORT=4322 corepack pnpm exec playwright test tests/smoke/public-routes.spec.ts tests/smoke/interaction.spec.ts

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #400
2026-05-26 00:11:27 +00:00
c261b1c632 feat(website): tighten homepage messaging and trust flow (#398)
## Summary
- tighten homepage messaging, hero support copy, trust teaser flow, and CTA routing for the website public-content rollout
- align shared website copy, smoke expectations, and spec 404 artifacts with the latest messaging pass
- replace the previously closed PR for `404-public-content-messaging`

## Commits
- `44d27395` feat(website): tighten homepage messaging and trust flow
- `1ddbd28b` feat(website): refine public content messaging rollout

## Validation
- `git diff --check`

## Notes
- local Playwright MCP output remains untracked and was not included

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #398
2026-05-25 20:35:33 +00:00
44e472ec18 feat(website): finalize public content messaging updates (#396)
## Summary
- refresh website copy and structured content datasets
- update docs content in EN and default locales
- adjust website UI auth-related components and smoke tests
- add Spec Kit artifacts for feature 404 public content messaging

## Validation
- committed and pushed from branch `404-public-content-messaging`

## Target
- base branch: `website-dev`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #396
2026-05-24 23:06:38 +00:00
b9c128163b feat: public website launch readiness updates (#394)
## Summary
- apply public website launch readiness updates across the Astro site shell, content, and navigation
- refine website components, metadata, and localization-related structure for launch prep
- update docs/content paths and smoke coverage to match the launch-ready public site state

## Scope
- touches the website app and related spec artifacts for feature 403
- does not modify `apps/platform`

## Testing
- not run in this step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #394
2026-05-21 21:41:33 +00:00
eeb5c98450 feat: rebuild website on ScrewFast foundation (#393)
## Summary
- rebuild `apps/website` on the pinned ScrewFast Astro foundation
- replace the legacy page/content/component structure with the new section and UI architecture
- add Starlight-based docs and the new public route set for platform, pricing, trust, legal, and guides
- refresh website tooling, dependencies, and Playwright smoke coverage for the new site shell

## Scope
- touches `apps/website` and the matching spec artifacts for feature 402
- does not modify `apps/platform`

## Testing
- not run in this step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #393
2026-05-20 21:36:29 +00:00
2000c17f33 feat: transition website product page to platform (#391)
## Summary
- rename the website product page to `/platform`
- add a redirect from `/product` to `/platform` and update navigation/content links
- refresh footer/layout metadata and align smoke tests with the new route
- add spec artifacts for 401-tenantial-platform-page

## Testing
- not run in this step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #391
2026-05-19 21:34:31 +00:00
be314c577f Spec 400: rebuild Tenantial homepage visuals (#387)
## Summary
- rebuild the public Tenantial homepage around an evidence-first Microsoft tenant governance narrative
- replace the old hero visual with a new static dashboard preview and add dedicated Trust Bar and Feature Pillars sections
- update the shared public shell, navigation, footer, dark design tokens, assets, and homepage content to match the new brand direction
- align website smoke coverage and Spec 400 artifacts with the rebuilt homepage

## Testing
- not run in this pass
- updated website smoke specs under apps/website/tests/smoke

## Note
- `website-dev` was pushed to `origin` so the requested PR base exists remotely
- the remote `website-dev` branch is an ancestor of `origin/dev`, so this PR may also show upstream `dev` history relative to that base

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #387
2026-05-18 14:38:11 +00:00
360 changed files with 36289 additions and 6093 deletions

View File

@ -266,6 +266,10 @@ ## Active Technologies
- PostgreSQL via existing `workspace_settings` rows plus existing audit log records; no new table or billing/account model (251-commercial-entitlements-billing-state)
- PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities` (253-remove-findings-backfill-runtime-surfaces)
- PostgreSQL existing `findings`, `operation_runs`, `audit_logs`, and related runtime tables only; no new persistence, migration, or data backfill is planned (253-remove-findings-backfill-runtime-surfaces)
- TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0 + Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwrigh (404-public-content-messaging)
- N/A - static website content and generated build output only; no database or product persistence (404-public-content-messaging)
- TypeScript 6.0.3 and Astro 6.3.3 content/runtime files + Astro, Playwright, Tailwind CSS v4 (`@tailwindcss/vite`), Starlight docs stack (408-review-evidence-decision)
- N/A (static public website content only) (408-review-evidence-decision)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -300,9 +304,9 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 253-remove-findings-backfill-runtime-surfaces: Added PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities`
- 251-commercial-entitlements-billing-state: Added PHP 8.4 (Laravel 12) + Filament v5 + Livewire v4, existing workspace settings stack (`SettingsRegistry`, `SettingsResolver`, `SettingsWriter`), `WorkspaceEntitlementResolver`, `ReviewPackService`, system directory detail page
- 249-customer-review-workspace: Added PHP 8.4, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing review/evidence/review-pack/audit/RBAC support services
- 409-evaluation-procurement-rollout: Added TypeScript 6.0.3 and Astro 6.3.3 content/runtime files + Astro, Playwright, Tailwind CSS v4 (`@tailwindcss/vite`), Starlight docs stack
- 408-review-evidence-decision: Added TypeScript 6.0.3 and Astro 6.3.3 content/runtime files + Astro, Playwright, Tailwind CSS v4 (`@tailwindcss/vite`), Starlight docs stack
- 404-public-content-messaging: Added TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0 + Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwrigh
<!-- MANUAL ADDITIONS START -->
### Pre-production compatibility check

View File

@ -942,6 +942,14 @@ ## Active Technologies
- PHP 8.4 (Laravel 12) + Filament v5 + Livewire v4
- PostgreSQL (Sail)
- Tailwind CSS v4
- TypeScript 5.9, Astro 6 static components, HTML, CSS + Astro 6.0.0, Tailwind CSS 4.2.2 through CSS-first `@theme` and `@tailwindcss/vite`, `astro-icon`, `@iconify-json/lucide`, Playwright 1.59.1 (400-tenantial-homepage-visual-rebuild)
- TypeScript 5.9.3, Astro 6.0.0, Tailwind CSS v4.2.2 via `@tailwindcss/vite`, `astro-icon`, `@iconify-json/lucide`, and Playwright smoke tests for the static website; no database, CMS, API, customer data, tenant data, or runtime persistence. (401-tenantial-platform-page)
- TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0 + Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwright; static website content only, no database or product persistence. (feat/403-public-website-launch-readiness)
- No new technology; reuses TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0, Starlight, Tailwind CSS v4, Preline, Lenis, GSAP, Sharp, and Playwright for static website content, docs content, route metadata, and generated build output only; no database or product persistence. (404-public-content-messaging)
- TypeScript 6, Astro 6, Node.js `>=20.0.0` + Astro, Tailwind CSS v4, Starlight, Playwrigh (405-dach-trust-datenschutz-security-website-surface)
- N/A - static Astro pages and centralized locale copy in TypeScrip (405-dach-trust-datenschutz-security-website-surface)
- TypeScript 6.0.3, Astro 6.3.3, Tailwind CSS 4.3.0 + Astro, `@astrojs/check`, `@astrojs/sitemap`, Tailwind CSS v4, Playwright smoke tests (406-provider-policy-domain-public-taxonomy)
- N/A - static public website content only; no runtime persistence (406-provider-policy-domain-public-taxonomy)
## Recent Changes
- 066-rbac-ui-enforcement-helper-v2-session-1769732329: Planned UiEnforcement v2 (spec + plan + design artifacts)

35
apps/website/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# build output
dist/
build/
# generated types
.astro/
.idea/
.vscode/
# dependencies
node_modules/
# logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# test artifacts
coverage/
playwright-report/
test-results/
blob-report/
# environment variables
.env
.env.*
.env.production
# macOS-specific files
.DS_Store
Thumbs.db
*.tmp
*.swp

View File

@ -0,0 +1,14 @@
node_modules/
dist/
build/
coverage/
.astro/
playwright-report/
test-results/
blob-report/
package-lock.json
yarn.lock
pnpm-lock.yaml
*.log
.env
.env.*

20
apps/website/.prettierrc Normal file
View File

@ -0,0 +1,20 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View File

@ -0,0 +1,27 @@
# Third-Party Notices
This website app vendors and adapts portions of the ScrewFast project.
## ScrewFast
MIT License
Copyright (c) 2024 Emil Gulamov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,25 +1,106 @@
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon';
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import starlight from '@astrojs/starlight';
const publicSiteUrl = process.env.PUBLIC_SITE_URL ?? 'https://tenantatlas.example';
import mdx from '@astrojs/mdx';
const redirectOnlyPaths = new Set([
'/product/',
'/products/',
'/services/',
'/blog/',
'/insights/',
'/en/product/',
'/en/products/',
'/en/services/',
'/en/blog/',
'/en/insights/',
]);
const isRedirectOnlySitemapPath = page => {
const pathname = page.startsWith('http') ? new URL(page).pathname : page;
const normalized = pathname.endsWith('/') ? pathname : `${pathname}/`;
return redirectOnlyPaths.has(normalized);
};
// https://astro.build/config
export default defineConfig({
integrations: [icon()],
output: 'static',
site: publicSiteUrl,
server: {
host: true,
port: 4321,
},
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
devToolbar: {
enabled: false,
},
// https://docs.astro.build/en/guides/images/#authorizing-remote-images
site: process.env.PUBLIC_SITE_URL ?? 'https://tenantial.com',
image: {
domains: ['images.unsplash.com'],
},
prefetch: true,
integrations: [
sitemap({
filter: page => !isRedirectOnlySitemapPath(page),
i18n: {
defaultLocale: 'de',
locales: {
de: 'de',
en: 'en',
},
},
},
}),
starlight({
title: 'Tenantial Docs',
// https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md
// If no Astro and Starlight i18n configurations are provided, the built-in default locale is used in Starlight and a matching Astro i18n configuration is generated/used.
// If only a Starlight i18n configuration is provided, an equivalent Astro i18n configuration is generated/used.
// If only an Astro i18n configuration is provided, the Starlight i18n configuration is updated to match it.
// If both an Astro and Starlight i18n configurations are provided, an error is thrown.
locales: {
root: {
label: 'Deutsch',
lang: 'de',
},
en: {
label: 'English',
lang: 'en',
},
},
// https://starlight.astro.build/guides/sidebar/
sidebar: [
{
label: 'Evaluierungsleitfaeden',
translations: {
en: 'Evaluation Guides',
},
items: [{ autogenerate: { directory: 'guides' } }],
},
{
label: 'Plattform-Notizen',
translations: {
en: 'Platform Notes',
},
items: [
{ label: 'Evidence Review', link: 'platform/evidence-review/' },
],
},
],
social: [],
disable404Route: true,
customCss: ['./src/assets/styles/starlight.css'],
favicon: '/favicon.ico',
components: {
SiteTitle: './src/components/ui/starlight/SiteTitle.astro',
Head: './src/components/ui/starlight/Head.astro',
MobileMenuFooter:
'./src/components/ui/starlight/MobileMenuFooter.astro',
ThemeSelect: './src/components/ui/starlight/ThemeSelect.astro',
},
}),
mdx(),
],
experimental: {
clientPrerender: true,
},
vite: {
plugins: [tailwindcss()],
},
});

View File

@ -1,28 +1,44 @@
{
"name": "@tenantatlas/website",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"build": "astro build",
"preview": "astro preview --host 0.0.0.0",
"test": "playwright test",
"test:smoke": "playwright test"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.102",
"astro": "^6.0.0",
"astro-icon": "^1.1.5"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.7.2",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3"
}
"name": "@tenantatlas/website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"start": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"build": "astro check && astro build && node process-html.mjs",
"preview": "astro preview --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"test": "playwright test",
"test:smoke": "playwright test",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/mdx": "^5.0.6",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"@tailwindcss/vite": "^4.3.0",
"astro": "^6.3.3",
"clipboard": "^2.0.11",
"globby": "^16.2.0",
"gsap": "^3.15.0",
"html-minifier-terser": "^7.2.0",
"lenis": "^1.3.23",
"preline": "^4.2.0",
"rimraf": "^6.1.3",
"sharp": "^0.34.5",
"sharp-ico": "^0.1.5",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"astro-vtbot": "^2.1.12",
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.8.0",
"typescript": "^6.0.3"
}
}

View File

@ -1,29 +1,35 @@
import { defineConfig, devices } from '@playwright/test';
const port = Number(process.env.WEBSITE_PORT ?? '4321');
const port = Number(process.env.WEBSITE_PORT ?? 4321);
const baseURL = `http://127.0.0.1:${port}`;
export default defineConfig({
testDir: './tests/smoke',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [['list']],
use: {
baseURL,
trace: 'on-first-retry',
testDir: './tests/smoke',
timeout: 30_000,
expect: {
timeout: 5_000,
},
fullyParallel: true,
retries: process.env.CI ? 1 : 0,
reporter: 'list',
use: {
baseURL,
trace: 'on-first-retry',
},
projects: [
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
webServer: {
command: `WEBSITE_PORT=${port} corepack pnpm dev`,
port,
reuseExistingServer: !process.env.CI,
timeout: 120000,
{
name: 'mobile',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'corepack pnpm preview',
reuseExistingServer: false,
timeout: 120_000,
url: baseURL,
},
});

5867
apps/website/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
import fs from 'node:fs/promises';
import { globby } from 'globby';
import { minify } from 'html-minifier-terser';
// Get all HTML files from the output directory
const path = './dist';
const files = await globby(`${path}/**/*.html`);
await Promise.all(
files.map(async file => {
console.log('Processing file:', file);
let html = await fs.readFile(file, 'utf-8');
// Minify the HTML
html = await minify(html, {
removeComments: true,
preserveLineBreaks: true,
collapseWhitespace: true,
minifyJS: true,
});
await fs.writeFile(file, html);
})
);

View File

@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1002 285">
<g clip-path="url(#a)">
<path fill="#FDE68A" d="M132.693 132.957V65.953h67.003l-67.003 67.004Z"/>
<path fill="#FDE68A" d="M67.003 132.957V65.953h67.004l-67.004 67.004Z"/>
<path fill="#F59E0B" d="M162.91 131.643H67.003v48.61h95.907v-48.61Z"/>
<path fill="#FDE68A" d="M67.004 131.643H0v48.61h67.004v-48.61Z"/>
<path fill="#FACC15" d="M134.007 65.953v67.004H67.003l67.004-67.004Z"/>
<path fill="#FACC15" d="M67.004 65.953v67.004H0l67.004-67.004Zm132.692-1.313v67.004h-67.003l67.003-67.003Z"/>
<path fill="#FDE68A" d="M251.535 52c-28.298 0-51.238 22.94-51.238 51.239 0 28.298 22.94 51.238 51.238 51.238s51.238-22.94 51.238-51.238-22.94-51.238-51.238-51.238Z"/>
<path fill="#FACC15" d="M251.535 82.216c-11.609 0-21.021 9.412-21.021 21.021 0 11.61 9.412 21.021 21.021 21.021s21.021-9.411 21.021-21.021c0-11.61-9.412-21.02-21.021-21.02Zm47.296 72.26V87.473h67.003l-67.003 67.003Z"/>
<path fill="#FDE68A" d="M999.983 42.52c-13.301 0-26.057 5.34-35.462 14.848-9.405 9.507-14.688 22.401-14.688 35.847 0 13.445 5.283 26.339 14.688 35.847 9.405 9.507 22.161 14.848 35.462 14.848v-23.535a26.726 26.726 0 0 1-18.999-7.955c-5.039-5.094-7.87-12.002-7.87-19.205 0-7.204 2.831-14.112 7.87-19.206a26.724 26.724 0 0 1 18.999-7.955V42.52Z"/>
<path fill="#FACC15" d="M999.982 0c-24.577 0-48.147 9.82-65.526 27.302-17.379 17.48-27.142 41.19-27.142 65.912 0 24.721 9.763 48.431 27.142 65.912 17.379 17.48 40.949 27.301 65.526 27.301v-43.274a49.5 49.5 0 0 1-35.106-14.627c-9.31-9.365-14.541-22.068-14.541-35.312 0-13.245 5.231-25.948 14.541-35.313a49.502 49.502 0 0 1 35.106-14.627V0Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.475 162.988c-33.116 0-59.962 26.846-59.962 59.962s26.846 59.962 59.962 59.962 59.962-26.846 59.962-59.962-26.846-59.962-59.962-59.962Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.476 173.89c-27.095 0-49.06 21.965-49.06 49.06 0 27.095 21.965 49.06 49.06 49.06 27.095 0 49.06-21.965 49.06-49.06 0-27.095-21.965-49.06-49.06-49.06Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.475 183.702c-21.676 0-39.248 17.572-39.248 39.247 0 21.676 17.572 39.248 39.248 39.248s39.248-17.572 39.248-39.248c0-21.675-17.572-39.247-39.248-39.247Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.476 194.604c-15.655 0-28.346 12.69-28.346 28.345s12.691 28.346 28.346 28.346 28.346-12.691 28.346-28.346-12.691-28.345-28.346-28.345Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.475 205.506c-9.634 0-17.444 7.809-17.444 17.443s7.81 17.444 17.444 17.444 17.443-7.81 17.443-17.444-7.809-17.443-17.443-17.443Z"/>
<path stroke="#FDE68A" stroke-width="3.271" d="M939.476 215.317a7.632 7.632 0 1 0 0 15.264 7.632 7.632 0 0 0 0-15.264Z"/>
<path fill="#F59E0B" d="M862.615 180.977h55.601v-40.338h-55.601v40.338Z"/>
<path fill="#FACC15" d="M862.615 235.488v-55.601h55.601l-55.601 55.601Z"/>
<path stroke="#FACC15" stroke-width="2.18" d="m778.311 199.75 55.165-89.232 55.166 89.232H778.311Z"/>
<path fill="#FACC15" d="m790.958 91 57.122 91.578H733.837L790.958 91Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h1002v285H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect width="64" height="64" rx="18" fill="#17120F" />
<path
d="M17 18H47V25H36V46H28V25H17V18Z"
fill="#FFF7F1"
/>
<path
d="M44 17C50.0751 17 55 21.9249 55 28C55 34.0751 50.0751 39 44 39H39V32H44C46.2091 32 48 30.2091 48 28C48 25.7909 46.2091 24 44 24H39V17H44Z"
fill="#CC5F2C"
/>
</svg>

Before

Width:  |  Height:  |  Size: 413 B

View File

@ -1,70 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500" fill="none">
<defs>
<linearGradient id="bg" x1="60" y1="40" x2="740" y2="460" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F8FAFC" />
<stop offset="1" stop-color="#E2E8F0" />
</linearGradient>
<linearGradient id="topbar" x1="0" y1="0" x2="800" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1D4ED8" />
<stop offset="1" stop-color="#4F46E5" />
</linearGradient>
</defs>
<rect x="8" y="8" width="784" height="484" rx="20" fill="url(#bg)" stroke="#D7E2EE" stroke-width="2" />
<rect x="8" y="8" width="784" height="52" rx="20" fill="url(#topbar)" />
<rect x="8" y="30" width="784" height="30" fill="url(#topbar)" />
<text x="32" y="40" font-family="system-ui" font-size="15" font-weight="700" fill="#FFFFFF">TenantAtlas</text>
<circle cx="742" cy="34" r="6" fill="#FFFFFF" fill-opacity="0.7" />
<circle cx="762" cy="34" r="6" fill="#FFFFFF" fill-opacity="0.45" />
<rect x="24" y="76" width="168" height="396" rx="18" fill="#F3F6FB" stroke="#D7E2EE" />
<text x="44" y="108" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">WORKSPACE</text>
<rect x="36" y="128" width="144" height="34" rx="10" fill="#DBEAFE" />
<text x="52" y="149" font-family="system-ui" font-size="12" font-weight="700" fill="#1D4ED8">Change history</text>
<text x="52" y="194" font-family="system-ui" font-size="12" fill="#475569">Restore preview</text>
<text x="52" y="230" font-family="system-ui" font-size="12" fill="#475569">Review queue</text>
<text x="52" y="266" font-family="system-ui" font-size="12" fill="#475569">Evidence</text>
<text x="52" y="302" font-family="system-ui" font-size="12" fill="#475569">Assignments</text>
<rect x="36" y="344" width="144" height="96" rx="14" fill="#FFFFFF" stroke="#D7E2EE" />
<text x="52" y="370" font-family="system-ui" font-size="11" font-weight="700" fill="#334155">Current tenant</text>
<text x="52" y="394" font-family="system-ui" font-size="12" fill="#0F172A">Northwind Services</text>
<text x="52" y="418" font-family="system-ui" font-size="11" fill="#64748B">Inventory linked to reviewable history</text>
<rect x="212" y="76" width="564" height="396" rx="18" fill="#FFFFFF" stroke="#D7E2EE" />
<text x="236" y="108" font-family="system-ui" font-size="12" font-weight="700" fill="#334155">Recent tenant changes</text>
<rect x="236" y="124" width="90" height="24" rx="12" fill="#EFF6FF" />
<text x="252" y="140" font-family="system-ui" font-size="10" font-weight="700" fill="#2563EB">Policies</text>
<rect x="336" y="124" width="82" height="24" rx="12" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="356" y="140" font-family="system-ui" font-size="10" font-weight="700" fill="#475569">Drift</text>
<rect x="428" y="124" width="108" height="24" rx="12" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="448" y="140" font-family="system-ui" font-size="10" font-weight="700" fill="#475569">Assignments</text>
<rect x="236" y="164" width="320" height="236" rx="16" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="256" y="192" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">CHANGE RECORD</text>
<line x1="256" y1="210" x2="536" y2="210" stroke="#E2E8F0" />
<text x="256" y="236" font-family="system-ui" font-size="13" font-weight="700" fill="#0F172A">Windows Compliance Baseline</text>
<text x="256" y="256" font-family="system-ui" font-size="11" fill="#475569">Version diff prepared for review</text>
<rect x="454" y="222" width="78" height="22" rx="11" fill="#DCFCE7" />
<text x="469" y="237" font-family="system-ui" font-size="10" font-weight="700" fill="#15803D">Ready</text>
<line x1="256" y1="274" x2="536" y2="274" stroke="#E2E8F0" />
<text x="256" y="300" font-family="system-ui" font-size="13" font-weight="700" fill="#0F172A">Conditional Access MFA</text>
<text x="256" y="320" font-family="system-ui" font-size="11" fill="#475569">Assignment drift surfaced before rollout</text>
<rect x="438" y="286" width="94" height="22" rx="11" fill="#FEF3C7" />
<text x="452" y="301" font-family="system-ui" font-size="10" font-weight="700" fill="#B45309">Needs review</text>
<line x1="256" y1="338" x2="536" y2="338" stroke="#E2E8F0" />
<text x="256" y="364" font-family="system-ui" font-size="13" font-weight="700" fill="#0F172A">BitLocker policy</text>
<text x="256" y="384" font-family="system-ui" font-size="11" fill="#475569">Restore candidate linked to prior snapshot</text>
<rect x="420" y="350" width="112" height="22" rx="11" fill="#DBEAFE" />
<text x="436" y="365" font-family="system-ui" font-size="10" font-weight="700" fill="#1D4ED8">Snapshot linked</text>
<rect x="576" y="164" width="176" height="112" rx="16" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="596" y="192" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">RESTORE PREVIEW</text>
<text x="596" y="220" font-family="system-ui" font-size="12" fill="#0F172A">Scope validated</text>
<text x="596" y="242" font-family="system-ui" font-size="12" fill="#0F172A">Assignments included</text>
<text x="596" y="264" font-family="system-ui" font-size="12" fill="#0F172A">Confirmation required</text>
<circle cx="580" cy="217" r="4" fill="#16A34A" />
<circle cx="580" cy="239" r="4" fill="#16A34A" />
<circle cx="580" cy="261" r="4" fill="#F59E0B" />
<rect x="576" y="292" width="176" height="108" rx="16" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="596" y="320" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">REVIEW QUEUE</text>
<text x="596" y="346" font-family="system-ui" font-size="12" fill="#0F172A">Conditional Access drift</text>
<text x="596" y="368" font-family="system-ui" font-size="12" fill="#0F172A">Restore plan awaiting approval</text>
<text x="596" y="390" font-family="system-ui" font-size="12" fill="#0F172A">Evidence attached to change record</text>
<rect x="236" y="420" width="516" height="28" rx="14" fill="#EEF2FF" />
<text x="256" y="438" font-family="system-ui" font-size="11" font-weight="700" fill="#4338CA">Change history, restore preview, and review queue stay connected on one screen.</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,3 +0,0 @@
User-agent: *
Allow: /
Sitemap: /sitemap.xml

View File

@ -0,0 +1,9 @@
import '@styles/lenis.css';
import Lenis from 'lenis';
// Script to handle Lenis library settings for smooth scrolling
// https://github.com/darkroomengineering/lenis
const lenis = new Lenis({
autoRaf: true,
});

View File

@ -0,0 +1,127 @@
@import 'tailwindcss';
/* Preline UI */
@source '../../../node_modules/preline/dist/*.js';
@import '../../../node_modules/preline/variants.css';
/* Plugins */
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:is(.dark *));
@theme {
/* https://tailwindcss.com/docs/colors#customizing-your-colors */
--color-*: initial;
--color-transparent: transparent;
--color-current: currentColor;
--color-black: #000;
--color-white: #fff;
--color-gray-50: oklch(0.985 0.002 247.839);
--color-gray-100: oklch(0.967 0.003 264.542);
--color-gray-200: oklch(0.928 0.006 264.531);
--color-gray-300: oklch(0.872 0.01 258.338);
--color-gray-400: oklch(0.707 0.022 261.325);
--color-gray-500: oklch(0.551 0.027 264.364);
--color-gray-600: oklch(0.446 0.03 256.802);
--color-gray-700: oklch(0.373 0.034 259.733);
--color-gray-800: oklch(0.278 0.033 256.848);
--color-gray-900: oklch(0.21 0.034 264.665);
--color-gray-950: oklch(0.13 0.028 261.692);
--color-indigo-50: oklch(0.962 0.018 272.314);
--color-indigo-100: oklch(0.93 0.034 272.788);
--color-indigo-200: oklch(0.87 0.065 274.039);
--color-indigo-300: oklch(0.785 0.115 274.713);
--color-indigo-400: oklch(0.673 0.182 276.935);
--color-indigo-500: oklch(0.585 0.233 277.117);
--color-indigo-600: oklch(0.511 0.262 276.966);
--color-indigo-700: oklch(0.457 0.24 277.023);
--color-indigo-800: oklch(0.398 0.195 277.366);
--color-indigo-900: oklch(0.359 0.144 278.697);
--color-indigo-950: oklch(0.257 0.09 281.288);
--color-neutral-50: oklch(0.985 0 0);
--color-neutral-100: oklch(0.97 0 0);
--color-neutral-200: oklch(0.922 0 0);
--color-neutral-300: oklch(0.87 0 0);
--color-neutral-400: oklch(0.708 0 0);
--color-neutral-500: oklch(0.556 0 0);
--color-neutral-600: oklch(0.439 0 0);
--color-neutral-700: oklch(0.371 0 0);
--color-neutral-800: oklch(0.269 0 0);
--color-neutral-900: oklch(0.205 0 0);
--color-neutral-950: oklch(0.145 0 0);
--color-yellow-50: oklch(0.987 0.026 102.212);
--color-yellow-100: oklch(0.973 0.071 103.193);
--color-yellow-200: oklch(0.945 0.129 101.54);
--color-yellow-300: oklch(0.905 0.182 98.111);
--color-yellow-400: oklch(0.852 0.199 91.936);
--color-yellow-500: oklch(0.795 0.184 86.047);
--color-yellow-600: oklch(0.681 0.162 75.834);
--color-yellow-700: oklch(0.554 0.135 66.442);
--color-yellow-800: oklch(0.476 0.114 61.907);
--color-yellow-900: oklch(0.421 0.095 57.708);
--color-yellow-950: oklch(0.286 0.066 53.813);
--color-orange-50: oklch(0.98 0.016 73.684);
--color-orange-100: oklch(0.954 0.038 75.164);
--color-orange-200: oklch(0.901 0.076 70.697);
--color-orange-300: oklch(70.72% 0.182 40.56);
--color-orange-400: oklch(67.4% 0.2072 39.23);
--color-orange-500: oklch(61.86% 0.1946 38.88);
--color-orange-600: oklch(0.646 0.222 41.116);
--color-orange-700: oklch(0.553 0.195 38.402);
--color-orange-800: oklch(0.47 0.157 37.304);
--color-orange-900: oklch(0.408 0.123 38.172);
--color-orange-950: oklch(0.266 0.079 36.259);
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
--color-red-300: oklch(0.808 0.114 19.571);
--color-red-400: oklch(0.704 0.191 22.216);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-700: oklch(0.505 0.213 27.518);
--color-red-800: oklch(0.444 0.177 26.899);
--color-red-900: oklch(0.396 0.141 25.723);
--color-red-950: oklch(0.258 0.092 26.042);
--color-zinc-50: oklch(0.985 0 0);
--color-zinc-100: oklch(0.967 0.001 286.375);
--color-zinc-200: oklch(0.92 0.004 286.32);
--color-zinc-300: oklch(0.871 0.006 286.286);
--color-zinc-400: oklch(0.705 0.015 286.067);
--color-zinc-500: oklch(0.552 0.016 285.938);
--color-zinc-600: oklch(0.442 0.017 285.786);
--color-zinc-700: oklch(0.37 0.013 285.805);
--color-zinc-800: oklch(0.274 0.006 286.033);
--color-zinc-900: oklch(0.21 0.006 285.885);
--color-zinc-950: oklch(0.141 0.005 285.823);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}

View File

@ -0,0 +1,22 @@
html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}

View File

@ -0,0 +1,191 @@
@import 'tailwindcss';
@layer base {
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
/* Dark mode colors. */
:root {
--border: hsla(var(--border-neutral), 0.4);
--backdrop-color: #272727cc;
--sl-color-accent: #ff801f;
--sl-color-accent-high: #ffa057;
--sl-color-accent-low: #562800;
--sl-color-black: #181818;
--sl-color-gray-1: #eee;
--sl-color-gray-2: #c2c2c2;
--sl-color-gray-3: #8b8b8b;
--sl-color-gray-4: #585858;
--sl-color-gray-5: #383838;
--sl-color-gray-6: #272727;
--sl-color-white: #fff;
--list-marker-color: #fb923c;
--border-neutral: 0, 0%, 25.1%;
}
/* Light mode colors. */
:root[data-theme='light'] {
--border: hsla(var(--border-yellow), 0.4);
--backdrop-color: #f6f6f699;
--sl-color-accent: #b73d00;
--sl-color-accent-high: #562800;
--sl-color-accent-low: #ffa057;
--sl-color-black: #fff;
--sl-color-gray-1: #272727;
--sl-color-gray-2: #383838;
--sl-color-gray-3: #585858;
--sl-color-gray-4: #8b8b8b;
--sl-color-gray-5: #c2c2c2;
--sl-color-gray-6: #eee;
--sl-color-gray-7: #f6f6f6;
--sl-color-white: #181818;
--list-marker-color: #fb923c;
--border-yellow: 54.9, 96.7%, 88%;
}
header {
border: none !important;
padding: 0 !important;
}
header.header {
background-color: transparent !important;
height: 4.5rem !important;
margin-inline: auto !important;
padding-block: 0 !important;
padding-inline: 2rem !important;
}
header > div:first-of-type {
backdrop-filter: blur(12px) !important;
background-color: var(--backdrop-color) !important;
border: 1px var(--border) solid;
border-radius: 36px;
height: 100% !important;
margin-inline: auto !important;
margin-top: 1rem !important;
max-width: 1536px;
padding-inline: 2rem !important;
width: auto !important;
}
#starlight__sidebar {
border-radius: 1rem;
margin-top: 2rem !important;
}
.content-panel:first-of-type {
margin-top: 2rem !important;
}
.right-sidebar {
top: 2rem !important;
}
#starlight__on-this-page--mobile {
border: none !important;
}
mobile-starlight-toc > nav {
border: none !important;
border-radius: 1rem;
margin-top: 2rem !important;
}
select {
background-image: none;
box-shadow: none;
}
select:focus-visible {
outline: -webkit-focus-ring-color auto 1px;
}
article.card {
border-radius: 0.5rem;
}
.pagination-links a:hover {
border-color: var(--sl-color-accent);
}
.sl-link-card:hover {
border-color: var(--sl-color-gray-4) !important;
}
.starlight-aside--tip {
background: linear-gradient(45deg, #ff512f, #f09819);
border: none;
border-radius: 0.5rem;
color: #66350c;
}
.starlight-aside--note {
background: linear-gradient(45deg, #00b4db, #2193b0);
border: none;
border-radius: 0.5rem;
color: #004558;
}
.starlight-aside__icon {
transform: scale(0.8);
}
.starlight-aside--tip .starlight-aside__title {
color: #ffe0c2;
}
.starlight-aside--note .starlight-aside__title {
color: #bbf3fef7;
}
.sl-markdown-content ul:not(:where(.not-content *)) {
list-style-type: none;
padding-left: 0;
}
.sl-markdown-content ul:not(:where(.not-content *)) > li {
padding-left: 1.75rem;
position: relative;
}
.sl-markdown-content li:not(:where(.not-content *)) > ul,
.sl-markdown-content li + li:not(:where(.not-content *)) {
margin-top: 0.625rem;
}
.sl-markdown-content ul:not(:where(.not-content *)) > li:before {
background: var(--list-marker-color);
border-radius: 1px;
content: '';
height: 2px;
left: 2px;
position: absolute;
top: 13px;
width: 0.875rem;
}
@media screen and (max-width: 800px) {
mobile-starlight-toc > nav {
border-radius: 1rem;
margin-top: 3rem !important;
}
header > div:first-of-type {
padding-inline-end: 5rem !important;
}
starlight-menu-button > button {
right: 3rem !important;
top: 2.2rem !important;
}
}
@media screen and (max-width: 1280px) {
header.header {
padding-inline: 1.5rem !important;
}
}

View File

@ -0,0 +1,100 @@
@import 'tailwindcss';
@layer base {
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
/* Dark mode colors. */
:root {
--primary-button-hover: #ff801f;
--backdrop-color: #272727cc;
--sl-color-accent: #ff801f;
--sl-color-accent-high: #ffa057;
--sl-color-accent-low: #562800;
--sl-color-black: #181818;
--sl-color-gray-1: #eee;
--sl-color-gray-2: #c2c2c2;
--sl-color-gray-3: #8b8b8b;
--sl-color-gray-4: #585858;
--sl-color-gray-5: #383838;
--sl-color-gray-6: #272727;
--sl-color-white: #fff;
--yellow-hsl: 43.3, 96.4%, 56.3%;
--overlay-yellow: hsla(var(--yellow-hsl), 0.2);
--border: hsla(var(--border-neutral), 0.4);
--border-neutral: 0, 0%, 25.1%;
}
/* Light mode colors. */
:root[data-theme='light'] {
--primary-button-hover: #ff801f;
--backdrop-color: #f6f6f699;
--sl-color-accent: #f76b15;
--sl-color-accent-high: #562800;
--sl-color-accent-low: #ffa057;
--sl-color-black: #fff;
--sl-color-gray-1: #272727;
--sl-color-gray-2: #383838;
--sl-color-gray-3: #585858;
--sl-color-gray-4: #8b8b8b;
--sl-color-gray-5: #c2c2c2;
--sl-color-gray-6: #eee;
--sl-color-gray-7: #f6f6f6;
--sl-color-white: #181818;
--yellow-hsl: 47.9, 95.8%, 53.1%;
--border-yellow: 54.9, 96.7%, 88%;
--border: hsla(var(--border-yellow), 0.4);
}
.page {
background:
linear-gradient(215deg, var(--overlay-yellow), transparent 40%),
radial-gradient(var(--overlay-yellow), transparent 40%) no-repeat center
center / cover,
radial-gradient(var(--overlay-yellow), transparent 65%) no-repeat center
center / cover;
background-blend-mode: overlay;
}
header {
border: none !important;
padding: 0 !important;
}
header.header {
background-color: transparent !important;
height: 4.5rem !important;
margin-inline: auto !important;
padding-block: 0 !important;
padding-inline: 2rem !important;
}
header > div:first-of-type {
backdrop-filter: blur(12px) !important;
background-color: var(--backdrop-color) !important;
border: 1px var(--border) solid;
border-radius: 36px;
height: 100% !important;
margin-inline: auto !important;
margin-top: 1rem !important;
max-width: 1536px;
padding-inline: 2rem !important;
width: auto !important;
}
select {
background-image: none;
box-shadow: none;
}
.sl-link-button.primary:hover {
background-color: var(--primary-button-hover);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.sl-link-button.primary:hover svg {
transform: translateX(0.25rem);
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@ -0,0 +1,54 @@
---
import logoLockupMask from '@images/tenantial-logo-lockup-mask.png';
const {
class: className,
style: inlineStyle,
'aria-label': ariaLabel = 'Tenantial',
...attrs
} = Astro.props;
const logoMaskUrl =
typeof logoLockupMask === 'string' ? logoLockupMask : logoLockupMask.src;
const logoStyle = `--tenantial-logo-mask: url("${logoMaskUrl}");${inlineStyle ?? ''}`;
---
<span
{...attrs}
class:list={[
'tenantial-wordmark inline-block shrink-0 align-middle text-neutral-700 transition-colors duration-300 dark:text-white',
className,
]}
style={logoStyle}
role="img"
aria-label={ariaLabel}
>
<span class="tenantial-wordmark__shape" aria-hidden="true"></span>
</span>
<style>
.tenantial-wordmark {
color: #4b4b4f;
}
:global(.dark) .tenantial-wordmark,
:global([data-theme='dark']) .tenantial-wordmark {
color: #ffffff;
}
.tenantial-wordmark__shape {
display: block;
width: 100%;
aspect-ratio: 898 / 167;
background: currentColor;
mask-image: var(--tenantial-logo-mask);
mask-mode: alpha;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-image: var(--tenantial-logo-mask);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
}
</style>

View File

@ -0,0 +1,147 @@
---
import { getImage } from 'astro:assets';
import { OG, SEO, SITE } from '@data/constants';
import faviconSvgSrc from '@images/icon.svg';
import faviconSrc from '@images/icon.png';
import {
getLocaleFromPath,
isLocale,
localeOg,
localizedPath,
localizeRoutePath,
stripLocalePrefix,
type Locale,
} from '@/i18n';
// Default properties for the Meta component. These values are used if props are not provided.
// 'meta' sets a default description meta tag to describe the page content.
// 'structuredData' defines default structured data in JSON-LD format to enhance search engine understanding of the page (for SEO purposes).
const defaultProps = {
meta: SITE.description,
structuredData: SEO.structuredData,
customDescription: null,
customOgTitle: null,
};
// Extract props with default values assigned from defaultProps. Values can be overridden when the component is used.
// For example:
// <MainLayout title="Custom Title" meta="Custom description." />
const {
meta = defaultProps.meta,
structuredData = defaultProps.structuredData,
customDescription = defaultProps.customDescription,
customOgTitle = defaultProps.customOgTitle,
locale: rawLocale = getLocaleFromPath(Astro.url.pathname),
} = Astro.props;
const locale: Locale = isLocale(rawLocale) ? rawLocale : 'de';
// Use custom description if provided, otherwise use default meta
const description = customDescription || meta;
// Use custom OG title if provided, otherwise use default OG title
const ogTitle = customOgTitle || OG.title;
const ogDescription = customDescription || OG.description;
// Define the metadata for your website and individual pages
const siteURL = `${Astro.site}`; // Set the website URL in astro.config.mjs
const author = SITE.author;
const cleanPath = stripLocalePrefix(Astro.url.pathname);
const canonical = new URL(
localizedPath(cleanPath, locale),
Astro.site || Astro.url.origin
).href;
const socialImageRes = await getImage({
src: OG.image,
width: 1200,
height: 600,
format: 'png',
});
const socialImage = new URL(socialImageRes.src, Astro.site || Astro.url.origin)
.href;
const twitterDomain = new URL(siteURL).hostname;
const alternateLocales: Locale[] = ['de', 'en'];
// Generate and optimize the favicon images
const faviconSvg = await getImage({
src: faviconSvgSrc,
format: 'svg',
});
const appleTouchIcon = await getImage({
src: faviconSrc,
width: 180,
height: 180,
format: 'png',
});
---
{
/* Inject structured data into the page if provided. This data is formatted as JSON-LD, a method recommended by Google for structured data pass:
https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data */
}{
structuredData && (
<script
type="application/ld+json"
set:html={JSON.stringify(structuredData)}
/>
)
}
{/* Define the character set, description, author, and viewport settings */}
<meta charset="utf-8" />
<meta content={description} name="description" />
<meta name="web_author" content={author} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="canonical" href={canonical} />
{
alternateLocales.map(lang => {
const href = new URL(
localizeRoutePath(cleanPath, locale, lang),
Astro.site || Astro.url.origin
).href;
return <link rel="alternate" hreflang={lang} href={href} />;
})
}
<link
rel="alternate"
hreflang="x-default"
href={new URL(
localizeRoutePath(cleanPath, locale, 'de'),
Astro.site || Astro.url.origin
).href}
/>
{/* Facebook Meta Tags */}
<meta property="og:locale" content={localeOg[locale]} />
<meta property="og:url" content={canonical} />
<meta property="og:type" content="website" />
<meta property="og:title" content={ogTitle} />
<meta property="og:site_name" content={SITE.title} />
<meta property="og:description" content={ogDescription} />
<meta property="og:image" content={socialImage} />
<meta content="1200" property="og:image:width" />
<meta content="600" property="og:image:height" />
<meta content="image/png" property="og:image:type" />
{/* Twitter Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content={twitterDomain} />
<meta property="twitter:url" content={canonical} />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={ogDescription} />
<meta name="twitter:image" content={socialImage} />
{/* Links to the webmanifest and sitemap */}
<link rel="manifest" href="/manifest.json" />
{/* https://docs.astro.build/en/guides/integrations-guide/sitemap/ */}
<link rel="sitemap" href="/sitemap-index.xml" />
{/* Links for favicons */}
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
<meta name="mobile-web-app-capable" content="yes" />
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
<link href={appleTouchIcon.src} rel="shortcut icon" />
{/* Set theme color */}
<meta name="theme-color" content="#facc15" />

View File

@ -0,0 +1,59 @@
{/* Dark Theme Toggle Button */}
{
/* This button is shown when the light theme is active, and when clicked, it switches the theme to dark */
}
<button
type="button"
aria-label="Dark Theme Toggle"
title="Toggle theme"
class="hs-dark-mode group hs-dark-mode-active:hidden flex h-8 w-8 items-center justify-center rounded-full font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:bg-neutral-200 hover:text-orange-400 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-orange-300 dark:focus:outline-hidden"
data-hs-theme-click-value="dark"
>
{
/* The SVG displayed shows an abstract icon that represents the moon (dark theme) */
}
<svg
class="size-4 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></svg
>
{/* Light Theme Toggle Button */}
{
/* This button is hidden by default and only appears when the dark theme is active, when clicked, it switches to the light theme */
}
</button>
<button
type="button"
aria-label="Light Theme Toggle"
title="Toggle theme"
class="hs-dark-mode group hs-dark-mode-active:flex hidden h-8 w-8 items-center justify-center rounded-full font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-orange-400 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-orange-300 dark:focus:outline-hidden"
data-hs-theme-click-value="light"
>
{
/* The SVG displayed shows a standard sun icon that stands for the light theme */
}
<svg
class="size-4.5 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="12" r="4"></circle><path d="M12 8a2 2 0 1 0 4 4"
></path><path d="M12 2v2"></path><path d="M12 20v2"></path><path
d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"
></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path
d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"
></path></svg
>
</button>

View File

@ -1,38 +0,0 @@
---
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import Card from '@/components/primitives/Card.astro';
import type { AudienceRowContent } from '@/types/site';
interface Props {
item: AudienceRowContent;
}
const { item } = Astro.props;
---
<Card class="h-full">
<Eyebrow>{item.audience}</Eyebrow>
<Headline as="h3" size="card" class="mt-4">
{item.title}
</Headline>
<Lead class="mt-3" size="body">
{item.description}
</Lead>
<ul class="mt-5 space-y-3 p-0">
{
item.bullets.map((bullet) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-border-subtle)] bg-white/70 px-4 py-3 text-sm text-[var(--color-ink-800)]">
{bullet}
</li>
))
}
</ul>
{item.cta && (
<div class="mt-6">
<SecondaryCTA cta={item.cta} />
</div>
)}
</Card>

View File

@ -1,27 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { CalloutContent } from '@/types/site';
interface Props {
content: CalloutContent;
}
const { content } = Astro.props;
const variant = content.tone === 'accent' ? 'accent' : content.tone === 'subtle' ? 'subtle' : 'default';
const barTone = content.tone === 'accent' ? undefined : content.tone === 'subtle' ? 'trust' : undefined;
---
<Card variant={variant} hoverable>
<div class="callout-bar" data-bar-tone={barTone}>
{content.eyebrow && <Eyebrow>{content.eyebrow}</Eyebrow>}
<Headline as="h3" size="card" class="mt-4">
{content.title}
</Headline>
<Lead class="mt-3" size="body">
{content.description}
</Lead>
</div>
</Card>

View File

@ -1,34 +0,0 @@
---
import Button from '@/components/primitives/Button.astro';
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
points: string[];
title: string;
}
const { cta, points, title } = Astro.props;
---
<Card variant="accent">
<Eyebrow>Qualified outreach</Eyebrow>
<Headline as="h3" size="card" class="mt-4 text-3xl">
{title}
</Headline>
<ul class="mt-5 space-y-3 p-0">
{
points.map((point) => (
<li class="list-none rounded-[1rem] bg-white/72 px-4 py-3 text-sm text-[var(--color-ink-800)]">
{point}
</li>
))
}
</ul>
<div class="mt-6">
<Button href={cta.href} variant={cta.variant ?? 'primary'}>{cta.label}</Button>
</div>
</Card>

View File

@ -1,23 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
interface Props {
description: string;
title: string;
}
const { description, title } = Astro.props;
---
<Card>
<Eyebrow>Conversation focus</Eyebrow>
<Headline as="h3" size="card" class="mt-4">
{title}
</Headline>
<Lead class="mt-3" size="body">
{description}
</Lead>
</Card>

View File

@ -1,23 +0,0 @@
---
interface Props {
class?: string;
tone?: 'accent' | 'neutral' | 'signal';
}
const { class: className = '', tone = 'accent' } = Astro.props;
const toneClasses = {
accent: 'text-[var(--color-brand)]',
neutral: 'text-[var(--color-muted-foreground)]',
signal: 'text-[var(--color-signal)]',
};
---
<p
class:list={[
'm-0 text-[var(--type-eyebrow-size)] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
toneClasses[tone],
className,
]}
>
<slot />
</p>

View File

@ -1,66 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { FeatureItemContent } from '@/types/site';
interface Props {
item: FeatureItemContent;
}
const { item } = Astro.props;
const lucideMap: Record<string, string> = {
'shield': 'lucide:shield',
'database': 'lucide:database',
'refresh': 'lucide:refresh-cw',
'eye': 'lucide:eye',
'file-check': 'lucide:file-check',
'layers': 'lucide:layers',
'search': 'lucide:search',
'lock': 'lucide:lock',
'zap': 'lucide:zap',
'clipboard': 'lucide:clipboard-list',
'git-branch': 'lucide:git-branch',
'bar-chart': 'lucide:bar-chart-3',
'activity': 'lucide:activity',
'settings': 'lucide:settings',
'globe': 'lucide:globe',
'users': 'lucide:users',
'check-circle': 'lucide:check-circle',
'archive': 'lucide:archive',
'trending-up': 'lucide:trending-up',
'cpu': 'lucide:cpu',
};
const iconName = item.icon ? lucideMap[item.icon] : undefined;
---
<Card class="h-full" hoverable>
<div class="space-y-3">
{iconName && (
<div class="feature-icon">
<Icon name={iconName} size={20} />
</div>
)}
{item.eyebrow && <Eyebrow>{item.eyebrow}</Eyebrow>}
<Headline as="h3" size="card">
{item.title}
</Headline>
<Lead size="body">
{item.description}
</Lead>
{(item.meta || item.href) && (
<div class="mt-2 flex flex-wrap items-center gap-3 text-sm">
{item.meta && <span class="text-[var(--color-brand)]">{item.meta}</span>}
{item.href && (
<a class="text-link font-semibold" href={item.href}>
Learn more
</a>
)}
</div>
)}
</div>
</Card>

View File

@ -1,22 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
size?: 'card' | 'display' | 'page' | 'section';
}
const { as = 'h2', class: className = '', size = 'section' } = Astro.props;
const Tag = as;
const sizeClasses = {
display:
'font-[var(--font-display)] font-bold text-[length:var(--type-display-size)] leading-[var(--line-display)] tracking-[var(--tracking-display)]',
page: 'font-[var(--font-display)] font-semibold text-[length:var(--type-page-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
section:
'font-[var(--font-display)] font-semibold text-[length:var(--type-section-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
card: 'font-medium text-[length:var(--type-card-size)] leading-[1.18] tracking-[var(--tracking-tight)]',
};
---
<Tag class:list={['m-0 text-[var(--color-ink-900)] [&>.accent]:text-[var(--color-primary)]', sizeClasses[size], className]}>
<slot />
</Tag>

View File

@ -1,467 +0,0 @@
---
---
<div
class="hero-dashboard"
role="img"
aria-label="TenantAtlas — change history, restore preview, and a review queue with baseline drift and evidence links"
>
<div class="dashboard-chrome">
<div class="dashboard-titlebar">
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<span class="dot dot-red"></span>
<span class="dot dot-yellow"></span>
<span class="dot dot-green"></span>
</div>
<span class="titlebar-label">TenantAtlas — Governance Surface</span>
</div>
<div class="titlebar-url">
<span class="url-text">app.tenantatlas.com/admin/governance/change-history</span>
</div>
</div>
<div class="dashboard-body">
<div class="dashboard-sidebar">
<div class="sidebar-logo">TA</div>
<div class="sidebar-nav">
<div class="nav-item active">
<div class="nav-dot"></div>
<span>Change history</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Restore preview</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Review queue</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Evidence</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Assignments</span>
</div>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-nav">
<div class="nav-item">
<div class="nav-dot muted"></div>
<span>Settings</span>
</div>
<div class="nav-item">
<div class="nav-dot muted"></div>
<span>Audit Log</span>
</div>
</div>
</div>
<div class="dashboard-main">
<div class="stats-row">
<div class="stat-card">
<span class="stat-label">Baseline drift</span>
<span class="stat-value">2 items need review</span>
<span class="stat-change neutral">Windows baseline and Conditional Access</span>
</div>
<div class="stat-card">
<span class="stat-label">Restore preview</span>
<span class="stat-value">Scope and assignments validated</span>
<span class="stat-change positive">Confirmation required before execution</span>
</div>
<div class="stat-card">
<span class="stat-label">Evidence linked</span>
<span class="stat-value">3 change records attached</span>
<span class="stat-change neutral">Review context stays with the operator path</span>
</div>
</div>
<div class="activity-grid">
<div class="activity-section">
<div class="activity-header">
<span class="activity-title">Change record</span>
<span class="activity-badge">Review active</span>
</div>
<div class="activity-table">
<div class="table-row">
<span class="row-status warning"></span>
<span class="row-name">Windows Compliance Baseline</span>
<span class="row-type">Baseline drift</span>
<span class="row-time">Needs review</span>
</div>
<div class="table-row">
<span class="row-status success"></span>
<span class="row-name">BitLocker policy restore</span>
<span class="row-type">Restore preview</span>
<span class="row-time">Assignments in scope</span>
</div>
<div class="table-row">
<span class="row-status success"></span>
<span class="row-name">Conditional Access MFA</span>
<span class="row-type">Evidence linked</span>
<span class="row-time">Ready for approval</span>
</div>
</div>
</div>
<div class="queue-column">
<div class="queue-card">
<span class="queue-label">Restore preview</span>
<p class="queue-title">Scope and assignment edges stay visible before execution.</p>
<ul class="queue-list">
<li>Scope validated</li>
<li>Assignments included</li>
<li>Confirmation required</li>
</ul>
</div>
<div class="queue-card">
<span class="queue-label">Review queue</span>
<ul class="queue-list">
<li>Baseline drift awaiting reviewer</li>
<li>Restore plan queued for approval</li>
<li>Evidence pack linked to the change record</li>
</ul>
</div>
</div>
</div>
<div class="evidence-strip">Evidence stays linked to the change record before an operator acts.</div>
</div>
</div>
</div>
</div>
<style>
.hero-dashboard {
border-radius: 1rem;
overflow: hidden;
border: 1px solid rgba(20, 20, 20, 0.08);
box-shadow:
0 24px 80px rgba(20, 20, 20, 0.1),
0 8px 24px rgba(0, 0, 0, 0.05);
background: white;
}
.dashboard-chrome {
display: flex;
flex-direction: column;
}
.dashboard-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 1rem;
background: #fafafa;
border-bottom: 1px solid rgba(20, 20, 20, 0.06);
}
.dot {
display: block;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
}
.dot-red { background: #ff5f57; }
.dot-yellow { background: #febc2e; }
.dot-green { background: #28c840; }
.titlebar-label {
font-size: 0.7rem;
font-weight: 500;
color: rgba(20, 20, 20, 0.45);
margin-left: 0.5rem;
}
.titlebar-url {
font-size: 0.65rem;
color: rgba(20, 20, 20, 0.35);
background: rgba(20, 20, 20, 0.04);
padding: 0.2rem 0.6rem;
border-radius: 0.25rem;
}
.url-text {
font-family: var(--font-mono);
}
.dashboard-body {
display: grid;
grid-template-columns: 140px 1fr;
min-height: 260px;
}
.dashboard-sidebar {
padding: 0.75rem;
background: #f8f9fb;
border-right: 1px solid rgba(20, 20, 20, 0.05);
}
.sidebar-logo {
width: 1.75rem;
height: 1.75rem;
border-radius: 0.5rem;
background: var(--color-ink-900);
color: white;
font-size: 0.6rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.sidebar-divider {
height: 1px;
background: rgba(20, 20, 20, 0.06);
margin: 0.5rem 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.4rem;
font-size: 0.65rem;
color: rgba(20, 20, 20, 0.5);
border-radius: 0.3rem;
}
.nav-item.active {
background: rgba(40, 60, 120, 0.06);
color: var(--color-ink-900);
font-weight: 600;
}
.nav-dot {
width: 0.35rem;
height: 0.35rem;
border-radius: 50%;
background: var(--color-brand-500);
opacity: 0.6;
}
.nav-dot.muted {
background: rgba(20, 20, 20, 0.18);
}
.nav-item.active .nav-dot {
opacity: 1;
}
.dashboard-main {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.65rem;
}
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.stat-card {
display: flex;
flex-direction: column;
gap: 0.15rem;
padding: 0.5rem 0.6rem;
border-radius: 0.5rem;
border: 1px solid rgba(20, 20, 20, 0.05);
background: #fbfbfd;
}
.stat-label {
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: rgba(20, 20, 20, 0.45);
font-weight: 500;
}
.stat-value {
font-size: 0.94rem;
font-weight: 700;
color: var(--color-ink-900);
letter-spacing: -0.02em;
line-height: 1.25;
}
.stat-change {
font-size: 0.55rem;
font-weight: 500;
line-height: 1.35;
}
.stat-change.positive {
color: var(--color-mint-700);
}
.stat-change.neutral {
color: rgba(20, 20, 20, 0.4);
}
.activity-section {
border: 1px solid rgba(20, 20, 20, 0.05);
border-radius: 0.5rem;
overflow: hidden;
}
.activity-grid {
display: grid;
grid-template-columns: minmax(0, 1.28fr) minmax(14rem, 0.92fr);
gap: 0.5rem;
}
.activity-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.45rem 0.6rem;
background: #fafafa;
border-bottom: 1px solid rgba(20, 20, 20, 0.05);
}
.activity-title {
font-size: 0.65rem;
font-weight: 600;
color: var(--color-ink-900);
}
.activity-badge {
font-size: 0.55rem;
font-weight: 600;
background: rgba(40, 60, 120, 0.07);
color: var(--color-brand-500);
padding: 0.1rem 0.4rem;
border-radius: 999px;
}
.activity-table {
display: flex;
flex-direction: column;
}
.queue-column {
display: grid;
gap: 0.5rem;
}
.queue-card {
border: 1px solid rgba(20, 20, 20, 0.05);
border-radius: 0.5rem;
background: #fbfbfd;
padding: 0.65rem 0.7rem;
}
.queue-label {
display: inline-flex;
font-size: 0.55rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(20, 20, 20, 0.42);
}
.queue-title {
margin: 0.45rem 0 0;
font-size: 0.72rem;
font-weight: 600;
line-height: 1.45;
color: var(--color-ink-900);
}
.queue-list {
margin: 0.55rem 0 0;
padding-left: 1rem;
display: grid;
gap: 0.35rem;
font-size: 0.64rem;
line-height: 1.45;
color: rgba(20, 20, 20, 0.56);
}
.evidence-strip {
border-radius: 0.5rem;
background: rgba(40, 60, 120, 0.06);
color: var(--color-ink-800);
font-size: 0.64rem;
font-weight: 600;
line-height: 1.4;
padding: 0.5rem 0.65rem;
}
.table-row {
display: grid;
grid-template-columns: 0.5rem 1fr auto auto;
gap: 0.5rem;
align-items: center;
padding: 0.4rem 0.6rem;
font-size: 0.6rem;
border-bottom: 1px solid rgba(20, 20, 20, 0.04);
}
.table-row:last-child {
border-bottom: none;
}
.row-status {
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
}
.row-status.success {
background: var(--color-mint-500);
}
.row-status.warning {
background: #febc2e;
}
.row-name {
font-weight: 500;
color: var(--color-ink-900);
}
.row-type {
color: rgba(20, 20, 20, 0.4);
font-weight: 500;
}
.row-time {
color: rgba(20, 20, 20, 0.35);
}
@media (max-width: 640px) {
.dashboard-body {
grid-template-columns: 1fr;
}
.dashboard-sidebar {
display: none;
}
.stats-row {
grid-template-columns: 1fr;
}
.titlebar-url {
display: none;
}
.activity-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,26 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { IntegrationEntry } from '@/types/site';
interface Props {
item: IntegrationEntry;
}
const { item } = Astro.props;
---
<Card class="h-full px-4 py-4" variant="subtle">
<div class="flex items-center gap-3">
<Badge tone="neutral">{item.category}</Badge>
<Headline as="h3" size="card" class="text-base">
{item.name}
</Headline>
</div>
<Lead class="mt-3 max-w-72" size="small">
{item.summary}
</Lead>
{item.note && <p class="mt-2 text-xs font-medium uppercase tracking-[0.14em] text-[var(--color-brand)]">{item.note}</p>}
</Card>

View File

@ -1,19 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
size?: 'body' | 'lead' | 'small';
}
const { as = 'p', class: className = '', size = 'lead' } = Astro.props;
const Tag = as;
const sizeClasses = {
lead: 'text-[length:var(--type-body-size)] leading-[var(--line-body)] sm:text-lg',
body: 'text-[length:var(--type-body-size)] leading-[var(--line-body)]',
small: 'text-[length:var(--type-small-size)] leading-[1.65]',
};
---
<Tag class:list={['m-0 text-[var(--color-copy)]', sizeClasses[size], className]}>
<slot />
</Tag>

View File

@ -1,22 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Lead from '@/components/content/Lead.astro';
import type { MetricItem } from '@/types/site';
interface Props {
item: MetricItem;
}
const { item } = Astro.props;
---
<Card variant="subtle">
<p class="m-0 text-3xl font-semibold tracking-[-0.04em] text-[var(--color-ink-900)]">{item.value}</p>
<Eyebrow class="mt-2">
{item.label}
</Eyebrow>
<Lead class="mt-2" size="small">
{item.description}
</Lead>
</Card>

View File

@ -1,20 +0,0 @@
---
import Button from '@/components/primitives/Button.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
class?: string;
showHelper?: boolean;
size?: 'lg' | 'md' | 'sm';
}
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
---
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="primary">
<Button href={cta.href} variant={cta.variant ?? 'primary'} size={size}>
{cta.label}
</Button>
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
</div>

View File

@ -1,32 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { LegalSection } from '@/types/site';
interface Props {
sections: LegalSection[];
}
const { sections } = Astro.props;
---
<div class="space-y-6">
{
sections.map((section) => (
<Card as="section" class="rounded-[var(--radius-lg)]" variant="subtle">
<Headline as="h2" size="card">
{section.title}
</Headline>
<div class="legal-prose mt-4">
{section.body.map((paragraph) => <Lead size="body">{paragraph}</Lead>)}
{section.bullets && section.bullets.length > 0 && (
<ul>
{section.bullets.map((bullet) => <li>{bullet}</li>)}
</ul>
)}
</div>
</Card>
))
}
</div>

View File

@ -1,20 +0,0 @@
---
import Button from '@/components/primitives/Button.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
class?: string;
showHelper?: boolean;
size?: 'lg' | 'md' | 'sm';
}
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
---
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="secondary">
<Button href={cta.href} variant={cta.variant ?? 'secondary'} size={size}>
{cta.label}
</Button>
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
</div>

View File

@ -1,24 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { TrustPrincipleContent } from '@/types/site';
interface Props {
item: TrustPrincipleContent;
}
const { item } = Astro.props;
---
<Card class="h-full" hoverable>
<div class="callout-bar" data-bar-tone="trust">
<Headline as="h3" size="card">
{item.title}
</Headline>
<Lead class="mt-3" size="body">
{item.description}
</Lead>
{item.note && <Lead class="mt-4 text-[var(--color-brand)]" size="small">{item.note}</Lead>}
</div>
</Card>

View File

@ -1,61 +0,0 @@
---
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import Container from '@/components/primitives/Container.astro';
import { getFooterLead, getFooterNavigationGroups, siteMetadata } from '@/lib/site';
interface Props {
currentPath: string;
}
const { currentPath: _currentPath } = Astro.props;
const currentYear = new Date().getFullYear();
const footerLead = getFooterLead(_currentPath);
const footerNavigationGroups = await getFooterNavigationGroups();
---
<footer class="section-divider px-[var(--space-page-x)] pt-10 sm:pt-12" data-footer-intent={footerLead.intent}>
<Container width="wide">
<div class="surface-card-muted grid gap-8 rounded-[var(--radius-panel)] p-6 lg:grid-cols-[1.3fr,1fr] lg:p-8">
<div class="space-y-5">
<p class="m-0 text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
{footerLead.eyebrow}
</p>
<h2 class="m-0 max-w-xl font-[var(--font-display)] text-3xl leading-[0.98] text-[var(--color-ink-900)] sm:text-4xl">
{footerLead.title}
</h2>
<p class="m-0 max-w-xl text-base leading-7 text-[var(--color-copy)]">
{footerLead.description}
</p>
<PrimaryCTA cta={footerLead.primaryCta} size="sm" />
</div>
<div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-4">
{
footerNavigationGroups.map((group) => (
<div>
<p class="m-0 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-ink-900)]">
{group.title}
</p>
<ul class="mt-4 space-y-3 p-0 text-sm text-[var(--color-copy)]">
{group.items.map((item) => (
<li class="list-none">
<a class="transition hover:text-[var(--color-brand)]" href={item.href}>
{item.label}
</a>
</li>
))}
</ul>
</div>
))
}
</div>
</div>
<div class="flex flex-col gap-3 py-6 text-sm text-[var(--color-copy)] sm:flex-row sm:items-center sm:justify-between">
<p class="m-0">© {currentYear} {siteMetadata.siteName}. Core public route foundation.</p>
<p class="m-0">
Built as a static Astro track with no platform auth, session, or API coupling.
</p>
</div>
</Container>
</footer>

View File

@ -1,108 +0,0 @@
---
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import Container from '@/components/primitives/Container.astro';
import { getHeaderCta, getPrimaryNavigation, isActiveNavigationPath, siteMetadata } from '@/lib/site';
interface Props {
currentPath: string;
}
const { currentPath } = Astro.props;
const headerCta = getHeaderCta(currentPath);
const primaryNavigation = await getPrimaryNavigation();
---
<header class="sticky top-0 z-30 px-[var(--space-page-x)] pt-4 sm:pt-6">
<Container width="wide">
<div
class="shell-panel flex items-center justify-between gap-4 rounded-[var(--radius-panel)] px-4 py-3 sm:px-5"
data-shell-surface="header"
>
<a href="/" class="flex min-w-0 items-center gap-3 no-underline">
<span
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--color-primary),#8eaed1)] font-[var(--font-display)] text-lg text-white shadow-[var(--shadow-inline)]"
>
TA
</span>
<span class="min-w-0">
<span class="block truncate text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-ink-900)]">
{siteMetadata.siteName}
</span>
<span class="block truncate text-sm text-[var(--color-copy)]">
{siteMetadata.siteTagline}
</span>
</span>
</a>
<nav class="hidden items-center gap-1 lg:flex" aria-label="Primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
class:list={[
'rounded-full px-4 py-2 text-sm font-medium transition',
isActiveNavigationPath(currentPath, item.href)
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
: 'text-[var(--color-ink-800)] hover:bg-white/70',
]}
data-nav-link="primary"
>
{item.label}
</a>
))
}
</nav>
<div class="hidden lg:block">
<SecondaryCTA cta={headerCta} size="sm" />
</div>
<details class="relative lg:hidden" data-mobile-nav>
<summary
aria-label="Open navigation menu"
class="flex h-11 w-11 cursor-pointer list-none items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 text-[var(--color-ink-900)]"
data-mobile-nav-trigger
>
<span class="sr-only">Open navigation menu</span>
<span class="flex flex-col gap-1">
<span class="block h-0.5 w-4 bg-current"></span>
<span class="block h-0.5 w-4 bg-current"></span>
<span class="block h-0.5 w-4 bg-current"></span>
</span>
</summary>
<div
class="glass-panel absolute right-0 top-[calc(100%+0.75rem)] w-[min(18rem,88vw)] rounded-[1.5rem] border border-white/80 p-3"
>
<nav class="flex flex-col gap-1" aria-label="Mobile primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
class:list={[
'rounded-[1rem] px-4 py-3 text-sm',
isActiveNavigationPath(currentPath, item.href)
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
: 'text-[var(--color-ink-800)] hover:bg-white/75',
]}
data-nav-link="mobile-primary"
>
<span class="block font-semibold">{item.label}</span>
{item.description && (
<span class="mt-1 block text-xs text-[var(--color-copy)]">
{item.description}
</span>
)}
</a>
))
}
<div class="mt-2 rounded-[1rem] bg-[rgba(47,111,183,0.08)] p-3">
<SecondaryCTA cta={headerCta} size="sm" showHelper />
</div>
</nav>
</div>
</details>
</div>
</Container>
</header>

View File

@ -1,50 +0,0 @@
---
import Footer from '@/components/layout/Footer.astro';
import Navbar from '@/components/layout/Navbar.astro';
import { getPageDefinition } from '@/lib/site';
import { resolveSeo } from '@/lib/seo';
import BaseLayout from '@/layouts/BaseLayout.astro';
interface Props {
currentPath: string;
description?: string;
title?: string;
}
const { currentPath, description, title } = Astro.props;
const seo =
title && description
? resolveSeo({ description, path: currentPath, title })
: undefined;
const pageDefinition = getPageDefinition(currentPath);
---
<BaseLayout
title={title}
description={description}
canonicalUrl={seo?.canonicalUrl}
openGraphTitle={seo?.ogTitle}
openGraphDescription={seo?.ogDescription}
robots={seo?.robots}
>
<div
class="foundation-page site-shell"
data-canonical-path={pageDefinition.canonicalPath}
data-page-family={pageDefinition.family}
data-page-priority={pageDefinition.priority}
data-page-role={pageDefinition.pageRole}
data-shell-tone={pageDefinition.shellTone}
data-surface-group={pageDefinition.surfaceGroup}
data-journey-stage={pageDefinition.journeyStage}
>
<div
class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.68),transparent_28%),radial-gradient(circle_at_top_right,rgba(47,111,183,0.14),transparent_26%)]"
>
</div>
<Navbar currentPath={currentPath} />
<main id="content" class="foundation-main pb-20 sm:pb-24">
<slot />
</main>
<Footer currentPath={currentPath} />
</div>
</BaseLayout>

View File

@ -0,0 +1,41 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import ContactSection from '@components/sections/misc/ContactSection.astro';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import { localeHtmlLang, localizedPath, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].contact;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath('/contact', locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<ContactSection locale={locale} />
</MainLayout>

View File

@ -0,0 +1,530 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Icon from '@components/ui/icons/Icon.astro';
import AccordionItem from '@components/ui/blocks/AccordionItem.astro';
import heroImage from '@images/tenantial-rollout-plan.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].evaluation;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath(copy.routePath, locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta.label}
primaryBtnURL={localizeHref(copy.primaryCta.href, locale)}
secondaryBtn={copy.secondaryCta.label}
secondaryBtnURL={localizeHref(copy.secondaryCta.href, locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroTitle}
/>
{/* Evaluation path */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionRoute" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.pathTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.pathIntro}
</p>
</div>
</div>
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.evaluationSteps.map((step: any, index: number) => (
<li class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{String(index + 1).padStart(2, '0')}
</span>
<h3 class="mt-4 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{step.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{step.content}
</p>
</li>
))
}
</ol>
</div>
</section>
{/* Preparation */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionPrep" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.preparationTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.preparationIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.preparationCards.map((card: any) => (
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Pilot scenarios */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionPilot" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.pilotTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.pilotIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{
copy.pilotScenarios.map((scenario: any) => (
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{scenario.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{scenario.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Stakeholders */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionStakeholders" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.stakeholderTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.stakeholderIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.stakeholderCards.map((card: any) => (
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</section>
{/* Security & procurement */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/20 text-yellow-600 ring-1 ring-yellow-400/40 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionSecurity" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.securityTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.securityIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.securityChecklist.map((item: any) => (
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{item.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{item.content}
</p>
</article>
))
}
</div>
<div
class="mt-8 border-t border-neutral-300 pt-6 dark:border-neutral-700"
>
<h3
class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.securityHandoffTitle}
</h3>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 dark:text-neutral-300"
>
{copy.securityHandoffText}
</p>
<div class="mt-5">
<PrimaryCTA
title={copy.securityHandoffCta.label}
url={localizeHref(copy.securityHandoffCta.href, locale)}
/>
</div>
</div>
</div>
</section>
{/* Microsoft 365 access principles */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionAccess" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.accessTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.accessIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2">
{
copy.accessPrinciples.map((principle: any) => (
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{principle.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{principle.content}
</p>
</article>
))
}
</div>
</section>
{/* Non-requirements */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionLimits" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.nonRequirementTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.nonRequirementIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.nonRequirementCards.map((card: any) => (
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Example timeline */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionTimeline" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.timelineTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.timelineIntro}
</p>
</div>
</div>
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{
copy.timelineEntries.map((entry: any, index: number) => (
<li class="rounded-3xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{String(index + 1).padStart(2, '0')}
</span>
<h3 class="mt-4 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{entry.title}
</h3>
<p class="mt-2 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{entry.content}
</p>
</li>
))
}
</ol>
</section>
{/* Buyer FAQ */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionFaq" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.faqTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.faqIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-x-12 md:grid-cols-2">
{
[0, 1].map((column: number) => (
<div class="hs-accordion-group divide-y divide-neutral-300 dark:divide-neutral-700">
{copy.faqItems
.filter((_item: any, index: number) => index % 2 === column)
.map((item: any, index: number) => {
const itemIndex = index * 2 + column;
return (
<AccordionItem
id={`evaluation-faq-heading-${itemIndex}`}
collapseId={`evaluation-faq-collapse-${itemIndex}`}
question={item.question}
answer={item.answer}
first={index === 0}
/>
);
})}
</div>
))
}
</div>
</div>
</section>
{/* Final CTA */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-wrap gap-4">
{
copy.finalCtas.map((cta: any, index: number) =>
index === 0 ? (
<PrimaryCTA
title={cta.label}
url={localizeHref(cta.href, locale)}
/>
) : (
<SecondaryCTA
title={cta.label}
url={localizeHref(cta.href, locale)}
/>
)
)
}
</div>
</div>
</section>
</MainLayout>

View File

@ -0,0 +1,259 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import FeaturesGeneral from '@components/sections/features/FeaturesGeneral.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import heroImage from '@images/tenantial-dashboard.avif';
import featureImage from '@images/tenantial-review-board.avif';
import { featuresByLocale, siteCopy } from '@data/site-copy';
import { localizeHref, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].home;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta}
primaryBtnURL={localizeHref('/contact', locale)}
secondaryBtn={copy.secondaryCta}
secondaryBtnURL={localizeHref('/platform', locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroAlt}
/>
<FeaturesGeneral
title={copy.featureTitle}
subTitle={copy.featureSubtitle}
src={featureImage}
alt={copy.featureAlt}
features={featuresByLocale[locale]}
/>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.audienceTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.audienceSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-3">
{
copy.audiences.map((audience: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{audience.title}
</h3>
<p class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400">
{audience.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.useCasesTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.useCasesSubtitle}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{
copy.useCases.map((useCase: any) => (
<article class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 shadow-xs dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800">
<p class="text-xs font-semibold tracking-[0.22em] text-neutral-500 uppercase dark:text-neutral-400">
{useCase.eyebrow}
</p>
<h3 class="mt-4 text-2xl font-semibold text-balance text-neutral-800 dark:text-neutral-200">
{useCase.title}
</h3>
<p class="mt-4 max-w-prose text-pretty text-neutral-600 dark:text-neutral-400">
{useCase.content}
</p>
<div class="mt-6">
<SecondaryCTA
title={useCase.cta}
url={localizeHref(useCase.href, locale)}
/>
</div>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.boundaryTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.boundarySubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-3">
{
copy.boundaries.map((boundary: any) => (
<article class="rounded-2xl bg-neutral-200/80 p-5 dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-800 dark:text-neutral-200">
{boundary.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{boundary.content}
</p>
</article>
))
}
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="grid gap-8 border-y border-neutral-300 py-10 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-center lg:py-14 dark:border-neutral-700"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.trustTeaserTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.trustTeaserSubtitle}
</p>
<div class="mt-6">
<SecondaryCTA
title={copy.trustTeaserCta}
url={localizeHref('/trust', locale)}
/>
</div>
</div>
<ul class="grid gap-3">
{
copy.trustPoints.map((point: string) => (
<li class="rounded-2xl border border-neutral-300 bg-neutral-100/70 px-5 py-4 text-sm font-medium text-neutral-700 dark:border-neutral-700 dark:bg-white/[0.04] dark:text-neutral-300">
{point}
</li>
))
}
</ul>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<span
class="text-xs font-semibold tracking-[0.18em] text-yellow-600 uppercase dark:text-yellow-400"
>
{siteCopy[locale].evaluation.discovery.homepageEyebrow}
</span>
<h2
class="mt-3 text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.homepageTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.homepageContent}
</p>
<div class="mt-6">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.homepageCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
</div>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] bg-linear-to-r from-neutral-900 to-neutral-700 px-6 py-8 text-neutral-50 md:px-10 md:py-12 dark:from-neutral-100 dark:to-neutral-300 dark:text-neutral-900"
>
<div class="max-w-(--breakpoint-lg)">
<h2 class="text-2xl font-bold text-balance md:text-3xl">
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-2xl text-pretty text-neutral-200 dark:text-neutral-700"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
<PrimaryCTA
title={copy.finalPrimaryCta}
url={localizeHref('/contact', locale)}
/>
<SecondaryCTA
title={copy.finalSecondaryCta}
url={localizeHref('/platform', locale)}
/>
</div>
</div>
</section>
</MainLayout>

View File

@ -0,0 +1,35 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import { siteCopy } from '@data/site-copy';
import { localizeHref, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].legal;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<MainSection title={copy.heading} subTitle={copy.subtitle} />
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8">
<div class="flex flex-wrap gap-3">
<PrimaryCTA title={copy.privacy} url={localizeHref('/privacy', locale)} />
<SecondaryCTA title={copy.terms} url={localizeHref('/terms', locale)} />
<SecondaryCTA
title={copy.imprint}
url={localizeHref('/imprint', locale)}
/>
</div>
</section>
</MainLayout>

View File

@ -0,0 +1,200 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import LeftSection from '@components/ui/blocks/LeftSection.astro';
import RightSection from '@components/ui/blocks/RightSection.astro';
import FeaturesStats from '@components/sections/features/FeaturesStats.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import dashboard from '@images/tenantial-dashboard.avif';
import evidenceImage from '@images/tenantial-evidence-panel.avif';
import reviewImage from '@images/tenantial-drift-workflow.avif';
import restoreImage from '@images/tenantial-rollout-plan.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].platform;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath('/platform', locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<MainSection
title={copy.heading}
subTitle={copy.subtitle}
btnExists={true}
btnTitle={siteCopy[locale].auth.walkthrough}
btnURL={localizeHref('/contact', locale)}
/>
<section
class="mx-auto max-w-[85rem] px-4 py-4 sm:px-6 lg:px-8 lg:py-8 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.focusTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.focusSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2">
{
copy.focusCards.map((card: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-4 sm:px-6 lg:px-8 lg:py-8 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.useCasesTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.useCasesSubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.useCases.map((useCase: any) => (
<article class="rounded-2xl bg-neutral-200/80 p-5 dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-800 dark:text-neutral-200">
{useCase.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{useCase.content}
</p>
<div class="mt-5">
<SecondaryCTA
title={useCase.cta}
url={localizeHref(useCase.href, locale)}
/>
</div>
</article>
))
}
</div>
</div>
</section>
<RightSection
title={copy.backupTitle}
subTitle={copy.backupSubtitle}
single={false}
imgOne={dashboard}
imgOneAlt={copy.dashboardAlt}
imgTwo={evidenceImage}
imgTwoAlt={copy.evidenceAlt}
/>
<LeftSection
title={copy.driftTitle}
subTitle={copy.driftSubtitle}
img={reviewImage}
imgAlt={copy.driftAlt}
btnExists={true}
btnTitle={copy.trustCta}
btnURL={localizeHref('/trust', locale)}
/>
<RightSection
title={copy.restoreTitle}
subTitle={copy.restoreSubtitle}
single={true}
imgOne={restoreImage}
imgOneAlt={copy.restoreAlt}
btnExists={true}
btnTitle={copy.rolloutCta}
btnURL={localizeHref('/contact', locale)}
/>
<FeaturesStats
title={copy.boundaryTitle}
subTitle={copy.boundarySubtitle}
mainStatTitle={copy.mainStatTitle}
mainStatSubTitle={copy.mainStatSubTitle}
stats={copy.stats}
/>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.platformTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.platformContent}
</p>
<div class="mt-6">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.platformCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
</div>
</div>
</div>
</section>
</MainLayout>

View File

@ -0,0 +1,25 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import PricingSection from '@components/sections/pricing/PricingSection.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import { pricingByLocale, siteCopy } from '@data/site-copy';
import type { Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].pricingIntro;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<MainSection title={copy.heading} subTitle={copy.subtitle} />
<PricingSection pricing={pricingByLocale[locale]} locale={locale} />
</MainLayout>

View File

@ -0,0 +1,575 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import heroImage from '@images/tenantial-review-board.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].reviewPacks;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath('/platform/review-packs', locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta}
primaryBtnURL={localizeHref('/contact', locale)}
secondaryBtn={copy.secondaryCta}
secondaryBtnURL={localizeHref('/platform', locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroAlt}
/>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.problemTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.problemSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-4">
{
copy.problemCards.map((card: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/80 p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="grid gap-8 lg:grid-cols-[minmax(0,0.7fr)_minmax(0,1.3fr)] lg:items-start"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.workflowTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.workflowSubtitle}
</p>
</div>
<div class="grid gap-4 md:grid-cols-2">
{
copy.workflowSteps.map((step: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/70 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{step.step}
</span>
<h3 class="mt-4 text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{step.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{step.content}
</p>
</article>
))
}
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-neutral-200/70 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.anatomyTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.anatomySubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{
copy.reviewPackCards.map((card: any) => (
<article class="flex flex-col rounded-3xl border border-neutral-200/50 bg-white p-6 shadow-xs transition-shadow hover:shadow-sm dark:border-neutral-800/50 dark:bg-neutral-950/80">
<div class="mb-4 flex flex-wrap items-start justify-between gap-3">
<span
class:list={[
'inline-flex shrink-0 items-center justify-center rounded-lg px-2.5 py-1 text-[11px] font-semibold tracking-[0.16em] uppercase',
card.availabilityTone === 'soft-availability'
? 'border border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400'
: 'border border-yellow-200/50 bg-yellow-100/50 text-yellow-800 dark:border-yellow-900/30 dark:bg-yellow-500/10 dark:text-yellow-500',
]}
>
{card.availabilityTone === 'soft-availability'
? copy.softAvailabilityLabel
: copy.availableNowLabel}
</span>
</div>
<h3 class="mb-2 text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="grid gap-8 lg:grid-cols-2">
<div>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.evidenceTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.evidenceSubtitle}
</p>
</div>
<div class="mt-8 grid gap-4">
{
copy.evidenceCards.map((card: any) => (
<article class="flex gap-4 rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 dark:border-neutral-700 dark:bg-white/[0.04]">
<div class="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-800 text-white dark:bg-neutral-200 dark:text-neutral-900">
<svg
class="h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-2 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</div>
</article>
))
}
</div>
</div>
<div>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.decisionTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.decisionSubtitle}
</p>
</div>
<figure
class="mt-8 overflow-hidden rounded-3xl border border-neutral-300 bg-white shadow-sm dark:border-neutral-700 dark:bg-neutral-950/80"
>
<figcaption
class="flex flex-wrap items-center justify-between gap-3 border-b border-neutral-200 bg-neutral-50 px-6 py-4 dark:border-neutral-800 dark:bg-neutral-900/70"
>
<span
class="text-[11px] font-semibold tracking-[0.16em] text-neutral-500 uppercase dark:text-neutral-400"
>
{copy.decisionSampleBadge}
</span>
<span
class="inline-flex items-center gap-2 rounded-full border border-yellow-200/60 bg-yellow-100/70 px-3 py-1 text-xs font-semibold text-yellow-800 dark:border-yellow-900/40 dark:bg-yellow-500/10 dark:text-yellow-300"
>
<span
class="h-1.5 w-1.5 rounded-full bg-yellow-500 dark:bg-yellow-400"
></span>
{copy.decisionSampleStatusValue}
</span>
</figcaption>
<div class="px-6 py-5">
<h3
class="text-base font-semibold text-balance text-neutral-800 dark:text-neutral-200"
>
{copy.decisionSampleTitle}
</h3>
<dl class="mt-4 grid gap-3">
{
copy.decisionSampleRows.map((row: any) => (
<div class="grid gap-1 border-t border-neutral-100 pt-3 first:border-0 first:pt-0 sm:grid-cols-[7rem_1fr] sm:gap-4 dark:border-neutral-800">
<dt class="text-xs font-semibold tracking-[0.14em] text-neutral-500 uppercase dark:text-neutral-400">
{row.label}
</dt>
<dd class="text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{row.value}
</dd>
</div>
))
}
</dl>
</div>
</figure>
<div class="mt-8 grid gap-4 sm:grid-cols-2">
{
copy.decisionCards.map((card: any) => (
<article class="flex gap-4 rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 dark:border-neutral-700 dark:bg-white/[0.04]">
<div class="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
<svg
class="h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-2 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</div>
</article>
))
}
</div>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.boundaryTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.boundarySubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 lg:grid-cols-2">
{
copy.boundaryColumns.map((column: any) => (
<article class="rounded-3xl bg-neutral-200/80 p-6 dark:bg-neutral-900/70">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{column.title}
</h3>
<ul class="mt-4 grid gap-3">
{column.items.map((item: string) => (
<li class="rounded-2xl border border-neutral-300 bg-white/70 px-4 py-3 text-sm leading-6 text-neutral-700 dark:border-neutral-700 dark:bg-neutral-950/50 dark:text-neutral-300">
{item}
</li>
))}
</ul>
</article>
))
}
</div>
<p
class="mt-6 max-w-3xl text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.boundaryNote}
</p>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.audienceTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.audienceSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
{
copy.audienceCards.map((card: any) => (
<article class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-neutral-200/70 p-6 shadow-xs dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800">
<h3 class="text-2xl font-semibold text-balance text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-4 max-w-prose text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] bg-linear-to-br from-neutral-900 to-neutral-700 p-6 md:p-10 dark:from-neutral-950 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2 class="text-2xl font-bold text-balance text-neutral-50 md:text-3xl">
{copy.comparisonTitle}
</h2>
<p class="mt-3 max-w-prose text-pretty text-neutral-300 md:text-lg">
{copy.comparisonSubtitle}
</p>
</div>
<div class="mt-8 hidden gap-4 px-2 md:grid md:grid-cols-[9rem_1fr_1fr]">
<span></span>
<span
class="text-xs font-semibold tracking-[0.18em] text-neutral-400 uppercase"
>
{copy.comparisonRawLabel}
</span>
<span
class="text-xs font-semibold tracking-[0.18em] text-yellow-300 uppercase"
>
{copy.comparisonStoryLabel}
</span>
</div>
<div class="mt-3 grid gap-3">
{
copy.comparisonRows.map((row: any) => (
<article class="grid gap-3 rounded-3xl border border-white/10 bg-white/[0.03] p-4 md:grid-cols-[9rem_1fr_1fr] md:items-stretch">
<h3 class="self-center text-sm font-semibold text-neutral-100">
{row.title}
</h3>
<div class="rounded-2xl bg-white/[0.04] p-4">
<p class="text-[11px] font-semibold tracking-[0.18em] text-neutral-400 uppercase md:hidden">
{copy.comparisonRawLabel}
</p>
<p class="mt-2 text-sm leading-6 text-neutral-400 md:mt-0">
{row.rawExport}
</p>
</div>
<div class="rounded-2xl border border-yellow-400/30 bg-yellow-500/10 p-4">
<p class="text-[11px] font-semibold tracking-[0.18em] text-yellow-300 uppercase md:hidden">
{copy.comparisonStoryLabel}
</p>
<div class="flex gap-2.5 md:items-start">
<span class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-yellow-400 text-neutral-900">
<svg
class="h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</span>
<p class="text-sm leading-6 text-neutral-100">
{row.reviewStory}
</p>
</div>
</div>
</article>
))
}
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="grid gap-8 border-y border-neutral-300 py-10 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-center lg:py-14 dark:border-neutral-700"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.trustTeaserTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.trustTeaserSubtitle}
</p>
<div class="mt-6">
<SecondaryCTA
title={copy.trustTeaserCta}
url={localizeHref('/trust', locale)}
/>
</div>
</div>
<ul class="grid gap-3">
{
copy.trustPoints.map((point: string) => (
<li class="rounded-2xl border border-neutral-300 bg-neutral-100/70 px-5 py-4 text-sm font-medium text-neutral-700 dark:border-neutral-700 dark:bg-white/[0.04] dark:text-neutral-300">
{point}
</li>
))
}
</ul>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.reviewPackTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.reviewPackContent}
</p>
<div class="mt-6">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.reviewPackCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
</div>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] bg-linear-to-r from-neutral-900 to-neutral-700 px-6 py-8 text-neutral-50 md:px-10 md:py-12 dark:from-neutral-100 dark:to-neutral-300 dark:text-neutral-900"
>
<div class="max-w-(--breakpoint-lg)">
<h2 class="text-2xl font-bold text-balance md:text-3xl">
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-2xl text-pretty text-neutral-200 dark:text-neutral-700"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
<PrimaryCTA
title={copy.finalPrimaryCta}
url={localizeHref('/contact', locale)}
/>
<SecondaryCTA
title={copy.finalSecondaryCta}
url={localizeHref('/platform', locale)}
/>
</div>
</div>
</section>
</MainLayout>

View File

@ -0,0 +1,24 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import { siteCopy } from '@data/site-copy';
import type { Locale } from '@/i18n';
const { locale, page } = Astro.props;
interface Props {
locale: Locale;
page: 'privacy' | 'terms' | 'imprint';
}
const copy = siteCopy[locale][page];
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<MainSection title={copy.heading} subTitle={copy.subtitle} />
</MainLayout>

View File

@ -0,0 +1,356 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import { siteCopy } from '@data/site-copy';
import { localizeHref, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].trust;
const statusByKey = new Map<string, any>(
copy.statusLegend.map((status: any) => [status.key, status])
);
const statusLabel = (key: string) => statusByKey.get(key)?.label ?? key;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<section
class="mx-auto max-w-[85rem] px-4 pt-16 pb-10 sm:px-6 lg:px-8 lg:pt-24 lg:pb-14 2xl:max-w-full"
>
<div
class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.42fr)] lg:items-start"
>
<div class="max-w-4xl">
<p class="text-sm font-semibold text-orange-500 dark:text-orange-300">
{copy.heroEyebrow}
</p>
<h1
class="mt-4 text-4xl font-bold text-balance text-neutral-900 md:text-5xl dark:text-neutral-100"
>
{copy.heading}
</h1>
<p
class="mt-5 max-w-3xl text-lg leading-8 text-pretty text-neutral-700 dark:text-neutral-300"
>
{copy.subtitle}
</p>
<p
class="mt-4 max-w-3xl text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.primaryHandoff.summary}
</p>
<div class="mt-8 flex flex-col gap-3 sm:flex-row">
<PrimaryCTA
title={copy.primaryCta}
url={localizeHref(copy.primaryHandoff.href, locale)}
/>
<SecondaryCTA
title={copy.secondaryCta}
url={localizeHref('/platform', locale)}
/>
</div>
</div>
<aside
class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]"
aria-labelledby="trust-status-legend"
>
<h2
id="trust-status-legend"
class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.statusLegendTitle}
</h2>
<p
class="mt-2 text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.statusLegendIntro}
</p>
<dl class="mt-5 grid gap-3">
{
copy.statusLegend.map((status: any) => (
<div class="rounded-lg bg-white/70 p-3 dark:bg-neutral-900/60">
<dt class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{status.label}
</dt>
<dd class="mt-1 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{status.description}
</dd>
</div>
))
}
</dl>
</aside>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-3xl">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.principlesTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.principlesIntro}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.principles.map((principle: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{principle.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{principle.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.topicsTitle}
</h2>
<div class="mt-8 grid gap-5 lg:grid-cols-2">
{
copy.topics.map((topic: any) => (
<article
id={topic.slug}
class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]"
>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{topic.title}
</h3>
<span class="inline-flex w-fit rounded-full border border-orange-300 bg-orange-100 px-3 py-1 text-xs font-semibold text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-200">
{statusLabel(topic.claimStatus)}
</span>
</div>
<p class="mt-4 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{topic.summary}
</p>
<ul class="mt-4 grid gap-2 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{topic.points.map((point: string) => (
<li class="flex gap-2">
<span
class="mt-2 size-1.5 shrink-0 rounded-full bg-orange-400"
aria-hidden="true"
/>
<span>{point}</span>
</li>
))}
</ul>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-3xl">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.documentReadinessTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.documentReadinessIntro}
</p>
</div>
<div class="mt-8 grid gap-4 lg:grid-cols-2">
{
copy.documentReadiness.map((item: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{item.documentType}
</h3>
<span class="inline-flex w-fit rounded-full border border-neutral-300 bg-white px-3 py-1 text-xs font-semibold text-neutral-700 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200">
{statusLabel(item.claimStatus)}
</span>
</div>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{item.availabilitySummary}
</p>
{item.requestPath ? (
<a
class="mt-4 inline-flex rounded-lg text-sm font-semibold text-orange-600 ring-zinc-500 outline-hidden hover:text-orange-700 focus-visible:ring-3 dark:text-orange-300 dark:ring-zinc-200 dark:hover:text-orange-200"
href={localizeHref(item.requestPath, locale)}
>
{copy.primaryHandoff.label}
</a>
) : null}
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="grid gap-8 lg:grid-cols-[minmax(0,0.7fr)_minmax(0,1fr)] lg:items-start"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.dataCategoriesTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.dataCategoriesIntro}
</p>
<div
class="mt-6 rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]"
>
<h3
class="text-base font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.avoidanceTitle}
</h3>
<p
class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.avoidanceText}
</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
{
copy.dataCategories.map((category: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<p class="text-xs font-semibold text-orange-600 uppercase dark:text-orange-300">
{category.boundary}
</p>
<h3 class="mt-2 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{category.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{category.description}
</p>
</article>
))
}
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-3xl">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.permissionTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.permissionIntro}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{
copy.permissionCards.map((card: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-lg border border-neutral-300 bg-neutral-900 p-6 text-neutral-50 md:p-8 dark:border-neutral-700 dark:bg-neutral-100 dark:text-neutral-900"
>
<h2 class="text-2xl font-bold text-balance md:text-3xl">
{copy.handoffTitle}
</h2>
<p
class="mt-3 max-w-3xl text-pretty text-neutral-200 dark:text-neutral-700"
>
{copy.handoffText}
</p>
<p
class="mt-3 max-w-3xl text-sm leading-6 text-neutral-300 dark:text-neutral-600"
>
{copy.hardClaimNote}
</p>
<div class="mt-6">
<PrimaryCTA
title={copy.handoffCta}
url={localizeHref(copy.primaryHandoff.href, locale)}
/>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.trustTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.trustContent}
</p>
<div class="mt-6">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.trustCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
</div>
</div>
</div>
</section>
</MainLayout>

View File

@ -1,26 +0,0 @@
---
interface Props {
class?: string;
tone?: 'accent' | 'neutral' | 'signal' | 'warm';
}
const { class: className = '', tone = 'accent' } = Astro.props;
const toneClasses = {
accent: 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]',
neutral: 'bg-white/78 text-[var(--color-ink-800)]',
signal: 'bg-[var(--surface-trust)] text-[var(--color-signal)]',
warm: 'bg-[rgba(175,109,67,0.14)] text-[var(--color-warm)]',
};
---
<span
class:list={[
'inline-flex w-fit items-center rounded-[var(--radius-pill)] px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
toneClasses[tone],
className,
]}
data-badge-tone={tone}
>
<slot />
</span>

View File

@ -1,72 +0,0 @@
---
import type { ButtonVariant } from '@/types/site';
interface Props {
ariaLabel?: string;
class?: string;
href?: string;
rel?: string;
size?: 'lg' | 'md' | 'sm';
target?: '_blank' | '_self';
type?: 'button' | 'reset' | 'submit';
variant?: ButtonVariant;
}
const {
ariaLabel,
class: className = '',
href,
rel,
size = 'md',
target,
type = 'button',
variant = 'primary',
} = Astro.props;
const baseClass =
'inline-flex items-center justify-center rounded-[var(--radius-pill)] border font-semibold tracking-[var(--tracking-tight)] transition-all duration-200 cursor-pointer';
const sizeClasses = {
sm: 'min-h-10 px-5 text-sm',
md: 'min-h-12 px-6 text-[0.95rem]',
lg: 'min-h-14 px-8 text-base',
};
const variantClasses = {
primary:
'border-transparent bg-[var(--color-ink-900)] text-white shadow-[0_2px_8px_rgba(0,0,0,0.12)] hover:bg-[var(--color-ink-800)] hover:shadow-[0_4px_16px_rgba(0,0,0,0.18)] active:scale-[0.98]',
secondary:
'border-[color:var(--color-border)] bg-white text-[var(--color-secondary-foreground)] hover:border-[var(--color-border-strong)] hover:bg-[var(--surface-muted)] active:scale-[0.98]',
ghost: 'border-transparent bg-transparent text-[var(--color-ink-800)] hover:bg-white/70',
};
const classes = [baseClass, sizeClasses[size], variantClasses[variant], className];
---
{
href ? (
<a
href={href}
target={target}
rel={rel}
aria-label={ariaLabel}
class:list={classes}
data-button-variant={variant}
data-cta-weight={variant}
data-interaction="button"
>
<slot />
</a>
) : (
<button
type={type}
aria-label={ariaLabel}
class:list={classes}
data-button-variant={variant}
data-cta-weight={variant}
data-interaction="button"
>
<slot />
</button>
)
}

View File

@ -1,27 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
hoverable?: boolean;
variant?: 'accent' | 'default' | 'subtle';
[key: string]: unknown;
}
const { as = 'article', class: className = '', hoverable = false, variant = 'default', ...rest } = Astro.props;
const variantClasses = {
default: 'surface-card',
accent: 'surface-card-accent',
subtle: 'surface-card-muted',
};
const Tag = as;
---
<Tag
class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], hoverable && 'card-hoverable', className]}
data-surface={variant}
{...rest}
>
<slot />
</Tag>

View File

@ -1,30 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
gap?: 'lg' | 'md' | 'sm';
justify?: 'between' | 'end' | 'start';
}
const {
as = 'div',
class: className = '',
gap = 'md',
justify = 'start',
} = Astro.props;
const Tag = as;
const gapClasses = {
sm: 'gap-[var(--space-cluster-sm)]',
md: 'gap-[var(--space-cluster)]',
lg: 'gap-[var(--space-cluster-lg)]',
};
const justifyClasses = {
start: 'justify-start',
between: 'justify-between',
end: 'justify-end',
};
---
<Tag class:list={['flex flex-wrap items-center', gapClasses[gap], justifyClasses[justify], className]}>
<slot />
</Tag>

View File

@ -1,32 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
width?: 'content' | 'measure' | 'wide';
wide?: boolean;
}
const {
as = 'div',
class: className = '',
width,
wide = false,
} = Astro.props;
const Tag = as;
const resolvedWidth = width ?? (wide ? 'wide' : 'content');
const widthClasses = {
content: 'max-w-[var(--content-max-width)]',
measure: 'max-w-[var(--reading-max-width)]',
wide: 'max-w-[var(--wide-max-width)]',
};
---
<Tag
class:list={[
'mx-auto w-full px-5 sm:px-6 lg:px-8',
widthClasses[resolvedWidth],
className,
]}
>
<slot />
</Tag>

View File

@ -1,23 +0,0 @@
---
interface Props {
class?: string;
cols?: '2' | '3' | '4';
gap?: 'lg' | 'md';
}
const { class: className = '', cols = '3', gap = 'md' } = Astro.props;
const colClasses = {
'2': 'grid-cols-1 md:grid-cols-2',
'3': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3',
'4': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-4',
};
const gapClasses = {
md: 'gap-[var(--space-grid)] lg:gap-[var(--space-grid-lg)]',
lg: 'gap-6 lg:gap-8',
};
---
<div class:list={['grid', gapClasses[gap], colClasses[cols], className]}>
<slot />
</div>

View File

@ -1,33 +0,0 @@
---
interface Props {
class?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
type?: string;
value?: string;
}
const {
class: className = '',
name,
placeholder,
readonly = false,
type = 'text',
value,
} = Astro.props;
---
<input
type={type}
name={name}
value={value}
placeholder={placeholder}
readonly={readonly}
data-interaction="input"
class:list={[
'min-h-12 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
readonly ? 'cursor-default' : '',
className,
]}
/>

View File

@ -1,47 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
density?: 'base' | 'compact' | 'spacious';
id?: string;
layer?: '1' | '2' | '3';
tone?: 'default' | 'emphasis' | 'muted' | 'tinted' | 'warm';
[key: string]: unknown;
}
const {
as = 'section',
class: className = '',
density = 'base',
id,
layer = '2',
tone = 'default',
...rest
} = Astro.props;
const Tag = as;
const densityClasses = {
compact: 'section-density-compact',
base: 'section-density-base',
spacious: 'section-density-spacious',
};
const toneClasses = {
default: '',
muted: 'section-shell-muted px-3 sm:px-4',
emphasis: 'section-shell-emphasis px-3 sm:px-4',
tinted: 'section-tinted px-3 sm:px-4',
warm: 'section-warm px-3 sm:px-4',
};
---
<Tag
id={id}
data-disclosure-layer={layer}
class:list={[
densityClasses[density],
toneClasses[tone],
className,
]}
{...rest}
>
<slot />
</Tag>

View File

@ -1,36 +0,0 @@
---
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
interface Props {
align?: 'center' | 'left';
class?: string;
description?: string;
eyebrow?: string;
title: string;
titleHtml?: string;
width?: 'default' | 'measure' | 'wide';
}
const {
align = 'left',
class: className = '',
description,
eyebrow,
title,
titleHtml,
width = 'default',
} = Astro.props;
const widthClasses = {
default: 'max-w-3xl',
measure: 'max-w-[var(--reading-max-width)]',
wide: 'max-w-4xl',
};
---
<div class:list={[widthClasses[width], align === 'center' ? 'mx-auto text-center' : '', className]}>
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
{titleHtml ? <Headline><Fragment set:html={titleHtml} /></Headline> : <Headline>{title}</Headline>}
{description && <Lead class="mt-4">{description}</Lead>}
</div>

View File

@ -1,20 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
gap?: 'lg' | 'md' | 'sm' | 'xl';
}
const { as = 'div', class: className = '', gap = 'md' } = Astro.props;
const gapClasses = {
sm: 'flex flex-col gap-[var(--space-stack-sm)]',
md: 'flex flex-col gap-[var(--space-stack)]',
lg: 'flex flex-col gap-[var(--space-stack-lg)]',
xl: 'flex flex-col gap-10',
};
const Tag = as;
---
<Tag class:list={[gapClasses[gap], className]}>
<slot />
</Tag>

View File

@ -1,32 +0,0 @@
---
interface Props {
class?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
rows?: number;
value?: string;
}
const {
class: className = '',
name,
placeholder,
readonly = false,
rows = 5,
value,
} = Astro.props;
---
<textarea
name={name}
rows={rows}
placeholder={placeholder}
readonly={readonly}
data-interaction="textarea"
class:list={[
'min-h-32 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 py-3 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
readonly ? 'cursor-default' : '',
className,
]}
>{value}</textarea>

View File

@ -1,33 +0,0 @@
---
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { CtaLink } from '@/types/site';
interface Props {
description: string;
eyebrow?: string;
primary: CtaLink;
secondary?: CtaLink;
title: string;
}
const { description, eyebrow, primary, secondary, title } = Astro.props;
---
<Section>
<Container width="wide">
<Card variant="accent" class="overflow-hidden" data-cta-section>
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr] lg:items-end">
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
<div class="flex flex-col gap-3 sm:flex-row lg:justify-end">
<PrimaryCTA cta={primary} />
{secondary && <SecondaryCTA cta={secondary} />}
</div>
</div>
</Card>
</Container>
</Section>

View File

@ -1,60 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { CapabilityClusterContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: CapabilityClusterContent[];
title: string;
titleHtml?: string;
}
const { description, eyebrow, items, title, titleHtml } = Astro.props;
---
<Section layer="2" tone="tinted" data-section="capability">
<Container width="wide">
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} titleHtml={titleHtml} description={description} />
<Grid cols="2" gap="lg">
{items.map((cluster) => (
<Card class="h-full" hoverable>
<div class="space-y-4">
<div class="flex items-start justify-between gap-3">
<Headline as="h3" size="card">
{cluster.title}
</Headline>
{cluster.meta && (
<Badge tone="signal">{cluster.meta}</Badge>
)}
</div>
<Lead size="body">
{cluster.description}
</Lead>
<ul class="flex flex-wrap gap-2 p-0">
{cluster.capabilities.map((cap) => (
<li class="list-none rounded-full border border-[color:var(--color-line)] bg-white/60 px-3 py-1.5 text-sm text-[var(--color-ink-800)]">
{cap}
</li>
))}
</ul>
{cluster.href && (
<a class="text-link mt-2 inline-block text-sm font-semibold" href={cluster.href}>
Learn more →
</a>
)}
</div>
</Card>
))}
</Grid>
</div>
</Container>
</Section>

View File

@ -1,30 +0,0 @@
---
import FeatureItem from '@/components/content/FeatureItem.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { FeatureItemContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: FeatureItemContent[];
title: string;
titleHtml?: string;
tone?: 'default' | 'tinted';
}
const { description, eyebrow, items, title, titleHtml, tone = 'default' } = Astro.props;
---
<Section layer="2" tone={tone === 'tinted' ? 'tinted' : 'default'}>
<Container width="wide">
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} titleHtml={titleHtml} description={description} />
<Grid cols="3">
{items.map((item) => <FeatureItem item={item} />)}
</Grid>
</div>
</Container>
</Section>

View File

@ -1,44 +0,0 @@
---
import IntegrationBadge from '@/components/content/IntegrationBadge.astro';
import Badge from '@/components/primitives/Badge.astro';
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import type { IntegrationEntry, LogoStripItem } from '@/types/site';
interface Props {
eyebrow?: string;
items: (IntegrationEntry | LogoStripItem)[];
title: string;
}
const { eyebrow, items, title } = Astro.props;
---
<Section class="pt-8 sm:pt-10" density="compact" layer="2">
<Container width="wide">
<div class="surface-card-muted rounded-[var(--radius-lg)] px-5 py-6">
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
<div class="space-y-3">
{eyebrow && <Badge tone="signal">{eyebrow}</Badge>}
<h2 class="m-0 max-w-2xl font-[var(--font-display)] text-3xl leading-tight text-[var(--color-ink-900)]">
{title}
</h2>
</div>
<div class="flex flex-wrap gap-3">
{
items.map((item) => (
<IntegrationBadge
item={{
category: 'category' in item ? item.category : 'Ecosystem',
name: item.label ?? item.name,
note: item.note,
summary: 'summary' in item ? item.summary : `${item.label} aligns with the launch story.`,
}}
/>
))
}
</div>
</div>
</div>
</Container>
</Section>

View File

@ -1,42 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { OutcomeSectionContent } from '@/types/site';
interface Props {
content: OutcomeSectionContent;
titleHtml?: string;
}
const { content, titleHtml } = Astro.props;
---
<Section layer="2" data-section="outcome">
<Container width="wide">
<div class="space-y-8">
<SectionHeader
eyebrow="Why it matters"
title={content.title}
titleHtml={titleHtml}
description={content.description}
/>
<Grid cols="3">
{content.outcomes.map((outcome) => (
<Card class="h-full" hoverable>
<Headline as="h3" size="card">
{outcome.title}
</Headline>
<Lead class="mt-3" size="body">
{outcome.description}
</Lead>
</Card>
))}
</Grid>
</div>
</Container>
</Section>

View File

@ -1,239 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Cluster from '@/components/primitives/Cluster.astro';
import Container from '@/components/primitives/Container.astro';
import Headline from '@/components/content/Headline.astro';
import HeroDashboard from '@/components/content/HeroDashboard.astro';
import Lead from '@/components/content/Lead.astro';
import Metric from '@/components/content/Metric.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import type { HeroContent, MetricItem } from '@/types/site';
interface Props {
calloutDescription?: string;
calloutTitle?: string;
hero: HeroContent;
metrics?: MetricItem[];
}
const { calloutDescription, calloutTitle, hero, metrics = [] } = Astro.props;
const isHomepageHero = Astro.url.pathname === '/';
const heroHeadlineSize = isHomepageHero ? 'page' : 'display';
const heroLeadSize = isHomepageHero ? 'body' : 'lead';
const heroPrimaryAnchor = hero.primaryAnchor ?? 'headline';
---
<section
class:list={[
isHomepageHero ? 'hero-gradient pt-2 sm:pt-8 lg:pt-14' : 'pt-8 sm:pt-10 lg:pt-14',
]}
data-hero-root
data-hero-surface={isHomepageHero ? 'homepage' : 'page'}
data-homepage-hero={isHomepageHero ? 'true' : undefined}
data-hero-primary-anchor={isHomepageHero && heroPrimaryAnchor === 'composition' ? 'composition' : undefined}
data-section={isHomepageHero ? 'hero' : undefined}
>
<Container width="wide">
{isHomepageHero ? (
<div class="space-y-6 sm:space-y-8 lg:space-y-10" data-disclosure-layer="1" data-hero-layout>
<div class="grid gap-6 lg:grid-cols-[minmax(0,0.82fr)_minmax(22rem,1.18fr)] lg:items-start lg:gap-10">
<div class="motion-rise flex flex-col gap-5 sm:gap-6 lg:max-w-[40rem]" data-hero-panel="text">
<div class="space-y-5" data-hero-anchor-group>
<div data-hero-text-core>
<div data-hero-eyebrow data-hero-segment="eyebrow">
<Badge>{hero.eyebrow}</Badge>
</div>
<div class="mt-3 space-y-4 sm:mt-5 sm:space-y-5">
<div
data-hero-heading
data-hero-primary-anchor={heroPrimaryAnchor === 'headline' ? 'headline' : undefined}
data-hero-segment="headline"
>
<Headline
as="h1"
size="page"
class="max-w-[13ch] text-balance text-[length:clamp(2.7rem,4.6vw,4.8rem)] leading-[0.94] tracking-[-0.045em]"
>
{hero.titleHtml ? <Fragment set:html={hero.titleHtml} /> : hero.title}
</Headline>
</div>
<div
data-hero-copy-role="supporting"
data-hero-supporting-copy
data-hero-segment="supporting-copy"
>
<Lead class="max-w-[36rem] text-[1.02rem] leading-8 text-[var(--color-copy)] sm:text-[1.08rem]">
{hero.description}
</Lead>
</div>
</div>
</div>
{(hero.primaryCta || hero.secondaryCta) && (
<div data-hero-cta-pair data-hero-segment="cta-pair">
<Cluster data-cta-cluster gap="sm" class="items-center sm:gap-[var(--space-cluster)]">
<PrimaryCTA cta={hero.primaryCta} size="lg" />
{hero.secondaryCta && (
<SecondaryCTA cta={hero.secondaryCta} />
)}
</Cluster>
</div>
)}
</div>
</div>
<div
class="motion-rise lg:pt-1"
style="animation-delay: 120ms;"
data-hero-panel="dashboard"
data-hero-primary-anchor={heroPrimaryAnchor === 'product-visual' ? 'product-visual' : undefined}
data-hero-visual
data-hero-visual-style="governance-surface"
data-hero-segment="product-near-visual"
>
<div class="overflow-hidden rounded-[2rem] border border-[color:var(--color-border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.9),rgba(246,248,252,0.94))] p-3 shadow-[var(--shadow-panel-strong)] sm:p-4">
{hero.visualFocus && (
<div class="mb-3 rounded-[1.45rem] border border-[color:var(--color-border-subtle)] bg-white/88 px-4 py-4 sm:px-5">
<p class="m-0 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)] text-[var(--color-brand-500)]">
{hero.visualFocus.eyebrow}
</p>
<p class="mt-2 max-w-[42rem] text-sm font-semibold leading-6 text-[var(--color-ink-900)] sm:text-[0.98rem]">
{hero.visualFocus.title}
</p>
<ul class="mt-3 grid gap-2 p-0 sm:grid-cols-3">
{hero.visualFocus.points.map((point) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-border-subtle)] bg-[var(--surface-muted)] px-3 py-2 text-sm font-medium leading-5 text-[var(--color-ink-800)]">
{point}
</li>
))}
</ul>
</div>
)}
<HeroDashboard />
</div>
</div>
</div>
{hero.trustSubclaims && hero.trustSubclaims.length > 0 && (
<div class="motion-rise space-y-2" data-hero-segment="trust-subclaims" data-hero-trust-signals>
<ul class="flex flex-wrap gap-3 p-0">
{hero.trustSubclaims.map((claim) => (
<li class="list-none rounded-full border border-[color:var(--color-border)] bg-white/80 px-4 py-1.5 text-sm font-medium text-[var(--color-ink-800)]">
{claim}
</li>
))}
</ul>
</div>
)}
</div>
) : (
<!-- Subpage hero: card-based 2-col layout -->
<div
class="grid gap-5 sm:gap-6 lg:grid-cols-[minmax(0,1.08fr)_minmax(20rem,0.92fr)] lg:items-start"
data-disclosure-layer="1"
data-hero-layout
>
<Card class="motion-rise overflow-hidden" data-hero-panel="text">
<div class="space-y-4 sm:space-y-6">
<div data-hero-text-core>
<div data-hero-eyebrow data-hero-segment="eyebrow">
<Badge>{hero.eyebrow}</Badge>
</div>
<div class="mt-3 space-y-4 sm:mt-4 sm:space-y-5">
<div data-hero-heading data-hero-segment="headline">
<Headline
as="h1"
size={heroHeadlineSize}
class="max-w-3xl text-balance"
>
{hero.titleHtml ? <Fragment set:html={hero.titleHtml} /> : hero.title}
</Headline>
</div>
<div data-hero-copy-role="supporting" data-hero-supporting-copy data-hero-segment="supporting-copy">
<Lead class="max-w-2xl" size={heroLeadSize}>
{hero.description}
</Lead>
</div>
</div>
</div>
{(hero.primaryCta || hero.secondaryCta) && (
<div data-hero-cta-pair data-hero-segment="cta-pair">
<Cluster data-cta-cluster gap="sm" class="sm:gap-[var(--space-cluster)]">
<PrimaryCTA cta={hero.primaryCta} />
{hero.secondaryCta && (
<SecondaryCTA cta={hero.secondaryCta} />
)}
</Cluster>
</div>
)}
{hero.highlights && hero.highlights.length > 0 && !hero.trustSubclaims?.length && (
<ul class="grid gap-3 p-0 sm:grid-cols-3">
{hero.highlights.map((highlight) => (
<li class="list-none rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/70 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{highlight}
</li>
))}
</ul>
)}
</div>
</Card>
<div class="grid gap-4 sm:gap-5" data-hero-panel="supporting">
{hero.productVisual && (
<Card
variant="accent"
class="motion-rise overflow-hidden"
data-hero-segment="product-near-visual"
data-hero-visual
>
<img
src={hero.productVisual.src}
alt={hero.productVisual.alt}
class="max-h-[22rem] w-full rounded-[var(--radius-lg)] object-cover object-top"
loading="eager"
/>
</Card>
)}
{hero.trustSubclaims && hero.trustSubclaims.length > 0 && (
<Card variant="subtle" class="motion-rise" data-hero-segment="trust-subclaims">
<div class="space-y-3" data-hero-trust-signals>
<p class="m-0 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)] text-[var(--color-copy)]">
Early trust
</p>
<ul class="grid gap-2.5 p-0">
{hero.trustSubclaims.map((claim) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-line)] bg-white/82 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{claim}
</li>
))}
</ul>
</div>
</Card>
)}
{!hero.productVisual && (calloutTitle || calloutDescription) && (
<Card variant="accent" class="motion-rise">
<p class="m-0 text-sm font-semibold uppercase tracking-[0.15em] text-[var(--color-brand)]">
Trust-first launch surface
</p>
{calloutTitle && (
<h2 class="mt-4 font-[var(--font-display)] text-3xl font-bold leading-tight text-[var(--color-ink-900)]">
{calloutTitle}
</h2>
)}
{calloutDescription && (
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{calloutDescription}</p>
)}
</Card>
)}
{metrics.length > 0 && (
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
{metrics.map((metric) => <Metric item={metric} />)}
</div>
)}
</div>
</div>
)}
</Container>
</section>

View File

@ -1,53 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { ProgressTeaserContent } from '@/types/site';
interface Props {
content: ProgressTeaserContent;
}
const { content } = Astro.props;
function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
---
<Section layer="2" data-section="progress">
<Container width="wide">
<div class="space-y-8">
<SectionHeader
eyebrow="Visible progress"
title={content.title}
description={content.description}
/>
<div class="space-y-4">
{content.entries.map((entry) => (
<Card class="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-6">
<Badge tone="signal">{formatDate(entry.date)}</Badge>
<div>
<Headline as="h3" size="card">
{entry.title}
</Headline>
<Lead class="mt-2" size="body">
{entry.description}
</Lead>
</div>
</Card>
))}
</div>
<PrimaryCTA cta={content.cta} />
</div>
</Container>
</Section>

View File

@ -1,29 +0,0 @@
---
import TrustPrincipleCard from '@/components/content/TrustPrincipleCard.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { TrustPrincipleContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: TrustPrincipleContent[];
title: string;
titleHtml?: string;
}
const { description, eyebrow, items, title, titleHtml } = Astro.props;
---
<Section layer="2" tone="warm">
<Container width="wide">
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} titleHtml={titleHtml} description={description} />
<Grid cols="3">
{items.map((item) => <TrustPrincipleCard item={item} />)}
</Grid>
</div>
</Container>
</Section>

View File

@ -0,0 +1,80 @@
---
// Import the necessary dependencies
import { Image } from 'astro:assets';
import IconBlock from '@components/ui/blocks/IconBlock.astro';
import Icon from '@components/ui/icons/Icon.astro';
interface Feature {
heading: string;
content: string;
svg: string;
}
interface Props {
title?: string;
subTitle?: string;
features?: Feature[];
src?: any;
alt?: string;
}
// Define props from Astro
const { title, subTitle, src, alt, features } = Astro.props;
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Block to display the feature image */}
<div class="relative mb-6 overflow-hidden md:mb-8">
{
src && alt && (
<Image
src={src}
alt={alt}
class="h-full w-full object-cover object-center"
draggable={'false'}
format={'avif'}
loading={'eager'}
/>
)
}
</div>
{
/* Displaying the main content consisting of title, subtitle, and several `IconBlock` components */
}
<div class="mt-5 grid gap-8 lg:mt-16 lg:grid-cols-3 lg:gap-12">
{/* Block for title and subtitle */}
<div class="lg:col-span-1">
{/* Rendering title */}
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{title}
</h2>
{/* Rendering subtitle */}
{
subTitle && (
<p class="mt-2 text-pretty text-neutral-600 md:mt-4 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{/* Block to display the IconBlock components */}
<div class="lg:col-span-2">
<div class="grid gap-8 sm:grid-cols-2 md:gap-12">
{/* Injecting IconBlock components with different properties */}
{
features &&
features.map(feature => (
<IconBlock heading={feature.heading} content={feature.content}>
<Icon name={feature.svg} />
</IconBlock>
))
}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,97 @@
---
// Import the necessary dependencies
import TabNav from '@components/ui/blocks/TabNav.astro';
import TabContent from '@components/ui/blocks/TabContent.astro';
import Icon from '@components/ui/icons/Icon.astro';
// Define props from Astro
const { title, tabs } = Astro.props;
// Define TypeScript interface for tab object
interface Tab {
heading: string;
content: string;
svg: string;
src: any;
alt: string;
first?: boolean;
second?: boolean;
}
// Define TypeScript interface for props
interface Props {
title?: string;
tabs: Tab[];
}
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="relative p-6 md:p-16">
<div
class="relative z-10 lg:grid lg:grid-cols-12 lg:items-center lg:gap-16"
>
{/* Section's heading and tab navigation */}
<div class="mb-10 lg:order-2 lg:col-span-6 lg:col-start-8 lg:mb-0">
<h2
class="text-2xl font-bold text-neutral-800 sm:text-3xl dark:text-neutral-200"
>
{
/* About Fragment: https://docs.astro.build/en/basics/astro-syntax/#fragments */
}
<Fragment set:html={title} />
</h2>
{
/* Tab navigation - use the attribute 'first' in the first TabNav for the component to work */
}
<nav class="mt-5 grid gap-4 md:mt-10" aria-label="Tabs" role="tablist">
{
tabs.map((tab, index) => (
<TabNav
id={`tabs-with-card-item-${index + 1}`}
dataTab={`#tabs-with-card-${index + 1}`}
aria={`tabs-with-card-${index + 1}`}
heading={tab.heading}
content={tab.content}
first={tab.first}
>
<Icon name={tab.svg} />
</TabNav>
))
}
</nav>
</div>
{
/* Contents for each tab - the 'first' attribute should be used in the first tab for that tab to be initially visible, 'second' changes the styles */
}
<div class="lg:col-span-6">
<div class="relative">
<div>
{
tabs.map((tab, index) => (
<TabContent
id={`tabs-with-card-${index + 1}`}
aria={`tabs-with-card-item-${index + 1}`}
src={tab.src}
alt={tab.alt}
first={tab.first}
second={tab.second}
/>
))
}
</div>
</div>
</div>
</div>
<div class="absolute inset-0 grid h-full w-full grid-cols-12">
{/* Decorative background and sizing */}
<div
class="col-span-full h-5/6 w-full rounded-xl bg-neutral-100 sm:h-3/4 lg:col-span-7 lg:col-start-6 lg:h-full dark:bg-white/[.075]"
>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,62 @@
---
// Import the necessary components
import StatsBig from '@components/ui/blocks/StatsBig.astro';
import StatsSmall from '@components/ui/blocks/StatsSmall.astro';
const { title, subTitle, stats, mainStatTitle, mainStatSubTitle } = Astro.props;
interface Props {
title: string;
subTitle?: string;
mainStatTitle: string;
mainStatSubTitle: string;
stats?: Stat[];
}
// TypeScript type for the statistics
type Stat = {
stat: string;
description: string;
};
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
{/* Main title */}
<h2
class="mb-4 text-3xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
{/* Subtitle */}
{
subTitle && (
<p class="mb-16 max-w-prose font-normal text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{/* Grid container for statistics */}
<div class="grid items-center gap-6 lg:grid-cols-12 lg:gap-12">
{/* First grid item, showing a big statistics */}
<div class="lg:col-span-4">
<StatsBig title={mainStatTitle} subTitle={mainStatSubTitle} />
</div>
{/* Second grid item, showing multiple small statistics */}
{
stats && (
<div class="relative lg:col-span-8 lg:before:absolute lg:before:-start-12 lg:before:top-0 lg:before:h-full lg:before:w-px lg:before:bg-neutral-300 lg:dark:before:bg-neutral-700">
<div class="grid grid-cols-2 gap-6 sm:gap-8 md:grid-cols-4 lg:grid-cols-3">
{/* Iterate over the 'stats' array and create a 'StatsSmall' component for each object in the array */}
{stats.map(stat => (
<StatsSmall title={stat.stat} subTitle={stat.description} />
))}
</div>
</div>
)
}
</div>
</section>

View File

@ -0,0 +1,60 @@
---
import { Image } from 'astro:assets';
import product5 from '@images/features-image.avif';
// Define props from Astro
const { title, subTitle, benefits } = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
benefits?: Array<string>;
}
// Define SVG marker to be used in the component
const ListItemMarker: string = `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mt-0.5 h-6 w-6 text-orange-400 dark:text-orange-300 flex-none"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>`;
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Grid */}
<div class="lg:grid lg:grid-cols-12 lg:items-center lg:gap-16">
<div class="lg:col-span-7">
<Image class="rounded-xl" src={product5} alt="Mockup of floating boxes" />
</div>
<div class="mt-5 sm:mt-10 lg:col-span-5 lg:mt-0">
<div class="space-y-6 sm:space-y-8">
<div class="space-y-2 md:space-y-4">
<h2
class="text-3xl font-bold text-balance text-neutral-800 lg:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{
subTitle && (
<p class="text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{
benefits && (
<ul class="space-y-2 sm:space-y-4">
{benefits.map(item => (
<li class="flex space-x-3">
<Fragment set:html={ListItemMarker} />
<span class="text-base font-medium text-pretty text-neutral-600 dark:text-neutral-400">
{item}
</span>
</li>
))}
</ul>
)
}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,58 @@
---
// Define props from Astro
const { title, subTitle, partners } = Astro.props;
interface Partner {
icon: any;
name?: string;
href?: string;
}
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
partners: Partner[];
}
const visiblePartners = partners.filter(
partner => partner.href && partner.icon
);
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Title and description */}
<div class="mx-auto mb-6 w-full space-y-1 text-center sm:w-1/2 lg:w-1/3">
<h2
class="text-2xl leading-tight font-bold text-balance text-neutral-800 sm:text-3xl dark:text-neutral-200"
>
{title}
</h2>
{
subTitle && (
<p class="leading-tight text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
<div
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
>
{/* Clients Group SVGs */}
{
visiblePartners.map(partner => (
<a
href={partner.href}
target="_blank"
rel="noopener noreferrer"
aria-label={partner.name}
>
<div set:html={partner.icon} />
</a>
))
}
</div>
</section>

View File

@ -0,0 +1,114 @@
---
// Import the necessary dependencies
import { Image } from 'astro:assets';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import ReviewComponent from '@components/ui/blocks/ReviewComponent.astro';
// Define props from Astro
const {
title,
subTitle,
supportingLine,
primaryBtn,
primaryBtnURL,
secondaryBtn,
secondaryBtnURL,
withReview,
avatars,
rating,
reviews,
src,
alt,
} = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
supportingLine?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
withReview?: boolean;
avatars?: Array<string>;
rating?: string;
reviews?: string;
src?: any;
alt?: string;
}
---
{/* Defining a grid container that holds all the content */}
<section
class="mx-auto grid max-w-[85rem] gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
>
{/* Title and description */}
<div>
{
/* Each h1 and p tag renders a portion of the title and subTitle defined above */
}
<h1
class="block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-6xl lg:leading-tight dark:text-neutral-200"
>
{
/* About Fragment: https://docs.astro.build/en/basics/astro-syntax/#fragments */
}
<Fragment set:html={title} />
</h1>
{
subTitle && (
<p class="mt-3 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-400">
{subTitle}
</p>
)
}
{
supportingLine && (
<p class="mt-4 max-w-2xl text-sm font-semibold leading-6 text-neutral-800 dark:text-neutral-300">
{supportingLine}
</p>
)
}
{
/* Action Button Section: This section includes two CTAs with their own styles and URL */
}
<div class="mt-7 grid w-full gap-3 sm:inline-flex">
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
{
secondaryBtn && (
<SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />
)
}
</div>
{
/* Review Section: This section presents avatars, review ratings and the number of reviews */
}
{
withReview ? (
<ReviewComponent avatars={avatars} rating={rating} reviews={reviews} />
) : (
''
)
}
</div>
{/* Hero Image Section */}
<div class="flex w-full">
<div class="top-12 overflow-hidden">
{
src && alt && (
<Image
src={src}
alt={alt}
class="h-full w-full scale-110 object-cover object-center"
draggable={'false'}
loading={'eager'}
format={'avif'}
/>
)
}
</div>
</div>
</section>

View File

@ -0,0 +1,148 @@
---
// Import the necessary dependencies
import GithubBtn from '@components/ui/buttons/GithubBtn.astro';
// Define props from Astro
const { title, subTitle, url } = Astro.props;
const btnTitle =
Astro.currentLocale === 'fr'
? 'Continuer avec Github'
: 'Continue with Github';
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
url?: string;
}
---
<section
class="relative mx-auto max-w-[85rem] px-4 pt-10 pb-24 sm:px-6 lg:px-8"
>
{/* Decorating SVG elements */}
<div
class="absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
>
<svg
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#ea580c"
viewBox="0 0 24 24"
>
<path
fill="#ea580c"
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="M12 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 18a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
></path>
<path
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="M21 7.353v9.294a.6.6 0 0 1-.309.525l-8.4 4.666a.6.6 0 0 1-.582 0l-8.4-4.666A.6.6 0 0 1 3 16.647V7.353a.6.6 0 0 1 .309-.524l8.4-4.667a.6.6 0 0 1 .582 0l8.4 4.667a.6.6 0 0 1 .309.524Z"
></path>
<path
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="m3.528 7.294 8.18 4.544a.6.6 0 0 0 .583 0l8.209-4.56M12 21v-9"
></path>
</svg>
</div>
<div class="absolute top-0 left-[85%] scale-75">
<svg
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#fbbf24"
viewBox="0 0 24 24"
>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
></path>
<path
fill="#fbbf24"
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 10.5V9M5 15v-1.5"></path>
<path
fill="#fbbf24"
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM19 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
></path>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19H9M15 19h-1.5"></path>
</svg>
</div>
<div
class="absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
>
<svg
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#a3a3a3"
viewBox="0 0 24 24"
>
<path
stroke="#a3a3a3"
stroke-linecap="round"
stroke-linejoin="round"
d="M5.164 17c.29-1.049.67-2.052 1.132-3M11.5 7.794A16.838 16.838 0 0 1 14 6.296M4.5 22a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
></path>
<path
stroke="#a3a3a3"
stroke-linecap="round"
stroke-linejoin="round"
d="M9.5 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM19.5 7a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
></path>
</svg>
</div>
{/* Hero Section Heading */}
<div class="mx-auto mt-5 max-w-xl text-center">
<h2
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
>
{title}
</h2>
</div>
{/* Hero Section Sub-heading */}
<div class="mx-auto mt-5 max-w-3xl text-center">
{
subTitle && (
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{/* Github Button */}
{
url && (
<div class="mt-8 flex justify-center gap-3">
<GithubBtn url={url} title={btnTitle} />
</div>
)
}
</section>

View File

@ -0,0 +1,36 @@
---
import { localizeHref, type Locale } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const { locale = 'de' } = Astro.props;
interface Props {
locale?: Locale;
}
const userSVG = `<svg
class="h-4 w-4 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>`;
const copy = siteCopy[locale].auth;
---
<a
href={localizeHref('/contact', locale)}
data-astro-prefetch
class="flex items-center gap-x-2 text-base font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-orange-400 focus-visible:ring-3 md:my-6 md:border-s md:border-neutral-300 md:ps-6 md:text-sm 2xl:text-base dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-orange-300 dark:focus:outline-hidden"
>
<Fragment set:html={userSVG} />
{copy.walkthrough}
</a>

View File

@ -0,0 +1,128 @@
---
// Import the necessary dependencies.
import AuthBtn from '@components/ui/buttons/AuthBtn.astro';
import ContactIconBlock from '@components/ui/blocks/ContactIconBlock.astro';
import TextInput from '@components/ui/forms/input/TextInput.astro';
import EmailContactInput from '@components/ui/forms/input/EmailContactInput.astro';
import PhoneInput from '@components/ui/forms/input/PhoneInput.astro';
import TextAreaInput from '@components/ui/forms/input/TextAreaInput.astro';
import Icon from '@components/ui/icons/Icon.astro';
import { localizeHref, type Locale } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const { locale = 'de' } = Astro.props;
interface Props {
locale?: Locale;
}
const copy = siteCopy[locale].contact;
---
{/* Contact Us */}
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<div class="mx-auto max-w-2xl lg:max-w-5xl">
<div class="text-center">
<h1
class="text-2xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
>
{copy.title}
</h1>
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
{copy.subtitle}
</p>
</div>
<div class="mt-12 grid items-center gap-6 lg:grid-cols-2 lg:gap-16">
<div class="flex flex-col rounded-xl p-4 sm:p-6 lg:p-8">
<h2
class="mb-8 text-xl font-bold text-neutral-700 dark:text-neutral-300"
>
{copy.formTitle}
</h2>
{
/* Form for user input with various input fields.-->
{/* Each field utilizes a different input component for the specific type of input (text, email, phone, and textarea)*/
}
<form
action="mailto:hello@tenantial.com"
method="post"
enctype="text/plain"
>
<div class="grid gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<TextInput
id="hs-firstname-contacts"
label={copy.firstName}
name="hs-firstname-contacts"
/>
<TextInput
id="hs-lastname-contacts"
label={copy.lastName}
name="hs-lastname-contacts"
/>
</div>
<EmailContactInput id="hs-email-contacts" label={copy.email} />
<PhoneInput id="hs-phone-number" label={copy.phone} />
<TextAreaInput
id="hs-about-contacts"
label={copy.details}
name="hs-about-contacts"
/>
</div>
<div class="mt-4 grid">
<AuthBtn title={copy.submit} />
</div>
<div class="mt-3 text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
{copy.formSubtitle}
</p>
</div>
</form>
</div>
{
/*ContactIconBlocks are used to display different methods of contacting, including visiting office, email, browsing knowledgebase, and FAQ.*/
}
<div class="divide-y divide-neutral-300 dark:divide-neutral-700">
<ContactIconBlock
heading={copy.blocks.docsHeading}
content={copy.blocks.docsContent}
isLinkVisible={true}
linkTitle={copy.blocks.docsLink}
linkURL={localizeHref('/welcome-to-docs/', locale)}
isArrowVisible={true}
><Icon name="question" />
</ContactIconBlock>
<ContactIconBlock
heading={copy.blocks.faqHeading}
content={copy.blocks.faqContent}
isLinkVisible={true}
linkTitle={copy.blocks.faqLink}
linkURL={localizeHref('/#faq', locale)}
isArrowVisible={true}
><Icon name="chatBubble" />
</ContactIconBlock>
<ContactIconBlock
heading={copy.blocks.boundaryHeading}
content={copy.blocks.boundaryContent}
isAddressVisible={false}
><Icon name="mapPin" />
</ContactIconBlock>
<ContactIconBlock
heading={copy.blocks.emailHeading}
content={copy.blocks.emailContent}
isLinkVisible={true}
linkTitle="hello@tenantial.com"
linkURL="mailto:hello@tenantial.com"
><Icon name="envelopeOpen" />
</ContactIconBlock>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,79 @@
---
// Import the necessary AccordionItem component and JSON data
import AccordionItem from '@components/ui/blocks/AccordionItem.astro';
// Define props from Astro
const { title, faqs } = Astro.props;
// Define TypeScript interface for props
interface Faq {
question: string;
answer: string;
}
interface FaqGroup {
subTitle?: string;
faqs: Faq[];
}
interface Props {
title: string;
faqs: FaqGroup;
}
// Define a helper function to generate ids dynamically.
const makeId = (base: any, index: any) => `${base}${index + 1}`;
---
{
/* Main container that holds all content. Customized for different viewport sizes. */
}
<section
id="faq"
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="grid gap-10 md:grid-cols-5">
<div class="md:col-span-2">
<div class="max-w-xs">
<h2
class="text-2xl font-bold text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
>
<Fragment set:html={title} />
</h2>
<p class="mt-1 hidden text-neutral-600 md:block dark:text-neutral-400">
{faqs.subTitle}
</p>
</div>
</div>
{/* FAQ accordion items */}
<div class="md:col-span-3">
<div
class="hs-accordion-group divide-y divide-neutral-200 dark:divide-neutral-700"
>
{
faqs.faqs.map((question, i) => {
{
/* Generate ids dynamically for each FAQ accordion item. */
}
let id = makeId(
'hs-basic-with-title-and-arrow-stretched-heading-',
i
);
let collapseId = makeId(
'hs-basic-with-title-and-arrow-stretched-collapse',
i
);
return (
<AccordionItem
{...question}
id={id}
collapseId={collapseId}
first={i === 0}
/>
);
})
}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,86 @@
---
// Import the necessary dependencies
import EmailFooterInput from '@components/ui/forms/input/EmailFooterInput.astro';
import BrandLogo from '@components/BrandLogo.astro';
import { SITE } from '@data/constants';
import { getLocaleFromPath, localizeHref } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const locale = getLocaleFromPath(Astro.url.pathname);
const copy = siteCopy[locale].footer;
type FooterSection = {
section: string;
links: Array<{ name: string; url: string }>;
};
---
<footer class="w-full bg-neutral-300 dark:bg-neutral-900">
<div
class="mx-auto w-full max-w-[85rem] px-4 py-10 sm:px-6 lg:px-16 lg:pt-20 2xl:max-w-(--breakpoint-2xl)"
>
<div class="grid grid-cols-2 gap-6 md:grid-cols-4 lg:grid-cols-5">
<div class="col-span-full lg:col-span-1">
{/* Brand Logo */}
<BrandLogo class="h-auto w-40" />
</div>
{/* An array of links for Product and Company sections */}
{
copy.sections.map((section: FooterSection) => (
<div class="col-span-1 min-w-0">
<h3
class="break-words font-bold text-neutral-800 dark:text-neutral-200"
>
{section.section}
</h3>
<ul class="mt-3 grid space-y-3">
{section.links.map(link => (
<li>
<a
href={localizeHref(link.url, locale)}
class="inline-flex max-w-full gap-x-2 rounded-lg break-words whitespace-normal text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-neutral-500 focus-visible:ring-3 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-neutral-300 dark:focus:outline-hidden"
>
{link.name}
</a>
</li>
))}
</ul>
</div>
))
}
<div class="col-span-2 min-w-0">
<h3 class="break-words font-bold text-neutral-800 dark:text-neutral-200">
{copy.conversationTitle}
</h3>
<div>
<EmailFooterInput
title={copy.contactButton}
url={localizeHref('/contact', locale)}
/>
<p class="mt-3 break-words text-sm text-neutral-600 dark:text-neutral-400">
{copy.conversationContent}
</p>
</div>
</div>
</div>
<div
class="mt-9 grid gap-y-2 sm:mt-12 sm:flex sm:items-center sm:justify-between sm:gap-y-0"
>
<div class="flex items-center justify-between">
<p class="break-words text-sm text-neutral-600 dark:text-neutral-400">
© <span id="current-year"></span>
{SITE.title}. {copy.copyrightSuffix}
</p>
</div>
</div>
<script>
const year = new Date().getFullYear();
const element = document.getElementById('current-year');
element!.innerText = year.toString();
</script>
</div>
</footer>

View File

@ -0,0 +1,213 @@
---
//Import relevant dependencies
import ThemeIcon from '@components/ThemeIcon.astro';
import NavLink from '@components/ui/links/NavLink.astro';
import Authentication from '../misc/Authentication.astro';
import BrandLogo from '@components/BrandLogo.astro';
import LanguagePicker from '@components/ui/LanguagePicker.astro';
import { getLocaleFromPath, localizeHref } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const locale = getLocaleFromPath(Astro.url.pathname);
const strings = siteCopy[locale];
const homeUrl = localizeHref('/', locale);
type NavItem = {
name: string;
url: string;
};
---
{/* Main header component */}
<header
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm md:flex-nowrap md:justify-start"
>
{/* Navigation container */}
<nav
class="relative mx-2 w-full rounded-[36px] border border-yellow-100/60 bg-yellow-50/95 px-4 py-3 shadow-sm backdrop-blur-lg md:flex md:items-center md:justify-between md:px-6 md:py-0 lg:px-8 xl:mx-auto dark:border-neutral-700/60 dark:bg-neutral-900/95 dark:backdrop-blur-lg"
aria-label="Global"
>
<div class="flex items-center justify-between">
{/* Brand logo */}
<a
class="flex-none rounded-lg text-xl font-bold ring-zinc-500 outline-hidden focus-visible:ring-3 dark:ring-zinc-200 dark:focus:outline-hidden"
href={homeUrl}
aria-label="Brand"
>
<BrandLogo class="h-auto w-36 sm:w-40" />
</a>
{/* Collapse toggle for smaller screens */}
<div class="mr-5 ml-auto md:hidden">
<button
type="button"
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-hidden"
data-hs-collapse="#navbar-collapse-with-animation"
aria-controls="navbar-collapse-with-animation"
aria-label="Toggle navigation"
>
<svg
class="hs-collapse-open:hidden h-[1.25rem] w-[1.25rem] shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="3" x2="21" y1="6" y2="6"></line>
<line x1="3" x2="21" y1="12" y2="12"></line>
<line x1="3" x2="21" y1="18" y2="18"></line>
</svg>
<svg
class="hs-collapse-open:block hidden h-[1.25rem] w-[1.25rem] shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
{/* ThemeIcon component specifically for smaller screens */}
<span class="inline-block md:hidden">
<ThemeIcon />
</span>
</div>
{/* Contains navigation links */}
<div
id="navbar-collapse-with-animation"
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block"
>
{/* Navigation links container */}
<div
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7"
>
{/* Navigation links and Authentication component */}
{
strings.nav.map((link: NavItem) => (
<NavLink url={localizeHref(link.url, locale)} name={link.name} />
))
}
<Authentication locale={locale} />
<LanguagePicker />
{/* ThemeIcon component specifically for larger screens */}
<span class="hidden md:inline-block">
<ThemeIcon />
</span>
</div>
</div>
</nav>
</header>
{/* Theme Appearance script to manage light/dark modes */}
<script is:inline>
const HSThemeAppearance = {
init() {
const defaultTheme = 'default';
let theme = localStorage.getItem('hs_theme') || defaultTheme;
if (document.querySelector('html').classList.contains('dark')) return;
this.setAppearance(theme);
},
_resetStylesOnLoad() {
const $resetStyles = document.createElement('style');
$resetStyles.innerText = `*{transition: unset !important;}`;
$resetStyles.setAttribute('data-hs-appearance-onload-styles', '');
document.head.appendChild($resetStyles);
return $resetStyles;
},
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
const $resetStylesEl = this._resetStylesOnLoad();
if (saveInStore) {
localStorage.setItem('hs_theme', theme);
}
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
}
document.querySelector('html').classList.remove('dark');
document.querySelector('html').classList.remove('default');
document.querySelector('html').classList.remove('auto');
document
.querySelector('html')
.classList.add(this.getOriginalAppearance());
setTimeout(() => {
$resetStylesEl.remove();
});
if (dispatchEvent) {
window.dispatchEvent(
new CustomEvent('on-hs-appearance-change', { detail: theme })
);
}
},
getAppearance() {
let theme = this.getOriginalAppearance();
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
}
return theme;
},
getOriginalAppearance() {
const defaultTheme = 'default';
return localStorage.getItem('hs_theme') || defaultTheme;
},
};
HSThemeAppearance.init();
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (HSThemeAppearance.getOriginalAppearance() === 'auto') {
HSThemeAppearance.setAppearance('auto', false);
}
});
window.addEventListener('load', () => {
const $clickableThemes = document.querySelectorAll(
'[data-hs-theme-click-value]'
);
const $switchableThemes = document.querySelectorAll(
'[data-hs-theme-switch]'
);
$clickableThemes.forEach($item => {
$item.addEventListener('click', () =>
HSThemeAppearance.setAppearance(
$item.getAttribute('data-hs-theme-click-value'),
true,
$item
)
);
});
$switchableThemes.forEach($item => {
$item.addEventListener('change', e => {
HSThemeAppearance.setAppearance(e.target.checked ? 'dark' : 'default');
});
$item.checked = HSThemeAppearance.getAppearance() === 'dark';
});
window.addEventListener('on-hs-appearance-change', e => {
$switchableThemes.forEach($item => {
$item.checked = e.detail === 'dark';
});
});
});
</script>

View File

@ -0,0 +1,163 @@
---
// Import SecondaryCTA component for use in this module
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Icon from '@components/ui/icons/Icon.astro';
import { localizeHref, type Locale } from '@/i18n';
// Define props from Astro
const { pricing, locale = 'de' } = Astro.props;
// Define TypeScript type for products.
type Product = {
name: string;
description: string;
price: string;
cents: string;
billingFrequency: string;
features: Array<string>;
purchaseBtnTitle: string;
purchaseLink: string;
};
type Pricing = {
title: string;
subTitle: string;
badge: string;
thirdOption: string;
btnText: string;
starterKit: Product;
professionalToolbox: Product;
};
interface Props {
pricing: Pricing;
locale?: Locale;
}
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Section heading and sub-heading */}
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h2
class="text-2xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
>
{pricing.title}
</h2>
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
{pricing.subTitle}
</p>
</div>
{/* Contains two main product blocks */}
<div class="flex flex-wrap items-center justify-center gap-4 sm:gap-0">
{/* Starter Kit product details */}
<div
class="w-full rounded-xl bg-gray-800 p-6 sm:w-1/2 sm:rounded-r-none sm:p-8 lg:w-1/3"
>
<div class="mb-4">
<h3 class="text-2xl font-bold text-neutral-100 sm:text-3xl">
{pricing.starterKit.name}
</h3>
<p class="text-indigo-300">{pricing.starterKit.description}</p>
</div>
<div class="mb-4">
<span class="text-4xl font-bold text-neutral-200"
>{pricing.starterKit.price}</span
>
<span class="text-lg font-bold text-neutral-300"
>{pricing.starterKit.cents}</span
>
<span class="ms-3 text-sm text-indigo-200"
>{pricing.starterKit.billingFrequency}</span
>
</div>
{
/* Features list - automatically created by mapping over `features` array */
}
<ul class="mb-6 space-y-2 text-neutral-300">
{
pricing.starterKit.features.map((feature: string) => (
<li class="flex items-center gap-1.5">
<Icon name="checkCircle" />
<span>{feature}</span>
</li>
))
}
</ul>
{/* CTA for purchasing the product */}
<a
href={localizeHref(pricing.starterKit.purchaseLink, locale)}
class="block rounded-lg bg-gray-500 px-8 py-3 text-center text-sm font-bold text-gray-100 ring-indigo-300 outline-hidden transition duration-100 hover:bg-gray-600 focus-visible:ring-3 active:text-gray-300 md:text-base"
>{pricing.starterKit.purchaseBtnTitle}</a
>
</div>
{/* Professional Toolbox product details */}
<div
class="w-full rounded-xl bg-linear-to-tr from-[#FF512F] to-[#F09819] p-6 shadow-xl sm:w-1/2 sm:p-8"
>
<div
class="mb-4 flex flex-col items-start justify-between gap-4 lg:flex-row"
>
<div>
<h3 class="text-2xl font-bold text-neutral-100 sm:text-3xl">
{pricing.professionalToolbox.name}
</h3>
<p class="text-orange-200">
{pricing.professionalToolbox.description}
</p>
</div>
<span
class="bg-opacity-50 order-first inline-block rounded-full bg-orange-200/60 px-3 py-1 text-center text-xs font-bold tracking-wider text-orange-600 uppercase lg:order-none"
>{pricing.badge}</span
>
</div>
<div class="mb-4">
<span class="text-6xl font-bold text-neutral-100"
>{pricing.professionalToolbox.price}</span
>
<span class="text-lg font-bold text-orange-100"
>{pricing.professionalToolbox.cents}</span
>
<span class="ms-3 text-orange-200"
>{pricing.professionalToolbox.billingFrequency}</span
>
</div>
{
/* Features list - automatically created by mapping over `features` array */
}
<ul class="mb-6 space-y-2 text-orange-100">
{
pricing.professionalToolbox.features.map((feature: string) => (
<li class="flex items-center gap-1.5">
<Icon name="checkCircle" />
<span>{feature}</span>
</li>
))
}
</ul>
{/* CTA for purchasing the product */}
<a
href={localizeHref(pricing.professionalToolbox.purchaseLink, locale)}
class="bg-opacity-50 block rounded-lg bg-orange-200/40 px-8 py-3 text-center text-sm font-bold text-neutral-100 ring-orange-300 outline-hidden transition duration-300 hover:bg-orange-300 focus-visible:ring-3 active:bg-orange-400 md:text-base"
>{pricing.professionalToolbox.purchaseBtnTitle}</a
>
</div>
</div>
{/* Call to action for Enterprise Solutions */}
<div class="mt-8 flex items-center justify-center gap-x-3 md:mt-12">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
{pricing.thirdOption}
</p>
<SecondaryCTA
title={pricing.btnText}
url={localizeHref('/contact', locale)}
/>
</div>
</section>

View File

@ -0,0 +1,43 @@
---
import { Image } from 'astro:assets';
import Icon from '../../ui/icons/Icon.astro';
const { content, author, role, avatarSrc } = Astro.props;
interface Props {
content: string;
author: string;
role: string;
avatarSrc: string;
}
---
<blockquote class="relative">
<Icon name="quotation" />
<div class="relative z-10">
<p class="text-xl text-neutral-800 italic dark:text-neutral-200">
{content}
</p>
</div>
<div class="mt-6">
<div class="flex items-center">
<div class="shrink-0">
<Image
class="h-8 w-8 rounded-full"
src={avatarSrc}
alt="Illustrative testimonial avatar"
loading={'eager'}
inferSize
/>
</div>
<div class="ms-4 grow">
<div class="font-bold text-neutral-800 dark:text-neutral-200">
{author}
</div>
<div class="text-xs text-neutral-500">{role}</div>
</div>
</div>
</div>
</blockquote>

View File

@ -0,0 +1,85 @@
---
import TestimonialItem from './TestimonialItem.astro';
import StatsGrid from '../../ui/blocks/StatsGrid.astro';
const { title, subTitle, testimonials, statistics } = Astro.props;
interface Props {
title: string;
subTitle?: string;
testimonials?: Testimonial[];
statistics?: StatProps[];
}
// TypeScript type for testimonials
type Testimonial = {
content: string;
author: string;
role: string;
avatarSrc: string;
};
// TypeScript type for stats.
type StatProps = {
count: string;
description: string;
};
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Container for the testimonials */}
<div
class="lg:grid lg:grid-cols-12 lg:items-center lg:justify-between lg:gap-16"
>
<div class="lg:col-span-5 lg:col-start-1">
{/* Title and Subtitle */}
<div class="mb-8">
<h2
class="mb-2 text-3xl font-bold text-neutral-800 lg:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{
subTitle && (
<p class="text-neutral-600 dark:text-neutral-400">{subTitle}</p>
)
}
</div>
{
/* Generate a blockquote for each testimonial in the testimonials array by mapping over the array. */
}
{
testimonials &&
testimonials.map(testimonial => (
<TestimonialItem
content={testimonial.content}
author={testimonial.author}
role={testimonial.role}
avatarSrc={testimonial.avatarSrc}
/>
))
}
</div>
{
statistics && (
<div class="mt-10 lg:col-span-6 lg:col-end-13 lg:mt-0">
<div class="space-y-6 sm:space-y-8">
<ul class="grid grid-cols-2 divide-x-2 divide-y-2 divide-neutral-300 overflow-hidden dark:divide-neutral-700">
{/* Generate a list item for each stat in the statistics array by mapping over the array. */}
{statistics.map((stat, index) => (
<StatsGrid
count={stat.count}
description={stat.description}
index={index}
/>
))}
</ul>
</div>
</div>
)
}
</div>
</section>

View File

@ -0,0 +1,73 @@
---
// Import AvatarTestimonialSection component for use in this module
import AvatarTestimonialSection from '../../ui/avatars/AvatarTestimonialSection.astro';
// Define props from Astro
const { title, testimonials } = Astro.props;
// Define TypeScript interface for Testimonial
interface Testimonial {
content: string;
author: string;
role: string;
avatarSrc: string;
avatarAlt: string;
}
// Define TypeScript interface for props
interface Props {
title: string;
testimonials: Testimonial[];
}
---
{/* Main div that wraps the testimonials section */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
id="testimonials"
>
{/* Title of the testimonials section */}
<div class="mb-6 w-3/4 max-w-2xl sm:mb-10 md:mb-16 lg:w-1/2">
<h2
class="text-2xl font-bold text-balance text-neutral-800 sm:text-3xl lg:text-4xl dark:text-neutral-200"
>
{title}
</h2>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Looping through each testimonial data and rendering it */}
{
testimonials.map(testimonial => (
<div class="flex h-auto">
<div class="flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-700">
<div class="flex-auto p-4 md:p-6">
{/* Testimonial content */}
<p class="text-base text-pretty text-neutral-600 italic md:text-lg dark:text-neutral-300">
{testimonial.content}
</p>
</div>
<div class="rounded-b-xl bg-neutral-300/30 p-4 md:px-7 dark:bg-neutral-900/30">
<div class="flex items-center">
<AvatarTestimonialSection
src={testimonial.avatarSrc}
alt={testimonial.avatarAlt}
/>
<div class="ms-3 grow">
<p class="text-sm font-bold text-neutral-800 sm:text-base dark:text-neutral-200">
{testimonial.author}
</p>
<p class="text-xs text-neutral-600 dark:text-neutral-400">
{testimonial.role}
</p>
</div>
</div>
</div>
</div>
</div>
))
}
</div>
</section>

View File

@ -0,0 +1,62 @@
---
import {
getLocaleFromPath,
localeLabels,
localizeRoutePath,
stripLocalePrefix,
} from '@/i18n';
import Icon from './icons/Icon.astro';
const currentLocale = getLocaleFromPath(Astro.url.pathname);
const currentPath = stripLocalePrefix(Astro.url.pathname);
---
<div class="hs-dropdown relative inline-flex">
<button
id="hs-language-dropdown"
type="button"
aria-label="Sprache wechseln"
title="Sprache wechseln"
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-1.5 py-1.5 text-sm font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:bg-neutral-200 hover:text-orange-400 dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-orange-300 dark:focus:outline-hidden"
>
<Icon name="earth" />
<span class="sr-only">{localeLabels[currentLocale]}</span>
<svg
class="hs-dropdown-open:rotate-180 size-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg
>
</button>
<div
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 top-[98%]! left-[20%]! mt-2 hidden transform-none! rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] before:absolute before:start-0 before:-top-4 before:h-4 before:w-full after:absolute after:start-0 after:-bottom-4 after:h-4 after:w-full md:top-[80%]! md:left-[90%]! dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
aria-labelledby="hs-language-dropdown"
>
{
Object.entries(localeLabels).map(([locale, label]) => (
<a
class:list={[
'flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-hidden dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700',
locale === currentLocale
? 'font-semibold text-orange-500 dark:text-orange-300'
: 'text-neutral-800 dark:text-neutral-400',
]}
href={localizeRoutePath(
currentPath,
currentLocale,
locale as 'de' | 'en'
)}
aria-current={locale === currentLocale ? 'true' : undefined}
>
{label}
</a>
))
}
</div>
</div>

View File

@ -0,0 +1,18 @@
---
import { Image } from 'astro:assets';
const { src, alt } = Astro.props;
interface Props {
src: string;
alt: string;
}
---
<Image
class="inline-block h-8 w-8 rounded-full ring-2 ring-neutral-50 dark:ring-zinc-800"
src={src}
alt={alt}
inferSize
loading={'eager'}
/>

View File

@ -0,0 +1,22 @@
---
// Import necessary components
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
const { blogEntry } = Astro.props;
interface Props {
blogEntry: CollectionEntry<'blog'>;
}
---
<div class="shrink-0">
<Image
class="size-[46px] rounded-full border-2 border-neutral-50"
src={blogEntry.data.authorImage}
alt={blogEntry.data.authorImageAlt}
draggable={'false'}
format={'avif'}
/>
</div>

View File

@ -0,0 +1,22 @@
---
// Import necessary components
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
const { blogEntry } = Astro.props;
interface Props {
blogEntry: CollectionEntry<'blog'>;
}
---
<div class="shrink-0">
<Image
class="size-10 rounded-full sm:h-14 sm:w-14"
src={blogEntry.data.authorImage}
alt={blogEntry.data.authorImageAlt}
draggable={'false'}
format={'avif'}
/>
</div>

View File

@ -0,0 +1,17 @@
---
const { src, alt } = Astro.props;
interface Props {
src: string;
alt: string;
}
---
<div class="shrink-0">
<img
class="size-8 rounded-full sm:h-[2.875rem] sm:w-[2.875rem]"
src={src}
alt={alt}
loading="lazy"
/>
</div>

View File

@ -0,0 +1,84 @@
---
const { title, btnId, btnTitle, url } = Astro.props;
interface Props {
title?: string;
btnId: string;
btnTitle: string;
url: string;
}
---
<astro-banner btnId={btnId}>
<div
class="fixed start-1/2 bottom-0 z-50 mx-auto w-full -translate-x-1/2 transform p-6 sm:max-w-4xl"
role="region"
aria-label="Informational Banner"
>
<div
class="rounded-xl bg-neutral-800 bg-[url('/banner-pattern.svg')] bg-cover bg-center bg-no-repeat p-4 text-center shadow-xs dark:bg-neutral-200"
>
<div class="flex items-center justify-center">
<div class="ml-auto">
{
title && (
<p class="me-2 inline-block font-medium text-neutral-50 dark:text-neutral-700">
{title}
</p>
)
}
<a
class="group inline-flex items-center gap-x-2 rounded-full border-2 border-neutral-50 px-3 py-2 text-sm font-semibold text-neutral-50 backdrop-brightness-75 transition duration-300 hover:border-neutral-100/70 hover:text-neutral-50/70 disabled:pointer-events-none disabled:opacity-50 sm:backdrop-brightness-100 dark:border-neutral-700 dark:text-neutral-700 dark:backdrop-brightness-100 dark:hover:border-neutral-700/70 dark:hover:text-neutral-800/70 dark:focus:outline-hidden"
href={url}
target="_blank"
>
{btnTitle}
<svg
class="size-4 shrink-0 transition duration-300 group-hover:translate-x-1"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="m9 18 6-6-6-6"></path></svg
>
</a>
</div>
<button
type="button"
class="ml-auto inline-flex items-center gap-x-2 rounded-full border border-transparent bg-gray-100 p-2 text-sm font-semibold text-gray-800 hover:bg-gray-200 disabled:pointer-events-none disabled:opacity-50 dark:bg-neutral-700 dark:text-neutral-50 dark:hover:bg-neutral-700/80 dark:hover:text-neutral-50 dark:focus:outline-hidden"
id={btnId}
>
<span class="sr-only">Dismiss</span>
<svg
class="size-5 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg
>
</button>
</div>
</div>
</div>
</astro-banner>
<script>
class AstroBanner extends HTMLElement {
connectedCallback() {
const btnId = this.getAttribute('btnId');
const button = this.querySelector(`#${btnId}`);
if (button != null) {
button.addEventListener('click', () => this.remove());
}
}
}
customElements.define('astro-banner', AstroBanner);
</script>

View File

@ -0,0 +1,51 @@
---
import Icon from '@components/ui/icons/Icon.astro';
// Define props from Astro
const { id, collapseId, question, answer, first } = Astro.props;
// Define TypeScript interface for props
interface Props {
id: string;
collapseId: string;
question: string;
answer: string;
first?: boolean;
}
// Define class names for the accordion and its content
const ACCORDION_CLASS_DEFAULT = 'hs-accordion pb-3 active';
const ACCORDION_CLASS_COLLAPSED = 'hs-accordion pt-6 pb-3';
const ACCORDION_CONTENT_CLASS =
'hs-accordion-content w-full overflow-hidden transition-[height] duration-300';
// Helper function to return the correct class for the accordion
function getAccordionClass(first: boolean = false) {
return first ? ACCORDION_CLASS_DEFAULT : ACCORDION_CLASS_COLLAPSED;
}
---
{/* The main container for the accordion item */}
<div class={getAccordionClass(first)} id={id}>
{/* The accordion button, which toggles the expanded/collapsed state */}
<button
class="hs-accordion-toggle group inline-flex w-full items-center justify-between gap-x-3 rounded-lg pb-3 text-start font-bold text-balance text-neutral-800 ring-zinc-500 outline-hidden transition hover:text-neutral-500 focus-visible:ring-3 md:text-lg dark:text-neutral-200 dark:ring-zinc-200 dark:hover:text-neutral-400 dark:focus:outline-hidden"
aria-expanded={first}
aria-controls={collapseId}
>
{question}
{/* SVG Icon that is shown when the accordion is NOT active */}
<Icon name="accordionNotActive" />
{/* SVG Icon that is shown when the accordion is active */}
<Icon name="accordionActive" />
</button>
{/* The collapsible content of the accordion */}
<div
id={collapseId}
role="region"
aria-labelledby={id}
class={`${first ? ACCORDION_CONTENT_CLASS : 'hidden ' + ACCORDION_CONTENT_CLASS}`}
>
{/* The content paragraph */}
<p class="text-pretty text-neutral-600 dark:text-neutral-400">
{answer}
</p>
</div>
</div>

View File

@ -0,0 +1,66 @@
---
// Define props from Astro
const {
heading,
content,
isAddressVisible,
addressContent,
isLinkVisible,
linkTitle,
linkURL,
isArrowVisible,
} = Astro.props;
// Define TypeScript interface for props
interface Props {
heading?: string;
content?: string;
isAddressVisible?: boolean;
addressContent?: string;
isLinkVisible?: boolean;
linkTitle?: string;
linkURL?: string;
isArrowVisible?: boolean;
}
// Define SVG arrow to be used in the component
const arrowSVG: string = `<svg
class="h-4 w-4 shrink-0 transition ease-in-out group-hover:translate-x-1"
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" >
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" /> </svg>`;
---
{/* Root container, which arranges the heading and content */}
<div class="flex gap-x-7 py-6">
{/* Slot to allow for extensibility of the component */}
<slot />
<div class="grow">
{/* Heading of the section */}
<h3 class="font-bold text-neutral-700 dark:text-neutral-300">
{heading}
</h3>
{/* Content of the section */}
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">{content}</p>
{/* Conditional rendering of address content if isAddressVisible is true */}
{
isAddressVisible ? (
<p class="mt-1 text-sm text-neutral-500 italic">{addressContent}</p>
) : null
}
{
/* Conditional rendering of a link if isLinkVisible is true.
The link also conditionally includes an arrow SVG if isArrowVisible is true */
}
{
isLinkVisible ? (
<a
class="group mt-2 inline-flex items-center gap-x-2 rounded-lg text-sm font-medium text-zinc-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-zinc-800 focus-visible:ring-3 dark:text-zinc-400 dark:ring-zinc-200 dark:hover:text-zinc-200 dark:focus:ring-1 dark:focus:outline-hidden"
href={linkURL}
>
{linkTitle}
{isArrowVisible ? <Fragment set:html={arrowSVG} /> : null}
</a>
) : null
}
</div>
</div>

View File

@ -0,0 +1,30 @@
---
// Get heading and content from Astro props
const { heading, content } = Astro.props;
// Define TypeScript interface for props
interface Props {
heading?: string;
content?: string;
}
// Define classes for heading and content
const headingClasses =
'text-balance text-lg font-bold text-neutral-800 dark:text-neutral-200';
const contentClasses =
'mt-1 text-pretty text-neutral-700 dark:text-neutral-300';
---
{/* The root container that arranges your slot and the heading/content */}
<div class="flex gap-x-5">
{/* Slot to allow for extensibility of the component */}
<div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-neutral-200/80 text-orange-500 dark:bg-white/[0.06] dark:text-orange-300 [&>svg]:!m-0 [&>svg]:!size-6 [&>svg]:!shrink-0 [&>svg]:!fill-current">
<slot />
</div>
<div class="grow">
{/* Heading of the section */}
<h3 class={headingClasses}>
{heading}
</h3>
{/* Content text of the section */}
<p class={contentClasses}>{content}</p>
</div>
</div>

View File

@ -0,0 +1,49 @@
---
// Import the necessary modules
import { Image } from 'astro:assets';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
// Destructure the props passed to the Astro component
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } =
Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
img: any;
imgAlt: any;
}
---
{/* The root section of the component */}
<section
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
>
{/* The Image component which renders the image */}
<Image
class="w-full rounded-xl"
src={img}
alt={imgAlt}
draggable={'false'}
format={'avif'}
/>
{/* The container for title, subtitle, and optional CTA button */}
<div class="mt-4 md:mt-0">
{/* The title of the section */}
<h2
class="mb-4 text-3xl font-extrabold tracking-tight break-words text-balance text-neutral-800 md:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{/* The subtitle of the section */}
<p
class="mb-4 max-w-prose font-normal text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{/* Conditionally render the Primary CTA button if btnExists is true */}
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
</section>

View File

@ -0,0 +1,45 @@
---
// Import PrimaryCTA component
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
// Destructure the props passed to the Astro component
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
---
{/* Root section of the component */}
<section
class="mx-auto mt-10 max-w-[85rem] px-4 py-10 sm:px-6 sm:py-16 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="min-w-0 max-w-(--breakpoint-md)">
{/* Section title */}
<h1
class="mb-4 text-4xl font-extrabold tracking-tight break-words text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h1>
{/* Section subtitle */}
<p
class="mb-8 max-w-prose font-normal break-words text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400"
>
{subTitle}
</p>
{
/* Conditional rendering of PrimaryCTA component if 'btnExists' property is truthy */
}
{
btnExists ? (
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
<PrimaryCTA title={btnTitle} url={btnURL} />
</div>
) : null
}
</div>
</section>

View File

@ -0,0 +1,51 @@
---
import Avatar from '@components/ui/avatars/Avatar.astro';
const { avatars, rating, reviews } = Astro.props;
interface Props {
avatars?: Array<string>;
rating?: string;
reviews?: string;
}
---
<div class="mt-6 lg:mt-10">
<div class="py-5">
<div class="text-center sm:flex sm:items-center sm:text-start">
<div class="shrink-0 pb-5 sm:flex sm:pe-5 sm:pb-0">
{/* Avatar Group */}
<div class="flex justify-center -space-x-3">
{
avatars?.map(src => (
<Avatar src={src} alt="Illustrative reviewer avatar" />
))
}
<span
class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-zinc-800 ring-2 ring-white dark:bg-zinc-900 dark:ring-zinc-800"
>
<span class="text-xs leading-none font-medium text-white uppercase"
>Demo</span
>
</span>
</div>
</div>
<div
class="mx-auto h-px w-32 border-t border-neutral-400 sm:mx-0 sm:h-8 sm:w-auto sm:border-s sm:border-t-0 dark:border-neutral-500"
>
</div>
<div class="flex flex-col items-center sm:items-start">
<div class="flex items-baseline space-x-1 pt-5 sm:ps-5 sm:pt-0">
<p class="text-neutral-800 dark:text-neutral-200">
<Fragment set:html={rating || 'Illustrative review preview'} />
</p>
</div>
<div class="text-sm text-neutral-800 sm:ps-5 dark:text-neutral-200">
<p>
<Fragment set:html={reviews || 'Static example only'} />
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,88 @@
---
// Import the required modules
import { Image } from 'astro:assets';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
// Extract properties from Astro.props
const {
title,
subTitle,
btnExists,
btnTitle,
btnURL,
single,
imgOne,
imgOneAlt,
imgTwo,
imgTwoAlt,
} = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
single?: boolean;
imgOne?: any;
imgOneAlt?: any;
imgTwo?: any;
imgTwoAlt?: any;
}
---
{/* Root section of the component */}
<section
class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div>
{/* Title of the section */}
<h2
class="mb-4 text-3xl font-extrabold tracking-tight break-words text-balance text-neutral-800 md:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{/* Subtitle of the section */}
<p
class="mb-4 max-w-prose font-normal text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{
/* Conditional rendering of the Primary Call-To-Action button if 'btnExists' is true */
}
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
{/* Conditionally render one or two images based on 'single' property */}
{
single ? (
<div class="mt-8">
{/* Single image */}
<Image
class="w-full rounded-lg"
src={imgOne}
alt={imgOneAlt}
format={'avif'}
/>
</div>
) : (
<div class="mt-8 grid grid-cols-2 gap-4">
{/* First image in a two-image layout */}
<Image
class="w-full rounded-xl"
src={imgOne}
alt={imgOneAlt}
draggable={'false'}
format={'avif'}
/>
{/* Second image in a two-image layout */}
<Image
class="mt-4 w-full rounded-xl lg:mt-10"
src={imgTwo}
alt={imgTwoAlt}
draggable={'false'}
format={'avif'}
/>
</div>
)
}
</section>

View File

@ -0,0 +1,19 @@
---
// Extract the properties from Astro.props
const { title, subTitle } = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title: string;
subTitle: string;
}
---
{/* Container for the title and subtitle */}
<div class="min-w-0 lg:pe-6 xl:pe-12">
<p class="text-6xl leading-10 font-bold break-words text-balance text-orange-400 dark:text-orange-300">
{title}
</p>
<p class="mt-2 break-words text-neutral-600 sm:mt-3 dark:text-neutral-400">
{subTitle}
</p>
</div>

View File

@ -0,0 +1,23 @@
---
import Icon from '@components/ui/icons/Icon.astro';
const { count, description, index } = Astro.props;
interface Props {
count: string;
description: string;
index: number;
}
---
<li class="-m-0.5 flex flex-col p-4 sm:p-8">
<div
class="mb-2 flex items-end gap-x-2 text-3xl font-bold text-neutral-800 sm:text-5xl dark:text-neutral-200"
>
{index === 1 || index === 2 ? <Icon name="arrowUp" /> : null}
{count}
</div>
<p class="text-sm text-neutral-600 sm:text-base dark:text-neutral-400">
{description}
</p>
</li>

Some files were not shown because too many files have changed in this diff Show More