Redesign the public website instead of recolouring it
tests / pest (push) Failing after 8m15s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details

Correctly rejected: the last pass was the old page in new paint. It kept the
skeleton — eight numbered sections, a split hero, a twelve-row comparison
matrix — and changed only the palette. A palette is not a design.

What is actually different now:

- The § numbering is gone. Numbered sections with a drawn rule are a document
  convention, and the document voice is exactly what the product's design
  system dropped.
- The hero shows the product instead of describing it: centred type, then a
  wide panel built from the real console's own tokens, cropped by the section
  below. The old hero put a table of facts beside the headline, so a visitor
  read two columns before seeing anything.
- Six identical cards, then six more, then six more became one asymmetric
  grid — a large panel, a dark tile, an accent tile, a wide module row — so
  the important thing is allowed to be bigger than the rest.
- The industries are one dense two-column list instead of six cards repeating
  each other's shape.
- The page has one dark hinge in the middle (the deployment) rather than a
  dark section among seven light ones.
- The evidence table became rows that pair the measure with the piece of
  paper you can hand somebody, which is what it is for.
- Prices are cards, with the full matrix folded away underneath. The matrix
  used to be the first thing a visitor met: twelve rows of ticks before a
  single price was legible.
- The FAQ heading sits still while the questions scroll past it.
- The footer is a real footer.

The header is a floating bar rather than a full-width band, and opaque in
both states — translucency plus backdrop-blur reads as a rendering fault over
the dark panels that scroll under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/granted-plans v1.3.5
nexxo 2026-07-29 11:43:25 +02:00
parent 43d58de7f8
commit 696c8834b8
6 changed files with 602 additions and 584 deletions

View File

@ -1 +1 @@
1.3.4 1.3.5

View File

