upload
This commit is contained in:
commit
e5901c12cf
32 changed files with 7121 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
10
.dockerignore
Normal file
10
.dockerignore
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
node_modules
|
||||
dist
|
||||
.astro
|
||||
.git
|
||||
.claude
|
||||
*.log
|
||||
.DS_Store
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
Caddyfile
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
|
||||
4
Caddyfile
Normal file
4
Caddyfile
Normal 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
20
Dockerfile
Normal 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
14
astro.config.mjs
Normal 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
7
docker-compose.yml
Normal 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
35
nginx.conf
Normal 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
5797
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal 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
5
public/favicon.svg
Normal 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
BIN
public/favicon256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/favicon64.png
Normal file
BIN
public/favicon64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/meta.png
Normal file
BIN
public/meta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
13
public/og.svg
Normal file
13
public/og.svg
Normal 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 |
88
src/components/ErrorPage.astro
Normal file
88
src/components/ErrorPage.astro
Normal 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="/">← back to madoka.network</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
131
src/components/Globe.astro
Normal file
131
src/components/Globe.astro
Normal 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
5
src/pages/400.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={400} />
|
||||
5
src/pages/403.astro
Normal file
5
src/pages/403.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={403} />
|
||||
5
src/pages/404.astro
Normal file
5
src/pages/404.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={404} />
|
||||
5
src/pages/408.astro
Normal file
5
src/pages/408.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={408} />
|
||||
5
src/pages/429.astro
Normal file
5
src/pages/429.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={429} />
|
||||
5
src/pages/451.astro
Normal file
5
src/pages/451.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={451} />
|
||||
5
src/pages/500.astro
Normal file
5
src/pages/500.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={500} />
|
||||
5
src/pages/502.astro
Normal file
5
src/pages/502.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={502} />
|
||||
5
src/pages/503.astro
Normal file
5
src/pages/503.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={503} />
|
||||
5
src/pages/504.astro
Normal file
5
src/pages/504.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={504} />
|
||||
5
src/pages/520.astro
Normal file
5
src/pages/520.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={520} />
|
||||
5
src/pages/521.astro
Normal file
5
src/pages/521.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import ErrorPage from '../components/ErrorPage.astro';
|
||||
---
|
||||
|
||||
<ErrorPage code={521} />
|
||||
202
src/pages/index.astro
Normal file
202
src/pages/index.astro
Normal 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 & 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
706
src/styles/global.css
Normal 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
3
tsconfig.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue