One wordmark, one typeface, and the address the server actually issues
tests / pest (push) Failing after 11m28s Details
tests / assets (push) Successful in 19s Details
tests / release (push) Has been skipped Details

Reported as "logo and font are different everywhere" — maintenance page,
customer panel, console, homepage. They were. Counting them up: the site
header set the name in ink at -0.02em, the sidebar split it into "Clu" plus
an orange "Pilot", the sign-in plate set it white at a third size, the
customer two-factor page set it in MONO with no mark at all, the error pages
and the placeholder each inlined their own — and the maintenance screen had
no wordmark whatsoever. Nine surfaces, seven ideas of the brand.

There is one now: resources/views/components/ui/brand.blade.php. Mark plus
wordmark in one piece, in ink, with an optional muted "Cloud". No orange half
— that made the accent a permanent fixture instead of something used
sparingly. The four places that cannot use a Blade component (the two
self-contained pages, the error layout, the mail layout) inline it and are
held to the same shape by a test.

The typeface. The console loads IBM Plex through Vite; the 503 page has no
stylesheet at all, so the maintenance screen — the one page shown when
somebody is already worried — was rendering in the system sans. It declares
the fonts itself now. And the bold weight was missing from public/fonts
entirely: the placeholder and the error pages both asked for
ibm-plex-sans-latin-700-normal.woff2, which was not there. A @font-face
pointing at nothing fails silently, which is why nobody noticed.

The error pages carried the same two faults the placeholder shipped with: an
empty gradient tile where the mark belongs, and `gap` on an inline-flex row
spacing "Clu", "Pilot" and "Cloud" apart as three separate words.

And the price sheet promised "Adresse auf clupilot.cloud" — a domain the
company does not own — while provisioning was issuing addresses under
whatever CLUPILOT_DNS_ZONE says. It is assembled from that setting now, the
same reason the prices are read from the catalogue rather than typed into the
page.

welcome.blade.php removed: Laravel's leftover, reachable from no route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus v1.3.10
nexxo 2026-07-29 14:30:44 +02:00
parent 4ef5b9519c
commit 17a6fabcc2
18 changed files with 207 additions and 70 deletions

View File

@ -1 +1 @@
1.3.9
1.3.10

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Services\Billing\PlanCatalogue;
use App\Support\ProvisioningSettings;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Log;
use Throwable;
@ -38,7 +39,12 @@ class LandingController extends Controller
'managed_updates' => 'Updates & Wartung',
'daily_backups' => 'Tägliche Sicherung',
'monitoring' => 'Überwachung rund um die Uhr',
'subdomain' => 'Adresse auf clupilot.cloud',
// Filled in from the configured zone by label(), because a
// hard-coded domain here is the same two-sources-of-truth mistake
// as a hard-coded price — and it shipped naming clupilot.cloud, a
// domain the company does not own, while provisioning was issuing
// addresses under whatever CLUPILOT_DNS_ZONE said.
'subdomain' => 'Adresse auf :zone',
'custom_domain' => 'Eigene Domain',
'office' => 'Office im Browser',
'branding' => 'Ihr Logo & Ihre Farben',
@ -73,9 +79,13 @@ class LandingController extends Controller
{
$offered = array_merge(...array_column($plans, 'features')) ?: [];
// Through label(), like the plans themselves. Compared against the raw
// constant this stopped matching the moment one label gained a
// placeholder, and the row for it would simply have vanished from the
// comparison table — with nothing anywhere saying why.
return array_values(array_filter(
self::FEATURES,
fn (string $label) => in_array($label, $offered, true),
array_map(fn (string $key) => $this->label($key), array_keys(self::FEATURES)),
fn (?string $label) => $label !== null && in_array($label, $offered, true),
));
}
@ -114,6 +124,22 @@ class LandingController extends Controller
return $plans;
}
/**
* One feature, in the words a customer uses.
*
* The address the platform hands out is a setting, not a constant a
* server can be installed against any zone so the sentence naming it is
* assembled rather than written down twice.
*/
private function label(string $key): ?string
{
$label = self::FEATURES[$key] ?? null;
return $label === null
? null
: str_replace(':zone', ProvisioningSettings::dnsZone(), $label);
}
/** Currencies we have a symbol for; anything else prints its ISO code. */
private const SYMBOLS = ['EUR' => '€', 'CHF' => 'CHF', 'USD' => '$', 'GBP' => '£'];
@ -156,7 +182,7 @@ class LandingController extends Controller
// the catalogue without a translation would otherwise appear on the
// public page as "premium_sla".
return array_values(array_filter(array_map(
fn (string $key) => self::FEATURES[$key] ?? null,
fn (string $key) => $this->label($key),
$keys,
)));
}

Binary file not shown.

View File

@ -73,9 +73,9 @@
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)}
.brand{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:34px}
.brand svg{width:36px;height:36px;flex:none}
.word{font-size:20px;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);

View File

@ -18,10 +18,7 @@
<div aria-hidden="true" class="pointer-events-none absolute inset-0"
style="background:radial-gradient(ellipse 52% 46% at 24% 18%,rgba(249,115,22,.20),transparent 68%);"></div>
<div class="relative flex items-center gap-2.5">
<x-ui.logo class="size-8" />
<span class="text-xl font-bold tracking-tight text-white">CluPilot</span>
</div>
<div class="relative"><x-ui.brand size="lg" tone="white" /></div>
<div class="relative">
<p class="mb-7 inline-flex items-center gap-2 rounded-pill px-3.5 py-1.5 text-xs font-semibold"

View File

@ -39,10 +39,7 @@
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-7" />
<span class="font-bold tracking-[-0.02em] text-ink">CluPilot</span>
</a>
<a href="{{ route('home') }}"><x-ui.brand size="md" /></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)
@ -76,10 +73,7 @@
<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-7" />
<span class="font-bold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
</div>
<x-ui.brand size="md" suffix />
<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.

View File

@ -39,7 +39,7 @@
</table>
</td>
<td style="font-size:17px;line-height:26px;color:#17171c;letter-spacing:-0.2px;">
<span style="font-weight:700;">Clu</span><span style="font-weight:700;color:#b8500a;">Pilot</span>
<span style="font-weight:700;color:#17171c;">CluPilot</span>
</td>
</tr>
</table>

View File

@ -22,10 +22,7 @@
>
<div class="flex shrink-0 items-center justify-between gap-2 px-2.5 pb-5">
<a href="{{ $console ? \App\Support\AdminArea::home() : route('dashboard') }}" class="flex items-center gap-2.5">
<x-ui.logo class="size-7" />
{{-- One element: as separate flex children the gap lands inside the
word and reads as "Clu Pilot". --}}
<span class="whitespace-nowrap text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
<x-ui.brand size="md" />
@if ($console)
<span class="rounded-sm bg-accent-subtle px-1.5 py-0.5 font-mono text-[9.5px] font-semibold uppercase tracking-[0.1em] text-accent-text">{{ __('admin.badge') }}</span>
@endif

View File

@ -0,0 +1,37 @@
@props([
'size' => 'md', // sm | md | lg
'suffix' => false, // append "Cloud" in the muted tone
'tone' => 'ink', // ink | white — for the dark sign-in plate
])
{{--
The lockup. Mark plus wordmark, one definition.
It had five. The website header set the word in ink at -0.02em, the
sidebar split it into "Clu" plus an orange "Pilot", the sign-in plate set
it white at a third size, the placeholder inlined its own, and the
maintenance screen had no wordmark at all five surfaces of one product,
each with its own idea of the brand.
The ink version wins because the website is the newest and most considered
of them, and because a two-tone wordmark makes the accent a permanent
fixture rather than something used sparingly.
The mark and the word are the only two children of the flex row, and the
word is a single element. `gap` applies between EVERY child of a flex
container and a bare text node is a child written the other way this
shipped reading "Clu Pilot Cloud".
--}}
@php
$marks = ['sm' => 'size-6', 'md' => 'size-7', 'lg' => 'size-9'];
$words = ['sm' => 'text-base', 'md' => 'text-lg', 'lg' => 'text-xl'];
@endphp
<span {{ $attributes->merge(['class' => 'inline-flex items-center gap-2.5']) }}>
<x-ui.logo class="{{ $marks[$size] ?? $marks['md'] }} shrink-0" />
{{-- The directive sits on its own line on purpose. Blade only compiles a
directive at a non-word boundary, so `CluPilot@if (…)` leaves the @if
as literal text and the matching @endif then blows up the compiled
view with a parse error. The newline also supplies the space before
"Cloud". --}}
<span class="whitespace-nowrap {{ $words[$size] ?? $words['md'] }} font-bold tracking-[-0.025em] {{ $tone === 'white' ? 'text-white' : 'text-ink' }}">CluPilot
@if ($suffix)<span class="font-semibold {{ $tone === 'white' ? 'text-white/70' : 'text-muted' }}">Cloud</span>@endif</span>
</span>

View File

