Redesign the pre-launch placeholder, and fix its wordmark and mark
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
parent
b85df4c141
commit
4755a4f362
|
|
@ -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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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@clupilot.com">office@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@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@clupilot.com
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="foot">clupilot.com</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue