Redesign the pre-launch placeholder, and fix its wordmark and mark
tests / pest (push) Failing after 8m7s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details

Reported from a phone on status.clupilot.com, which serves this page because
the site is still switched to private.

Two real defects behind the complaint:

The wordmark read "Clu Pilot Cloud". `gap` on a flex container applies
between EVERY child and a bare text node is a child — so an inline-flex row
wrapped around "Clu", <i>Pilot</i> and " Cloud" put nine pixels between each
of them. The wordmark is one element now and the gap only ever separates it
from the mark.

The logo was a plain gradient tile with nothing in it, which reads as an
image that failed to load. That is a poor thing to leave somebody with on a
page whose whole message is "we are building this properly". The real mark —
cloud plus autopilot ascent — is inlined; it has to be in the file because
this page renders when the asset build may not exist at all.

The redesign itself: the card is gone. The page is now a centred composition
on the open ground — the state as a pill above everything, the mark, the
headline at the size the rest of the site uses, and the three promises the
paragraph was already making set as their own line so there is something to
look at besides one block of text. One warm bloom behind it, drawn as a
gradient rather than a blurred disc, which banded into visible rings.

Still self-contained, and still the only page exempt from the shared
stylesheet: it is served mid-deploy and on a fresh install, where @vite would
throw instead of rendering the reassurance somebody came for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/plan-marketing v1.3.7
nexxo 2026-07-29 13:43:56 +02:00
parent b85df4c141
commit 4755a4f362
5 changed files with 165 additions and 52 deletions

View File

@ -1 +1 @@
1.3.6
1.3.7

View File

@ -6,4 +6,13 @@ return [
'contact' => 'Fragen beantworten wir gern:',
'label' => 'Betriebszustand',
'state' => 'In Vorbereitung',
// The three promises already made in the body text, set as their own line
// so the page has something to look at besides one paragraph.
'points' => [
'operated' => 'Betreut',
'backed_up' => 'Täglich gesichert',
'current' => 'Aktuell gehalten',
],
'write' => 'Schreiben Sie uns',
];

View File

@ -6,4 +6,13 @@ return [
'contact' => 'Questions are welcome:',
'label' => 'Service status',
'state' => 'Being prepared',
// The three promises already made in the body text, set as their own line
// so the page has something to look at besides one paragraph.
'points' => [
'operated' => 'Operated',
'backed_up' => 'Backed up daily',
'current' => 'Kept current',
],
'write' => 'Write to us',
];

View File