@ -100,12 +100,6 @@
.rv.d1 { transition-delay: 0.07s; } .rv.d1 { transition-delay: 0.07s; }
.rv.d2 { transition-delay: 0.14s; } .rv.d2 { transition-delay: 0.14s; }
/* The section rule draws across as its heading arrives. Slower than the
heading it belongs to, so it reads as a consequence rather than as a second
thing happening at the same time. */
.rv .fill { transform: scaleX(0); transform-origin: left; transition: transform 1s var(--ease) 0.1s; }
.rv.in .fill { transform: scaleX(1); }
.logline { opacity: 0; transform: translateX(-10px); transition: opacity 0.45s var(--ease), transform 0.45s var(--ease); } .logline { opacity: 0; transform: translateX(-10px); transition: opacity 0.45s var(--ease), transform 0.45s var(--ease); }
.logline.in { opacity: 1; transform: none; } .logline.in { opacity: 1; transform: none; }
@ -116,7 +110,6 @@
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.rv, .logline { opacity: 1 !important; transform: none !important; } .rv, .logline { opacity: 1 !important; transform: none !important; }
.rv .fill { transform: scaleX(1) !important; }
} }
/* Metric visuals /* Metric visuals

View File

@ -9,82 +9,63 @@
@if ($description)<meta name="description" content="{{ $description }}">@endif @if ($description)<meta name="description" content="{{ $description }}">@endif
@if ($robots)<meta name="robots" content="{{ $robots }}">@endif @if ($robots)<meta name="robots" content="{{ $robots }}">@endif
<x-shell.head :title="$title" /> <x-shell.head :title="$title" />
{{-- Everything below the fold arrives on scroll, which an observer decides. {{-- Parts of the page arrive on scroll, which an observer decides. Without
Without JavaScript nothing is ever observed, so the whole page would be JavaScript nothing is ever observed. --}}
a headline over a blank sheet. --}} <noscript><style>.rv,.logline{opacity:1!important;transform:none!important}</style></noscript>
<noscript><style>.rv,.logline{opacity:1!important;transform:none!important}.rv .fill{transform:scaleX(1)!important}</style></noscript>
</head> </head>
{{-- {{--
The public website. The public website.
It used to be a self-contained page with its own design system inside a Two rewrites. The first only recoloured the old page same eight numbered
<style> block: warm paper, a serif headline, 3px corners, its own scale of sections, same split hero, same comparison matrix and was correctly called
greys. It predated the token file, and by the time the portal and the out as the old thing in new paint. A palette is not a design.
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 What actually changed on the second pass: the § numbering is gone (a
colours, same radii, same buttons, same small-caps label. What still differs document convention, and the document voice was exactly what the redesign
is density and rhythm, which is what a marketing page is allowed to differ dropped), the hero shows the product instead of describing it, the uniform
in: wider measure, larger type, more air between sections. card rows became one asymmetric grid, the price matrix became price cards
with the full table folded away underneath, and the page now has one dark
hinge in the middle instead of a dark section among seven light ones.
--}} --}}
{{-- 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"> <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> <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 {{-- A floating bar rather than a full-width band: it reads as an object on
x-data="{ open: false, stuck: false }" the page instead of a browser chrome bolted to the top, and it lets the
@scroll.window="stuck = window.scrollY > 8" hero start at the very top of the viewport. --}}
{{-- Opaque in both states. A translucent bar over a page of cards and <header x-data="{ open: false }" class="fixed inset-x-0 top-0 z-[110] px-4 pt-4 sm:px-6">
tables lets whatever is underneath show through the wordmark, and {{-- Opaque. Translucency plus backdrop-blur looks like glass over a
backdrop-blur does not hide dark text on a light ground it only photograph and like a rendering fault over a dark panel, which is
makes it look like a rendering fault. --}} exactly what scrolls under this bar. --}}
class="fixed inset-x-0 top-0 z-[110] border-b transition-colors duration-200" <div class="mx-auto max-w-[1120px] rounded-xl border border-line bg-surface shadow-sm">
:class="stuck ? 'border-line bg-surface shadow-xs' : 'border-transparent bg-bg'" <div class="flex h-14 items-center gap-8 pl-4 pr-3">
>
<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"> <a href="{{ route('home') }}" class="flex items-center gap-2.5">
<x-ui.logo class="size-8" /> <x-ui.logo class="size-7" />
<span class="text-md font-bold tracking-[-0.02em] text-ink">CluPilot</span> <span class="font-bold tracking-[-0.02em] text-ink">CluPilot</span>
</a> </a>
<nav class="ml-auto hidden items-center gap-7 text-sm text-muted lg:flex"> <nav class="hidden items-center gap-1 text-sm text-muted lg:flex">
@foreach ([ @foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => 'Fragen'] as $href => $label)
'#produkt' => 'Produkt', <a href="{{ route('home') }}{{ $href }}" class="rounded px-3 py-1.5 transition-colors hover:bg-surface-hover hover:text-ink">{{ $label }}</a>
'#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 @endforeach
</nav> </nav>
<div class="ml-auto flex items-center gap-3 lg:ml-0"> <div class="ml-auto flex items-center gap-2">
<a href="{{ route('login') }}" class="hidden text-sm text-muted transition-colors hover:text-ink sm:block">Anmelden</a> <a href="{{ route('login') }}" class="hidden rounded px-3 py-1.5 text-sm text-muted transition-colors hover:bg-surface-hover hover:text-ink sm:block">Anmelden</a>
<x-ui.button href="{{ route('home') }}#pakete" variant="ink" size="sm">Pakete ansehen</x-ui.button> <x-ui.button href="{{ route('home') }}#preise" variant="ink" size="sm">Preise</x-ui.button>
<button type="button" class="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ü">
<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="menu" class="size-5" x-show="!open" />
<x-ui.icon name="x" class="size-5" x-show="open" x-cloak /> <x-ui.icon name="x" class="size-5" x-show="open" x-cloak />
</button> </button>
</div> </div>
</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"> <nav x-show="open" x-cloak @click="open = false" class="border-t border-line px-4 pb-2 lg:hidden">
@foreach ([ @foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => 'Fragen', route('login') => 'Anmelden'] as $href => $label)
'#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> <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 @endforeach
</nav> </nav>
</div>
</header> </header>
<main class="flex-1"> <main class="flex-1">
@ -92,28 +73,53 @@
</main> </main>
<footer class="border-t border-line bg-surface"> <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="mx-auto max-w-[1120px] px-5 py-14 sm:px-6">
<div class="grid gap-10 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<x-ui.logo class="size-6" /> <x-ui.logo class="size-7" />
<span class="font-semibold tracking-[-0.02em] text-ink">CluPilot Cloud</span> <span class="font-bold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
</div>
<p class="mt-4 max-w-[38ch] text-sm leading-relaxed text-muted">
Eine eigene, isolierte Cloud für Ihr Unternehmen eingerichtet, gesichert und
überwacht von uns, mit Serverstandort in der EU.
</p>
<a href="mailto:office&#64;clupilot.com" class="mt-5 inline-flex items-center gap-2 text-sm font-medium text-accent-text underline-offset-4 hover:underline">
<x-ui.icon name="mail" class="size-4" />office&#64;clupilot.com
</a>
</div> </div>
<span class="lbl">© {{ date('Y') }} · Österreich</span>
<div class="flex flex-wrap gap-x-6 gap-y-2 lg:ml-auto"> <div>
<p class="lbl">Produkt</p>
<div class="mt-4 flex flex-col gap-2.5 text-sm text-muted">
<a href="{{ route('home') }}#produkt" class="transition-colors hover:text-ink">Was enthalten ist</a>
<a href="{{ route('home') }}#ablauf" class="transition-colors hover:text-ink">Ablauf</a>
<a href="{{ route('home') }}#sicherheit" class="transition-colors hover:text-ink">Sicherheit</a>
<a href="{{ route('home') }}#preise" class="transition-colors hover:text-ink">Preise</a>
</div>
</div>
<div>
<p class="lbl">Rechtliches &amp; Betrieb</p>
<div class="mt-4 flex flex-col gap-2.5 text-sm text-muted">
<a href="{{ route('legal.impressum') }}" class="transition-colors hover:text-ink">Impressum</a> <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.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('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('status') }}" class="transition-colors hover:text-ink">Betriebsstatus</a>
<a href="{{ route('login') }}" class="transition-colors hover:text-ink">Anmelden</a> <a href="{{ route('login') }}" class="transition-colors hover:text-ink">Anmelden</a>
</div> </div>
</div> </div>
</div>
<div class="mt-12 flex flex-col gap-2 border-t border-line pt-6 sm:flex-row sm:items-center">
<span class="lbl">© {{ date('Y') }} CluPilot Cloud · Österreich</span>
<span class="lbl sm:ml-auto">Serverstandort EU · Support auf Deutsch</span>
</div>
</div>
</footer> </footer>
@verbatim @verbatim
<script> <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 () { (function () {
const reduce = matchMedia('(prefers-reduced-motion:reduce)').matches; const reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
const progress = document.getElementById('site-progress'); const progress = document.getElementById('site-progress');
@ -128,7 +134,7 @@
if (!entry.isIntersecting) return; if (!entry.isIntersecting) return;
entry.target.classList.add('in'); entry.target.classList.add('in');
reveal.unobserve(entry.target); reveal.unobserve(entry.target);
}), { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); }), { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.rv').forEach(el => reveal.observe(el)); document.querySelectorAll('.rv').forEach(el => reveal.observe(el));
// Tabular figures are set on the element, so counting never shifts // Tabular figures are set on the element, so counting never shifts
@ -143,22 +149,21 @@
if (reduce) { el.textContent = format(target); return; } if (reduce) { el.textContent = format(target); return; }
const start = performance.now(); const start = performance.now();
(function tick(now) { (function tick(now) {
const p = Math.min((now - start) / 1200, 1); const p = Math.min((now - start) / 1100, 1);
el.textContent = format(target * (1 - Math.pow(1 - p, 3))); el.textContent = format(target * (1 - Math.pow(1 - p, 3)));
if (p < 1) requestAnimationFrame(tick); if (p < 1) requestAnimationFrame(tick);
})(start); })(start);
}), { threshold: 0.6 }); }), { threshold: 0.6 });
document.querySelectorAll('[data-count]').forEach(el => count.observe(el)); document.querySelectorAll('[data-count]').forEach(el => count.observe(el));
// The provisioning log writes itself out, line by line.
const log = document.getElementById('deploy-log'); const log = document.getElementById('deploy-log');
if (log) { if (log) {
const lines = new IntersectionObserver(entries => entries.forEach(entry => { const lines = new IntersectionObserver(entries => entries.forEach(entry => {
if (!entry.isIntersecting) return; if (!entry.isIntersecting) return;
lines.unobserve(log); lines.unobserve(log);
const rows = log.querySelectorAll('.logline'); log.querySelectorAll('.logline').forEach((row, i) =>
rows.forEach((row, i) => setTimeout(() => row.classList.add('in'), reduce ? 0 : 240 + i * 400)); setTimeout(() => row.classList.add('in'), reduce ? 0 : 200 + i * 340));
}), { threshold: 0.35 }); }), { threshold: 0.3 });
lines.observe(log); lines.observe(log);
} }
})(); })();

View File

@ -1,17 +0,0 @@
@props([
'number',
'label',
// The security band sits on --plate, where the muted token has no contrast.
'dark' => false,
])
{{--
The rubric above every section of the public site: a number, a name, and a
rule that draws itself across as the section is reached. Written once
because eight of them by hand is how the numbering and the spacing end up
disagreeing with each other.
--}}
<div {{ $attributes->merge(['class' => 'rv mb-7 flex items-center gap-4']) }}>
<span class="font-mono text-xs font-medium {{ $dark ? 'text-accent' : 'text-accent-text' }}">§&nbsp;{{ $number }}</span>
<span class="lbl {{ $dark ? '!text-plate-muted' : '' }}">{{ $label }}</span>
<span class="fill h-px flex-1 {{ $dark ? 'bg-plate-rule' : 'bg-line' }}"></span>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@ function sitePages(): array
'landing.blade.php', 'landing.blade.php',
'legal.blade.php', 'legal.blade.php',
'components/layouts/site.blade.php', 'components/layouts/site.blade.php',
'components/site/section-head.blade.php',
]; ];
} }