This commit is contained in:
krip 2026-08-13 03:59:46 +03:00
commit e5901c12cf
32 changed files with 7121 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

10
.dockerignore Normal file
View file

@ -0,0 +1,10 @@
node_modules
dist
.astro
.git
.claude
*.log
.DS_Store
Dockerfile
docker-compose.yml
Caddyfile

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
dist/
.astro/

4
Caddyfile Normal file
View file

@ -0,0 +1,4 @@
# madoka.network — reverse proxy to the Docker container
madoka.network {
reverse_proxy 127.0.0.1:4321
}

20
Dockerfile Normal file
View file

@ -0,0 +1,20 @@
# syntax=docker/dockerfile:1
# ── Build stage ───────────────────────────────────────────
FROM node:22-alpine AS build
WORKDIR /app
# Install deps first for better layer caching
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Runtime stage ─────────────────────────────────────────
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 4321

14
astro.config.mjs Normal file
View file

@ -0,0 +1,14 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
site: 'https://madoka.network',
build: {
// Emit 400.html, 404.html, ... instead of 400/index.html —
// convenient for `error_page` directives on static hosting.
format: 'file',
},
vite: {
plugins: [tailwindcss()],
},
});

7
docker-compose.yml Normal file
View file

@ -0,0 +1,7 @@
services:
web:
build: .
image: madoka-network
ports:
- "127.0.0.1:4321:4321"
restart: unless-stopped

35
nginx.conf Normal file
View file

@ -0,0 +1,35 @@
server {
listen 4321;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Static error pages generated by Astro (build.format: 'file')
error_page 400 /400.html;
error_page 403 /403.html;
error_page 404 /404.html;
error_page 408 /408.html;
error_page 429 /429.html;
error_page 451 /451.html;
error_page 500 /500.html;
error_page 502 /502.html;
error_page 503 /503.html;
error_page 504 /504.html;
error_page 520 /520.html;
error_page 521 /521.html;
location / {
try_files $uri $uri/ =404;
}
# Hashed Astro assets cache aggressively
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_min_length 1024;
gzip_types text/css application/javascript image/svg+xml application/json;
}

5797
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "madoka-network",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@fontsource/zen-kaku-gothic-new": "^5.3.0",
"@lucide/astro": "^1.31.0",
"@tailwindcss/vite": "^4.3.3",
"astro": "7.2.1",
"cobe": "^2.0.1",
"tailwindcss": "^4.3.3",
"typescript": "^5.9.2"
}
}

5
public/favicon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="16" fill="#fff8fb"/>
<path d="M32 8 37.8 26.2 56 32l-18.2 5.8L32 56l-5.8-18.2L8 32l18.2-5.8L32 8Z" fill="#e85b91"/>
<circle cx="32" cy="32" r="4" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

BIN
public/favicon256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/favicon64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/meta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

13
public/og.svg Normal file
View file

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<defs>
<radialGradient id="a" cx="50%" cy="42%" r="70%"><stop offset="0" stop-color="#fff"/><stop offset=".5" stop-color="#fbe5ed"/><stop offset="1" stop-color="#dca3b7"/></radialGradient>
<filter id="b"><feGaussianBlur stdDeviation="28"/></filter>
</defs>
<rect width="1200" height="630" fill="url(#a)"/>
<circle cx="600" cy="290" r="170" fill="#f16d9d" opacity=".12" filter="url(#b)"/>
<g fill="#53172d" text-anchor="middle" font-family="Inter, Arial, sans-serif">
<text x="600" y="315" font-size="86" font-weight="500" letter-spacing="-4">madoka.network</text>
<text x="600" y="375" font-size="27" opacity=".68" letter-spacing="2">meow</text>
</g>
<path d="M600 105 607 127 629 134l-22 7-7 22-7-22-22-7 22-7 7-22Z" fill="#e85b91" opacity=".78"/>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View file