@ -12,83 +12,148 @@
Self-contained on purpose: this is shown while the application is mid-deploy
or freshly installed, which is exactly when the Vite manifest may not exist.
Pulling it in through @vite would throw an exception instead of rendering
the reassurance the visitor came for.
the reassurance the visitor came for. Every value below is a copy of a token
from resources/css/portal-tokens.css, which is the price of that.
The fonts are the site's own static files. If they are missing the page
degrades to a system serif and still reads correctly.
degrades to a system sans and still reads correctly.
--}}
@verbatim
<style>
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-500-normal.woff2") format("woff2");font-weight:500;font-display:swap}
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-600-normal.woff2") format("woff2");font-weight:600;font-display:swap}
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-700-normal.woff2") format("woff2");font-weight:700;font-display:swap}
@font-face{font-family:"Plex Mono";src:url("/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");font-weight:400;font-display:swap}
:root{
--bg:#f6f6f8; --card:#fff; --card-2:#fafafb;
--bg:#f6f6f8; --surface:#fff; --surface-2:#fafafb;
--ink:#17171c; --body:#43434e; --muted:#6e6e7a;
--line:#e9e9ee; --line-2:#dcdce4;
--accent:#f97316; --accent-ink:#b8500a; --accent-wash:#fff3e9;
--plate:#16151b;
--accent:#f97316; --accent-ink:#b8500a; --accent-press:#974208;
--accent-wash:#fff3e9; --accent-line:#e8cdb2;
--success:#30b15c;
}
*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
font-family:"Plex Sans",-apple-system,BlinkMacSystemFont,sans-serif;
background:var(--bg);color:var(--body);line-height:1.55;
min-height:100vh;display:grid;place-items:center;padding:clamp(24px,6vw,64px);
-webkit-font-smoothing:antialiased;
min-height:100dvh;display:grid;place-items:center;
padding:clamp(24px,7vw,72px) clamp(20px,6vw,48px);
-webkit-font-smoothing:antialiased;position:relative;overflow-x:hidden;
}
main{max-width:560px;width:100%}
.card{border:1px solid var(--line);border-radius:16px;background:var(--card);
box-shadow:0 16px 50px rgba(40,24,15,.10);overflow:hidden}
.head{display:flex;align-items:center;justify-content:space-between;gap:14px;
padding:16px 24px;border-bottom:1px solid var(--line);background:var(--card-2);
font-size:13px;font-weight:600;color:var(--muted)}
.head b{color:var(--accent-ink);font-family:"Plex Mono",monospace;font-weight:500}
.body{padding:30px 24px 28px}
.brand{display:inline-flex;align-items:center;gap:9px;font-size:17px;font-weight:700;
letter-spacing:-.02em;color:var(--ink)}
.brand i{font-style:normal;color:var(--accent-ink)}
.mark{width:26px;height:26px;border-radius:7px;flex:none;
background:linear-gradient(135deg,#fb923c,#f97316 55%,#c2560a)}
h1{margin-top:18px;font-weight:700;font-size:clamp(1.6rem,4vw,2.1rem);
letter-spacing:-.032em;line-height:1.1;color:var(--ink)}
p{margin-top:12px;color:var(--muted);font-size:15px}
.hint{margin-top:18px;padding-top:16px;border-top:1px solid var(--line);font-size:14px}
.acts{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px}
.btn{display:inline-flex;align-items:center;min-height:46px;padding:0 22px;border-radius:12px;
font-size:15px;font-weight:600;text-decoration:none;border:1.5px solid transparent;
transition:background .18s,border-color .18s}
.ink{background:var(--accent-ink);color:#fff}
.ink:hover{background:#974208}
.line{border-color:var(--line-2);color:var(--ink)}
.line:hover{border-color:var(--ink)}
.foot{margin-top:12px;font-size:14px}
.foot a{color:var(--accent-ink);font-weight:600;text-decoration:none}
.foot a:hover{text-decoration:underline}
.mark-b{margin-top:22px;font-size:12.5px;color:var(--muted);text-align:center}
.dot{width:7px;height:7px;border-radius:50%;background:var(--accent);animation:pulse 1.8s ease-in-out infinite}
@keyframes pulse{50%{opacity:.35}}
/* One warm bloom behind the mark. The ground is otherwise flat, and a page
that is flat everywhere reads as unfinished rather than as restrained.
A gradient rather than a blurred circle: blur() over a solid disc banded
into visible rings on the first screen it was looked at. */
body::before{
content:"";position:fixed;inset:0;pointer-events:none;
background:radial-gradient(60% 42% at 50% 0%, rgba(249,115,22,.14), rgba(249,115,22,0) 70%);
}
main{max-width:600px;width:100%;text-align:center;position:relative}
/* ── The state, as its own object above everything ───────────────────── */
.state{
display:inline-flex;align-items:center;gap:9px;
border:1px solid var(--accent-line);background:var(--accent-wash);
border-radius:999px;padding:7px 15px;
font-family:"Plex Mono",ui-monospace,monospace;font-size:11.5px;
text-transform:uppercase;letter-spacing:.07em;color:var(--accent-ink);
}
.dot{width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none;
animation:pulse 2s ease-in-out infinite}
@keyframes pulse{50%{opacity:.3}}
/* ── Brand ───────────────────────────────────────────────────────────────
`gap` on a flex container applies between EVERY child, and a bare text
node is a child. Written as one inline-flex row around "Clu", <i>Pilot</i>
and " Cloud", it spaced the brand out into three separate words which is
how the page went out reading "Clu Pilot Cloud". The wordmark is a single
element now, and the gap only ever separates it from the mark. */
.brand{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:34px}
.brand svg{width:44px;height:44px;flex:none;border-radius:11px}
.word{font-size:22px;font-weight:700;letter-spacing:-.025em;color:var(--ink)}
.word span{color:var(--muted);font-weight:600}
h1{margin-top:26px;font-weight:700;font-size:clamp(2rem,6vw,2.9rem);
letter-spacing:-.038em;line-height:1.06;color:var(--ink)}
.lead{margin:18px auto 0;max-width:46ch;font-size:16px;color:var(--muted)}
/* ── The three promises the paragraph already makes ──────────────────── */
.points{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:30px}
.point{
display:inline-flex;align-items:center;gap:7px;
border:1px solid var(--line);background:var(--surface);border-radius:999px;
padding:8px 15px;font-size:14px;font-weight:500;color:var(--body);
}
.point svg{width:15px;height:15px;flex:none;stroke:var(--success);stroke-width:2.6;fill:none}
/* ── Contact ─────────────────────────────────────────────────────────── */
.contact{margin-top:38px;padding-top:30px;border-top:1px solid var(--line)}
.contact p{font-size:15px;color:var(--muted)}
.btn{
display:inline-flex;align-items:center;gap:9px;margin-top:14px;
min-height:46px;padding:0 22px;border-radius:11px;
background:var(--accent-ink);color:#fff;
font-size:15px;font-weight:600;text-decoration:none;
transition:background .18s;
}
.btn:hover{background:var(--accent-press)}
.btn svg{width:17px;height:17px;stroke:currentColor;stroke-width:1.9;fill:none}
.foot{margin-top:34px;font-family:"Plex Mono",ui-monospace,monospace;
font-size:11.5px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
@media (prefers-reduced-motion: reduce){.dot{animation:none}}
</style>
@endverbatim
</head>
<body>
<main>
<div class="card">
<div class="head">
<span>{{ __('coming_soon.label') }}</span>
<b style="display:inline-flex;align-items:center;gap:8px"><span class="dot" aria-hidden="true"></span>{{ __('coming_soon.state') }}</b>
</div>
<div class="body">
<span class="brand"><span class="mark" aria-hidden="true"></span>Clu<i>Pilot</i> Cloud</span>
<h1>{{ __('coming_soon.title') }}</h1>
<p>{{ __('coming_soon.body') }}</p>
<p class="foot">{{ __('coming_soon.contact') }}
<a href="mailto:office&#64;clupilot.com">office&#64;clupilot.com</a>
</p>
</div>
<span class="state"><span class="dot" aria-hidden="true"></span>{{ __('coming_soon.state') }}</span>
<div class="brand">
{{-- The real mark cloud plus autopilot ascent not the plain gradient
tile that stood here. A brand square with nothing in it reads as an
image that failed to load, which on a "we are still building" page is
the last impression to leave. --}}
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<linearGradient id="cs-mark" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
<stop stop-color="#FB923C"/><stop offset="0.55" stop-color="#F97316"/><stop offset="1" stop-color="#C2560A"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="15" fill="url(#cs-mark)"/>
<path d="M22 41h20a8 8 0 0 0 .7-15.97A11 11 0 0 0 21.4 27.2 8.5 8.5 0 0 0 22 44z" fill="#ffffff" fill-opacity="0.22"/>
<path d="M32 17 44 44l-12-6-12 6z" fill="#ffffff"/>
</svg>
<span class="word">CluPilot <span>Cloud</span></span>
</div>
<h1>{{ __('coming_soon.title') }}</h1>
<p class="lead">{{ __('coming_soon.body') }}</p>
<div class="points">
@foreach (['operated', 'backed_up', 'current'] as $point)
<span class="point">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>
{{ __("coming_soon.points.{$point}") }}
</span>
@endforeach
</div>
<div class="contact">
<p>{{ __('coming_soon.contact') }}</p>
<a class="btn" href="mailto:office&#64;clupilot.com">
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></svg>
office&#64;clupilot.com
</a>
</div>
<p class="foot">clupilot.com</p>
</main>
</body>
</html>

View File

@ -94,3 +94,33 @@ it('does not leave the old marketing stylesheet behind anywhere', function () {
->not->toContain('Plex Serif');
}
});
it('does not space the wordmark out into separate words', function () {
// It went out reading "Clu Pilot Cloud". `gap` on a flex container applies
// between EVERY child, and a bare text node is a child — so an inline-flex
// row wrapped around "Clu", <i>Pilot</i> and " Cloud" put nine pixels
// between each of them.
//
// The rule that prevents it: the mark and the wordmark are the only two
// children of the flex row, and the wordmark is one element.
$source = File::get(resource_path('views/coming-soon.blade.php'));
expect($source)->toContain('<span class="word">CluPilot')
->and($source)->not->toContain('Clu<i>Pilot</i>');
});
it('draws the actual mark on the placeholder, not an empty tile', function () {
// It was a plain gradient square. A brand tile with nothing in it reads as
// an image that failed to load — on a "we are still building" page that is
// the last impression to leave.
//
// Inlined rather than pulled from x-ui.logo: this page renders when the
// asset build may not exist, and a Blade component is fine but the SVG has
// to be in the file either way.
$source = File::get(resource_path('views/coming-soon.blade.php'));
expect($source)->toContain('viewBox="0 0 64 64"')
// The ascent triangle — the part that makes it a mark rather than a
// rounded rectangle.
->and($source)->toContain('M32 17 44 44l-12-6-12 6z');
});