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.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.in { opacity: 1; transform: none; }
@ -116,7 +110,6 @@
@media (prefers-reduced-motion: reduce) {
.rv, .logline { opacity: 1 !important; transform: none !important; }
.rv .fill { transform: scaleX(1) !important; }
}
/* Metric visuals

View File

@ -9,82 +9,63 @@
@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>
{{-- Parts of the page arrive on scroll, which an observer decides. Without
JavaScript nothing is ever observed. --}}
<noscript><style>.rv,.logline{opacity:1!important;transform:none!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.
Two rewrites. The first only recoloured the old page same eight numbered
sections, same split hero, same comparison matrix and was correctly called
out as the old thing in new paint. A palette is not a design.
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.
What actually changed on the second pass: the § numbering is gone (a
document convention, and the document voice was exactly what the redesign
dropped), the hero shows the product instead of describing it, the uniform
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">
<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 floating bar rather than a full-width band: it reads as an object on
the page instead of a browser chrome bolted to the top, and it lets the
hero start at the very top of the viewport. --}}
<header x-data="{ open: false }" class="fixed inset-x-0 top-0 z-[110] px-4 pt-4 sm:px-6">
{{-- Opaque. Translucency plus backdrop-blur looks like glass over a
photograph and like a rendering fault over a dark panel, which is
exactly what scrolls under this bar. --}}
<div class="mx-auto max-w-[1120px] rounded-xl border border-line bg-surface shadow-sm">
<div class="flex h-14 items-center gap-8 pl-4 pr-3">
<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>
<x-ui.logo class="size-7" />
<span class="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>
<nav class="hidden items-center gap-1 text-sm text-muted lg:flex">
@foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => 'Fragen'] as $href => $label)
<a href="{{ route('home') }}{{ $href }}" class="rounded px-3 py-1.5 transition-colors hover:bg-surface-hover hover:text-ink">{{ $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ü">
<div class="ml-auto flex items-center gap-2">
<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') }}#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ü">
<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)
<nav x-show="open" x-cloak @click="open = false" class="border-t border-line px-4 pb-2 lg:hidden">
@foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => '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>
</div>
</header>
<main class="flex-1">
@ -92,28 +73,53 @@
</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="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">
<x-ui.logo class="size-6" />
<span class="font-semibold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
<x-ui.logo class="size-7" />
<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>
<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.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('status') }}" class="transition-colors hover:text-ink">Betriebsstatus</a>
<a href="{{ route('login') }}" class="transition-colors hover:text-ink">Anmelden</a>
</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>
@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');
@ -128,7 +134,7 @@
if (!entry.isIntersecting) return;
entry.target.classList.add('in');
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));
// Tabular figures are set on the element, so counting never shifts
@ -143,22 +149,21 @@
if (reduce) { el.textContent = format(target); return; }
const start = performance.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)));
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 });
log.querySelectorAll('.logline').forEach((row, i) =>
setTimeout(() => row.classList.add('in'), reduce ? 0 : 200 + i * 340));
}), { threshold: 0.3 });
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',
'legal.blade.php',
'components/layouts/site.blade.php',
'components/site/section-head.blade.php',
];
}