feat: initial humanAI landing page

- Astro 6 static site, PL default (/) + EN (/en/)
- All copy in src/i18n/pl.json and src/i18n/en.json
- Sections: Hero, Problem, HowWeWork, Offers, Process, Contact, Footer
- Security section hidden behind SHOW_SECURITY flag
- Conservative tone for Industry and Med (human-in-the-loop)
- Contact form via mailto prefill
- Dockerfile multi-stage (node build -> nginx)
- docker-compose.yml on npm_proxy network (NPM visible)
- deploy.sh for SATURN -> VPS workflow
This commit is contained in:
Oskar Kapala 2026-06-02 17:24:37 +02:00
commit fc5741316f
29 changed files with 6676 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
# Stage 1: build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: serve
FROM nginx:alpine AS server
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

74
README.md Normal file
View file

@ -0,0 +1,74 @@
# humanAI landing page
Static bilingual landing page for **humanAI** (gethumanai.com).
Stack: Astro 6, static output, i18n PL (default `/`) + EN (`/en/`), nginx in Docker.
---
## Local development
```sh
# Host: SATURN
npm install
npm run dev # dev server at http://localhost:4321
npm run build # build to ./dist/
npm run preview # preview built site at http://localhost:4321
```
## Docker local test
```sh
# Host: SATURN
# Create a local test network if npm_proxy doesn't exist locally
docker network create npm_proxy 2>/dev/null || true
docker compose up --build
# site at http://localhost:80
```
## Deploy to VPS
```sh
# Host: SATURN
./deploy.sh
```
The script:
1. Commits any pending changes and pushes to Forgejo (`oskar/gethumanai-landing`).
2. SSH-es to `ubuntu-4gb-hel1-1`, pulls the repo and runs `docker compose up -d --build`.
The container joins the `npm_proxy` Docker network. In **Nginx Proxy Manager** on the VPS,
create a proxy host: `gethumanai.com``http://humanai-landing:80`.
## Structure
```
src/
i18n/
pl.json # Polish copy (all text)
en.json # English copy (all text)
index.ts # translation helpers
layouts/
Layout.astro # HTML shell, meta/OG/hreflang
components/
Nav.astro
Hero.astro
Problem.astro
HowWeWork.astro
Offers.astro
Process.astro
Security.astro # hidden on MVP (SHOW_SECURITY flag)
Contact.astro
Footer.astro
pages/
index.astro # PL — /
en/
index.astro # EN — /en/
```
## Enabling the Security section
Open `src/components/Security.astro` and set `SHOW_SECURITY = true`.
## Swapping the logo
The logo text and SVG icon are in `src/components/Nav.astro` (`.logo`).

13
astro.config.mjs Normal file
View file

@ -0,0 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
i18n: {
defaultLocale: 'pl',
locales: ['pl', 'en'],
routing: {
prefixDefaultLocale: false,
},
},
});

39
deploy.sh Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
# Host: SATURN
# Run this script from SATURN to commit, push and deploy to VPS.
VPS="ubuntu-4gb-hel1-1"
REPO_DIR="/home/oskar/projects/gethumanai-landing"
VPS_DEPLOY_DIR="/opt/gethumanai-landing"
FORGEJO_REMOTE="ssh://git@100.108.208.3:222/oskar/gethumanai-landing.git"
# ── 1. Commit & push (SATURN) ──────────────────────────────────────────────
# Host: SATURN
cd "$REPO_DIR"
if [ -n "$(git status --porcelain)" ]; then
git add -A
git commit -m "chore: deploy $(date '+%Y-%m-%d %H:%M')"
fi
git push origin main
# ── 2. Pull & rebuild on VPS ───────────────────────────────────────────────
# Host: ubuntu-4gb-hel1-1
ssh "$VPS" bash -s <<'REMOTE'
set -euo pipefail
DEPLOY_DIR="/opt/gethumanai-landing"
if [ ! -d "$DEPLOY_DIR/.git" ]; then
git clone ssh://git@100.108.208.3:222/oskar/gethumanai-landing.git "$DEPLOY_DIR"
fi
cd "$DEPLOY_DIR"
git pull origin main
docker compose up -d --build --remove-orphans
echo "Deploy complete."
REMOTE
echo "Done — humanAI landing is live."

13
docker-compose.yml Normal file
View file

@ -0,0 +1,13 @@
services:
humanai-landing:
build: .
container_name: humanai-landing
restart: unless-stopped
networks:
- npm_proxy
# Port is NOT exposed to host — traffic goes through Nginx Proxy Manager only.
# In NPM, proxy host → http://humanai-landing:80
networks:
npm_proxy:
external: true

29
nginx.conf Normal file
View file

@ -0,0 +1,29 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
location / {
try_files $uri $uri/ $uri.html =404;
}
location ~* \.(js|css|woff2?|ttf|svg|ico|png|jpg|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
error_page 404 /404.html;
}

4761
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

17
package.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "gethumanai-landing",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^6.4.3"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

5
public/favicon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="7" fill="#1a3d2e"/>
<circle cx="16" cy="16" r="8.5" stroke="white" stroke-width="1.4"/>
<path d="M16 7.5 A8.5 8.5 0 0 1 16 24.5" stroke="white" stroke-width="1.4" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View file

@ -0,0 +1,176 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
const contactId = locale === 'en' ? 'contact' : 'kontakt';
---
<section class="contact-section" id={contactId} aria-labelledby="contact-heading">
<div class="container contact-inner">
<div class="contact-intro">
<span class="section-label" aria-hidden="true">
{locale === 'en' ? 'Contact' : 'Kontakt'}
</span>
<h2 id="contact-heading">{tr.contact.h2}</h2>
<p>{tr.contact.subtitle}</p>
<a href={`mailto:${tr.contact.email}`} class="contact-email">
{tr.contact.email}
</a>
</div>
<div class="contact-form-wrap">
<form
class="contact-form"
id="contact-form"
aria-label={locale === 'en' ? 'Contact form' : 'Formularz kontaktowy'}
novalidate
>
<div class="field">
<label for="name">{tr.contact.field_name}</label>
<input
type="text"
id="name"
name="name"
autocomplete="name"
required
aria-required="true"
/>
</div>
<div class="field">
<label for="email">{tr.contact.field_email}</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
aria-required="true"
/>
</div>
<div class="field">
<label for="message">{tr.contact.field_message}</label>
<textarea
id="message"
name="message"
rows="5"
required
aria-required="true"
></textarea>
</div>
<button type="submit" class="btn-primary submit-btn">
{tr.contact.button}
</button>
</form>
</div>
</div>
</section>
<script define:vars={{ email: tr.contact.email, locale }}>
// TODO: podłączyć backend — na razie mailto prefill
const form = document.getElementById('contact-form');
form?.addEventListener('submit', (e) => {
e.preventDefault();
const fd = new FormData(form);
const name = fd.get('name') || '';
const emailVal = fd.get('email') || '';
const message = fd.get('message') || '';
const subject = locale === 'en'
? `Message from ${name} — humanAI`
: `Wiadomość od ${name} — humanAI`;
const body = locale === 'en'
? `From: ${name}\nEmail: ${emailVal}\n\n${message}`
: `Od: ${name}\nE-mail: ${emailVal}\n\n${message}`;
window.location.href = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
});
</script>
<style>
.contact-section {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
}
.contact-inner {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
align-items: start;
}
@media (min-width: 768px) {
.contact-inner {
grid-template-columns: 1fr 1.2fr;
gap: 5rem;
}
}
.contact-intro h2 {
margin-bottom: 1rem;
}
.contact-intro p {
max-width: 38ch;
margin-bottom: 1.5rem;
}
.contact-email {
display: inline-block;
font-size: 0.9rem;
font-weight: 500;
color: var(--color-accent);
text-decoration: none;
}
.contact-email:hover {
text-decoration: underline;
}
.contact-form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
label {
font-size: 0.88rem;
font-weight: 500;
color: var(--color-text);
}
input, textarea {
font-family: var(--font-sans);
font-size: 0.95rem;
padding: 0.7rem 0.9rem;
border: 1.5px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
color: var(--color-text);
width: 100%;
transition: border-color 0.18s;
-webkit-appearance: none;
appearance: none;
}
input:focus, textarea:focus {
outline: none;
border-color: var(--color-accent);
}
textarea {
resize: vertical;
min-height: 120px;
}
.submit-btn {
align-self: flex-start;
}
</style>

100
src/components/Footer.astro Normal file
View file

@ -0,0 +1,100 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
const year = new Date().getFullYear();
---
<footer class="footer" role="contentinfo">
<div class="container footer-inner">
<div class="footer-brand">
<span class="footer-logo">human<span class="logo-ai">AI</span></span>
<p class="footer-tagline">{tr.footer.tagline}</p>
</div>
<nav class="footer-nav" aria-label={locale === 'en' ? 'Footer navigation' : 'Nawigacja stopki'}>
<ul role="list">
{tr.footer.links.map((link) => (
<li><a href={link.anchor}>{link.label}</a></li>
))}
</ul>
</nav>
</div>
<div class="footer-bottom">
<div class="container">
<small>© {year} {tr.footer.copyright}</small>
</div>
</div>
</footer>
<style>
.footer {
background: var(--color-text);
color: #e8e8e4;
border-top: 1px solid rgba(255,255,255,0.08);
}
.footer-inner {
display: flex;
flex-direction: column;
gap: 2rem;
padding-block: 3rem 2rem;
}
@media (min-width: 640px) {
.footer-inner {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
}
.footer-logo {
font-size: 1.15rem;
font-weight: 500;
letter-spacing: -0.02em;
}
.logo-ai {
color: #7fc5a0;
}
.footer-tagline {
font-size: 0.85rem;
color: rgba(255,255,255,0.5);
margin-top: 0.4rem;
max-width: 24ch;
}
.footer-nav ul {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1.5rem;
}
.footer-nav a {
font-size: 0.88rem;
color: rgba(255,255,255,0.6);
text-decoration: none;
transition: color 0.15s;
}
.footer-nav a:hover {
color: #fff;
}
.footer-bottom {
border-top: 1px solid rgba(255,255,255,0.08);
padding-block: 1.25rem;
}
.footer-bottom small {
font-size: 0.8rem;
color: rgba(255,255,255,0.35);
}
</style>

152
src/components/Hero.astro Normal file
View file

@ -0,0 +1,152 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
const contactAnchor = locale === 'en' ? '#contact' : '#kontakt';
---
<section class="hero" id="home" aria-labelledby="hero-heading">
<div class="container hero-inner">
<div class="hero-content">
<span class="hero-badge" role="text">{tr.hero.badge}</span>
<h1 id="hero-heading">{tr.hero.h1}</h1>
<p class="hero-sub">{tr.hero.subtitle}</p>
<div class="hero-actions">
<a href={contactAnchor} class="btn-primary">{tr.hero.cta_primary}</a>
<a href={locale === 'en' ? '#offers' : '#oferty'} class="btn-outline">{tr.hero.cta_secondary}</a>
</div>
</div>
<div class="pillars-row" role="list" aria-label={locale === 'en' ? 'Our four focus areas' : 'Cztery obszary zastosowań'}>
{(['home','business','industry','med'] as const).map((key) => (
<a
href={locale === 'en' ? `#${key}` : `#${key}`}
class={`pillar pillar-${key}`}
role="listitem"
aria-label={`${tr.pillars[key].title} — ${tr.pillars[key].tagline}`}
>
<span class="pillar-icon" aria-hidden="true">
{key === 'home' && (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>
)}
{key === 'business' && (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
</svg>
)}
{key === 'industry' && (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 20h20M4 20V10l5-5 5 5V20M14 20V14h6v6"/><line x1="9" y1="20" x2="9" y2="14"/><line x1="12" y1="20" x2="12" y2="14"/>
</svg>
)}
{key === 'med' && (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
)}
</span>
<span class="pillar-title">{tr.pillars[key].title}</span>
<span class="pillar-tagline">{tr.pillars[key].tagline}</span>
</a>
))}
</div>
</div>
</section>
<style>
.hero {
padding-block: clamp(4rem, 10vw, 8rem) clamp(2rem, 5vw, 4rem);
background: var(--color-surface);
}
.hero-inner {
display: flex;
flex-direction: column;
gap: 3.5rem;
}
.hero-content {
max-width: 680px;
}
.hero-badge {
display: inline-block;
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.04em;
background: var(--color-surface-alt);
border: 1px solid var(--color-border);
color: var(--color-text-muted);
padding: 0.3rem 0.8rem;
border-radius: 999px;
margin-bottom: 1.25rem;
}
h1 {
font-size: clamp(2.25rem, 7vw, 4rem);
font-weight: 500;
line-height: 1.1;
letter-spacing: -0.025em;
margin-bottom: 1.25rem;
color: var(--color-text);
}
.hero-sub {
font-size: clamp(1rem, 2.5vw, 1.2rem);
color: var(--color-text-muted);
max-width: 52ch;
margin-bottom: 2rem;
line-height: 1.6;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.pillars-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}
.pillar {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.25rem;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
text-decoration: none;
color: var(--color-text);
transition: border-color 0.18s, box-shadow 0.18s;
}
.pillar:hover {
border-color: var(--color-accent);
box-shadow: 0 2px 12px rgba(26, 61, 46, 0.07);
}
.pillar-icon {
color: var(--color-accent);
line-height: 0;
}
.pillar-title {
font-weight: 500;
font-size: 0.95rem;
}
.pillar-tagline {
font-size: 0.82rem;
color: var(--color-text-muted);
line-height: 1.4;
}
</style>

View file

@ -0,0 +1,102 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
const features = [
{
key: 'local',
data: tr.how.local,
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
</svg>`,
},
{
key: 'integrations',
data: tr.how.integrations,
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>`,
},
{
key: 'human',
data: tr.how.human,
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>`,
},
] as const;
---
<section class="how-section" aria-labelledby="how-heading">
<div class="container">
<span class="section-label" aria-hidden="true">
{locale === 'en' ? 'Our approach' : 'Nasze podejście'}
</span>
<h2 id="how-heading">{tr.how.h2}</h2>
<div class="features-grid" role="list">
{features.map(({ data, icon }) => (
<article class="feature-card" role="listitem">
<div class="feature-icon" aria-hidden="true" set:html={icon} />
<h3>{data.title}</h3>
<p>{data.body}</p>
</article>
))}
</div>
</div>
</section>
<style>
.how-section {
background: var(--color-surface);
}
.how-section h2 {
max-width: 40ch;
margin-bottom: 3rem;
}
.features-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 640px) {
.features-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.feature-card {
padding: 1.75rem;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
}
.feature-icon {
color: var(--color-accent);
margin-bottom: 1rem;
line-height: 0;
}
.feature-card h3 {
margin-bottom: 0.6rem;
font-size: 1rem;
}
.feature-card p {
font-size: 0.9rem;
line-height: 1.6;
max-width: 32ch;
}
</style>

194
src/components/Nav.astro Normal file
View file

@ -0,0 +1,194 @@
---
import type { Locale } from '../i18n/index.ts';
import { t, getAlternateUrl } from '../i18n/index.ts';
interface Props {
locale: Locale;
url: URL;
}
const { locale, url } = Astro.props;
const tr = t(locale);
const altLocale: Locale = locale === 'pl' ? 'en' : 'pl';
const altUrl = getAlternateUrl(url, altLocale);
---
<header class="nav-wrap">
<nav class="container nav-inner" aria-label="Nawigacja główna">
<a href={locale === 'en' ? '/en/' : '/'} class="logo" aria-label="humanAI — strona główna">
<span class="logo-text">human<span class="logo-ai">AI</span></span>
<span class="logo-icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="10" cy="10" r="8.5" stroke="currentColor" stroke-width="1.5"/>
<path d="M10 1.5 A8.5 8.5 0 0 1 10 18.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
</span>
</a>
<button class="nav-toggle" id="nav-toggle" aria-expanded="false" aria-controls="nav-menu" aria-label="Otwórz menu">
<span></span><span></span><span></span>
</button>
<ul class="nav-links" id="nav-menu" role="list">
<li><a href={locale === 'en' ? '/en/#home' : '/#home'}>{tr.nav.home}</a></li>
<li><a href={locale === 'en' ? '/en/#business' : '/#business'}>{tr.nav.business}</a></li>
<li><a href={locale === 'en' ? '/en/#industry' : '/#industry'}>{tr.nav.industry}</a></li>
<li><a href={locale === 'en' ? '/en/#med' : '/#med'}>{tr.nav.med}</a></li>
<li><a href={locale === 'en' ? '/en/#contact' : '/#kontakt'}>{tr.nav.contact}</a></li>
<li class="lang-item">
<a href={altUrl} class="lang-switch" hreflang={altLocale} lang={altLocale}>
{tr.nav.lang_switch}
</a>
</li>
</ul>
</nav>
</header>
<script>
const toggle = document.getElementById('nav-toggle');
const menu = document.getElementById('nav-menu');
toggle?.addEventListener('click', () => {
const open = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!open));
menu?.classList.toggle('is-open', !open);
});
</script>
<style>
.nav-wrap {
position: sticky;
top: 0;
z-index: 100;
background: rgba(250, 250, 248, 0.92);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--color-border);
}
.nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
height: 3.5rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.45rem;
text-decoration: none;
color: var(--color-text);
}
.logo-text {
font-size: 1.1rem;
font-weight: 500;
letter-spacing: -0.02em;
}
.logo-ai {
color: var(--color-accent);
}
.logo-icon {
color: var(--color-accent);
opacity: 0.7;
line-height: 0;
}
.nav-links {
display: flex;
align-items: center;
gap: 0.25rem;
list-style: none;
}
.nav-links a {
display: block;
padding: 0.4rem 0.75rem;
font-size: 0.9rem;
text-decoration: none;
color: var(--color-text-muted);
border-radius: var(--radius);
transition: color 0.15s, background 0.15s;
}
.nav-links a:hover {
color: var(--color-text);
background: var(--color-surface-alt);
}
.lang-item {
margin-left: 0.5rem;
border-left: 1px solid var(--color-border);
padding-left: 0.75rem;
}
.lang-switch {
font-weight: 500 !important;
color: var(--color-accent) !important;
}
.nav-toggle {
display: none;
flex-direction: column;
gap: 4px;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: var(--radius);
}
.nav-toggle span {
display: block;
width: 22px;
height: 2px;
background: var(--color-text);
border-radius: 2px;
transition: transform 0.2s, opacity 0.2s;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] span:nth-child(2) {
opacity: 0;
}
.nav-toggle[aria-expanded="true"] span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
@media (max-width: 640px) {
.nav-toggle {
display: flex;
}
.nav-links {
display: none;
position: absolute;
top: 3.5rem;
left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
padding: 0.75rem 1.25rem 1rem;
gap: 0.1rem;
}
.nav-links.is-open {
display: flex;
}
.lang-item {
margin-left: 0;
border-left: none;
padding-left: 0;
border-top: 1px solid var(--color-border);
margin-top: 0.5rem;
padding-top: 0.5rem;
}
}
</style>

167
src/components/Offers.astro Normal file
View file

@ -0,0 +1,167 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
const offerKeys = ['home', 'business', 'industry', 'med'] as const;
const icons: Record<typeof offerKeys[number], string> = {
home: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>`,
business: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
<line x1="12" y1="12" x2="12" y2="17"/><line x1="9.5" y1="14.5" x2="14.5" y2="14.5"/>
</svg>`,
industry: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>`,
med: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>`,
};
---
<section class="offers-section" id="oferty" aria-labelledby="offers-heading">
<div class="container">
<span class="section-label" aria-hidden="true">
{locale === 'en' ? 'Solutions' : 'Oferta'}
</span>
<h2 id="offers-heading">{tr.offers.h2}</h2>
<div class="offers-grid">
{offerKeys.map((key) => {
const offer = tr.offers[key];
const isConservative = 'conservative' in offer && offer.conservative;
return (
<article
class={`offer-card offer-${key}`}
id={key}
aria-labelledby={`offer-${key}-title`}
>
<div class="offer-header">
<div class="offer-icon" aria-hidden="true" set:html={icons[key]} />
<div>
<h3 id={`offer-${key}-title`}>{offer.title}</h3>
<p class="offer-who">{offer.who}</p>
</div>
</div>
<p class="offer-desc">{offer.desc}</p>
{isConservative && (
<p class="conservative-note" role="note">
{locale === 'en'
? 'Advisory only — all outputs require human verification before any action.'
: 'Rola wyłącznie doradcza — każdy wynik wymaga weryfikacji przez człowieka przed podjęciem działania.'}
</p>
)}
<blockquote class="offer-example">
<p>{offer.example}</p>
</blockquote>
</article>
);
})}
</div>
</div>
</section>
<style>
.offers-section {
background: var(--color-bg);
border-top: 1px solid var(--color-border);
}
.offers-section h2 {
margin-bottom: 3rem;
}
.offers-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 640px) {
.offers-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.offer-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.offer-card.offer-industry,
.offer-card.offer-med {
border-top: 3px solid var(--color-industry);
}
.offer-header {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.offer-icon {
color: var(--color-accent);
flex-shrink: 0;
line-height: 0;
margin-top: 2px;
}
.offer-industry .offer-icon,
.offer-med .offer-icon {
color: var(--color-industry);
}
.offer-card h3 {
font-size: 1.05rem;
margin-bottom: 0.2rem;
}
.offer-who {
font-size: 0.82rem;
color: var(--color-text-muted);
max-width: none;
}
.offer-desc {
font-size: 0.9rem;
line-height: 1.65;
max-width: none;
}
.conservative-note {
font-size: 0.8rem;
color: var(--color-industry);
background: rgba(44, 62, 80, 0.05);
border-left: 3px solid var(--color-industry);
padding: 0.6rem 0.85rem;
border-radius: 0 var(--radius) var(--radius) 0;
max-width: none;
line-height: 1.5;
}
.offer-example {
background: var(--color-surface-alt);
border-left: none;
border-radius: var(--radius);
padding: 0.9rem 1rem;
margin: 0;
}
.offer-example p {
font-size: 0.88rem;
font-style: italic;
color: var(--color-text-muted);
max-width: none;
}
</style>

View file

@ -0,0 +1,95 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
---
<section class="problem-section" aria-labelledby="problem-heading">
<div class="container problem-inner">
<div class="problem-text">
<span class="section-label" aria-hidden="true">
{locale === 'en' ? 'The challenge' : 'Wyzwanie'}
</span>
<h2 id="problem-heading">{tr.problem.h2}</h2>
<p>{tr.problem.body}</p>
</div>
<div class="problem-visual" aria-hidden="true">
<div class="contrast-list">
<div class="contrast-bad">
<span class="contrast-icon">✗</span>
<span>{locale === 'en' ? 'Generic chatbot in the cloud' : 'Generyczny chatbot w chmurze'}</span>
</div>
<div class="contrast-bad">
<span class="contrast-icon">✗</span>
<span>{locale === 'en' ? 'Data sent to third parties' : 'Dane wysyłane na zewnątrz'}</span>
</div>
<div class="contrast-bad">
<span class="contrast-icon">✗</span>
<span>{locale === 'en' ? 'No integration with your tools' : 'Brak integracji z Twoimi narzędziami'}</span>
</div>
<div class="contrast-bad">
<span class="contrast-icon">✗</span>
<span>{locale === 'en' ? 'One more tool to manage' : 'Kolejne narzędzie do obsługi'}</span>
</div>
</div>
</div>
</div>
</section>
<style>
.problem-section {
background: var(--color-bg);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.problem-inner {
display: grid;
grid-template-columns: 1fr;
gap: 2.5rem;
align-items: center;
}
@media (min-width: 768px) {
.problem-inner {
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
}
.problem-text h2 {
color: var(--color-text);
}
.problem-text p {
max-width: 48ch;
}
.contrast-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.contrast-bad {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.9rem 1.1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-size: 0.9rem;
color: var(--color-text-muted);
}
.contrast-icon {
font-size: 0.85rem;
color: #b06060;
flex-shrink: 0;
font-weight: 500;
}
</style>

View file

@ -0,0 +1,102 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
---
<section class="process-section" aria-labelledby="process-heading">
<div class="container">
<span class="section-label" aria-hidden="true">
{locale === 'en' ? 'Process' : 'Proces'}
</span>
<h2 id="process-heading">{tr.process.h2}</h2>
<ol class="steps" aria-label={locale === 'en' ? 'How we start — 3 steps' : 'Jak zaczynamy — 3 kroki'}>
{tr.process.steps.map((step, i) => (
<li class="step">
<div class="step-number" aria-hidden="true">{i + 1}</div>
<div class="step-content">
<h3>{step.title}</h3>
<p>{step.body}</p>
</div>
</li>
))}
</ol>
</div>
</section>
<style>
.process-section {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.process-section h2 {
margin-bottom: 3rem;
}
.steps {
list-style: none;
display: flex;
flex-direction: column;
gap: 0;
max-width: 600px;
position: relative;
}
.step {
display: flex;
gap: 1.5rem;
align-items: flex-start;
position: relative;
padding-bottom: 2.5rem;
}
.step:last-child {
padding-bottom: 0;
}
.step::after {
content: '';
position: absolute;
left: 1.1rem;
top: 2.5rem;
bottom: 0;
width: 1px;
background: var(--color-border);
}
.step:last-child::after {
display: none;
}
.step-number {
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
background: var(--color-accent);
color: #fff;
font-size: 0.88rem;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.step-content h3 {
margin-top: 0.35rem;
margin-bottom: 0.4rem;
}
.step-content p {
font-size: 0.92rem;
max-width: 44ch;
}
</style>

View file

@ -0,0 +1,35 @@
---
import type { Locale } from '../i18n/index.ts';
import { t } from '../i18n/index.ts';
interface Props { locale: Locale }
const { locale } = Astro.props;
const tr = t(locale);
// Flag to show/hide this section on MVP
const SHOW_SECURITY = false;
---
{SHOW_SECURITY && (
<section class="security-section" aria-labelledby="security-heading">
<div class="container">
<span class="section-label" aria-hidden="true">
{locale === 'en' ? 'Security' : 'Bezpieczeństwo'}
</span>
<h2 id="security-heading">{tr.security.h2}</h2>
<p>{tr.security.body}</p>
</div>
</section>
)}
<style>
.security-section {
background: var(--color-bg);
border-top: 1px solid var(--color-border);
}
.security-section p {
max-width: 56ch;
font-size: 1.05rem;
}
</style>

133
src/i18n/en.json Normal file
View file

@ -0,0 +1,133 @@
{
"lang": "en",
"dir": "ltr",
"meta": {
"title": "humanAI — the human side of AI",
"description": "Private, local AI systems deeply integrated with your environment. humanAI Home, Business, Industry and Med.",
"og_title": "humanAI — the human side of AI",
"og_description": "We build AI systems with a human in the decision loop. Private, local, deeply integrated."
},
"nav": {
"home": "Home",
"business": "Business",
"industry": "Industry",
"med": "Med",
"contact": "Contact",
"lang_switch": "PL"
},
"hero": {
"badge": "Private, local AI systems",
"h1": "The human side of AI",
"subtitle": "We build AI systems deeply integrated with your environment, always with a human in the decision loop. Not another chatbot.",
"cta_primary": "Let's talk",
"cta_secondary": "See what we do"
},
"pillars": {
"home": {
"title": "Home",
"tagline": "Smart home with voice and automation"
},
"business": {
"title": "Business",
"tagline": "Knowledge, email, docs for SMBs"
},
"industry": {
"title": "Industry",
"tagline": "Telemetry and alerts for manufacturing"
},
"med": {
"title": "Med",
"tagline": "Clinical admin with human oversight"
}
},
"problem": {
"h2": "AI promises a lot. Most deployments disappoint.",
"body": "Generic cloud chatbots don't know your context, send your data elsewhere, and don't integrate with what you actually use. The result: one more tool to manage, instead of help that works in the background."
},
"how": {
"h2": "We do this differently",
"local": {
"title": "Local and private",
"body": "Your data stays with you. Models run on your infrastructure, with no sensitive information sent to the cloud."
},
"integrations": {
"title": "Deep integrations",
"body": "We connect AI to the systems you actually use — from smart home (KNX, Home Assistant) to business tools."
},
"human": {
"title": "Human in the loop",
"body": "AI advises and prepares; people decide. Especially where the stakes are high."
}
},
"offers": {
"h2": "Four areas of application",
"home": {
"title": "humanAI Home",
"who": "Smart homes and private clients",
"desc": "AI on top of your smart home. Automations described in plain language, integrations with KNX, Loxone, FIBARO and Home Assistant.",
"example": "\"Set the house for the evening\" — lights, heating and music adapted to the time and your habits."
},
"business": {
"title": "humanAI Business",
"who": "Small and medium businesses",
"desc": "An AI knowledge base, an email and document assistant, and simple CRM/ERP workflows — all grounded in your own data.",
"example": "An assistant that knows your procedures, answers staff questions, and drafts replies to customer emails."
},
"industry": {
"title": "humanAI Industry",
"who": "Manufacturing plants",
"desc": "Telemetry dashboards, anomaly detection and a maintenance assistant. Advisory by design — the system flags, the team decides.",
"example": "An early warning about unusual machine behavior, with suggested causes to verify.",
"conservative": true
},
"med": {
"title": "humanAI Med",
"who": "Clinics and hospitals",
"desc": "Administrative AI — billing code classification and document workflow automation. Every output goes through human review.",
"example": "A draft billing-code suggestion, prepared for staff approval.",
"conservative": true
}
},
"process": {
"h2": "How we start",
"steps": [
{
"title": "A conversation",
"body": "We learn your context, systems and goal. No commitment."
},
{
"title": "A pilot",
"body": "We deploy one narrow, measurable use case."
},
{
"title": "Rollout",
"body": "We scale what works and integrate it with the rest."
}
]
},
"security": {
"h2": "Security built in from the start",
"body": "Local data, TLS encryption, admin access over a private network. We build so sensitive information never leaves your infrastructure."
},
"contact": {
"h2": "Let's talk about your case",
"subtitle": "Tell us what you need — we'll come back with a concrete proposal.",
"field_name": "Name",
"field_email": "Email",
"field_message": "Message",
"button": "Get in touch",
"email": "oskar@gethumanai.com"
},
"footer": {
"tagline": "The human side of AI",
"links": [
{ "label": "Home", "anchor": "#home" },
{ "label": "Business", "anchor": "#business" },
{ "label": "Industry", "anchor": "#industry" },
{ "label": "Med", "anchor": "#med" },
{ "label": "Contact", "anchor": "#contact" },
{ "label": "Privacy policy", "anchor": "/en/privacy-policy" }
],
"copyright": "humanAI"
}
}

28
src/i18n/index.ts Normal file
View file

@ -0,0 +1,28 @@
import pl from './pl.json';
import en from './en.json';
export type Locale = 'pl' | 'en';
const translations = { pl, en } as const;
export function t(locale: Locale) {
return translations[locale];
}
export function getLangFromUrl(url: URL): Locale {
const [, first] = url.pathname.split('/');
if (first === 'en') return 'en';
return 'pl';
}
export function getAlternateUrl(url: URL, targetLocale: Locale): string {
const path = url.pathname;
if (targetLocale === 'en') {
if (path.startsWith('/en')) return path;
return '/en' + (path === '/' ? '/' : path);
} else {
if (path.startsWith('/en/')) return path.slice(3) || '/';
if (path === '/en') return '/';
return path;
}
}

133
src/i18n/pl.json Normal file
View file

@ -0,0 +1,133 @@
{
"lang": "pl",
"dir": "ltr",
"meta": {
"title": "humanAI — ludzka strona AI",
"description": "Prywatne, lokalne systemy AI głęboko zintegrowane z Twoim otoczeniem. humanAI Home, Business, Industry i Med.",
"og_title": "humanAI — ludzka strona AI",
"og_description": "Budujemy systemy AI z człowiekiem w pętli decyzji. Prywatne, lokalne, głęboko zintegrowane."
},
"nav": {
"home": "Home",
"business": "Business",
"industry": "Industry",
"med": "Med",
"contact": "Kontakt",
"lang_switch": "EN"
},
"hero": {
"badge": "Prywatne, lokalne systemy AI",
"h1": "Ludzka strona AI",
"subtitle": "Budujemy systemy AI głęboko zintegrowane z Twoim otoczeniem i zawsze z człowiekiem w pętli decyzji. Nie kolejny chatbot.",
"cta_primary": "Porozmawiajmy",
"cta_secondary": "Zobacz, co robimy"
},
"pillars": {
"home": {
"title": "Home",
"tagline": "Smart home z głosem i automatyzacją"
},
"business": {
"title": "Business",
"tagline": "Wiedza, maile, dokumenty dla MŚP"
},
"industry": {
"title": "Industry",
"tagline": "Telemetria i alerty dla produkcji"
},
"med": {
"title": "Med",
"tagline": "Administracja kliniczna z nadzorem człowieka"
}
},
"problem": {
"h2": "AI obiecuje dużo. Większość wdrożeń rozczarowuje.",
"body": "Generyczne chatboty w chmurze nie znają Twojego kontekstu, wysyłają dane na zewnątrz i nie integrują się z tym, czego naprawdę używasz. Efekt: kolejne narzędzie do obsługi, zamiast pomocy, która działa w tle."
},
"how": {
"h2": "Inaczej do tego podchodzimy",
"local": {
"title": "Lokalnie i prywatnie",
"body": "Twoje dane zostają u Ciebie. Modele działają na Twojej infrastrukturze, bez wysyłania wrażliwych informacji do chmury."
},
"integrations": {
"title": "Głębokie integracje",
"body": "Łączymy AI z systemami, których realnie używasz — od inteligentnego domu (KNX, Home Assistant) po narzędzia firmowe."
},
"human": {
"title": "Człowiek w pętli",
"body": "AI doradza i przygotowuje, decyzję podejmuje człowiek. Szczególnie tam, gdzie stawka jest wysoka."
}
},
"offers": {
"h2": "Cztery obszary zastosowań",
"home": {
"title": "humanAI Home",
"who": "Inteligentne domy i klienci prywatni",
"desc": "AI nad Twoim systemem smart home. Automatyzacje opisane zwykłym językiem, integracje z KNX, Loxone, FIBARO i Home Assistant.",
"example": "\"Przygotuj dom na wieczór\" — światło, ogrzewanie i muzyka dopasowane do pory i Twoich nawyków."
},
"business": {
"title": "humanAI Business",
"who": "Małe i średnie firmy",
"desc": "Baza wiedzy AI, asystent do maili i dokumentów oraz proste workflow CRM/ERP — wszystko oparte o Twoje dane.",
"example": "Asystent, który zna Twoje procedury, odpowiada pracownikom i przygotowuje odpowiedzi na maile klientów."
},
"industry": {
"title": "humanAI Industry",
"who": "Zakłady produkcyjne",
"desc": "Pulpity telemetrii, wykrywanie anomalii i asystent utrzymania ruchu. Rola doradcza — system sygnalizuje, decyzje należą do zespołu.",
"example": "Wczesne ostrzeżenie o nietypowym zachowaniu maszyny, z podpowiedzią możliwych przyczyn do weryfikacji.",
"conservative": true
},
"med": {
"title": "humanAI Med",
"who": "Kliniki i szpitale",
"desc": "AI administracyjne — klasyfikacja kodów rozliczeniowych i automatyzacja obiegu dokumentów. Każdy wynik przechodzi weryfikację człowieka.",
"example": "Wstępna propozycja kodowania rozliczeń, przygotowana do zatwierdzenia przez personel.",
"conservative": true
}
},
"process": {
"h2": "Jak zaczynamy",
"steps": [
{
"title": "Rozmowa",
"body": "Poznajemy Twój kontekst, systemy i cel. Bez zobowiązań."
},
{
"title": "Pilotaż",
"body": "Wdrażamy jeden wąski, mierzalny przypadek użycia."
},
{
"title": "Wdrożenie",
"body": "Rozszerzamy to, co działa, i integrujemy z resztą."
}
]
},
"security": {
"h2": "Bezpieczeństwo wbudowane od początku",
"body": "Dane lokalnie, szyfrowanie TLS, dostęp administracyjny przez prywatną sieć. Budujemy tak, żeby wrażliwe informacje nie opuszczały Twojej infrastruktury."
},
"contact": {
"h2": "Porozmawiajmy o Twoim przypadku",
"subtitle": "Opowiedz, czego potrzebujesz — odezwiemy się z konkretną propozycją.",
"field_name": "Imię",
"field_email": "E-mail",
"field_message": "Wiadomość",
"button": "Napisz do nas",
"email": "oskar@gethumanai.com"
},
"footer": {
"tagline": "Ludzka strona AI",
"links": [
{ "label": "Home", "anchor": "#home" },
{ "label": "Business", "anchor": "#business" },
{ "label": "Industry", "anchor": "#industry" },
{ "label": "Med", "anchor": "#med" },
{ "label": "Kontakt", "anchor": "#kontakt" },
{ "label": "Polityka prywatności", "anchor": "/polityka-prywatnosci" }
],
"copyright": "humanAI"
}
}

193
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,193 @@
---
import type { Locale } from '../i18n/index.ts';
import { t, getAlternateUrl } from '../i18n/index.ts';
interface Props {
locale: Locale;
url: URL;
}
const { locale, url } = Astro.props;
const tr = t(locale);
const altLocale: Locale = locale === 'pl' ? 'en' : 'pl';
const altUrl = getAlternateUrl(url, altLocale);
const canonicalUrl = url.href;
const plUrl = locale === 'pl' ? url.pathname : getAlternateUrl(url, 'pl');
const enUrl = locale === 'en' ? url.pathname : getAlternateUrl(url, 'en');
---
<!doctype html>
<html lang={locale} dir={tr.dir}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{tr.meta.title}</title>
<meta name="description" content={tr.meta.description} />
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="pl" href={`https://gethumanai.com${plUrl}`} />
<link rel="alternate" hreflang="en" href={`https://gethumanai.com${enUrl}`} />
<link rel="alternate" hreflang="x-default" href="https://gethumanai.com/" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:title" content={tr.meta.og_title} />
<meta property="og:description" content={tr.meta.og_description} />
<meta property="og:locale" content={locale === 'pl' ? 'pl_PL' : 'en_US'} />
<meta property="og:site_name" content="humanAI" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<slot />
</body>
</html>
<style is:global>
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--color-bg: #fafaf8;
--color-surface: #ffffff;
--color-surface-alt: #f4f4f0;
--color-text: #1a1a18;
--color-text-muted: #6b6b65;
--color-accent: #1a3d2e;
--color-accent-hover: #122d21;
--color-border: #e4e4de;
--color-industry: #2c3e50;
--color-med: #2c3e50;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--radius: 6px;
--radius-lg: 12px;
--max-w: 1100px;
--section-py: clamp(4rem, 8vw, 7rem);
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-sans);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.65;
font-size: 1rem;
}
img {
max-width: 100%;
height: auto;
}
a {
color: inherit;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
border-radius: 2px;
}
.container {
width: 100%;
max-width: var(--max-w);
margin-inline: auto;
padding-inline: clamp(1.25rem, 5vw, 2.5rem);
}
.btn-primary {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.75rem;
background: var(--color-accent);
color: #fff;
text-decoration: none;
border-radius: var(--radius);
font-weight: 500;
font-size: 0.95rem;
transition: background 0.18s;
border: none;
cursor: pointer;
}
.btn-primary:hover {
background: var(--color-accent-hover);
}
.btn-outline {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.75rem;
background: transparent;
color: var(--color-accent);
text-decoration: none;
border-radius: var(--radius);
font-weight: 500;
font-size: 0.95rem;
border: 1.5px solid var(--color-accent);
transition: background 0.18s, color 0.18s;
cursor: pointer;
}
.btn-outline:hover {
background: var(--color-accent);
color: #fff;
}
section {
padding-block: var(--section-py);
}
.section-label {
display: inline-block;
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-text-muted);
margin-bottom: 1rem;
}
h2 {
font-size: clamp(1.6rem, 4vw, 2.2rem);
font-weight: 500;
line-height: 1.25;
margin-bottom: 1.25rem;
}
h3 {
font-size: 1.1rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
p {
color: var(--color-text-muted);
max-width: 60ch;
}
/* Utility */
.text-center { text-align: center; }
.text-center p { margin-inline: auto; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
</style>

29
src/pages/en/index.astro Normal file
View file

@ -0,0 +1,29 @@
---
import Layout from '../../layouts/Layout.astro';
import Nav from '../../components/Nav.astro';
import Hero from '../../components/Hero.astro';
import Problem from '../../components/Problem.astro';
import HowWeWork from '../../components/HowWeWork.astro';
import Offers from '../../components/Offers.astro';
import Process from '../../components/Process.astro';
import Security from '../../components/Security.astro';
import Contact from '../../components/Contact.astro';
import Footer from '../../components/Footer.astro';
const locale = 'en' as const;
const url = Astro.url;
---
<Layout locale={locale} url={url}>
<Nav locale={locale} url={url} />
<main id="main" tabindex="-1">
<Hero locale={locale} />
<Problem locale={locale} />
<HowWeWork locale={locale} />
<Offers locale={locale} />
<Process locale={locale} />
<Security locale={locale} />
<Contact locale={locale} />
</main>
<Footer locale={locale} />
</Layout>

29
src/pages/index.astro Normal file
View file

@ -0,0 +1,29 @@
---
import Layout from '../layouts/Layout.astro';
import Nav from '../components/Nav.astro';
import Hero from '../components/Hero.astro';
import Problem from '../components/Problem.astro';
import HowWeWork from '../components/HowWeWork.astro';
import Offers from '../components/Offers.astro';
import Process from '../components/Process.astro';
import Security from '../components/Security.astro';
import Contact from '../components/Contact.astro';
import Footer from '../components/Footer.astro';
const locale = 'pl' as const;
const url = Astro.url;
---
<Layout locale={locale} url={url}>
<Nav locale={locale} url={url} />
<main id="main" tabindex="-1">
<Hero locale={locale} />
<Problem locale={locale} />
<HowWeWork locale={locale} />
<Offers locale={locale} />
<Process locale={locale} />
<Security locale={locale} />
<Contact locale={locale} />
</main>
<Footer locale={locale} />
</Layout>

5
tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}