diff --git a/app/Livewire/Settings.php b/app/Livewire/Settings.php
index 336216f..a3a00ad 100644
--- a/app/Livewire/Settings.php
+++ b/app/Livewire/Settings.php
@@ -19,16 +19,52 @@ use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
+use Livewire\Attributes\Url;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\WithFileUploads;
use RuntimeException;
+/**
+ * The customer's own settings.
+ *
+ * It was one column, 768 pixels wide inside a 1240-pixel shell — a third of the
+ * window left empty beside it — and everything a customer might ever change was
+ * stacked in it: the company address, their password, two-factor, the devices
+ * they are signed in on, the branding of their cloud, the contract, and the
+ * button that closes the account. Two thousand pixels of scroll, and the way to
+ * a particular thing was to know how far down it lived.
+ *
+ * Four tabs now, by what somebody came to do: their data, how the account is
+ * protected, how the cloud looks, and the contract. The choice lives in the
+ * query string, so a reload, a bookmark and the back button all land where they
+ * were — and a link to this page can name the part it means.
+ */
class Settings extends Component
{
use ChangesOwnPassword;
use ConfirmsPassword;
+ /**
+ * The tabs, in the order they are shown.
+ *
+ * The list IS the schema: it validates the query string, builds the tab bar
+ * and decides which section renders. A fourth place to add a tab is a fourth
+ * place to forget one.
+ */
+ public const TABS = ['profile', 'security', 'branding', 'contract'];
+
+ /**
+ * Which one is open, taken from and written back to the address.
+ *
+ * `history: true`, so each tab is a step the back button can take, and no
+ * `except`, so the parameter is there from the first render — hiding it on
+ * the default reads as "the tab is not in the URL", which is exactly how it
+ * was reported on the console's settings page.
+ */
+ #[Url(history: true)]
+ public string $tab = 'profile';
+
/** Shown once, right after setting two-factor up. */
public ?array $recoveryCodes = null;
@@ -158,6 +194,13 @@ class Settings extends Component
public function mount(): void
{
+ // A tab name out of the URL is a string a stranger typed. Anything
+ // unknown falls back to the first rather than rendering a settings page
+ // with no section on it at all.
+ if (! in_array($this->tab, self::TABS, true)) {
+ $this->tab = self::TABS[0];
+ }
+
$c = $this->customer();
if ($c === null) {
return;
@@ -358,6 +401,7 @@ class Settings extends Component
$withdrawal = WithdrawalRight::for($contract);
return view('livewire.settings', [
+ 'tabs' => self::TABS,
// Never the secret itself — only whether it exists, and the SVG
// Fortify renders from it. The secret in a Livewire property would
// travel to the browser and back in the component snapshot.
diff --git a/lang/de/settings.php b/lang/de/settings.php
index 3b79ce7..3cd6a1f 100644
--- a/lang/de/settings.php
+++ b/lang/de/settings.php
@@ -1,8 +1,18 @@
'Konto',
+ 'company_sub' => 'Diese Angaben stehen auf Ihren Rechnungen.',
+ // The tab bar. The list in App\Livewire\Settings::TABS decides which
+ // exist; these only name them.
+ 'tab' => [
+ 'profile' => 'Ihre Daten',
+ 'security' => 'Sicherheit',
+ 'branding' => 'Erscheinungsbild',
+ 'contract' => 'Vertrag',
+ ],
'title' => 'Einstellungen',
- 'subtitle' => 'Firmendaten, Branding und Ihr Paket verwalten.',
+ 'subtitle' => 'Alles, was zu Ihrem Konto gehört — nach Themen getrennt.',
'save' => 'Speichern',
'company_title' => 'Firmendaten',
diff --git a/lang/en/settings.php b/lang/en/settings.php
index 7a41a3b..803d952 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -1,8 +1,18 @@
'Account',
+ 'company_sub' => 'These details appear on your invoices.',
+ // The tab bar. The list in App\Livewire\Settings::TABS decides which
+ // exist; these only name them.
+ 'tab' => [
+ 'profile' => 'Your details',
+ 'security' => 'Security',
+ 'branding' => 'Appearance',
+ 'contract' => 'Contract',
+ ],
'title' => 'Settings',
- 'subtitle' => 'Manage your company details, branding and package.',
+ 'subtitle' => 'Everything that belongs to your account, grouped by subject.',
'save' => 'Save',
'company_title' => 'Company details',
diff --git a/resources/views/livewire/settings.blade.php b/resources/views/livewire/settings.blade.php
index 7920ca4..f1f8aef 100644
--- a/resources/views/livewire/settings.blade.php
+++ b/resources/views/livewire/settings.blade.php
@@ -1,299 +1,350 @@
-
-
-
{{ __('settings.title') }}
-
{{ __('settings.subtitle') }}
+{{--
+ The customer's own settings.
+
+ It was one column 768 pixels wide inside a 1240-pixel shell — a third of the
+ window empty beside it — holding everything a customer might ever change:
+ the company address, the password, two-factor, the signed-in devices, the
+ branding, the contract and the button that closes the account. Two thousand
+ pixels of scroll, and the way to a particular thing was knowing how far down
+ it lived.
+
+ Four tabs, by what somebody came to do. Inside a tab the short cards sit side
+ by side instead of each taking a full row to hold three lines. The choice is
+ in the query string (see the #[Url] attribute on the component), so a reload,
+ a bookmark and the back button all land where they were.
+--}}
+
+
+
+
{{ __('settings.eyebrow') }}
+
+ {{ __('settings.title') }}
+
+
{{ __('settings.subtitle') }}
+
+
+ {{-- The same tab bar the console uses, so one vocabulary holds on both sides
+ of the login. --}}
+
+ @foreach ($tabs as $name)
+
+ @endforeach
- {{-- Company / billing profile --}}
-
-
- {{-- Branding --}}
-
- {{-- Own password. There was no way to change one at all — Fortify's
- updatePasswords feature was switched off. --}}
-
-
-
- {{-- Two-factor. Every action is re-checked server-side against a recent
- password confirmation — hiding the buttons stops nobody who can post
- to /livewire/update. --}}
-
- @if (! $passwordConfirmed)
- {{-- The gate. A signed-in session is not enough to change how the
- account is protected — the risk is an unlocked machine. --}}
-
+ @endif
+
+ {{-- ── Sicherheit ───────────────────────────────────────────────────── --}}
+ @if ($tab === 'security')
+
+
+ {{-- Own password. There was no way to change one at all — Fortify's
+ updatePasswords feature was switched off. --}}
+
- @elseif ($twoFactorOn)
-
- @else
-
- {{ __('settings.twofa_enable') }}
-
- @endif
- @if ($recoveryCodes)
- {{-- Shown once, on purpose: nobody writes down what they were never
- shown, and these are the way back in when the phone is gone. --}}
-
-
{{ __('settings.twofa_codes_title') }}
-
{{ __('settings.twofa_codes_hint') }}
-
- @foreach ($recoveryCodes as $code)
{{ $code }}
@endforeach
-
-
- @endif
-
-
- {{-- Directly under two-factor, because the two answer the same question:
- who can get into this account, and what can I do about it. --}}
-
- @livewire('sessions')
-
-
-
-
- {{-- Package & account lifecycle --}}
-
-
{{ __('settings.package_title') }}
-
- @if ($cancellationScheduled)
-
-
+ {{-- Two-factor. Every action is re-checked server-side against a
+ recent password confirmation — hiding the buttons stops nobody
+ who can post to /livewire/update. --}}
+
+
+ @if (! $passwordConfirmed)
+ {{-- The gate. A signed-in session is not enough to change how
+ the account is protected — the risk is an unlocked
+ machine. --}}
+
+ @elseif ($twoFactorOn)
+
+ @else
+
+ {{ __('settings.twofa_enable') }}
+
+ @endif
+
+ @if ($recoveryCodes)
+ {{-- Shown once, on purpose: nobody writes down what they were
+ never shown, and these are the way back in when the phone
+ is gone. --}}
+
+
{{ __('settings.twofa_codes_title') }}
+
{{ __('settings.twofa_codes_hint') }}
+
+ @foreach ($recoveryCodes as $code)
{{ $code }}
@endforeach
+
+
+ @endif
+
+
+ {{-- Under both, across the width: it answers the same question they
+ do — who can get into this account — and it is a table. --}}
+