aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js62
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css159
-rw-r--r--packages/meshbay-hub/tests/test_welcome_layout_measured.py142
13 files changed, 601 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index 4c00137..9ded87b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -6,6 +6,7 @@ import {
hubFetch, navigate, session, HUB, loadAuth, _storeRecoveryKey,
} from './hub-client.js';
import * as platform from './platform.js';
+import { Icon } from './icon.js';
const PASSWORD_MIN_BITS = 60;
const PASSWORD_MIN_LEN = 12;
@@ -166,6 +167,7 @@ export function LoginPage({ onLogin }) {
return html`
<div class="page-center">
+ <div class="welcome">
<div class="card login-card">
<h2>${t('login.title')}</h2>
<form onSubmit=${onSubmit}>
@@ -192,10 +194,70 @@ export function LoginPage({ onLogin }) {
<a href="#/reset">${t('login.forgot')}</a>
</div>
</div>
+ ${!platform.isNative && html`<${WelcomePitch} />`}
+ </div>
</div>
`;
}
+// What MeshBay is, beside the sign-in form. Browser only: inside the desktop
+// application the reader has already downloaded it, and the links point at the
+// project's own site rather than at whichever hub the application is set to.
+//
+// Every sentence here is held to MESHBAY_DESIGN.md §2.3. "Data never transits
+// the hub" is a claim the design makes; "the hub cannot read anything" is one it
+// forbids (T3), which is why the list below says what never *reaches* the hub
+// and stops there. "End-to-end" means device to node, as §2.3 defines it.
+const WELCOME_APPS = [
+ ['chat', 'welcome.app_chat'], ['image', 'welcome.app_photos'],
+ ['video', 'welcome.app_media'], ['play', 'welcome.app_video'],
+ ['music', 'welcome.app_music'],
+];
+const WELCOME_USES = [
+ ['chat', 'welcome.use_chat'], ['image', 'welcome.use_photos'],
+ ['cast', 'welcome.use_media'], ['pencil', 'welcome.use_apps'],
+];
+const WELCOME_NEVER = ['welcome.never_transit', 'welcome.never_stored', 'welcome.never_e2e'];
+
+function WelcomePitch() {
+ return html`
+ <section class="welcome-pitch" aria-labelledby="welcome-title">
+ <h1 id="welcome-title" class="welcome-title">${t('welcome.title')}</h1>
+ <p class="welcome-lead">${t('welcome.lead')}</p>
+ <ul class="welcome-apps">
+ ${WELCOME_APPS.map(([icon, key]) => html`
+ <li key=${key}><${Icon} name=${icon} />${t(key)}</li>`)}
+ </ul>
+ <p>${t('welcome.groups')}</p>
+ <p class="welcome-e2e"><${Icon} name="lock" /><span>${t('welcome.e2e')}</span></p>
+
+ <h2 class="welcome-h">${t('welcome.uses_title')}</h2>
+ <ul class="welcome-uses">
+ ${WELCOME_USES.map(([icon, key]) => html`
+ <li key=${key}><span class="welcome-use-icon"><${Icon} name=${icon} /></span>
+ <span>${t(key)}</span></li>`)}
+ </ul>
+
+ <div class="welcome-hub">
+ <h2 class="welcome-h">${t('welcome.hub_title')}</h2>
+ <p>${t('welcome.hub_body')}</p>
+ <p class="welcome-hub-never">${t('welcome.hub_never')}</p>
+ <ul class="welcome-never">
+ ${WELCOME_NEVER.map(key => html`
+ <li key=${key}><${Icon} name="check" /><span>${t(key)}</span></li>`)}
+ </ul>
+ <p class="welcome-hub-free">${t('welcome.hub_free')}</p>
+ </div>
+
+ <div class="welcome-links">
+ <a class="welcome-cta" href="https://meshbay.org/downloads/">
+ <${Icon} name="download" />${t('welcome.download')}</a>
+ <a class="welcome-legal" href="https://meshbay.org/legal/">${t('welcome.legal')}</a>
+ </div>
+ </section>
+ `;
+}
+
export function RegisterPage() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 9d4aef8..116bbe9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -86,6 +86,30 @@ export default {
'register.resend_sent': 'Ein neuer Code wurde gesendet.',
'login.pending_verification': 'Ihre E-Mail-Adresse ist noch nicht bestätigt. Bitte prüfen Sie Ihr Postfach auf den Bestätigungscode.',
'login.verify_link': 'E-Mail bestätigen',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'Das Internet, wie es gedacht war.',
+ 'welcome.lead': 'MeshBay ist Open-Source-Software, mit der Sie aus der Ferne auf Ihre persönlichen Dateien zugreifen und Anwendungen direkt auf dem Speicher Ihres eigenen Computers betreiben:',
+ 'welcome.app_chat': 'Chat',
+ 'welcome.app_photos': 'Fotos',
+ 'welcome.app_media': 'Mediacenter',
+ 'welcome.app_video': 'Videoplayer',
+ 'welcome.app_music': 'Musikplayer',
+ 'welcome.groups': 'Erstellen Sie Gruppen, um Ihren Liebsten Zugriff auf einige dieser Anwendungen zu geben.',
+ 'welcome.e2e': 'Ihre Daten sind Ende-zu-Ende-verschlüsselt und gehen Peer-to-Peer direkt von jedem Gerät zu dem Computer, auf dem sie liegen (Ihrem Node). Sie laufen niemals über den Hub meshbay.org.',
+ 'welcome.uses_title': 'Was Sie damit tun können',
+ 'welcome.use_chat': 'Sofortnachrichten innerhalb einer Gruppe',
+ 'welcome.use_photos': 'Fotoalben mit Familie und Freunden teilen',
+ 'welcome.use_media': 'Von überall auf Ihr Mediacenter zu Hause zugreifen, für Sie und Ihren Haushalt (mit Streaming und Chromecast über die Desktop-App)',
+ 'welcome.use_apps': 'Eigene Anwendungen auf dem Node entwickeln, den Sie betreiben',
+ 'welcome.hub_title': 'Was meshbay.org tut',
+ 'welcome.hub_body': 'meshbay.org ist lediglich ein Signalisierungsdienst: Er meldet Sie an und verbindet die Beteiligten miteinander (Plug-and-Play, über Heimnetzwerke hinweg).',
+ 'welcome.hub_never': 'Ihre Inhalte erreichen ihn nie:',
+ 'welcome.never_transit': 'Nichts läuft über ihn: keine Inhalte, keine Indexierung',
+ 'welcome.never_stored': 'Keine Ihrer Dateien oder Nachrichten wird dort gespeichert',
+ 'welcome.never_e2e': 'Durchgehende Verschlüsselung, von jedem Gerät bis zu Ihrem Node',
+ 'welcome.hub_free': 'Der Dienst ist kostenlos. Es gibt bewusst kein Geschäftsmodell, kein Tracking und keine Werbung. Viel Spaß!',
+ 'welcome.download': 'Herunterladen (Beta)',
+ 'welcome.legal': 'Rechtliche Hinweise',
'register.err_mismatch': 'Die Passwörter stimmen nicht überein',
'register.err_min_len': {
one: 'Verwenden Sie mindestens {n} Zeichen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 8385caf..80dd819 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -89,6 +89,30 @@ export default {
'register.resend_sent': 'A new code has been sent.',
'login.pending_verification': 'Your email is not yet verified. Please check your inbox for the verification code.',
'login.verify_link': 'Verify email',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'The Internet as it was meant to be.',
+ 'welcome.lead': 'MeshBay is open-source software that gives you remote access to your personal files, and runs applications on top of the storage on your own computer:',
+ 'welcome.app_chat': 'Chat',
+ 'welcome.app_photos': 'Photos',
+ 'welcome.app_media': 'Media center',
+ 'welcome.app_video': 'Video player',
+ 'welcome.app_music': 'Music player',
+ 'welcome.groups': 'Create groups to give the people close to you access to some of these applications.',
+ 'welcome.e2e': 'Your data is end-to-end encrypted and travels peer to peer, from each device to the computer that hosts it (your node). It never passes through the meshbay.org hub.',
+ 'welcome.uses_title': 'What you can do with it',
+ 'welcome.use_chat': 'Instant messaging within a group',
+ 'welcome.use_photos': 'Share photo albums with your family and friends',
+ 'welcome.use_media': 'Reach your home media center from anywhere, for the whole household (streaming, plus Chromecast from the desktop app)',
+ 'welcome.use_apps': 'Build your own applications on the node you host',
+ 'welcome.hub_title': 'What meshbay.org does',
+ 'welcome.hub_body': 'meshbay.org is only a signaling service: it signs you in and connects the parties to one another, plug-and-play, across home networks.',
+ 'welcome.hub_never': 'Your content never reaches it:',
+ 'welcome.never_transit': 'Nothing passes through it: no content, no indexing',
+ 'welcome.never_stored': 'None of your files or messages are stored on it',
+ 'welcome.never_e2e': 'Encryption runs end to end, from each device to your node',
+ 'welcome.hub_free': 'The service is free. There is no business model by design, no tracking and no ads. Enjoy!',
+ 'welcome.download': 'Download (beta)',
+ 'welcome.legal': 'Legal information',
'register.err_mismatch': 'Passwords do not match',
'register.err_min_len': {
one: 'Use at least {n} character',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index cb8f312..9f8b60e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -85,6 +85,30 @@ export default {
'register.resend_sent': 'Se ha enviado un nuevo código.',
'login.pending_verification': 'Su correo electrónico aún no ha sido verificado. Revise su bandeja de entrada para obtener el código de verificación.',
'login.verify_link': 'Verificar correo',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'Internet como debió ser.',
+ 'welcome.lead': 'MeshBay es un software de código abierto que le da acceso remoto a sus archivos personales y ejecuta aplicaciones sobre el almacenamiento de su propio ordenador:',
+ 'welcome.app_chat': 'Chat',
+ 'welcome.app_photos': 'Fotos',
+ 'welcome.app_media': 'Centro multimedia',
+ 'welcome.app_video': 'Reproductor de vídeo',
+ 'welcome.app_music': 'Reproductor de música',
+ 'welcome.groups': 'Cree grupos para dar a sus allegados acceso a algunas de estas aplicaciones.',
+ 'welcome.e2e': 'Sus datos están cifrados de extremo a extremo y viajan entre pares, directamente de cada dispositivo al ordenador que los aloja: su nodo. Nunca pasan por el hub meshbay.org.',
+ 'welcome.uses_title': 'Qué puede hacer con él',
+ 'welcome.use_chat': 'Mensajería instantánea dentro de un grupo',
+ 'welcome.use_photos': 'Compartir álbumes de fotos con su familia y amigos',
+ 'welcome.use_media': 'Acceder desde cualquier lugar a su centro multimedia, para usted y su hogar, con streaming y Chromecast desde la aplicación de escritorio',
+ 'welcome.use_apps': 'Crear sus propias aplicaciones en el nodo que aloja',
+ 'welcome.hub_title': 'Qué hace meshbay.org',
+ 'welcome.hub_body': 'meshbay.org es solo un servicio de señalización: le autentica y conecta a las partes entre sí, sin configuración, a través de redes domésticas.',
+ 'welcome.hub_never': 'Su contenido nunca llega a él:',
+ 'welcome.never_transit': 'Nada pasa por él: ni contenido ni indexación',
+ 'welcome.never_stored': 'Ninguno de sus archivos o mensajes se almacena en él',
+ 'welcome.never_e2e': 'Cifrado de extremo a extremo, de cada dispositivo a su nodo',
+ 'welcome.hub_free': 'El servicio es gratuito. Deliberadamente no hay modelo de negocio, ni rastreo, ni publicidad. ¡Disfrútelo!',
+ 'welcome.download': 'Descargar (beta)',
+ 'welcome.legal': 'Información legal',
'register.err_mismatch': 'Las contraseñas no coinciden',
'register.err_min_len': {
one: 'Use al menos {n} carácter',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 608da62..d928f52 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -85,6 +85,30 @@ export default {
'register.resend_sent': 'Un nouveau code a été envoyé.',
'login.pending_verification': 'Votre e-mail n\'est pas encore vérifié. Consultez votre boîte de réception pour le code de vérification.',
'login.verify_link': 'Vérifier l\'e-mail',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'L’Internet tel qu’il aurait dû être.',
+ 'welcome.lead': 'MeshBay est un logiciel open source qui vous donne accès à distance à vos fichiers personnels et fait tourner des applications sur le stockage de votre ordinateur :',
+ 'welcome.app_chat': 'Messagerie',
+ 'welcome.app_photos': 'Photos',
+ 'welcome.app_media': 'Médiathèque',
+ 'welcome.app_video': 'Lecteur vidéo',
+ 'welcome.app_music': 'Lecteur de musique',
+ 'welcome.groups': 'Créez des groupes pour donner accès à certaines de ces applications à vos proches.',
+ 'welcome.e2e': 'Vos données sont chiffrées de bout en bout et circulent en pair à pair, de chaque appareil jusqu’à l’ordinateur qui les héberge (votre nœud). Elles ne passent jamais par le hub meshbay.org.',
+ 'welcome.uses_title': 'Ce que vous pouvez en faire',
+ 'welcome.use_chat': 'Messagerie instantanée au sein d’un groupe',
+ 'welcome.use_photos': 'Partager des albums photo avec votre famille et vos amis',
+ 'welcome.use_media': 'Accéder à distance à votre médiathèque personnelle, pour vous et votre foyer (en streaming, et avec Chromecast depuis l’application de bureau)',
+ 'welcome.use_apps': 'Créer vos propres applications sur le nœud que vous hébergez',
+ 'welcome.hub_title': 'Le rôle de meshbay.org',
+ 'welcome.hub_body': 'meshbay.org est un simple service de signalisation : il vous authentifie et met les participants en relation, sans configuration, à travers les réseaux domestiques.',
+ 'welcome.hub_never': 'Vos contenus ne l’atteignent jamais :',
+ 'welcome.never_transit': 'Rien ne transite par lui : aucun contenu, aucune indexation',
+ 'welcome.never_stored': 'Aucun de vos fichiers ou messages n’y est stocké',
+ 'welcome.never_e2e': 'Chiffrement de bout en bout, de chaque appareil jusqu’à votre nœud',
+ 'welcome.hub_free': 'Le service est gratuit : aucun modèle économique (c’est voulu), aucun traçage, aucune publicité. Profitez-en !',
+ 'welcome.download': 'Télécharger (bêta)',
+ 'welcome.legal': 'Informations légales',
'register.err_mismatch': 'Les mots de passe ne correspondent pas',
'register.err_min_len': {
one: 'Utilisez au moins {n} caractère',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index be5fe29..58e2389 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -86,6 +86,30 @@ export default {
'register.resend_sent': 'Un nuovo codice è stato inviato.',
'login.pending_verification': 'Il suo indirizzo e-mail non è ancora verificato. Controlli la sua casella di posta per il codice di verifica.',
'login.verify_link': 'Verifica e-mail',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'Internet come doveva essere.',
+ 'welcome.lead': 'MeshBay è un software open source che le permette di accedere da remoto ai suoi file personali e di usare applicazioni basate sullo spazio di archiviazione del suo computer:',
+ 'welcome.app_chat': 'Chat',
+ 'welcome.app_photos': 'Foto',
+ 'welcome.app_media': 'Media center',
+ 'welcome.app_video': 'Lettore video',
+ 'welcome.app_music': 'Lettore musicale',
+ 'welcome.groups': 'Crei gruppi per dare alle persone a lei care l’accesso ad alcune di queste applicazioni.',
+ 'welcome.e2e': 'I suoi dati sono cifrati end-to-end e viaggiano peer-to-peer, direttamente da ogni dispositivo al computer che li ospita: il suo nodo. Non passano mai dall’hub meshbay.org.',
+ 'welcome.uses_title': 'Cosa può farci',
+ 'welcome.use_chat': 'Messaggistica istantanea all’interno di un gruppo',
+ 'welcome.use_photos': 'Condividere album fotografici con famiglia e amici',
+ 'welcome.use_media': 'Accedere ovunque al media center di casa, per lei e la sua famiglia, con streaming e Chromecast dall’app desktop',
+ 'welcome.use_apps': 'Creare le proprie applicazioni sul nodo che ospita',
+ 'welcome.hub_title': 'Cosa fa meshbay.org',
+ 'welcome.hub_body': 'meshbay.org è solo un servizio di segnalazione: la autentica e mette in contatto le parti, plug-and-play, attraverso le reti domestiche.',
+ 'welcome.hub_never': 'I suoi contenuti non lo raggiungono mai:',
+ 'welcome.never_transit': 'Nulla passa da lì: nessun contenuto, nessuna indicizzazione',
+ 'welcome.never_stored': 'Nessun suo file o messaggio vi è archiviato',
+ 'welcome.never_e2e': 'Crittografia end-to-end, da ogni dispositivo al suo nodo',
+ 'welcome.hub_free': 'Il servizio è gratuito. Nessun modello di business (per scelta), nessun tracciamento, nessuna pubblicità. Buon divertimento!',
+ 'welcome.download': 'Scarica (beta)',
+ 'welcome.legal': 'Note legali',
'register.err_mismatch': 'Le password non coincidono',
'register.err_min_len': {
one: 'Usi almeno {n} carattere',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index f2d94d9..234b238 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -86,6 +86,30 @@ export default {
'register.resend_sent': '新しいコードを送信しました。',
'login.pending_verification': 'メールアドレスがまだ確認されていません。受信トレイで確認コードをご確認ください。',
'login.verify_link': 'メールを確認',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': '本来あるべき姿のインターネット。',
+ 'welcome.lead': 'MeshBay は、個人のファイルにどこからでもアクセスでき、自分のパソコンのストレージ上でアプリを動かせるオープンソースソフトウェアです。',
+ 'welcome.app_chat': 'チャット',
+ 'welcome.app_photos': '写真',
+ 'welcome.app_media': 'メディアセンター',
+ 'welcome.app_video': '動画プレーヤー',
+ 'welcome.app_music': '音楽プレーヤー',
+ 'welcome.groups': 'グループを作成して、家族や友人に一部のアプリへのアクセスを許可できます。',
+ 'welcome.e2e': 'データはエンドツーエンドで暗号化され、各デバイスからデータを保管するパソコン(あなたのノード)へピアツーピアで直接届きます。meshbay.org のハブを経由することは一切ありません。',
+ 'welcome.uses_title': 'できること',
+ 'welcome.use_chat': 'グループ内でのインスタントメッセージ',
+ 'welcome.use_photos': '家族や友人とのフォトアルバム共有',
+ 'welcome.use_media': '自宅のメディアセンターに外出先からアクセス。ご家族みんなで使え、ストリーミングに対応し、デスクトップアプリからは Chromecast も利用できます',
+ 'welcome.use_apps': 'ホストしているノード上で独自のアプリを開発',
+ 'welcome.hub_title': 'meshbay.org の役割',
+ 'welcome.hub_body': 'meshbay.org はシグナリング専用のサービスです。ログインを処理し、家庭のネットワーク越しに参加者同士を設定不要でつなぎます。',
+ 'welcome.hub_never': 'あなたのコンテンツが届くことはありません:',
+ 'welcome.never_transit': '何も経由しません(コンテンツもインデックスもなし)',
+ 'welcome.never_stored': 'ファイルやメッセージは一切保存されません',
+ 'welcome.never_e2e': '各デバイスからノードまでエンドツーエンドで暗号化',
+ 'welcome.hub_free': 'サービスは無料です。ビジネスモデルは意図的に持たず、トラッキングも広告もありません。どうぞお楽しみください!',
+ 'welcome.download': 'ダウンロード(ベータ版)',
+ 'welcome.legal': '法的情報',
'register.err_mismatch': 'パスワードが一致しません',
'register.err_min_len': {
other: '{n} 文字以上でご入力ください',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index f7f0775..72511f5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -86,6 +86,30 @@ export default {
'register.resend_sent': 'Er is een nieuwe code verzonden.',
'login.pending_verification': 'Uw e-mailadres is nog niet geverifieerd. Controleer uw inbox voor de verificatiecode.',
'login.verify_link': 'E-mail verifiëren',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'Het internet zoals het bedoeld was.',
+ 'welcome.lead': 'MeshBay is opensourcesoftware waarmee u op afstand bij uw persoonlijke bestanden kunt, en die toepassingen draait bovenop de opslag van uw eigen computer:',
+ 'welcome.app_chat': 'Chat',
+ 'welcome.app_photos': 'Foto’s',
+ 'welcome.app_media': 'Mediacenter',
+ 'welcome.app_video': 'Videospeler',
+ 'welcome.app_music': 'Muziekspeler',
+ 'welcome.groups': 'Maak groepen aan om uw naasten toegang te geven tot een aantal van deze toepassingen.',
+ 'welcome.e2e': 'Uw gegevens zijn end-to-end versleuteld en gaan peer-to-peer, rechtstreeks van elk apparaat naar de computer waarop ze staan: uw node. Ze gaan nooit via de hub meshbay.org.',
+ 'welcome.uses_title': 'Wat u ermee kunt doen',
+ 'welcome.use_chat': 'Chatten binnen een groep',
+ 'welcome.use_photos': 'Fotoalbums delen met familie en vrienden',
+ 'welcome.use_media': 'Overal bij uw mediacenter thuis, voor u en uw huishouden, met streaming en Chromecast via de desktop-app',
+ 'welcome.use_apps': 'Uw eigen toepassingen bouwen op de node die u host',
+ 'welcome.hub_title': 'Wat meshbay.org doet',
+ 'welcome.hub_body': 'meshbay.org is alleen een signaleringsdienst: het meldt u aan en brengt de partijen met elkaar in contact, plug-and-play, over thuisnetwerken heen.',
+ 'welcome.hub_never': 'Uw inhoud komt er nooit terecht:',
+ 'welcome.never_transit': 'Er gaat niets doorheen: geen inhoud, geen indexering',
+ 'welcome.never_stored': 'Geen van uw bestanden of berichten wordt er opgeslagen',
+ 'welcome.never_e2e': 'End-to-end versleuteling, van elk apparaat tot uw node',
+ 'welcome.hub_free': 'De dienst is gratis. Er is bewust geen verdienmodel, geen tracking en geen advertenties. Veel plezier!',
+ 'welcome.download': 'Downloaden (bèta)',
+ 'welcome.legal': 'Juridische informatie',
'register.err_mismatch': 'De wachtwoorden komen niet overeen',
'register.err_min_len': {
one: 'Gebruik minstens {n} teken',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 8c50790..12a31aa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -89,6 +89,30 @@ export default {
'register.resend_sent': 'Nowy kod został wysłany.',
'login.pending_verification': 'Twój adres e-mail nie został jeszcze zweryfikowany. Sprawdź skrzynkę odbiorczą.',
'login.verify_link': 'Zweryfikuj e-mail',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'Internet taki, jaki miał być.',
+ 'welcome.lead': 'MeshBay to oprogramowanie open source, które daje zdalny dostęp do Twoich plików osobistych i uruchamia aplikacje korzystające z pamięci Twojego własnego komputera:',
+ 'welcome.app_chat': 'Czat',
+ 'welcome.app_photos': 'Zdjęcia',
+ 'welcome.app_media': 'Centrum multimedialne',
+ 'welcome.app_video': 'Odtwarzacz wideo',
+ 'welcome.app_music': 'Odtwarzacz muzyki',
+ 'welcome.groups': 'Twórz grupy, aby dać bliskim dostęp do wybranych aplikacji.',
+ 'welcome.e2e': 'Twoje dane są szyfrowane end-to-end i przesyłane bezpośrednio (peer-to-peer) z każdego urządzenia do komputera, który je przechowuje (Twojego węzła). Nigdy nie przechodzą przez hub meshbay.org.',
+ 'welcome.uses_title': 'Co możesz z nim robić',
+ 'welcome.use_chat': 'Komunikator w obrębie grupy',
+ 'welcome.use_photos': 'Udostępnianie albumów ze zdjęciami rodzinie i znajomym',
+ 'welcome.use_media': 'Zdalny dostęp do domowego centrum multimedialnego dla Ciebie i domowników (ze streamingiem i Chromecastem w aplikacji desktopowej)',
+ 'welcome.use_apps': 'Tworzenie własnych aplikacji na węźle, który hostujesz',
+ 'welcome.hub_title': 'Czym zajmuje się meshbay.org',
+ 'welcome.hub_body': 'meshbay.org to wyłącznie usługa sygnalizacyjna: uwierzytelnia Cię i łączy uczestników ze sobą, bez konfiguracji, także przez sieci domowe.',
+ 'welcome.hub_never': 'Twoje treści nigdy do niego nie trafiają:',
+ 'welcome.never_transit': 'Nic przez niego nie przechodzi: żadnych treści, żadnego indeksowania',
+ 'welcome.never_stored': 'Żadne Twoje pliki ani wiadomości nie są na nim przechowywane',
+ 'welcome.never_e2e': 'Szyfrowanie end-to-end, od każdego urządzenia do Twojego węzła',
+ 'welcome.hub_free': 'Usługa jest bezpłatna. Celowo nie ma modelu biznesowego, śledzenia ani reklam. Miłego korzystania!',
+ 'welcome.download': 'Pobierz (beta)',
+ 'welcome.legal': 'Informacje prawne',
'register.err_mismatch': 'Hasła nie są zgodne',
'register.err_min_len': {
one: 'Proszę użyć co najmniej {n} znaku',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 8f903ba..c5f826b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -87,6 +87,30 @@ export default {
'register.resend_sent': 'Um novo código foi enviado.',
'login.pending_verification': 'Seu e-mail ainda não foi verificado. Verifique sua caixa de entrada.',
'login.verify_link': 'Verificar e-mail',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': 'A internet como deveria ser.',
+ 'welcome.lead': 'O MeshBay é um software de código aberto que dá acesso remoto aos seus arquivos pessoais e executa aplicativos sobre o armazenamento do seu próprio computador:',
+ 'welcome.app_chat': 'Chat',
+ 'welcome.app_photos': 'Fotos',
+ 'welcome.app_media': 'Central de mídia',
+ 'welcome.app_video': 'Player de vídeo',
+ 'welcome.app_music': 'Player de música',
+ 'welcome.groups': 'Crie grupos para dar às pessoas próximas acesso a alguns desses aplicativos.',
+ 'welcome.e2e': 'Seus dados são criptografados de ponta a ponta e trafegam ponto a ponto, direto de cada dispositivo para o computador que os hospeda: o seu nó. Eles nunca passam pelo hub meshbay.org.',
+ 'welcome.uses_title': 'O que você pode fazer com ele',
+ 'welcome.use_chat': 'Mensagens instantâneas dentro de um grupo',
+ 'welcome.use_photos': 'Compartilhar álbuns de fotos com a família e os amigos',
+ 'welcome.use_media': 'Acessar de qualquer lugar a sua central de mídia, para você e sua casa, com streaming e Chromecast pelo app para desktop',
+ 'welcome.use_apps': 'Criar seus próprios aplicativos no nó que você hospeda',
+ 'welcome.hub_title': 'O que o meshbay.org faz',
+ 'welcome.hub_body': 'O meshbay.org é apenas um serviço de sinalização: ele autentica você e conecta as partes entre si, plug-and-play, através de redes domésticas.',
+ 'welcome.hub_never': 'Seu conteúdo nunca chega até ele:',
+ 'welcome.never_transit': 'Nada passa por ele: nenhum conteúdo, nenhuma indexação',
+ 'welcome.never_stored': 'Nenhum dos seus arquivos ou mensagens fica armazenado nele',
+ 'welcome.never_e2e': 'Criptografia de ponta a ponta, de cada dispositivo até o seu nó',
+ 'welcome.hub_free': 'O serviço é gratuito. Não há modelo de negócio (de propósito), rastreamento nem anúncios. Aproveite!',
+ 'welcome.download': 'Baixar (beta)',
+ 'welcome.legal': 'Informações legais',
'register.err_mismatch': 'As senhas não coincidem',
'register.err_min_len': {
one: 'Use pelo menos {n} caractere',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index c836de3..e06c5c6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -86,6 +86,30 @@ export default {
'register.resend_sent': '新验证码已发送。',
'login.pending_verification': '您的邮箱尚未验证。请查看收件箱中的验证码。',
'login.verify_link': '验证邮箱',
+ // Sign-in page: what MeshBay is (browser only)
+ 'welcome.title': '互联网本该有的样子。',
+ 'welcome.lead': 'MeshBay 是一款开源软件,让您远程访问个人文件,并在您自己电脑的存储之上运行应用:',
+ 'welcome.app_chat': '聊天',
+ 'welcome.app_photos': '照片',
+ 'welcome.app_media': '媒体中心',
+ 'welcome.app_video': '视频播放器',
+ 'welcome.app_music': '音乐播放器',
+ 'welcome.groups': '创建群组,让亲友使用其中部分应用。',
+ 'welcome.e2e': '您的数据经过端到端加密,以点对点方式从每台设备直接传输到存放数据的电脑(也就是您的节点)。数据从不经过 meshbay.org 中心服务器。',
+ 'welcome.uses_title': '您可以用它做什么',
+ 'welcome.use_chat': '群组内即时通讯',
+ 'welcome.use_photos': '与家人朋友分享相册',
+ 'welcome.use_media': '随时随地访问家中的媒体中心,供您和家人使用(支持流媒体播放,桌面应用还支持 Chromecast)',
+ 'welcome.use_apps': '在您托管的节点上开发自己的应用',
+ 'welcome.hub_title': 'meshbay.org 的作用',
+ 'welcome.hub_body': 'meshbay.org 仅是一个信令服务:它负责登录验证,并以即插即用的方式跨家庭网络连接各方。',
+ 'welcome.hub_never': '您的内容永远不会到达这里:',
+ 'welcome.never_transit': '没有任何数据经过它:没有内容,也没有索引',
+ 'welcome.never_stored': '不存储您的任何文件或消息',
+ 'welcome.never_e2e': '从每台设备到您的节点全程端到端加密',
+ 'welcome.hub_free': '本服务免费。我们刻意不设商业模式,没有跟踪,也没有广告。尽情享用吧!',
+ 'welcome.download': '下载(测试版)',
+ 'welcome.legal': '法律信息',
'register.err_mismatch': '两次输入的密码不一致',
'register.err_min_len': {
other: '请至少使用 {n} 个字符',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index d51e5b0..ff6fe57 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -478,8 +478,11 @@ a:hover { text-decoration: underline; }
exactly that much — 48px, and a scrollbar on sign-in at every window size.
Measured, not deduced: `.page-center` ended at 795 and `main` at 819.
The page brings its own 24px, so dropping main's here changes nothing
- visible and removes the duplicated arithmetic rather than adding more. */
-.main:has(> .page-center) { padding: 0; }
+ visible and removes the duplicated arithmetic rather than adding more.
+ `max-width` goes too: signed out there is no sidebar, so the 960px column sat
+ at the left of the window and a "centred" form was centred in that — at
+ x=290 in a 1440px window. */
+.main:has(> .page-center) { padding: 0; max-width: none; }
/* ── Sticky chrome ────────────────────────────────────────────────────────── */
@@ -631,6 +634,158 @@ a:hover { text-decoration: underline; }
color: var(--text-dim);
}
+/* ── Welcome (sign-in page, browser only) ─────────────────────────────────── */
+
+/* What MeshBay is on the left, the form on the right and level with the
+ title; stacked, form first, once the text would be narrower than the form.
+ The form comes first in the markup (first on a phone, first to the
+ keyboard) and `row-reverse` is what moves it right. Headings are
+ `.welcome-pitch .welcome-h` rather than a bare class so they outrank
+ `.main h2`. */
+.welcome {
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 96px;
+ width: 100%;
+ max-width: 1180px;
+}
+.welcome > .login-card { flex: 0 0 380px; }
+
+.welcome-pitch {
+ flex: 1 1 0;
+ min-width: 0;
+ max-width: 660px;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.5;
+}
+.welcome-pitch p { margin-bottom: 10px; }
+
+.welcome-apps, .welcome-uses, .welcome-never { list-style: none; }
+
+.welcome-title {
+ margin-bottom: 10px;
+ color: var(--text);
+ font-size: 1.9em;
+ font-weight: 700;
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+ text-wrap: balance;
+}
+.welcome-lead { font-size: 1.05em; }
+
+.welcome-apps { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 14px; }
+.welcome-apps li {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 12px;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ background: var(--bg-surface);
+ color: var(--text);
+ font-size: 0.88em;
+}
+.welcome-apps .icon { color: var(--accent); }
+
+.welcome-e2e {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 8px 14px;
+ border-left: 3px solid var(--accent);
+ border-radius: 0 6px 6px 0;
+ background: var(--accent-bg);
+ color: var(--text);
+}
+.welcome-e2e .icon { margin-top: 0.3em; color: var(--accent); }
+
+.welcome-pitch .welcome-h {
+ margin: 18px 0 8px;
+ color: var(--text-dim);
+ font-size: 0.78em;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.welcome-uses { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 18px; }
+.welcome-uses li {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ color: var(--text);
+ font-size: 0.92em;
+ line-height: 1.45;
+}
+.welcome-use-icon {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ background: var(--accent-bg);
+ color: var(--accent);
+ font-size: 17px;
+}
+
+.welcome-hub {
+ margin-top: 18px;
+ padding: 12px 18px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg-surface);
+ font-size: 0.92em;
+}
+.welcome-pitch .welcome-hub .welcome-h { margin-top: 0; }
+.welcome-pitch .welcome-hub-never { margin-bottom: 6px; color: var(--text); font-weight: 600; }
+.welcome-never { display: flex; flex-direction: column; gap: 4px; color: var(--text); }
+.welcome-never li { display: flex; align-items: flex-start; gap: 8px; }
+.welcome-never .icon { margin-top: 0.3em; color: var(--success); }
+.welcome-pitch .welcome-hub-free {
+ margin: 10px 0 0;
+ padding-top: 8px;
+ border-top: 1px solid var(--border);
+ color: var(--text);
+ font-weight: 500;
+}
+
+.welcome-links {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12px 20px;
+ margin-top: 14px;
+}
+.welcome-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 18px;
+ border-radius: 6px;
+ background: var(--accent);
+ color: var(--accent-text);
+ font-size: 0.95em;
+ font-weight: 600;
+}
+.welcome-cta:hover { background: var(--accent-hover); text-decoration: none; }
+.welcome-legal { color: var(--text-secondary); font-size: 0.9em; }
+.welcome-legal:hover { color: var(--accent); }
+
+@media (max-width: 1000px) {
+ .welcome { flex-direction: column; align-items: center; gap: 36px; }
+ .welcome > .login-card { flex: none; }
+ .welcome-pitch { flex: none; width: 100%; max-width: 520px; }
+}
+@media (max-width: 520px) {
+ .welcome-title { font-size: 1.6em; }
+ .welcome-uses { grid-template-columns: 1fr; }
+}
+
/* ── Form elements ────────────────────────────────────────────────────────── */
input[type="text"],
diff --git a/packages/meshbay-hub/tests/test_welcome_layout_measured.py b/packages/meshbay-hub/tests/test_welcome_layout_measured.py
new file mode 100644
index 0000000..da72f34
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_welcome_layout_measured.py
@@ -0,0 +1,142 @@
+"""
+The sign-in page with the welcome text beside it, measured in a browser.
+
+Two columns on a desktop, one on a phone with the form first, and nothing wider
+than the screen at any width. The text is the English catalogue's, at its real
+length: a fixture shorter than the real copy measures the fixture.
+
+The markup is `LoginPage` and `WelcomePitch` as `auth-page.js` renders them,
+inside the shell `app.js` puts every page in. What is under test is the
+stylesheet, which is served as it ships.
+"""
+
+import json
+import re
+import shutil
+import subprocess
+import textwrap
+from pathlib import Path
+
+import pytest
+
+HARNESS = Path(__file__).parent / "harness" / "layout_probe.py"
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("google-chrome") is None or not (STATIC / "style.css").exists(),
+ reason="Chrome or the SPA stylesheet is not available")
+
+
+def _en(key: str) -> str:
+ source = (STATIC / "locales" / "en.js").read_text(encoding="utf-8")
+ m = re.search(rf"^\s*'{re.escape(key)}': '([^']*)',$", source, re.M)
+ assert m, f"{key} is missing from en.js"
+ return m.group(1)
+
+
+def _items(*keys: str) -> str:
+ return "".join(f"<li><span>{_en(k)}</span></li>" for k in keys)
+
+
+def _page() -> str:
+ li = _items
+ return textwrap.dedent(f"""
+ <nav class="nav"><div class="nav-left"><a class="nav-brand" href="#/">MeshBay</a></div>
+ <div class="nav-right"><button class="nav-btn">Login</button></div></nav>
+ <div class="layout"><main class="main"><div class="page-center"><div class="welcome">
+ <div class="card login-card"><h2>Login</h2>
+ <form><input type="text" placeholder="Username" />
+ <input type="password" placeholder="Password" /><button>Login</button></form>
+ <div class="login-footer">No account? <a href="#/register">Register</a></div>
+ <div class="login-footer"><a href="#/reset">Forgot your passphrase?</a></div>
+ </div>
+ <section class="welcome-pitch">
+ <h1 class="welcome-title">{_en('welcome.title')}</h1>
+ <p class="welcome-lead">{_en('welcome.lead')}</p>
+ <ul class="welcome-apps">{li('welcome.app_chat', 'welcome.app_photos',
+ 'welcome.app_media', 'welcome.app_video', 'welcome.app_music')}</ul>
+ <p>{_en('welcome.groups')}</p>
+ <p class="welcome-e2e"><span>{_en('welcome.e2e')}</span></p>
+ <h2 class="welcome-h">{_en('welcome.uses_title')}</h2>
+ <ul class="welcome-uses">{li('welcome.use_chat', 'welcome.use_photos',
+ 'welcome.use_media', 'welcome.use_apps')}</ul>
+ <div class="welcome-hub"><h2 class="welcome-h">{_en('welcome.hub_title')}</h2>
+ <p>{_en('welcome.hub_body')}</p>
+ <p class="welcome-hub-never">{_en('welcome.hub_never')}</p>
+ <ul class="welcome-never">{li('welcome.never_transit', 'welcome.never_stored',
+ 'welcome.never_e2e')}</ul>
+ <p class="welcome-hub-free">{_en('welcome.hub_free')}</p></div>
+ <div class="welcome-links"><a class="welcome-cta" href="#">{_en('welcome.download')}</a>
+ <a class="welcome-legal" href="#">{_en('welcome.legal')}</a></div>
+ </section>
+ </div></div></main></div>
+ """)
+
+
+PHONES = [320, 360, 412]
+DESKTOPS = [1100, 1440]
+WIDTHS = PHONES + [768] + DESKTOPS
+SELECTORS = [".welcome", ".login-card", ".welcome-pitch"]
+
+
+@pytest.fixture(scope="module")
+def measured(tmp_path_factory):
+ fragment = tmp_path_factory.mktemp("welcome") / "fragment.html"
+ fragment.write_text(_page(), encoding="utf-8")
+ proc = subprocess.run(
+ ["python3", str(HARNESS), ",".join(str(w) for w in WIDTHS),
+ str(fragment), *SELECTORS],
+ capture_output=True, text=True, timeout=180)
+ assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}"
+ out = json.loads(proc.stdout)
+ assert "error" not in out, f"no measurement: {out}"
+ for w in WIDTHS:
+ assert out[str(w)]["viewport"]["w"] == w
+ return out
+
+
+def _box(measured, width, selector):
+ box = measured[str(width)]["boxes"][selector]
+ assert box is not None, f"{selector} did not render at {width} px"
+ return box
+
+
+@pytest.mark.parametrize("width", WIDTHS)
+def test_nothing_is_wider_than_the_screen(measured, width):
+ """A document wider than the screen unpins every sticky header on Android."""
+ assert measured[str(width)]["docScrollW"] <= width, (
+ f"the page is {measured[str(width)]['docScrollW']} px wide on a {width} px screen")
+ for sel in SELECTORS:
+ box = _box(measured, width, sel)
+ assert box["offLeft"] == 0 and box["offRight"] == 0, f"{sel} hangs off at {width} px"
+
+
+@pytest.mark.parametrize("width", PHONES + [768])
+def test_a_narrow_screen_shows_the_form_first(measured, width):
+ card, pitch = _box(measured, width, ".login-card"), _box(measured, width, ".welcome-pitch")
+ assert pitch["top"] >= card["top"] + card["height"], (
+ f"at {width} px the welcome text is not below the sign-in form")
+
+
+@pytest.mark.parametrize("width", DESKTOPS)
+def test_a_desktop_puts_the_form_right_of_the_text(measured, width):
+ """Text on the left, the form on the right with room between them, and
+ the form up by the title rather than centred on a column far taller."""
+ card, pitch = _box(measured, width, ".login-card"), _box(measured, width, ".welcome-pitch")
+ gap = card["left"] - (pitch["left"] + pitch["width"])
+ assert gap >= 80, f"at {width} px the form is {gap} px right of the text"
+ assert pitch["width"] >= 480, f"the text column is {pitch['width']} px at {width} px"
+ assert 0 <= card["top"] - pitch["top"] <= 80, (
+ f"at {width} px the form starts {card['top'] - pitch['top']} px below the text")
+
+
+def test_the_pair_is_centred_in_the_window(measured):
+ """Signed out there is no sidebar, and `.main` is capped at 960 px — so
+ before `.page-center` lifted the cap, a centred form sat centred in the
+ left 960 px of the window: x=290 in 1440."""
+ # Against the document's width, not the window's: the probe's frame is
+ # shorter than the page, so a vertical scrollbar takes its share.
+ box = _box(measured, 1440, ".welcome")
+ middle = measured["1440"]["docScrollW"] / 2
+ centre = box["left"] + box["width"] / 2
+ assert abs(centre - middle) <= 2, f"the page is centred on x={centre}, not {middle}"