@ -0,0 +1,88 @@
---
import '../styles/global.css';
import '@fontsource/zen-kaku-gothic-new/300.css';
interface Props {
code: number;
}
const { code } = Astro.props;
const ERRORS: Record<number, { title: string; description: string }> = {
400: {
title: 'Bad Request',
description: "The server couldn't understand your request — it appears to be malformed.",
},
403: {
title: 'Forbidden',
description: "You don't have permission to access this resource.",
},
404: {
title: 'Not Found',
description: "The page you're looking for doesn't exist or has been moved.",
},
408: {
title: 'Request Timeout',
description: 'The server timed out waiting for your request. Please try again.',
},
429: {
title: 'Too Many Requests',
description: 'Too many requests in too little time. Slow down and retry shortly.',
},
451: {
title: 'Unavailable For Legal Reasons',
description: 'This resource is unavailable for legal reasons.',
},
500: {
title: 'Internal Server Error',
description: 'Something went wrong on our side. Please try again later.',
},
502: {
title: 'Bad Gateway',
description: 'The server received an invalid response from upstream. Try again shortly.',
},
503: {
title: 'Service Unavailable',
description: 'The service is temporarily unavailable — likely maintenance or overload.',
},
504: {
title: 'Gateway Timeout',
description: "The upstream server didn't respond in time. Please try again later.",
},
520: {
title: 'Unknown Error',
description: 'The origin server returned an unexpected response.',
},
521: {
title: 'Web Server Is Down',
description: 'The origin server is refusing connections.',
},
};
const error = ERRORS[code] ?? { title: 'Error', description: 'An unexpected error occurred.' };
const title = `${code} ${error.title} · madoka.network`;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="robots" content="noindex" />
<link rel="canonical" href={canonical} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<div class="background" aria-hidden="true"></div>
<main class="error-page">
<p class="error-code">{code}</p>
<h1 class="error-title">{error.title}</h1>
<p class="error-desc">{error.description}</p>
<a class="error-home" href="/">&larr; back to madoka.network</a>
</main>
</body>
</html>

131
src/components/Globe.astro Normal file
View file

