diff --git a/pygitweb/src/pygitweb/auth_test.py b/pygitweb/src/pygitweb/auth_test.py
index 7fc0e76..f0c0820 100644
--- a/pygitweb/src/pygitweb/auth_test.py
+++ b/pygitweb/src/pygitweb/auth_test.py
@@ -144,6 +144,27 @@ def test_users_me_with_bearer(client: TestClient, local_auth_config: AuthConfig)
 		assert r.json()["username"] == "admin"
 
 
+def test_login_page_is_a_centered_card(client: TestClient, local_auth_config: AuthConfig) -> None:
+	with patch.object(settings, "AUTH", True):
+		page = client.get("/login")
+		failed = client.post(
+			"/login",
+			data={"username": "admin", "password": "wrong", "next": "/"},
+		)
+	assert page.status_code == 200
+	assert 'class="page page-center"' in page.text
+	assert 'class="theme-dark-auto"' in page.text
+	assert 'height="120"' in page.text
+	assert "Login to your account" in page.text
+	assert 'class="btn btn-primary w-100"' in page.text
+	assert "navbar-vertical" not in page.text
+	assert "I forgot password" not in page.text
+	assert "Remember me" not in page.text
+	assert failed.status_code == 200
+	assert "Incorrect username or password." in failed.text
+	assert 'class="page page-center"' in failed.text
+
+
 def test_login_form_sets_cookie_and_users_me(client: TestClient, local_auth_config: AuthConfig) -> None:
 	with patch.object(settings, "AUTH", True):
 		r = client.post(
diff --git a/pygitweb/src/pygitweb/static/login.js b/pygitweb/src/pygitweb/static/login.js
new file mode 100644
index 0000000..deae98e
--- /dev/null
+++ b/pygitweb/src/pygitweb/static/login.js
@@ -0,0 +1,19 @@
+(function () {
+	document.querySelectorAll("[data-password-toggle]").forEach(function (button) {
+		var id = button.getAttribute("data-password-toggle");
+		var input = id ? document.getElementById(id) : null;
+		if (!input) return;
+		var showIcon = button.querySelector(".icon-toggle-show");
+		var hideIcon = button.querySelector(".icon-toggle-hide");
+		button.addEventListener("click", function () {
+			var visible = input.type === "text";
+			input.type = visible ? "password" : "text";
+			var label = visible ? "Show password" : "Hide password";
+			button.setAttribute("aria-pressed", visible ? "false" : "true");
+			button.setAttribute("aria-label", label);
+			button.setAttribute("title", label);
+			if (showIcon) showIcon.classList.toggle("d-none", !visible);
+			if (hideIcon) hideIcon.classList.toggle("d-none", visible);
+		});
+	});
+})();
diff --git a/pygitweb/src/pygitweb/static/main.css b/pygitweb/src/pygitweb/static/main.css
index e329a74..c43cfb6 100644
--- a/pygitweb/src/pygitweb/static/main.css
+++ b/pygitweb/src/pygitweb/static/main.css
@@ -416,6 +416,116 @@ radio-picker {
 	}
 }
 
+body:has(.page.page-center) {
+	border: 0;
+	background-color: var(--tblr-body-bg);
+	color: var(--tblr-body-color);
+	font-family: var(--tblr-font-sans-serif);
+	font-size: var(--tblr-body-font-size);
+	line-height: var(--tblr-body-line-height);
+}
+
+.page.page-center {
+	min-height: 100vh;
+
+	.navbar-brand-image {
+		height: 7.5rem;
+		width: auto;
+	}
+
+	h1,
+	h2,
+	h3 {
+		margin: 0;
+		color: inherit;
+		font-weight: var(--tblr-font-weight-bold);
+	}
+
+	p {
+		color: inherit;
+	}
+
+	a:not(.btn) {
+		color: var(--tblr-link-color, var(--tblr-primary));
+
+		&:hover,
+		&:visited {
+			color: var(--tblr-link-hover-color, var(--tblr-primary));
+		}
+	}
+
+	.card,
+	.card .card-body {
+		background-color: var(--tblr-bg-surface, var(--tblr-card-bg));
+		border-color: var(--tblr-border-color);
+		color: inherit;
+	}
+
+	.form-label {
+		color: var(--tblr-body-color);
+	}
+
+	.form-control {
+		background-color: var(--tblr-bg-forms);
+		border-color: var(--tblr-border-color);
+		color: var(--tblr-body-color);
+
+		&::placeholder {
+			color: var(--tblr-secondary);
+		}
+
+		&:focus {
+			background-color: var(--tblr-bg-forms);
+			border-color: var(--tblr-primary);
+			color: var(--tblr-body-color);
+		}
+	}
+
+	.form-footer {
+		border-top-color: transparent;
+	}
+
+	.btn-primary {
+		background-color: var(--tblr-primary);
+		border-color: var(--tblr-primary);
+		color: var(--tblr-light);
+
+		&:hover {
+			background-color: color-mix(in srgb, var(--tblr-primary) 88%, black);
+			border-color: color-mix(in srgb, var(--tblr-primary) 88%, black);
+			color: var(--tblr-light);
+		}
+	}
+
+	code {
+		background-color: var(--tblr-bg-surface-secondary, var(--tblr-gray-100));
+		border-color: var(--tblr-border-color);
+		color: inherit;
+	}
+
+	.link-secondary {
+		color: var(--tblr-secondary-text) !important;
+
+		&:hover,
+		&:focus {
+			color: var(--tblr-body-color) !important;
+		}
+	}
+
+	.hr-text {
+		color: var(--tblr-muted);
+
+		&::before,
+		&::after {
+			background-color: var(--tblr-border-color);
+		}
+	}
+
+	.text-secondary {
+		color: var(--tblr-secondary-text) !important;
+	}
+}
+
 /* Fluid vertical layout: fixed sidebar + main content offset */
 .page {
 	.navbar-vertical.position-fixed {
diff --git a/pygitweb/src/pygitweb/static_tests/test_login.py b/pygitweb/src/pygitweb/static_tests/test_login.py
new file mode 100644
index 0000000..0c4c412
--- /dev/null
+++ b/pygitweb/src/pygitweb/static_tests/test_login.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+_FORM = (
+	'<input type="password" id="login_password">'
+	'<button type="button" data-password-toggle="login_password" aria-pressed="false"'
+	' aria-label="Show password" title="Show password">'
+	'<svg class="icon icon-toggle-show"></svg>'
+	'<svg class="icon icon-toggle-hide d-none"></svg>'
+	"</button>"
+)
+
+
+def test_password_toggle_reveals_and_hides(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("login.js")
+	js.click("[data-password-toggle]")
+	assert js.prop("#login_password", "type") == "text"
+	assert js.attr("[data-password-toggle]", "aria-pressed") == "true"
+	assert js.attr("[data-password-toggle]", "aria-label") == "Hide password"
+	assert "d-none" in js.classes(".icon-toggle-show")
+	assert "d-none" not in js.classes(".icon-toggle-hide")
+	js.click("[data-password-toggle]")
+	assert js.prop("#login_password", "type") == "password"
+	assert js.attr("[data-password-toggle]", "aria-pressed") == "false"
+	assert js.attr("[data-password-toggle]", "title") == "Show password"
+	assert "d-none" not in js.classes(".icon-toggle-show")
+	assert "d-none" in js.classes(".icon-toggle-hide")
+
+
+def test_password_toggle_ignores_a_missing_field(js: JsPage) -> None:
+	js.set_html('<button type="button" data-password-toggle="missing">Show</button>')
+	js.load("login.js")
+	js.click("[data-password-toggle]")
+	assert js.attr("[data-password-toggle]", "aria-pressed") is None
diff --git a/pygitweb/src/pygitweb/templates/base.html b/pygitweb/src/pygitweb/templates/base.html
index bf720fd..f902054 100644
--- a/pygitweb/src/pygitweb/templates/base.html
+++ b/pygitweb/src/pygitweb/templates/base.html
@@ -9,7 +9,8 @@
 {% block head %}{% endblock %}
 <script defer src="/static/vendor/tabler-1.0.0-beta17.min.js"></script>
 <script defer src="/static/navbar-auth.js"></script>
-</head><body>
+</head><body{% block body_attrs %}{% endblock %}>
+{% block body %}
 <div class="page">
   <aside class="navbar navbar-vertical navbar-expand-sm position-fixed">
     <div class="container-fluid d-flex flex-column vh-100">
@@ -103,5 +104,6 @@
     </div>
   </div>
 </div>
+{% endblock %}
 {% block scripts %}{% endblock %}
 </body></html>
diff --git a/pygitweb/src/pygitweb/templates/login.html b/pygitweb/src/pygitweb/templates/login.html
index 23b8759..f884cdf 100644
--- a/pygitweb/src/pygitweb/templates/login.html
+++ b/pygitweb/src/pygitweb/templates/login.html
@@ -1,45 +1,88 @@
 {% extends "base.html" %}
-{% from "macros.html" import page_header %}
-{% block content %}
-{{ page_header("Sign in") }}
-<div class="row justify-content-center">
-<div class="col-md-6 col-lg-5">
-<div class="card card-md">
-  <div class="card-body">
-    {% if error %}
-    <div class="alert alert-danger mb-3" role="alert">{{ error }}</div>
-    {% endif %}
-    {% if oauth_available %}
-    <p class="mb-3">
-      <a class="btn btn-outline-primary" href="/auth/oauth/start?next={{ next | urlencode }}">
-        Sign in with {{ oauth_provider | e }}
+{% macro oauth_button() %}
+<a class="btn w-100" href="/auth/oauth/start?next={{ next | urlencode }}">
+  {% if oauth_provider | lower == "github" %}
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon text-github">
+    <path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
+  </svg>
+  {% elif oauth_provider | lower == "google" %}
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon">
+    <path d="M20.945 11a9 9 0 1 1 -3.284 -5.997l-2.655 2.392a5.5 5.5 0 1 0 2.119 6.605h-4.125v-3h7.945z" />
+  </svg>
+  {% endif %}
+  Login with {{ oauth_provider | e }}
+</a>
+{% endmacro %}
+{% block body_attrs %} class="theme-dark-auto"{% endblock %}
+{% block head %}
+<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+{% endblock %}
+{% block body %}
+<main class="page page-center" id="content">
+  <div class="container container-tight py-4">
+    <div class="text-center mb-4">
+      <a href="/" aria-label="{{ site_name | default('PyGitWeb') }}" class="navbar-brand">
+        <img src="/static/logo.svg" height="120" alt="" class="navbar-brand-image">
       </a>
-    </p>
-    {% if local_available %}
-    <p class="text-muted small mb-3">or use a local account</p>
-    {% endif %}
-    {% endif %}
-    {% if local_available %}
-    <form method="post" action="/login" class="needs-validation" novalidate>
-      <input type="hidden" name="next" value="{{ next }}">
-      <div class="mb-3">
-        <label class="form-label required" for="login_username">Username</label>
-        <input type="text" class="form-control" id="login_username" name="username" required
-               autocomplete="username" autofocus>
+    </div>
+    <div class="card card-md">
+      <div class="card-body">
+        <h1 class="h2 text-center mb-4">Login to your account</h1>
+        {% if error %}
+        <div class="alert alert-danger" role="alert">{{ error }}</div>
+        {% endif %}
+        {% if local_available %}
+        <form method="post" action="/login" autocomplete="off" novalidate>
+          <input type="hidden" name="next" value="{{ next }}">
+          <div class="mb-3">
+            <label class="form-label" for="login_username">Username</label>
+            <input type="text" class="form-control" id="login_username" name="username" placeholder="Your username" required autocomplete="username" autofocus>
+          </div>
+          <div class="mb-2">
+            <label class="form-label" for="login_password">Password</label>
+            <div class="input-group input-group-flat">
+              <input type="password" class="form-control" id="login_password" name="password" placeholder="Your password" required autocomplete="current-password">
+              <span class="input-group-text">
+                <button type="button" class="link-secondary input-group-link border-0 bg-transparent p-0" title="Show password" aria-label="Show password" aria-pressed="false" data-password-toggle="login_password">
+                  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon icon-toggle-show">
+                    <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
+                    <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
+                  </svg>
+                  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon icon-toggle-hide d-none">
+                    <path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
+                    <path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
+                    <path d="M3 3l18 18" />
+                  </svg>
+                </button>
+              </span>
+            </div>
+          </div>
+          <div class="form-footer">
+            <button type="submit" class="btn btn-primary w-100">Sign in</button>
+          </div>
+        </form>
+        {% elif oauth_available %}
+        <div class="row">
+          <div class="col">{{ oauth_button() }}</div>
+        </div>
+        {% else %}
+        <p class="text-secondary text-center mb-0">No sign-in methods are configured.</p>
+        {% endif %}
       </div>
-      <div class="mb-3">
-        <label class="form-label required" for="login_password">Password</label>
-        <input type="password" class="form-control" id="login_password" name="password" required
-               autocomplete="current-password">
+      {% if oauth_available and local_available %}
+      <div class="hr-text">or</div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col">{{ oauth_button() }}</div>
+        </div>
       </div>
-      <button type="submit" class="btn btn-primary">Sign in</button>
-    </form>
-    <p class="mt-3 mb-0 text-muted small">API clients can use <code>POST /token</code> (OAuth2 password) instead.</p>
-    {% elif not oauth_available %}
-    <p class="text-muted mb-0">No sign-in methods are configured.</p>
+      {% endif %}
+    </div>
+    {% if local_available %}
     {% endif %}
   </div>
-</div>
-</div>
-</div>
+</main>
+{% endblock %}
+{% block scripts %}
+<script defer src="/static/login.js"></script>
 {% endblock %}
