Redesign the public website instead of recolouring it
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
parent
43d58de7f8
commit
696c8834b8
|
|
@ -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 ───────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
>
|
<a href="{{ route('home') }}" class="flex items-center gap-2.5">
|
||||||
<div class="mx-auto flex h-16 max-w-[1180px] items-center gap-8 px-5 sm:px-8">
|
<x-ui.logo class="size-7" />
|
||||||
<a href="{{ route('home') }}" class="flex items-center gap-2.5">
|
<span class="font-bold tracking-[-0.02em] text-ink">CluPilot</span>
|
||||||
<x-ui.logo class="size-8" />
|
</a>
|
||||||
<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">
|
<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',
|
@endforeach
|
||||||
'#sicherheit' => 'Sicherheit',
|
</nav>
|
||||||
'#pakete' => 'Pakete',
|
|
||||||
'#faq' => 'Fragen',
|
<div class="ml-auto flex items-center gap-2">
|
||||||
] as $href => $label)
|
<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>
|
||||||
<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>
|
<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 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
|
@endforeach
|
||||||
</nav>
|
</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>
|
</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>
|
</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="flex items-center gap-2.5">
|
<div class="grid gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<x-ui.logo class="size-6" />
|
<div class="lg:col-span-2">
|
||||||
<span class="font-semibold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
|
<div class="flex items-center gap-2.5">
|
||||||
</div>
|
<x-ui.logo class="size-7" />
|
||||||
<span class="lbl">© {{ date('Y') }} · Österreich</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@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@clupilot.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-x-6 gap-y-2 lg:ml-auto">
|
<div>
|
||||||
<a href="{{ route('legal.impressum') }}" class="transition-colors hover:text-ink">Impressum</a>
|
<p class="lbl">Produkt</p>
|
||||||
<a href="{{ route('legal.datenschutz') }}" class="transition-colors hover:text-ink">Datenschutz</a>
|
<div class="mt-4 flex flex-col gap-2.5 text-sm text-muted">
|
||||||
<a href="{{ route('legal.agb') }}" class="transition-colors hover:text-ink">AGB</a>
|
<a href="{{ route('home') }}#produkt" class="transition-colors hover:text-ink">Was enthalten ist</a>
|
||||||
<a href="{{ route('status') }}" class="transition-colors hover:text-ink">Status</a>
|
<a href="{{ route('home') }}#ablauf" class="transition-colors hover:text-ink">Ablauf</a>
|
||||||
<a href="{{ route('login') }}" class="transition-colors hover:text-ink">Anmelden</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 & 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">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>
|
||||||
</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);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -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' }}">§ {{ $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
|
|
@ -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',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue