feat: version-aware docs built + staged on ISO

- Copy Astro landing page source into docs/website/ (20K, no node_modules)
- Add ISO version badge to LandingBody.astro (only shown when
  ASTRO_ISO_VERSION is set during build)
- Add build_and_stage_docs() to build.sh: builds the Astro site with
  the ISO version, stages output at /usr/local/share/clawdie-iso/docs/
- Skips gracefully when node/npm unavailable
- On the booted USB: open docs/index.html to see version-matched docs
This commit is contained in:
Sam & Claude 2026-06-23 11:58:03 +02:00
parent 6173e185ec
commit fdd0d260d0
26 changed files with 7498 additions and 0 deletions

View file

@ -1083,6 +1083,41 @@ install_zot_agent() {
# Stage an on-image NVIDIA pkg repo (all branches) so clawdie_live_gpu can
# `pkg install` the detected branch at boot (NVIDIA_UNIVERSAL lane).
#
# FreeBSD-build-host step (authored on Linux; runs + must be validated on
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
# Build the Astro docs site and stage it as static HTML on the ISO.
# When ISO_VERSION is set, the docs carry a version badge matching the image.
# Skips silently if node/npm are unavailable.
build_and_stage_docs() {
local _docs_src="${SCRIPT_DIR}/docs/website"
local _docs_out="${MOUNT_POINT}/usr/local/share/clawdie-iso/docs"
if [ ! -d "${_docs_src}" ]; then
echo " Docs source not found at ${_docs_src} — skipping"
return 0
fi
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
echo " Node/npm not available — skipping docs build"
return 0
fi
echo " Building docs (ISO ${ISO_VERSION})..."
(
cd "${_docs_src}"
npm ci --silent 2>&1 || { echo " WARN: npm ci failed, skipping docs"; exit 0; }
ASTRO_ISO_VERSION="${ISO_VERSION}" \
ASTRO_SITE_URL="file:///usr/local/share/clawdie-iso/docs" \
npm run build --silent 2>&1 || { echo " WARN: astro build failed, skipping docs"; exit 0; }
)
if [ -d "${_docs_src}/dist" ]; then
mkdir -p "${_docs_out}"
cp -a "${_docs_src}/dist/." "${_docs_out}/"
echo " Docs staged at /usr/local/share/clawdie-iso/docs/ (version ${ISO_VERSION})"
fi
}
# FreeBSD-build-host step (authored on Linux; runs + must be validated on
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
@ -2483,6 +2518,7 @@ configure_live_operator_session
install_colibri_service
install_zot_agent
install_nvidia_universal_repo
build_and_stage_docs
# Copy payload
# Rebuild payload paths from scratch inside the reusable work image. A failed

View file

@ -0,0 +1,28 @@
import { defineConfig, passthroughImageService } from 'astro/config';
import sitemap from '@astrojs/sitemap';
const site = process.env.ASTRO_SITE_URL || 'https://clawdie.si';
const outDir = process.env.ASTRO_OUT_DIR || './dist';
export default defineConfig({
site,
outDir,
output: 'static',
trailingSlash: 'always',
image: {
service: passthroughImageService(),
},
i18n: {
defaultLocale: 'en',
locales: ['en', 'sl'],
routing: { prefixDefaultLocale: true, redirectToDefaultLocale: false },
},
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: { en: 'en', sl: 'sl' },
},
}),
],
});

6581
docs/website/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

23
docs/website/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "clawdie-si",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "astro dev --host 0.0.0.0",
"start": "astro dev --host 0.0.0.0",
"build": "astro build",
"predeploy": "npm run build",
"deploy": "node scripts/deploy.mjs",
"preview": "astro preview --host 0.0.0.0",
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.16.11"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,22 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
const localRoot = process.cwd();
const distDir = path.join(localRoot, 'dist');
const webroot = process.env.CMS_WEBROOT || '/usr/local/www/clawdie-si';
if (!fs.existsSync(distDir)) {
console.error(`Build output not found: ${distDir}`);
process.exit(1);
}
const result = spawnSync(
'rsync',
['-av', '--delete', `${distDir}/`, `${webroot}/`],
{ stdio: 'inherit', env: process.env },
);
if (result.status !== 0) {
process.exit(result.status ?? 1);
}

View file

@ -0,0 +1,77 @@
---
import { getCollection, render } from 'astro:content';
import type { Locale } from '../i18n';
import { t } from '../i18n';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const strings = t(locale);
const all = await getCollection('landing', ({ id }) => id.startsWith(`${locale}/`));
const bySection = Object.fromEntries(all.map((e) => [e.data.section, e]));
const heroEntry = bySection['hero'];
const aboutEntry = bySection['about'];
const archEntry = bySection['architecture'];
const ctaEntry = bySection['install-cta'];
const Hero = heroEntry ? (await render(heroEntry)).Content : null;
const About = aboutEntry ? (await render(aboutEntry)).Content : null;
const Arch = archEntry ? (await render(archEntry)).Content : null;
const Cta = ctaEntry ? (await render(ctaEntry)).Content : null;
// Only set during ISO builds — shows the exact image version the docs were built against.
const isoVersion = import.meta.env.ASTRO_ISO_VERSION || null;
---
<header class="hero">
<div class="header-inner">
<div class="brand-mark">△</div>
<div class="header-text">
<h1>Clawdie<br /><span>{locale === 'sl' ? 'AI v vaših rokah' : 'AI you own'}</span></h1>
<p class="tagline">{strings.meta.description}</p>
</div>
</div>
</header>
<div class="hero-statement">
{Hero && <Hero />}
</div>
{
About && (
<section class="landing-section" id="about">
<h2>{aboutEntry?.data.title}</h2>
<About />
</section>
)
}
{
Arch && (
<section class="landing-section" id="architecture">
<h2>{archEntry?.data.title}</h2>
<Arch />
</section>
)
}
{
Cta && (
<section class="landing-section" id="install">
<h2>{ctaEntry?.data.title}</h2>
<Cta />
</section>
)
}
{
isoVersion && (
<footer class="iso-version-badge">
<p>Clawdie ISO {isoVersion} — these docs match the image they were built with.</p>
</footer>
)
}

View file

@ -0,0 +1,38 @@
---
import type { Locale } from '../i18n';
import { locales, t } from '../i18n';
interface Props {
current: Locale;
}
const { current } = Astro.props;
// Astro's pathname is the request path (e.g. "/en/", "/sl/about/").
// Strip the current locale prefix so we can prepend the target locale and
// preserve the rest of the path on language switch.
const pathname = Astro.url.pathname;
const prefix = `/${current}`;
const rest = pathname.startsWith(prefix)
? pathname.slice(prefix.length) || '/'
: pathname;
const flagFor: Record<Locale, string> = {
en: '🇬🇧',
sl: '🇸🇮',
};
---
<nav class="lang-switch" aria-label={t(current).switcher.label}>
{
locales.map((loc) => {
const href = `/${loc}${rest.startsWith('/') ? rest : `/${rest}`}`;
const label = t(current).switcher[loc];
return (
<a href={href} class={loc === current ? 'active' : ''}>
{flagFor[loc]} {label}
</a>
);
})
}
</nav>

View file

@ -0,0 +1,31 @@
---
import type { Locale } from '../i18n';
import { t } from '../i18n';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const s = t(locale).banner;
---
<div id="operator-banner" class="operator-banner" hidden>
<div class="operator-banner-text">
<strong>{s.heading}</strong> {s.body}
</div>
<div class="operator-banner-actions">
<a href="/controlplane/">{s.openControlplane}</a>
<a href="https://docs.clawdie.si/">{s.readDocs}</a>
<a href="https://docs.clawdie.si/operate/public-domain/">{s.claimDomain}</a>
</div>
</div>
<script is:inline>
(function () {
if (typeof window === 'undefined') return;
if (window.location.hostname === 'clawdie.si') return;
var el = document.getElementById('operator-banner');
if (el) el.hidden = false;
})();
</script>

View file

@ -0,0 +1,13 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const landing = defineCollection({
loader: glob({ base: './src/content/landing', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
order: z.number(),
section: z.enum(['hero', 'about', 'architecture', 'install-cta']),
}),
});
export const collections = { landing };

View file

@ -0,0 +1,16 @@
---
title: What is Clawdie
order: 2
section: about
---
Clawdie is a **self-hosted AI control plane** for operators who refuse to rent
their compute, their data, or their users' privacy.
You install it on your own hardware — a workstation, a colocated box, a basement
server. It provisions FreeBSD jails, runs your models, and gives every tenant on
your machine a clean slice of the platform: their own database, their own site,
their own backups.
No telemetry leaks upstream. No model weights walk out the door. No vendor
decides what your assistant is allowed to say tomorrow.

View file

@ -0,0 +1,16 @@
---
title: Architecture
order: 3
section: architecture
---
A Clawdie install has three layers:
- **Host** — FreeBSD with ZFS, nginx, Postgres, jail manager.
- **Control plane** — the operator's command surface; provisions tenants, runs
the assistant, ships the publish report.
- **Tenants** — isolated jails, each with its own site, database, and backup
policy. Tenants don't see each other; the operator sees everything.
Everything is reproducible. The same `setup` command that builds your machine
today will build the same machine on a fresh box tomorrow.

View file

@ -0,0 +1,9 @@
---
title: Hero
order: 1
section: hero
---
**Sovereign AI is an engineering problem, not a regulatory one.**
The hardware exists. The models exist. The know-how exists.
**The only thing missing is the decision to install it yourself.**

View file

@ -0,0 +1,12 @@
---
title: Install
order: 4
section: install-cta
---
Boot the ISO, answer four questions, walk away.
By the time you come back, your machine is provisioned, your assistant is
online, and your first tenant site is reachable.
Read the [installation guide](https://docs.clawdie.si/install/) or jump
straight to the [ISO download](https://docs.clawdie.si/install/iso/).

View file

@ -0,0 +1,10 @@
---
title: Kaj je Clawdie
order: 2
section: about
---
Clawdie je **samogosotvana AI nadzorna ravan** za operaterje, ki nočejo najemati
svoje računske moči, svojih podatkov ali zasebnosti svojih uporabnikov.
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,9 @@
---
title: Arhitektura
order: 3
section: architecture
---
Namestitev Clawdie ima tri plasti: gostitelj, nadzorna ravan, najemniki.
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,11 @@
---
title: Hero
order: 1
section: hero
---
**Suverena AI je inženirski problem, ne regulativni.**
Strojna oprema obstaja. Modeli obstajajo. Znanje obstaja.
**Edino, kar manjka, je odločitev, da to namestite sami.**
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,11 @@
---
title: Namestitev
order: 4
section: install-cta
---
Zaženite ISO, odgovorite na štiri vprašanja, odidite.
Preberite [vodič za namestitev](https://docs.clawdie.si/install/).
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,32 @@
import type { Strings } from './types';
export const en: Strings = {
meta: {
title: 'Clawdie — Sovereign AI infrastructure',
description:
'Self-hosted AI control plane: your data, your jails, your rules. Operator-owned infrastructure that ships ready-to-use.',
},
nav: {
docs: 'Docs',
controlplane: 'Control plane',
source: 'Source',
},
switcher: {
label: 'Language',
en: 'EN',
sl: 'SL',
},
banner: {
heading: 'You are not on clawdie.si.',
body: 'This page is being served from an operator install. Use the controls below.',
openControlplane: 'Open control plane',
readDocs: 'Read docs',
claimDomain: 'Claim a public domain',
},
footer: {
tagline: 'Operator-owned AI · No surveillance, no rent extraction',
docsLink: 'docs.clawdie.si',
sourceLink: 'codeberg.org/Clawdie/Clawdie-AI',
rights: 'Released under AGPL-3.0',
},
};

View file

@ -0,0 +1,14 @@
import { en } from './en';
import { sl } from './sl';
import type { Locale, Strings } from './types';
export const locales: Locale[] = ['en', 'sl'];
export const defaultLocale: Locale = 'en';
const registry: Record<Locale, Strings> = { en, sl };
export function t(locale: Locale): Strings {
return registry[locale];
}
export type { Locale, Strings };

View file

@ -0,0 +1,32 @@
import type { Strings } from './types';
export const sl: Strings = {
meta: {
title: 'Clawdie — Suverena AI infrastruktura',
description:
'Samogosotvana AI nadzorna ravan: vaši podatki, vaši jaili, vaša pravila. Operaterjeva infrastruktura, ki je takoj uporabna.',
},
nav: {
docs: 'Dokumentacija',
controlplane: 'Nadzorna ravan',
source: 'Izvorna koda',
},
switcher: {
label: 'Jezik',
en: 'EN',
sl: 'SL',
},
banner: {
heading: 'Niste na clawdie.si.',
body: 'Ta stran se streže iz operaterjeve namestitve. Uporabite spodnje gumbe.',
openControlplane: 'Odpri nadzorno ravan',
readDocs: 'Preberi dokumentacijo',
claimDomain: 'Prevzemi javno domeno',
},
footer: {
tagline: 'AI v lasti operaterja · Brez nadzora, brez najemnine',
docsLink: 'docs.clawdie.si',
sourceLink: 'codeberg.org/Clawdie/Clawdie-AI',
rights: 'Izdano pod AGPL-3.0',
},
};

View file

@ -0,0 +1,31 @@
export type Locale = 'en' | 'sl';
export interface Strings {
meta: {
title: string;
description: string;
};
nav: {
docs: string;
controlplane: string;
source: string;
};
switcher: {
label: string;
en: string;
sl: string;
};
banner: {
heading: string;
body: string;
openControlplane: string;
readDocs: string;
claimDomain: string;
};
footer: {
tagline: string;
docsLink: string;
sourceLink: string;
rights: string;
};
}

View file

@ -0,0 +1,70 @@
---
import '../styles/global.css';
import LangSwitcher from '../components/LangSwitcher.astro';
import OperatorBanner from '../components/OperatorBanner.astro';
import type { Locale } from '../i18n';
import { locales, t } from '../i18n';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const strings = t(locale);
const site = Astro.site?.toString().replace(/\/$/, '') ?? 'https://clawdie.si';
const path = Astro.url.pathname;
const stripPrefix = (p: string, l: Locale) => {
const pref = `/${l}`;
return p.startsWith(pref) ? p.slice(pref.length) || '/' : p;
};
const sharedPath = stripPrefix(path, locale);
---
<!doctype html>
<html lang={locale}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{strings.meta.title}</title>
<meta name="description" content={strings.meta.description} />
{
locales.map((l) => (
<link
rel="alternate"
hreflang={l}
href={`${site}/${l}${sharedPath === '/' ? '/' : sharedPath}`}
/>
))
}
<link
rel="alternate"
hreflang="x-default"
href={`${site}/en${sharedPath === '/' ? '/' : sharedPath}`}
/>
<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=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Mono:wght@300;400&display=swap"
rel="stylesheet"
/>
</head>
<body>
<OperatorBanner locale={locale} />
<div class="triangle-bg"></div>
<div class="container">
<LangSwitcher current={locale} />
<slot />
<footer class="site-footer">
<div>
{strings.footer.tagline}<br />
<a href="https://docs.clawdie.si/">{strings.footer.docsLink}</a>
·
<a href="https://codeberg.org/Clawdie/Clawdie-AI">{strings.footer.sourceLink}</a>
<br />
{strings.footer.rights}
</div>
<div class="footer-mark">△</div>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
---
import Landing from '../../layouts/Landing.astro';
import LandingBody from '../../components/LandingBody.astro';
---
<Landing locale="en">
<LandingBody locale="en" />
</Landing>

View file

@ -0,0 +1,8 @@
---
import Landing from '../../layouts/Landing.astro';
import LandingBody from '../../components/LandingBody.astro';
---
<Landing locale="sl">
<LandingBody locale="sl" />
</Landing>

View file

@ -0,0 +1,355 @@
:root {
--bg: #0d1117;
--accent: #00b4d8;
--accent-dark: #0096b7;
--fg: #e2e8f0;
--fg-dim: #c9d1d9;
--grey: #8b949e;
--rule: #21262d;
--paper: #161b22;
--paper-2: #1c2333;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg);
color: var(--fg-dim);
font-family: 'Cormorant Garamond', Georgia, serif;
font-size: 22px;
line-height: 1.7;
overflow-x: hidden;
}
.triangle-bg {
position: fixed;
inset: 0;
opacity: 0.06;
pointer-events: none;
z-index: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cg stroke='%2300b4d8' stroke-width='0.8' fill='none'%3E%3Cline x1='0' y1='25' x2='40' y2='25'/%3E%3Cline x1='60' y1='25' x2='100' y2='25'/%3E%3Cline x1='0' y1='75' x2='40' y2='75'/%3E%3Cline x1='60' y1='75' x2='100' y2='75'/%3E%3Cline x1='25' y1='0' x2='25' y2='40'/%3E%3Cline x1='25' y1='60' x2='25' y2='100'/%3E%3Cline x1='75' y1='0' x2='75' y2='40'/%3E%3Cline x1='75' y1='60' x2='75' y2='100'/%3E%3Cpath d='M40 25 L50 25 L50 40'/%3E%3Cpath d='M60 25 L50 25 L50 40'/%3E%3Cpath d='M40 75 L50 75 L50 60'/%3E%3Cpath d='M60 75 L50 75 L50 60'/%3E%3C/g%3E%3Cg fill='%2300b4d8'%3E%3Ccircle cx='25' cy='25' r='2.5'/%3E%3Ccircle cx='75' cy='25' r='2.5'/%3E%3Ccircle cx='25' cy='75' r='2.5'/%3E%3Ccircle cx='75' cy='75' r='2.5'/%3E%3Ccircle cx='50' cy='50' r='2.5'/%3E%3C/g%3E%3C/svg%3E");
background-size: 100px 100px;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 2rem;
position: relative;
z-index: 1;
}
.top-nav {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1.5rem;
padding: 1.2rem 0;
border-bottom: 1px solid var(--rule);
font-family: 'DM Mono', monospace;
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.top-nav a {
color: var(--grey);
text-decoration: none;
transition: color 0.2s;
}
.top-nav a:hover,
.top-nav a.active {
color: var(--accent);
}
header.hero {
padding: 5rem 0 3rem;
position: relative;
animation: fadeUp 1.2s ease forwards;
}
.header-inner {
display: flex;
align-items: flex-start;
gap: 3rem;
}
.brand-mark {
font-size: 5rem;
line-height: 1;
animation: float 6s ease-in-out infinite;
flex-shrink: 0;
color: var(--accent);
font-family: 'DM Mono', monospace;
}
.header-text h1 {
font-size: clamp(3rem, 8vw, 5.5rem);
font-weight: 300;
letter-spacing: -0.02em;
line-height: 0.95;
color: var(--fg);
}
.header-text h1 span {
color: var(--accent);
font-style: italic;
}
.tagline {
font-family: 'DM Mono', monospace;
font-size: 0.85rem;
font-weight: 300;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--grey);
margin-top: 1.2rem;
border-left: 2px solid var(--accent);
padding-left: 1rem;
}
.hero-statement {
background: var(--paper-2);
color: var(--fg);
padding: 4rem 3rem;
margin: 3rem 0;
position: relative;
overflow: hidden;
}
.hero-statement::before {
content: '△';
position: absolute;
right: -1rem;
top: -2rem;
font-size: 12rem;
color: var(--accent);
opacity: 0.08;
font-family: 'DM Mono', monospace;
line-height: 1;
}
.hero-statement p {
font-size: clamp(1.4rem, 3vw, 2rem);
font-weight: 300;
line-height: 1.5;
color: var(--fg);
}
.hero-statement strong {
color: var(--accent);
font-weight: 600;
}
section.landing-section {
margin: 4rem 0;
}
section.landing-section h2 {
font-size: 1.8rem;
font-weight: 300;
font-style: italic;
color: var(--fg);
margin-bottom: 1.2rem;
position: relative;
padding-bottom: 0.5rem;
}
section.landing-section h2::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 1px;
background: var(--accent);
}
section.landing-section p {
color: var(--fg-dim);
font-size: 1.1em;
margin-bottom: 1rem;
font-weight: 300;
}
section.landing-section a {
color: var(--accent);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
section.landing-section a:hover {
border-bottom-color: var(--accent);
}
footer.site-footer {
margin-top: 6rem;
padding: 3rem 0;
border-top: 1px solid var(--rule);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
font-family: 'DM Mono', monospace;
font-size: 0.85rem;
letter-spacing: 0.1em;
color: var(--grey);
text-transform: uppercase;
line-height: 1.8;
}
footer.site-footer a {
color: var(--accent);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
footer.site-footer a:hover {
border-bottom-color: var(--accent);
}
.footer-mark {
font-size: 2rem;
opacity: 0.3;
color: var(--accent);
animation: float 8s ease-in-out infinite reverse;
}
/* --- LANG SWITCHER (OSA-style) --- */
.lang-switch {
position: absolute;
top: 1.5rem;
right: 2rem;
display: flex;
gap: 0.3rem;
font-family: 'DM Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.12em;
text-transform: uppercase;
z-index: 10;
}
.lang-switch a {
color: var(--grey);
text-decoration: none;
padding: 0.25rem 0.55rem;
border: 1px solid var(--rule);
transition:
color 0.2s,
border-color 0.2s;
}
.lang-switch a.active,
.lang-switch a:hover {
color: var(--accent);
border-color: var(--accent);
}
/* --- OPERATOR BANNER --- */
.operator-banner {
background: var(--paper);
border-bottom: 1px solid var(--accent);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
position: relative;
z-index: 2;
}
.operator-banner-text {
font-family: 'DM Mono', monospace;
font-size: 0.78rem;
letter-spacing: 0.08em;
color: var(--fg-dim);
}
.operator-banner-text strong {
color: var(--accent);
}
.operator-banner-actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.operator-banner-actions a {
font-family: 'DM Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--fg);
text-decoration: none;
border: 1px solid var(--accent);
padding: 0.45rem 0.9rem;
transition:
background 0.2s,
color 0.2s;
}
.operator-banner-actions a:hover {
background: var(--accent);
color: var(--bg);
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(-5deg);
}
50% {
transform: translateY(-12px) rotate(3deg);
}
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px) {
.header-inner {
flex-direction: column;
gap: 1.5rem;
}
.brand-mark {
font-size: 3.5rem;
}
.header-text h1 {
font-size: 3rem;
}
.hero-statement {
padding: 2.5rem 1.5rem;
}
footer.site-footer {
flex-direction: column;
text-align: center;
}
.lang-switch {
right: 1rem;
top: 1rem;
}
}

View file

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