169 lines
8.7 KiB
PHP
169 lines
8.7 KiB
PHP
@props([
|
|
'title' => null,
|
|
'description' => null,
|
|
'robots' => null,
|
|
])
|
|
<!DOCTYPE html>
|
|
<html lang="de" class="scroll-smooth">
|
|
<head>
|
|
@if ($description)<meta name="description" content="{{ $description }}">@endif
|
|
@if ($robots)<meta name="robots" content="{{ $robots }}">@endif
|
|
<x-shell.head :title="$title" />
|
|
{{-- Everything below the fold arrives on scroll, which an observer decides.
|
|
Without JavaScript nothing is ever observed, so the whole page would be
|
|
a headline over a blank sheet. --}}
|
|
<noscript><style>.rv,.logline{opacity:1!important;transform:none!important}.rv .fill{transform:scaleX(1)!important}</style></noscript>
|
|
</head>
|
|
{{--
|
|
The public website.
|
|
|
|
It used to be a self-contained page with its own design system inside a
|
|
<style> block: warm paper, a serif headline, 3px corners, its own scale of
|
|
greys. It predated the token file, and by the time the portal and the
|
|
console had been rebuilt it was the only surface left speaking the old
|
|
language — a visitor moved from the website to the sign-in form and arrived
|
|
somewhere that looked like a different company.
|
|
|
|
So it draws from resources/css/portal-tokens.css like everything else. Same
|
|
colours, same radii, same buttons, same small-caps label. What still differs
|
|
is density and rhythm, which is what a marketing page is allowed to differ
|
|
in: wider measure, larger type, more air between sections.
|
|
--}}
|
|
{{-- Column, so the footer sits at the bottom of the viewport on a short page
|
|
rather than halfway up it with the ground colour showing underneath. --}}
|
|
<body class="flex min-h-screen flex-col bg-bg text-body antialiased">
|
|
<div id="site-progress" class="fixed inset-x-0 top-0 z-[120] h-0.5 w-0 bg-accent" aria-hidden="true"></div>
|
|
|
|
<header
|
|
x-data="{ open: false, stuck: false }"
|
|
@scroll.window="stuck = window.scrollY > 8"
|
|
{{-- Opaque in both states. A translucent bar over a page of cards and
|
|
tables lets whatever is underneath show through the wordmark, and
|
|
backdrop-blur does not hide dark text on a light ground — it only
|
|
makes it look like a rendering fault. --}}
|
|
class="fixed inset-x-0 top-0 z-[110] border-b transition-colors duration-200"
|
|
:class="stuck ? 'border-line bg-surface shadow-xs' : 'border-transparent bg-bg'"
|
|
>
|
|
<div class="mx-auto flex h-16 max-w-[1180px] items-center gap-8 px-5 sm:px-8">
|
|
<a href="{{ route('home') }}" class="flex items-center gap-2.5">
|
|
<x-ui.logo class="size-8" />
|
|
<span class="text-md font-bold tracking-[-0.02em] text-ink">CluPilot</span>
|
|
</a>
|
|
|
|
<nav class="ml-auto hidden items-center gap-7 text-sm text-muted lg:flex">
|
|
@foreach ([
|
|
'#produkt' => 'Produkt',
|
|
'#ablauf' => 'Ablauf',
|
|
'#sicherheit' => 'Sicherheit',
|
|
'#pakete' => 'Pakete',
|
|
'#faq' => 'Fragen',
|
|
] as $href => $label)
|
|
<a href="{{ route('home') }}{{ $href }}" class="relative py-1 transition-colors hover:text-ink after:absolute after:inset-x-0 after:-bottom-px after:h-px after:origin-left after:scale-x-0 after:bg-accent after:transition-transform after:duration-300 hover:after:scale-x-100">{{ $label }}</a>
|
|
@endforeach
|
|
</nav>
|
|
|
|
<div class="ml-auto flex items-center gap-3 lg:ml-0">
|
|
<a href="{{ route('login') }}" class="hidden text-sm text-muted transition-colors hover:text-ink sm:block">Anmelden</a>
|
|
<x-ui.button href="{{ route('home') }}#pakete" variant="ink" size="sm">Pakete ansehen</x-ui.button>
|
|
|
|
<button type="button" class="-mr-1 grid size-9 place-items-center rounded text-muted transition-colors hover:bg-surface-hover hover:text-ink lg:hidden" @click="open = !open" :aria-expanded="open" aria-label="Menü">
|
|
<x-ui.icon name="menu" class="size-5" x-show="!open" />
|
|
<x-ui.icon name="x" class="size-5" x-show="open" x-cloak />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<nav x-show="open" x-cloak @click="open = false" class="border-t border-line bg-surface px-5 py-2 sm:px-8 lg:hidden">
|
|
@foreach ([
|
|
'#produkt' => 'Produkt',
|
|
'#ablauf' => 'Ablauf',
|
|
'#sicherheit' => 'Sicherheit',
|
|
'#pakete' => 'Pakete',
|
|
'#faq' => 'Fragen',
|
|
route('login') => 'Anmelden',
|
|
] as $href => $label)
|
|
<a href="{{ Str::startsWith($href, '#') ? route('home').$href : $href }}" class="flex min-h-11 items-center border-b border-line text-sm text-body last:border-0">{{ $label }}</a>
|
|
@endforeach
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="flex-1">
|
|
{{ $slot }}
|
|
</main>
|
|
|
|
<footer class="border-t border-line bg-surface">
|
|
<div class="mx-auto flex max-w-[1180px] flex-col gap-5 px-5 py-9 text-sm text-muted sm:px-8 lg:flex-row lg:items-center">
|
|
<div class="flex items-center gap-2.5">
|
|
<x-ui.logo class="size-6" />
|
|
<span class="font-semibold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
|
|
</div>
|
|
<span class="lbl">© {{ date('Y') }} · Österreich</span>
|
|
|
|
<div class="flex flex-wrap gap-x-6 gap-y-2 lg:ml-auto">
|
|
<a href="{{ route('legal.impressum') }}" class="transition-colors hover:text-ink">Impressum</a>
|
|
<a href="{{ route('legal.datenschutz') }}" class="transition-colors hover:text-ink">Datenschutz</a>
|
|
<a href="{{ route('legal.agb') }}" class="transition-colors hover:text-ink">AGB</a>
|
|
<a href="{{ route('status') }}" class="transition-colors hover:text-ink">Status</a>
|
|
<a href="{{ route('login') }}" class="transition-colors hover:text-ink">Anmelden</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
@verbatim
|
|
<script>
|
|
// Three small behaviours, none of which Alpine expresses more clearly
|
|
// than an IntersectionObserver does: a scroll indicator, sections that
|
|
// arrive as they are reached, and figures that count up once.
|
|
(function () {
|
|
const reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
|
|
const progress = document.getElementById('site-progress');
|
|
|
|
addEventListener('scroll', () => {
|
|
const el = document.documentElement;
|
|
const max = el.scrollHeight - el.clientHeight;
|
|
progress.style.width = (max > 0 ? (scrollY / max) * 100 : 0) + '%';
|
|
}, { passive: true });
|
|
|
|
const reveal = new IntersectionObserver(entries => entries.forEach(entry => {
|
|
if (!entry.isIntersecting) return;
|
|
entry.target.classList.add('in');
|
|
reveal.unobserve(entry.target);
|
|
}), { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
|
|
document.querySelectorAll('.rv').forEach(el => reveal.observe(el));
|
|
|
|
// Tabular figures are set on the element, so counting never shifts
|
|
// the layout around it.
|
|
const count = new IntersectionObserver(entries => entries.forEach(entry => {
|
|
if (!entry.isIntersecting) return;
|
|
count.unobserve(entry.target);
|
|
const el = entry.target;
|
|
const target = parseFloat(el.dataset.count);
|
|
const decimals = parseInt(el.dataset.decimals || 0, 10);
|
|
const format = v => v.toFixed(decimals).replace('.', ',');
|
|
if (reduce) { el.textContent = format(target); return; }
|
|
const start = performance.now();
|
|
(function tick(now) {
|
|
const p = Math.min((now - start) / 1200, 1);
|
|
el.textContent = format(target * (1 - Math.pow(1 - p, 3)));
|
|
if (p < 1) requestAnimationFrame(tick);
|
|
})(start);
|
|
}), { threshold: 0.6 });
|
|
document.querySelectorAll('[data-count]').forEach(el => count.observe(el));
|
|
|
|
// The provisioning log writes itself out, line by line.
|
|
const log = document.getElementById('deploy-log');
|
|
if (log) {
|
|
const lines = new IntersectionObserver(entries => entries.forEach(entry => {
|
|
if (!entry.isIntersecting) return;
|
|
lines.unobserve(log);
|
|
const rows = log.querySelectorAll('.logline');
|
|
rows.forEach((row, i) => setTimeout(() => row.classList.add('in'), reduce ? 0 : 240 + i * 400));
|
|
}), { threshold: 0.35 });
|
|
lines.observe(log);
|
|
}
|
|
})();
|
|
</script>
|
|
@endverbatim
|
|
</body>
|
|
</html>
|