@ -39,6 +39,7 @@
<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-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{
@ -66,8 +67,9 @@
.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;
.word{font-size:20px;font-weight:700;letter-spacing:-.025em;color:var(--ink)}
.word span{font-weight:600;color:var(--muted)}
.mark{width:28px;height:28px;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)}
@ -96,7 +98,21 @@
<div class="card">
<div class="head"><span>{{ __('errors.heading') }}</span><b>{{ $code }}</b></div>
<div class="body">
<span class="brand"><span class="mark" aria-hidden="true"></span>Clu<i>Pilot</i></span>
<span class="brand">
<svg class="mark" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs><linearGradient id="err-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(#err-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>
{{-- One element. `gap` applies between every child of a flex row and a
bare text node is a child, so "Clu", <i>Pilot</i> and the space
between them were being spaced apart the same fault the
placeholder shipped with. --}}
<span class="word">CluPilot <span>Cloud</span></span>
</span>
<h1>{{ $title }}</h1>
@if ($body)
<p>{{ $body }}</p>

View File

@ -12,10 +12,7 @@
{{-- Form panel --}}
<main class="flex flex-1 items-center justify-center px-6 py-12">
<div class="w-full max-w-sm animate-rise">
<div class="mb-9 flex items-center gap-2.5 lg:hidden">
<x-ui.logo class="size-8" />
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
</div>
<x-ui.brand size="md" class="mb-9 lg:hidden" />
<h1 class="text-3xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.login_title') }}</h1>
<p class="mt-2 text-sm text-muted">{{ __('auth.login_subtitle') }}</p>

View File

@ -1,9 +1,6 @@
<div class="flex min-h-screen items-center justify-center bg-bg px-6 py-12">
<div class="w-full max-w-sm animate-rise">
<div class="mb-9 flex items-center gap-2.5">
<x-ui.logo class="size-8" />
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
</div>
<x-ui.brand size="md" class="mb-9" />
<h1 class="text-2xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.console_login_title') }}</h1>
<p class="mt-2 text-sm text-muted">{{ __('auth.console_login_subtitle') }}</p>

View File

@ -1,9 +1,6 @@
<div class="flex min-h-screen items-center justify-center bg-bg px-6 py-12">
<div class="w-full max-w-sm animate-rise">
<div class="mb-9 flex items-center gap-2.5">
<x-ui.logo class="size-8" />
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
</div>
<x-ui.brand size="md" class="mb-9" />
<h1 class="text-2xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.twofa_title') }}</h1>
<p class="mt-2 text-sm text-muted">

View File

@ -12,10 +12,7 @@
{{-- Form panel --}}
<main class="flex flex-1 items-center justify-center px-6 py-12">
<div class="w-full max-w-sm animate-rise">
<div class="mb-9 flex items-center gap-2.5 lg:hidden">
<x-ui.logo class="size-8" />
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
</div>
<x-ui.brand size="md" class="mb-9 lg:hidden" />
<h1 class="text-3xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.register_title') }}</h1>
<p class="mt-2 text-sm text-muted">{{ __('auth.register_subtitle') }}</p>

View File

@ -1,8 +1,6 @@
<div class="flex min-h-screen items-center justify-center p-6" x-data="{ recovery: false }">
<div class="w-full max-w-sm">
<div class="mb-6 text-center">
<span class="font-mono text-xl font-semibold tracking-tight text-ink">CluPilot</span>
</div>
<div class="mb-6 flex justify-center"><x-ui.brand size="md" /></div>
<x-ui.card>
<h1 class="text-lg font-semibold text-ink">{{ __('auth.twofa_title') }}</h1>

View File

@ -28,6 +28,17 @@
for one page is not a dark mode; it is one page that does not match.
--}}
<style>
/* The fonts, declared here too. The console loads them through Vite, but
the 503 page has no stylesheet at all so the maintenance screen was the
one surface rendering in the system sans while every other page was IBM
Plex. Same files the placeholder uses; if they are missing the page
degrades to a system sans and still reads. */
@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}
.cpu-wrap {
/* portal-tokens.css: --bg, --text-strong, --text-muted, --accent,
--accent-text, --surface, --border, --surface-2 */
@ -47,7 +58,10 @@
font-family: "IBM Plex Sans", "Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}
.cpu-main { max-width: 34rem; width: 100%; }
.cpu-mark { width: 2.75rem; height: 2.75rem; margin: 0 auto; display: block; }
.cpu-brand { display: flex; align-items: center; justify-content: center; gap: 0.625rem; }
.cpu-mark { width: 2.25rem; height: 2.25rem; flex: none; }
.cpu-word { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; color: var(--cpu-fg); }
.cpu-word span { font-weight: 600; color: var(--cpu-muted); }
.cpu-badge { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 1.5rem;
padding: 0.4rem 0.85rem; border: 1px solid #e8cdb2; border-radius: 9999px;
background: #fff3e9; color: var(--cpu-accent-ink);
@ -79,7 +93,7 @@
reads as an image that failed to load, and this panel is shown at
the one moment somebody is already wondering whether something is
broken. --}}
<svg class="cpu-mark" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<div class="cpu-brand"><svg class="cpu-mark" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<linearGradient id="cpu-mark-grad" 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"/>
@ -89,6 +103,8 @@
<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="cpu-word">CluPilot <span>Cloud</span></span>
</div>
<div class="cpu-badge"><span class="cpu-dot"></span>{{ __('updating.badge') }}</div>
<h1 class="cpu-title">{{ __('updating.title') }}</h1>

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="flex min-h-full items-center justify-center bg-bg p-6 text-body antialiased">
<main class="text-center">
<h1 class="font-mono text-2xl font-semibold tracking-tight text-ink">CluPilot</h1>
<p class="mt-2 text-sm text-muted">{{ __('common.tagline') }}</p>
@if (Route::has('login'))
<a href="{{ route('login') }}"
class="mt-6 inline-flex items-center justify-center rounded bg-accent-active px-4 py-2 text-sm font-semibold text-on-accent transition hover:bg-accent-press">
{{ __('common.sign_in') }}
</a>
@endif
</main>
</body>
</html>

