diff --git a/lang/de/settings.php b/lang/de/settings.php
index fc58f40..098d932 100644
--- a/lang/de/settings.php
+++ b/lang/de/settings.php
@@ -49,6 +49,9 @@ return [
'brand_using_default' => 'Es werden aktuell die CluPilot-Standardwerte verwendet.',
'branding_saved' => 'Branding gespeichert.',
+ 'contract_title' => 'Vertrag und Konto',
+ 'contract_sub' => 'Ihr Paket, Ihre Rechte daraus und die Unterlagen dazu.',
+ 'to_packages' => 'Pakete ansehen',
'package_title' => 'Paket',
'package_active' => 'Aktives Paket: :plan.',
'no_package' => 'Kein aktives Paket.',
diff --git a/lang/en/settings.php b/lang/en/settings.php
index 158611a..e72e94f 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -49,6 +49,9 @@ return [
'brand_using_default' => 'Currently using the CluPilot defaults.',
'branding_saved' => 'Branding saved.',
+ 'contract_title' => 'Contract and account',
+ 'contract_sub' => 'Your package, the rights that come with it, and the documents.',
+ 'to_packages' => 'See the packages',
'package_title' => 'Package',
'package_active' => 'Active package: :plan.',
'no_package' => 'No active package.',
diff --git a/resources/views/components/ui/panel.blade.php b/resources/views/components/ui/panel.blade.php
new file mode 100644
index 0000000..3b2740b
--- /dev/null
+++ b/resources/views/components/ui/panel.blade.php
@@ -0,0 +1,40 @@
+@props([
+ 'title' => null,
+ 'subtitle' => null,
+])
+
+{{-- A settings panel: one card, a header that names it, a body of rows, and an
+ optional footer for whatever finishes the group.
+
+ The page was a heap of cards of different heights, each holding one thing,
+ which produced a staircase down the screen and left half the width empty
+ beside it. A panel with divided rows is what every settings page worth
+ copying does — the group is the card, the settings are its rows, and the
+ rhythm comes from the dividers rather than from the gaps between boxes. --}}
+merge(['class' => 'overflow-hidden rounded-lg border border-line bg-surface shadow-xs']) }}>
+ @if ($title || isset($header))
+
+ @isset($header)
+ {{ $header }}
+ @else
+
+
{{ $title }}
+ @if ($subtitle)
+
{{ $subtitle }}
+ @endif
+
+ @isset($action)
+
{{ $action }}
+ @endisset
+ @endisset
+
+ @endif
+
+
{{ $slot }}
+
+ @isset($footer)
+
+ {{ $footer }}
+
+ @endisset
+
diff --git a/resources/views/components/ui/row.blade.php b/resources/views/components/ui/row.blade.php
new file mode 100644
index 0000000..f0e6609
--- /dev/null
+++ b/resources/views/components/ui/row.blade.php
@@ -0,0 +1,27 @@
+@props([
+ 'label' => null,
+ 'hint' => null,
+ 'for' => null,
+])
+
+{{-- One row of a settings panel: what it is on the left, the control on the
+ right.
+
+ The two-column shape is what uses the width. A single column of full-width
+ inputs on a 1240px shell leaves two thirds of the line empty and makes a
+ four-field form look like a questionnaire; a label column of 280px with the
+ control beside it reads as a settings page and fits the screen it is on.
+
+ Stacks below `sm`, because on a telephone the label belongs above its
+ field. --}}
+
diff --git a/resources/views/livewire/settings.blade.php b/resources/views/livewire/settings.blade.php
index 2663fd1..2298e20 100644
--- a/resources/views/livewire/settings.blade.php
+++ b/resources/views/livewire/settings.blade.php
@@ -1,17 +1,26 @@
{{--
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.
+ ## What was wrong, three times over
- 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.
+ First it was one column 768px wide in a 1240px shell, holding everything a
+ customer might ever change, two thousand pixels deep. Then it became four
+ tabs — better — but the cards inside them were still boxes of different
+ heights in a grid, which on the contract tab produced a short one, a wide one
+ and a short one again: a staircase with no rhythm and half the width empty
+ beside it.
+
+ ## What it is now
+
+ Panels and rows (x-ui.panel, x-ui.row). A group is a card with a header that
+ names it; a setting is a ROW inside that card — what it is on the left, the
+ control on the right, dividers between. That is the shape every settings page
+ worth copying uses, and it is the shape that uses the width: a label column
+ of 280px with the control beside it fills the line, where a stack of
+ full-width inputs leaves two thirds of it empty.
+
+ Below `sm` the rows stack, because on a telephone a label belongs above its
+ field.
--}}
@@ -24,7 +33,8 @@
{{-- The same tab bar the console uses, so one vocabulary holds on both sides
- of the login. --}}
+ of the login. The choice lives in the query string — see the #[Url]
+ attribute on the component. --}}