@ -0,0 +1,131 @@
---
// Port of Magic UI's Globe (https://magicui.design/docs/components/globe)
// built on top of cobe v2 — the render loop is driven manually via
// globe.update() + requestAnimationFrame, spring is integrated manually.
interface Props {
class?: string;
}
const { class: className = '' } = Astro.props;
---
<div class:list={['globe relative mx-auto aspect-square w-full max-w-[460px]', className]}>
<canvas
id="globe-canvas"
class="size-full cursor-grab opacity-0 transition-opacity duration-500 contain-[layout_paint_size]"
></canvas>
</div>
<script>
import createGlobe, { type COBEOptions } from 'cobe';
const MOVEMENT_DAMPING = 1400;
// Mirrors motion's useSpring({ mass: 1, damping: 30, stiffness: 100 })
const SPRING = { mass: 1, damping: 30, stiffness: 100 };
type GlobeBaseConfig = Omit<COBEOptions, 'width' | 'height' | 'phi' | 'theta' | 'devicePixelRatio'>;
// Same markers as Magic UI's GLOBE_CONFIG, colors adapted to site theme
const GLOBE_CONFIG: GlobeBaseConfig = {
dark: 0,
diffuse: 0.4,
mapSamples: 16000,
mapBrightness: 1.2,
baseColor: [1, 1, 1],
markerColor: [223 / 255, 78 / 255, 134 / 255],
glowColor: [1, 1, 1],
markers: [
{ location: [52.5958, 21.4543], size: 0.07 },
{ location: [52.5174, 4.8180], size: 0.07 },
{ location: [38.7377, -9.1594], size: 0.07 },
{ location: [22.3124, 114.1794], size: 0.07 },
{ location: [40.5496, -74.4719], size: 0.07 },
{ location: [59.4338, 24.7474], size: 0.07 },
{ location: [50.1094, 8.6799], size: 0.07 },
{ location: [55.7519, 37.6290], size: 0.07 },
{ location: [60.1785, 24.9240], size: 0.07 },
],
};
function initGlobe() {
const canvas = document.getElementById('globe-canvas') as HTMLCanvasElement | null;
if (!canvas) return;
let phi = 0;
let size = canvas.offsetWidth;
let pointerInteracting: number | null = null;
// Spring state
let target = 0;
let current = 0;
let velocity = 0;
let lastTime: number | null = null;
const globe = createGlobe(canvas, {
...GLOBE_CONFIG,
width: size,
height: size,
devicePixelRatio: Math.min(window.devicePixelRatio || 1, 2),
phi: 0,
theta: 0.3,
});
const onResize = () => {
size = canvas.offsetWidth;
globe.update({ width: size, height: size });
};
window.addEventListener('resize', onResize);
// cobe v2 has no built-in loop — drive it ourselves
const render = (now: number) => {
if (lastTime === null) lastTime = now;
const dt = Math.min((now - lastTime) / 1000, 0.064);
lastTime = now;
const force = -SPRING.stiffness * (current - target) - SPRING.damping * velocity;
velocity += (force / SPRING.mass) * dt;
current += velocity * dt;
if (pointerInteracting === null) phi += 0.005;
globe.update({ phi: phi + current });
requestAnimationFrame(render);
};
requestAnimationFrame(render);
const updatePointerInteraction = (value: number | null) => {
pointerInteracting = value;
canvas.style.cursor = value !== null ? 'grabbing' : 'grab';
};
const updateMovement = (clientX: number) => {
if (pointerInteracting !== null) {
const delta = clientX - pointerInteracting;
target += delta / MOVEMENT_DAMPING;
}
};
canvas.addEventListener('pointerdown', (e) => updatePointerInteraction(e.clientX));
canvas.addEventListener('pointerup', () => updatePointerInteraction(null));
canvas.addEventListener('pointerout', () => updatePointerInteraction(null));
canvas.addEventListener('mousemove', (e) => updateMovement(e.clientX));
canvas.addEventListener(
'touchmove',
(e) => {
if (e.touches[0]) updateMovement(e.touches[0].clientX);
},
{ passive: true },
);
setTimeout(() => {
canvas.style.opacity = '1';
}, 0);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGlobe);
} else {
initGlobe();
}
</script>

5
src/pages/400.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={400} />

5
src/pages/403.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={403} />

5
src/pages/404.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={404} />

5
src/pages/408.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={408} />

5
src/pages/429.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={429} />

5
src/pages/451.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={451} />

5
src/pages/500.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={500} />

5
src/pages/502.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={502} />

5
src/pages/503.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={503} />

5
src/pages/504.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={504} />

5
src/pages/520.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={520} />

5
src/pages/521.astro Normal file
View file

@ -0,0 +1,5 @@
---
import ErrorPage from '../components/ErrorPage.astro';
---
<ErrorPage code={521} />

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

@ -0,0 +1,202 @@
---
import '../styles/global.css';
import '@fontsource/zen-kaku-gothic-new/300.css';
import '@fontsource/zen-kaku-gothic-new/700.css';
import { Check, MapPin as MapPin, Zap, ArrowUpDown, ChevronsLeftRightEllipsis, BrickWallFire, Waypoints } from '@lucide/astro';
import Globe from '../components/Globe.astro';
const title = 'madoka.network';
const canonical = new URL(Astro.url.pathname, Astro.site);
const stickers = [
{ icon: MapPin, title: "9 PoP's", text: 'Over world', color: 'pink-1' },
{ icon: Zap, title: 'CDN Network', text: 'with 4 storage servers with summary 5+ TB of storage', color: 'pink-2' },
{ icon: ArrowUpDown, title: 'Dual-stack support', text: 'We are support IPv4 and IPv6 configuration for your projects', color: 'pink-3' },
{ icon: ChevronsLeftRightEllipsis, title: 'Direct connections with\nTier-1 ISP', text: 'and most popular IX-s', color: 'pink-4' },
{ icon: BrickWallFire, title: 'L4/L7 DDoS Protection', text: 'Our upstreams can mitigate up to 6+ tbit/s attacks', color: 'pink-5' },
{ icon: Waypoints, title: '48.8 Gbit/sec.', text: "Summary capacity through over our Edge's", color: 'pink-6' },
];
const plans = [
{
name: 'Free',
price: '$0',
period: 'forever',
features: ['1 project', 'Shared resources', 'Community support', 'Basic analytics'],
cta: 'Get Started',
shade: 'pink-1',
},
{
name: 'Starter',
price: '$5',
period: '/month',
features: ['5 projects', 'Dedicated resources', 'Email support', 'Advanced analytics', 'Custom domains'],
cta: 'Start Free Trial',
featured: true,
shade: 'pink-2',
},
{
name: 'Pro',
price: '$29',
period: '/month',
features: ['Unlimited projects', 'Priority resources', 'Priority support', 'Full analytics', 'Custom domains', 'API access'],
cta: 'Start Free Trial',
shade: 'pink-3',
},
{
name: 'Enterprise',
price: '$99',
period: '/month',
features: ['Unlimited everything', 'Dedicated infrastructure', '24/7 support', 'SLA guarantee', 'Custom integrations', 'Dedicated manager'],
cta: 'Contact Sales',
shade: 'pink-4',
},
];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#f7dce6" />
<link rel="canonical" href={canonical} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:title" content={title} />
<meta property="og:image" content={new URL('/meta.png', Astro.site)} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:image" content={new URL('/meta.png', Astro.site)} />
<title>{title}</title>
</head>
<body>
<div class="background" aria-hidden="true"></div>
<!-- ── Block 1: Hero ──────────────────────────────── -->
<main class="hero">
<section class="intro" aria-labelledby="site-title">
<h1 id="site-title"><span class="word">madoka</span><span class="dot">.</span><span class="word">network</span></h1>
<p>meow</p>
</section>
</main>
<!-- ── Block: About ─────────────────────────────── -->
<section class="about" id="about">
<div class="about-wrap">
<h2 class="about-title">Some words...</h2>
<p class="about-text">
Madoka Network is an ambitious indie project by an international IT community.
What began as a couple of self-hosted NS servers has grown into a fully
decentralized CDN &amp; WAF platform — built as an open alternative to Cloudflare.
Our infrastructure spans geo-distributed PoPs in the USA, Russia, Estonia,
Germany, Portugal, Finland and Poland, with a combined capacity of 45+ Gbit/s,
linked by a secure Headscale mesh network and backed by a distributed
Garage-based S3 storage.
</p>
</div>
</section>
<!-- ── Block 2: Features ──────────────────────────── -->
<section class="features" id="features">
<div class="features-wrap">
<h2 class="features-title">Our advantages</h2>
<div class="features-inner">
<div class="features-cards">
<div class="sticker-grid">
{stickers.map((s) => {
const Icon = s.icon;
return (
<div class={`sticker sticker--${s.color}`}>
<div class="sticker-icon"><Icon size={26} /></div>
<p class="sticker-title">{s.title}</p>
<p class="sticker-text">{s.text}</p>
</div>
);
})}
</div>
</div>
<div class="features-globe">
<Globe />
</div>
</div>
</div>
</section>
<!-- ── Block 3: Pricing ───────────────────────────── -->
<section class="pricing" id="pricing">
<div class="pricing-wrap">
<h2 class="pricing-title">Pricing</h2>
<p class="pricing-subtitle">Choose the plan that works for you</p>
<div class="pricing-grid">
{plans.map((plan) => (
<div class={`pricing-card pricing-card--${plan.shade}`}>
{plan.featured && <span class="pricing-badge">Popular</span>}
<div class="pricing-info">
<h3 class="pricing-name">{plan.name}</h3>
<div class="pricing-price">{plan.price}</div>
<p class="pricing-period">{plan.period}</p>
</div>
<ul class="pricing-features">
{plan.features.map((f) => (
<li><span>{f}</span><Check size={14} /></li>
))}
</ul>
<a
href="https://panel.madoka.network"
class={`pricing-btn${plan.featured ? ' pricing-btn--primary' : ''}`}
>
{plan.cta}
</a>
</div>
))}
</div>
</div>
</section>
<!-- ── Footer ─────────────────────────────────────── -->
<footer class="site-footer">
<div class="footer-inner">
<div class="footer-left">
<h4 class="footer-brand">madoka.network</h4>
<p class="footer-desc">WAF with a total throughput of 48.8 Gbps/sec., self NameServers, CDN network with storage servers and GeoIP, Edge-PoP over world, direct connection with Tier 1 providers and IX-s, Email routing and dual-stack support (IPv4/IPv6)</p>
<p class="footer-abuse">
abuse: <a href="mailto:abuse@madoka.network">abuse@madoka.network</a>
</p>
<div class="footer-badges">
<a class="footer-badge" href="https://sshi.pw" target="_blank" rel="noopener">
<img src="https://sshi.pw/badge.png" alt="sshi.pw" width="88" height="31" loading="lazy" onerror="this.parentElement.style.display='none'" />
</a>
<a class="footer-badge" href="https://webstealing.cc" target="_blank" rel="noopener">
<img src="https://files.webstealing.cc/u/badge.png" alt="webstealing.cc" width="88" height="31" loading="lazy" onerror="this.parentElement.style.display='none'" />
</a>
<a class="footer-badge" href="https://www.debian.org" target="_blank" rel="noopener">
<img src="https://cyber.dabamos.de/88x31/powered-by-debian.gif" alt="Powered by Debian" width="88" height="31" loading="lazy" onerror="this.parentElement.style.display='none'" />
</a>
<a class="footer-badge" href="https://tetopie.lol" target="_blank" rel="noopener">
<img src="https://tetopie.lol/buttons/tetodev.gif" alt="tetodev" width="88" height="31" loading="lazy" onerror="this.parentElement.style.display='none'" />
</a>
<a class="footer-badge" href="https://iamtsunde.re" target="_blank" rel="noopener">
<img src="https://iamtsunde.re/site/buttons/mybutton.png" alt="iamtsunde.re" width="88" height="31" loading="lazy" onerror="this.parentElement.style.display='none'" />
</a>
</div>
</div>
<div class="footer-right">
<a href="https://status.madoka.network" class="footer-link">Status</a>
<a href="https://madoka.network/readme" class="footer-link">If you see this, read me pls</a>
</div>
</div>
</footer>
</body>
</html>

706
src/styles/global.css Normal file
View file

@ -0,0 +1,706 @@
@import "tailwindcss";
@theme {
--color-brand: #df4e86;
--color-brand-deep: #4b1428;
}
:root {
color-scheme: light;
font-family: "Zen Kaku Gothic New", ui-sans-serif, sans-serif;
color: #481426;
background: #f8e9ee;
font-synthesis: none;
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
margin: 0;
}
body {
min-width: 280px;
}
/* ── Fixed background ─────────────────────────────────── */
.background {
position: fixed;
inset: -10%;
background:
radial-gradient(circle at 50% 42%, #ffffff 0 12%, rgba(255, 250, 252, 0.88) 26%, transparent 55%),
radial-gradient(circle at 10% 8%, rgba(211, 100, 141, 0.22), transparent 36%),
radial-gradient(circle at 88% 92%, rgba(181, 61, 105, 0.16), transparent 40%),
linear-gradient(135deg, #efd2dc, #fffafb 48%, #edc6d4);
background-size: 120% 120%;
animation: background-drift 24s ease-in-out infinite alternate;
z-index: -1;
}
/* ── Hero (Block 1) ───────────────────────────────────── */
.hero {
position: relative;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(28px, 5vw, 64px);
text-align: center;
}
.intro {
transform: translateY(-1vh);
animation: reveal 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
h1 {
display: inline-flex;
align-items: baseline;
gap: 0.018em;
margin: 0;
color: #4b1428;
font-size: clamp(2.65rem, 8.2vw, 7.4rem);
font-weight: 300;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 12px 45px rgba(101, 19, 50, 0.08);
transition: color 0.45s ease, text-shadow 0.45s ease;
}
.word {
letter-spacing: -0.055em;
}
h1:hover {
color: #641b37;
text-shadow: 0 0 22px rgba(230, 91, 143, 0.22);
}
.dot {
display: inline-grid;
width: 0.08em;
place-items: center;
color: #df4e86;
font-size: 0.68em;
line-height: 1;
letter-spacing: 0;
}
.intro p {
margin: clamp(18px, 2.5vw, 30px) 0 0;
color: rgba(72, 20, 38, 0.58);
font-size: clamp(0.82rem, 1.3vw, 1.06rem);
font-weight: 300;
letter-spacing: 0.14em;
}
/* ── About ────────────────────────────────────────────── */
.about {
position: relative;
display: flex;
align-items: center;
padding: clamp(60px, 10vw, 120px) clamp(20px, 4vw, 80px);
}
.about-wrap {
width: 100%;
max-width: 760px;
margin: 0 auto;
}
.about-title {
margin: 0 0 clamp(18px, 2.5vw, 28px);
color: #4b1428;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 300;
text-align: left;
}
.about-text {
margin: 0;
color: rgba(72, 20, 38, 0.62);
font-size: clamp(0.92rem, 1.3vw, 1.08rem);
font-weight: 300;
line-height: 1.75;
text-align: left;
}
/* ── Features (Block 2) ───────────────────────────────── */
.features {
position: relative;
min-height: 100svh;
display: flex;
align-items: center;
padding: clamp(40px, 5vw, 80px) clamp(20px, 4vw, 80px);
}
.features-wrap {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.features-title {
margin: 0 0 clamp(24px, 3vw, 40px);
color: #4b1428;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 300;
text-align: left;
}
.features-inner {
display: flex;
align-items: center;
gap: clamp(32px, 4vw, 64px);
width: 100%;
}
.features-cards {
flex: 1.2;
min-width: 0;
}
.sticker-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.sticker {
position: relative;
padding: 22px 18px 18px;
border-radius: 12px;
box-shadow:
2px 3px 0 rgba(72, 20, 38, 0.08),
4px 6px 16px rgba(72, 20, 38, 0.06);
transition: transform 0.25s ease, box-shadow 0.25s ease;
cursor: default;
}
.sticker:hover {
transform: rotate(0deg) scale(1.04) !important;
box-shadow:
2px 3px 0 rgba(72, 20, 38, 0.12),
6px 8px 24px rgba(72, 20, 38, 0.1);
}
.sticker::before,
.pricing-card::before {
content: "";
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-50%) rotate(-2deg);
width: 44px;
height: 14px;
background: rgba(255, 255, 255, 0.62);
border-radius: 2px;
border: 1px solid rgba(72, 20, 38, 0.06);
}
.sticker-icon {
margin-bottom: 6px;
color: #df4e86;
}
.sticker-icon svg {
display: block;
}
.sticker-title {
font-size: 0.88rem;
font-weight: 700;
margin: 0 0 4px;
color: #4b1428;
white-space: pre-line;
}
.sticker-text {
font-size: 0.74rem;
font-weight: 300;
margin: 0;
color: rgba(72, 20, 38, 0.6);
line-height: 1.4;
}
/* Sticker color variants — pink shades */
.sticker--pink-1 {
background: #fdeef6;
transform: rotate(-1.5deg);
}
.sticker--pink-2 {
background: #fce4f1;
transform: rotate(0.8deg);
}
.sticker--pink-3 {
background: #fbdaeb;
transform: rotate(-0.6deg);
}
.sticker--pink-4 {
background: #facfe4;
transform: rotate(1.3deg);
}
.sticker--pink-5 {
background: #f9c3dc;
transform: rotate(-0.9deg);
}
.sticker--pink-6 {
background: #f7b6d3;
transform: rotate(1.1deg);
}
/* Globe */
.features-globe {
flex: 0.8;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Pricing (Block 3) ────────────────────────────────── */
.pricing {
position: relative;
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: clamp(40px, 5vw, 80px) clamp(20px, 4vw, 80px);
}
.pricing-wrap {
width: 100%;
max-width: 920px;
}
.pricing-title {
margin: 0 0 8px;
color: #4b1428;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 300;
text-align: left;
}
.pricing-subtitle {
margin: 0 0 clamp(32px, 4vw, 56px);
color: rgba(72, 20, 38, 0.5);
font-size: clamp(0.82rem, 1.2vw, 1rem);
font-weight: 300;
text-align: left;
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 18px;
width: 100%;
}
.pricing-card {
position: relative;
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"info btn"
"features features";
align-items: center;
column-gap: clamp(20px, 3vw, 44px);
row-gap: 16px;
padding: 24px clamp(20px, 2.5vw, 32px);
border-radius: 12px;
box-shadow:
2px 3px 0 rgba(72, 20, 38, 0.08),
4px 6px 16px rgba(72, 20, 38, 0.06);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.pricing-card:hover {
transform: scale(1.01);
box-shadow:
2px 3px 0 rgba(72, 20, 38, 0.12),
6px 8px 24px rgba(72, 20, 38, 0.1);
}
/* Pricing card shades */
.pricing-card--pink-1 {
background: #fce4f1;
}
.pricing-card--pink-2 {
background: #f9c3dc;
}
.pricing-card--pink-3 {
background: #fbdaeb;
}
.pricing-card--pink-4 {
background: #facfe4;
}
.pricing-badge {
position: absolute;
top: -9px;
right: 28px;
display: inline-block;
background: #df4e86;
color: #fff;
font-size: 0.68rem;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 8px;
}
.pricing-info {
grid-area: info;
min-width: 0;
}
.pricing-name {
margin: 0 0 4px;
color: #4b1428;
font-size: clamp(1.1rem, 1.4vw, 1.3rem);
font-weight: 400;
}
.pricing-price {
margin: 0 0 2px;
color: #df4e86;
font-size: clamp(1.6rem, 2.4vw, 2.2rem);
font-weight: 300;
}
.pricing-period {
margin: 0;
color: rgba(72, 20, 38, 0.42);
font-size: 0.78rem;
font-weight: 300;
}
.pricing-features {
grid-area: features;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px 28px;
list-style: none;
margin: 0;
padding: 0;
text-align: left;
}
.pricing-features li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 0;
color: rgba(72, 20, 38, 0.68);
font-size: 0.82rem;
font-weight: 300;
border-bottom: 1px solid rgba(72, 20, 38, 0.06);
}
.pricing-features li svg {
flex-shrink: 0;
color: #df4e86;
}
.pricing-btn {
grid-area: btn;
justify-self: end;
display: inline-block;
min-width: 168px;
padding: 10px 20px;
border-radius: 10px;
border: 1px solid rgba(223, 78, 134, 0.25);
background: transparent;
color: #df4e86;
font-family: inherit;
font-size: 0.86rem;
font-weight: 400;
text-decoration: none;
text-align: center;
white-space: nowrap;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.pricing-btn:hover {
background: #df4e86;
color: #fff;
border-color: #df4e86;
}
.pricing-btn--primary {
background: #df4e86;
color: #fff;
border-color: #df4e86;
}
.pricing-btn--primary:hover {
background: #c93d74;
border-color: #c93d74;
}
/* ── Footer ────────────────────────────────────────────── */
.site-footer {
position: relative;
padding: clamp(32px, 4vw, 56px) clamp(20px, 4vw, 80px);
border-top: 1px solid rgba(72, 20, 38, 0.08);
}
.footer-inner {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 32px;
max-width: 1100px;
margin: 0 auto;
}
.footer-left {
flex: 1;
min-width: 0;
}
.footer-brand {
margin: 0 0 6px;
color: #4b1428;
font-size: 1.15rem;
font-weight: 400;
}
.footer-desc {
margin: 0 0 14px;
color: rgba(72, 20, 38, 0.5);
font-size: 0.82rem;
font-weight: 300;
}
.footer-abuse {
margin: 0 0 18px;
font-size: 0.78rem;
font-weight: 300;
}
.footer-abuse a {
color: #df4e86;
text-decoration: none;
border-bottom: 1px solid rgba(223, 78, 134, 0.3);
transition: border-color 0.2s ease;
}
.footer-abuse a:hover {
border-color: #df4e86;
}
.footer-badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.footer-badge {
display: inline-block;
width: 88px;
height: 31px;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.footer-badge:hover {
transform: scale(1.06);
}
.footer-badge img {
width: 100%;
height: 100%;
display: block;
}
/* Footer right links */
.footer-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
flex-shrink: 0;
}
.footer-link {
color: #df4e86;
text-decoration: none;
font-size: 0.82rem;
font-weight: 300;
border-bottom: 1px solid rgba(223, 78, 134, 0.25);
transition: border-color 0.2s ease;
}
.footer-link:hover {
border-color: #df4e86;
}
/* ── Error pages ───────────────────────────────────────── */
.error-page {
min-height: 100svh;
display: grid;
place-content: center;
place-items: center;
text-align: center;
padding: clamp(28px, 5vw, 64px);
animation: reveal 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.error-code {
margin: 0;
color: #df4e86;
font-size: clamp(4.5rem, 16vw, 10rem);
font-weight: 300;
line-height: 1;
letter-spacing: -0.04em;
text-shadow: 0 12px 45px rgba(101, 19, 50, 0.08);
}
.error-title {
display: block;
margin: clamp(10px, 1.6vw, 18px) 0 0;
color: #4b1428;
font-size: clamp(1.3rem, 3vw, 2rem);
font-weight: 300;
letter-spacing: 0;
text-shadow: none;
}
.error-desc {
margin: clamp(12px, 1.8vw, 20px) 0 0;
max-width: 46ch;
color: rgba(72, 20, 38, 0.58);
font-size: clamp(0.85rem, 1.2vw, 1rem);
font-weight: 300;
line-height: 1.6;
}
.error-home {
display: inline-block;
margin-top: clamp(26px, 3.5vw, 40px);
padding: 10px 22px;
border: 1px solid rgba(223, 78, 134, 0.25);
border-radius: 10px;
background: transparent;
color: #df4e86;
font-size: 0.86rem;
font-weight: 400;
text-decoration: none;
transition: background 0.2s ease, color 0.2s ease;
}
.error-home:hover {
background: #df4e86;
color: #fff;
}
/* ── Focus ─────────────────────────────────────────────── */
a:focus-visible,
button:focus-visible {
outline: 2px solid rgba(223, 78, 134, 0.55);
outline-offset: 2px;
border-radius: 6px;
}
/* ── Animations ────────────────────────────────────────── */
@keyframes background-drift {
to {
transform: translate3d(2%, -1.5%, 0) scale(1.02);
}
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(10px);
}
}
/* ── Responsive ────────────────────────────────────────── */
@media (max-width: 960px) {
.features-inner {
flex-direction: column;
}
.features-globe {
order: -1;
}
.globe {
max-width: 340px;
}
.pricing-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 680px) {
.sticker-grid {
grid-template-columns: repeat(2, 1fr);
}
.pricing-card {
grid-template-columns: 1fr;
grid-template-areas:
"info"
"features"
"btn";
}
.pricing-features {
grid-template-columns: 1fr;
}
.pricing-btn {
justify-self: stretch;
width: 100%;
min-width: 0;
}
.footer-inner {
flex-direction: column;
}
.footer-right {
align-items: flex-start;
}
}
@media (max-width: 520px) {
.hero {
padding-inline: 20px;
}
h1 {
font-size: clamp(2.45rem, 13vw, 4.1rem);
}
.word {
letter-spacing: -0.06em;
}
.intro p {
letter-spacing: 0.1em;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
h1 {
transition-duration: 0.01ms;
}
}

3
tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}