View File

@ -124,3 +124,92 @@ it('draws the actual mark on the placeholder, not an empty tile', function () {
// rounded rectangle.
->and($source)->toContain('M32 17 44 44l-12-6-12 6z');
});
it('draws the wordmark from one definition, not five', function () {
// It had five: the site header set it in ink, the sidebar split it into
// "Clu" plus an orange "Pilot", the sign-in plate set it white at another
// size, the placeholder inlined its own, and the maintenance screen had no
// wordmark at all. Five surfaces of one product, each with its own idea of
// the brand.
$offenders = [];
foreach (File::allFiles(resource_path('views')) as $file) {
// The two self-contained pages cannot use a Blade component's compiled
// output — they render when the asset build may not exist — and the
// component itself obviously spells the word out.
// The self-contained pages cannot use a Blade component's compiled
// output — they render when the asset build may not exist — and an
// email has no stylesheet of ours at all, so its lockup is inline by
// necessity. They still have to AGREE with the component, which is
// what the next assertion checks.
if (in_array($file->getFilename(), ['coming-soon.blade.php', 'updating-panel.blade.php', 'brand.blade.php', 'layout.blade.php'], true)) {
continue;
}
$source = preg_replace('/\{\{--.*?--\}\}/s', '', File::get($file->getPathname()));
// The wordmark is markup around the name; a plain sentence mentioning
// the company is not. `>Clu` catches exactly the lockup forms.
if (preg_match('/>\s*Clu(Pilot|<)/', $source)) {
$offenders[] = str_replace(resource_path().'/', '', $file->getPathname());
}
}
expect($offenders)->toBeEmpty();
// The four that must inline it write the name in one piece, in ink, with
// no orange half — the shape the component defines.
foreach ([
'coming-soon.blade.php',
'partials/updating-panel.blade.php',
'errors/layout.blade.php',
'components/mail/layout.blade.php',
] as $page) {
expect(File::get(resource_path('views/'.$page)))
->toContain('CluPilot')
->not->toContain('Clu<i>Pilot</i>')
->not->toContain('>Clu</span>');
}
});
it('gives the maintenance screen the same typeface as everything else', function () {
// The console loads IBM Plex through Vite. The 503 page has no stylesheet
// at all, so the maintenance screen — the one page shown when somebody is
// already worried — was rendering in the system sans.
$panel = File::get(resource_path('views/partials/updating-panel.blade.php'));
expect($panel)->toContain('@font-face')
->and($panel)->toContain('/fonts/ibm-plex-sans-latin-700-normal.woff2');
// And the file it names is actually there. A @font-face pointing at
// nothing fails silently, which is how the bold weight went missing from
// the placeholder without anyone noticing.
foreach (['400', '500', '600', '700'] as $weight) {
expect(public_path("fonts/ibm-plex-sans-latin-{$weight}-normal.woff2"))->toBeFile();
}
});
it('names the address zone the installation is configured for', function () {
// It said "Adresse auf clupilot.cloud" — a domain the company does not own
// — while provisioning issued addresses under whatever CLUPILOT_DNS_ZONE
// said. The same two-sources-of-truth mistake as a hard-coded price.
App\Support\Settings::set('provisioning.dns_zone', 'example.test');
// Comments stripped: the controller explains at length that it used to
// name clupilot.cloud, and a raw scan reads the explanation as the
// offence. Third time this suite has had to learn it.
$source = implode('', array_map(
fn ($token) => is_array($token) && in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true) ? '' : (is_array($token) ? $token[1] : $token),
token_get_all(File::get(app_path('Http/Controllers/LandingController.php'))),
));
expect($source)->not->toContain('clupilot.cloud');
$page = $this->get('/');
$page->assertOk();
// Only where the catalogue actually sells the feature; either way the old
// literal must be gone.
$page->assertDontSee('clupilot.cloud');
});