New Sign in

fierj

Public

Tiny personal git forge

40e5273a9a6361cb659398d8d690d5b0a9b03de1
diff --git a/handlers.go b/handlers.go
index 917915b..e3e5f51 100644
--- a/handlers.go
+++ b/handlers.go
@@ -164,8 +164,9 @@ func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			}
 		}
 		if git.IsEmpty() {
-			tmpl.ExecuteTemplate(w, "tree.html", map[string]any{
+			tmpl.ExecuteTemplate(w, "tree.html", withUser(r, map[string]any{
 				"Repo":        git.Repo(),
+				"IsPrivate":   git.IsPrivate(),
 				"Ref":         ref,
 				"Path":        "",
 				"Entries":     []FileEntry{},
@@ -176,7 +177,7 @@ func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
 				"Tags":        []string{},
 				"Commits":     0,
 				"Empty":       true,
-			})
+			}))
 			return
 		}
 		entries, err := git.List(ref, treePath)
@@ -202,8 +203,9 @@ func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			fileEntries = append(fileEntries, fe)
 		}
 		readme := git.Readme(ref, treePath)
-		tmpl.ExecuteTemplate(w, "tree.html", map[string]any{
+		tmpl.ExecuteTemplate(w, "tree.html", withUser(r, map[string]any{
 			"Repo":        git.Repo(),
+			"IsPrivate":   git.IsPrivate(),
 			"Ref":         ref,
 			"Path":        treePath,
 			"Breadcrumb":  Breadcrumb(treePath),
@@ -216,7 +218,7 @@ func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			"Commits":     git.CommitCount(ref),
 			"Readme":      readme,
 			"ActiveTab":   "code",
-		})
+		}))
 	}
 }
 
@@ -237,8 +239,9 @@ func Blob(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			renderError(w, tmpl, http.StatusNotFound, err.Error())
 			return
 		}
-		tmpl.ExecuteTemplate(w, "blob.html", map[string]any{
+		tmpl.ExecuteTemplate(w, "blob.html", withUser(r, map[string]any{
 			"Repo":        git.Repo(),
+			"IsPrivate":   git.IsPrivate(),
 			"Ref":         ref,
 			"Path":        filePath,
 			"Breadcrumb":  Breadcrumb(filePath),
@@ -248,7 +251,7 @@ func Blob(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			"Tags":        git.Tags(),
 			"Commits":     git.CommitCount(ref),
 			"ActiveTab":   "code",
-		})
+		}))
 	}
 }
 
@@ -267,6 +270,7 @@ func Log(cfg Config, tmpl *template.Template) http.HandlerFunc {
 		data := map[string]any{
 			"User":        User(r),
 			"Repo":        git.Repo(),
+			"IsPrivate":   git.IsPrivate(),
 			"Ref":         ref,
 			"CommitList":  commits,
 			"Description": git.Description(),
@@ -291,8 +295,9 @@ func Diff(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			renderError(w, tmpl, http.StatusNotFound, "commit not found")
 			return
 		}
-		if err := tmpl.ExecuteTemplate(w, "commit.html", map[string]any{
+		if err := tmpl.ExecuteTemplate(w, "commit.html", withUser(r, map[string]any{
 			"Repo":        git.Repo(),
+			"IsPrivate":   git.IsPrivate(),
 			"Ref":         ref,
 			"Diff":        commit,
 			"Hash":        ref,
@@ -301,7 +306,7 @@ func Diff(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			"Tags":        git.Tags(),
 			"Commits":     git.CommitCount(ref),
 			"ActiveTab":   "commits",
-		}); err != nil {
+		})); err != nil {
 			slog.Error("failed to execute template", "error", err)
 		}
 	}
@@ -356,15 +361,16 @@ func Refs(cfg Config, tmpl *template.Template) http.HandlerFunc {
 			return
 		}
 		ref := git.DefaultBranch()
-		tmpl.ExecuteTemplate(w, "refs.html", map[string]any{
+		tmpl.ExecuteTemplate(w, "refs.html", withUser(r, map[string]any{
 			"Repo":        git.Repo(),
+			"IsPrivate":   git.IsPrivate(),
 			"Ref":         ref,
 			"Branches":    git.Branches(),
 			"Tags":        git.Tags(),
 			"Description": git.Description(),
 			"Commits":     git.CommitCount(ref),
 			"ActiveTab":   "refs",
-		})
+		}))
 	}
 }
 
@@ -376,15 +382,19 @@ func SettingsGet(cfg Config, tmpl *template.Template) http.HandlerFunc {
 		}
 		git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
 		meta := git.LoadMeta()
+		ref := git.DefaultBranch()
 		tmpl.ExecuteTemplate(w, "settings.html", map[string]any{
-			"Repo":             git.Repo(),
-			"Ref":              git.DefaultBranch(),
-			"Description":      meta.Description,
-			"IsPrivate":        meta.IsPrivate,
-			"AuthorizedKeys":   strings.Join(meta.AuthorizedKeys, "\n"),
+			"Repo":              git.Repo(),
+			"Ref":               ref,
+			"IsPrivate":         meta.IsPrivate,
+			"Description":       meta.Description,
+			"Branches":          git.Branches(),
+			"Tags":              git.Tags(),
+			"Commits":           git.CommitCount(ref),
+			"AuthorizedKeys":    strings.Join(meta.AuthorizedKeys, "\n"),
 			"ProtectedBranches": strings.Join(meta.ProtectedBranches, "\n"),
-			"ActiveTab":        "settings",
-			"User":             User(r),
+			"ActiveTab":         "settings",
+			"User":              User(r),
 		})
 	}
 }
diff --git a/templates/error.html b/templates/error.html
index b09548e..3272f20 100644
--- a/templates/error.html
+++ b/templates/error.html
@@ -1,9 +1,9 @@
 {{template "head" .}}
 {{define "title"}}Error {{.Code}} - fierj{{end}}
-<div style="text-align:center;padding:3rem 1rem;">
-    <h1 style="font-size:3rem;font-weight:300;color:var(--text-secondary);margin-bottom:0.5rem;">{{.Code}}</h1>
-    <p style="font-size:1.1rem;color:var(--text-secondary);margin-bottom:1.5rem;">{{.Message}}</p>
-    <a href="/" class="btn" style="display:inline-flex;">← Back to repositories</a>
+<div class="error-page">
+    <h1>{{.Code}}</h1>
+    <p>{{.Message}}</p>
+    <a href="/" class="btn">← Back to repositories</a>
 </div>
 {{template "foot" .}}
 
diff --git a/templates/layout.html b/templates/layout.html
index 9a123ca..20c1d72 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -146,6 +146,15 @@
             text-decoration: underline;
         }
 
+        /* Default icon size; contexts below override where needed
+           (branch-picker/clone-btn use 14px, visibility badges 12px). */
+        .icon {
+            width: 16px;
+            height: 16px;
+            flex-shrink: 0;
+            vertical-align: text-bottom;
+        }
+
         /* Top bar */
         .topbar {
             background: var(--bg-topbar);
@@ -159,6 +168,7 @@
         }
 
         .topbar a,
+        .topbar .nav-link,
         .topbar .logo {
             color: var(--text-topbar);
             text-decoration: none;
@@ -177,6 +187,9 @@
         }
 
         .topbar .nav-link {
+            display: inline-flex;
+            align-items: center;
+            gap: 4px;
             font-size: 0.85rem;
             padding: var(--space-xs) var(--space-sm);
             border-radius: var(--radius-sm);
@@ -202,6 +215,22 @@
             background: rgba(255, 255, 255, 0.1);
         }
 
+        .topbar .nav-link-muted {
+            opacity: 0.7;
+        }
+
+        .topbar .nav-form {
+            display: inline;
+        }
+
+        .topbar .nav-form .nav-link {
+            background: none;
+            border: none;
+            cursor: pointer;
+            font-size: 0.85rem;
+            font-family: inherit;
+        }
+
         /* Main container */
         .container {
             max-width: var(--max-width);
@@ -231,7 +260,10 @@
             font-weight: 600;
         }
 
-        .repo-header .visibility {
+        .visibility {
+            display: inline-flex;
+            align-items: center;
+            gap: 0.3em;
             font-size: 0.7rem;
             border: 1px solid var(--border);
             border-radius: 2em;
@@ -240,6 +272,17 @@
             font-weight: 500;
         }
 
+        .visibility .icon {
+            width: 12px;
+            height: 12px;
+        }
+
+        .visibility-private {
+            color: var(--text);
+            border-color: var(--border);
+            background: var(--bg-inset);
+        }
+
         .repo-desc {
             color: var(--text-secondary);
             margin-bottom: var(--space-md);
@@ -322,6 +365,14 @@
             flex: 1;
         }
 
+        .code-bar .breadcrumb {
+            margin-bottom: 0;
+        }
+
+        .clone-btn.icon-btn {
+            text-decoration: none;
+        }
+
         .clone-btn {
             display: inline-flex;
             align-items: center;
@@ -335,6 +386,11 @@
             color: var(--text-secondary);
         }
 
+        .clone-btn .icon {
+            width: 14px;
+            height: 14px;
+        }
+
         /* File table */
         .file-table {
             border: 1px solid var(--border);
@@ -506,7 +562,7 @@
         }
 
         /* Forms */
-        input,
+        input:not([type="checkbox"]):not([type="radio"]),
         textarea {
             font-family: var(--font-sans);
             font-size: 0.9rem;
@@ -518,13 +574,21 @@
             width: 100%;
         }
 
-        input:focus,
+        input:not([type="checkbox"]):not([type="radio"]):focus,
         textarea:focus {
             outline: none;
             border-color: var(--accent);
             box-shadow: 0 0 0 3px var(--accent-subtle);
         }
 
+        input[type="checkbox"],
+        input[type="radio"] {
+            width: 1rem;
+            height: 1rem;
+            flex-shrink: 0;
+            accent-color: var(--accent);
+        }
+
         label {
             font-size: 0.85rem;
             font-weight: 600;
@@ -536,6 +600,87 @@
             margin-bottom: var(--space-md);
         }
 
+        .form-narrow {
+            max-width: 600px;
+        }
+
+        /* Auth pages (login/setup) */
+        .auth-card {
+            max-width: 360px;
+            margin: var(--space-xl) auto;
+        }
+
+        .auth-card h1 {
+            margin-bottom: var(--space-lg);
+            text-align: center;
+        }
+
+        .auth-card .subtitle {
+            text-align: center;
+            color: var(--text-secondary);
+            margin-bottom: var(--space-lg);
+        }
+
+        .auth-card .btn {
+            width: 100%;
+            justify-content: center;
+        }
+
+        .alert-danger {
+            background: var(--danger);
+            color: #fff;
+            padding: var(--space-sm) var(--space-md);
+            border-radius: var(--radius);
+            margin-bottom: var(--space-md);
+            font-size: 0.85rem;
+        }
+
+        /* Error page */
+        .error-page {
+            text-align: center;
+            padding: 3rem 1rem;
+        }
+
+        .error-page h1 {
+            font-size: 3rem;
+            font-weight: 300;
+            color: var(--text-secondary);
+            margin-bottom: 0.5rem;
+        }
+
+        .error-page p {
+            font-size: 1.1rem;
+            color: var(--text-secondary);
+            margin-bottom: 1.5rem;
+        }
+
+        .error-page .btn {
+            display: inline-flex;
+        }
+
+        /* Checkbox / radio row: input and its text sit side by side,
+           unlike a regular label which stacks above its field */
+        .form-check {
+            display: flex;
+            align-items: center;
+            gap: var(--space-sm);
+            font-size: 0.9rem;
+            font-weight: 400;
+            margin: 0;
+            cursor: pointer;
+        }
+
+        .hint {
+            font-size: 0.8rem;
+            color: var(--text-secondary);
+            margin-top: var(--space-xs);
+        }
+
+        .mono-input {
+            font-family: var(--font-mono);
+            font-size: 0.8rem;
+        }
+
         /* Commit log */
         .commit-list {
             border: 1px solid var(--border);
@@ -573,6 +718,106 @@
             font-size: 0.8rem;
         }
 
+        /* Cards & simple layout grids (users.html, etc.) */
+        .card {
+            border: 1px solid var(--border);
+            border-radius: var(--radius);
+            padding: var(--space-lg);
+            background: var(--bg);
+        }
+
+        .card h2,
+        .section-title {
+            font-size: 1rem;
+            margin-bottom: var(--space-md);
+        }
+
+        .grid-2 {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: var(--space-xl);
+        }
+
+        .list-box {
+            border: 1px solid var(--border);
+            border-radius: var(--radius);
+            background: var(--bg);
+        }
+
+        .list-row {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: var(--space-sm) var(--space-md);
+            border-bottom: 1px solid var(--border-subtle);
+        }
+
+        .list-row:last-child {
+            border-bottom: none;
+        }
+
+        .list-row .tag-self {
+            font-size: 0.8rem;
+            color: var(--text-tertiary);
+        }
+
+        .btn-sm {
+            padding: 2px 10px;
+            font-size: 0.8rem;
+        }
+
+        .mt-xl {
+            margin-top: var(--space-xl);
+        }
+
+        @media (max-width: 600px) {
+            .grid-2 {
+                grid-template-columns: 1fr;
+            }
+        }
+
+        /* Repo list (repos.html) */
+        .page-header {
+            margin-bottom: var(--space-lg);
+            font-size: 1.5rem;
+        }
+
+        .repo-list {
+            display: grid;
+            gap: var(--space-md);
+        }
+
+        .repo-card {
+            border: 1px solid var(--border);
+            border-radius: var(--radius);
+            padding: var(--space-md) var(--space-lg);
+            background: var(--bg);
+        }
+
+        .repo-card-header {
+            display: flex;
+            align-items: center;
+            gap: var(--space-sm);
+        }
+
+        .repo-card-header > .icon {
+            width: 16px;
+            height: 16px;
+            color: var(--text-secondary);
+            flex-shrink: 0;
+        }
+
+        .repo-card-header a {
+            font-weight: 600;
+            font-size: 1rem;
+        }
+
+        .repo-card-desc {
+            color: var(--text-secondary);
+            margin-top: var(--space-xs);
+            font-size: 0.85rem;
+        }
+
         /* Empty state */
         .empty-state {
             border: 1px dashed var(--border);
@@ -662,7 +907,7 @@
         }
 
         .threads-state-toggle a.active {
-            color: var(--text-primary);
+            color: var(--text);
             font-weight: 600;
         }
 
@@ -700,6 +945,11 @@
             margin-top: 2px;
         }
 
+        .icon-ref {
+            color: var(--text-secondary);
+            margin-top: 2px;
+        }
+
         .thread-list-content {
             flex: 1;
             min-width: 0;
@@ -708,7 +958,7 @@
 
         .thread-title {
             font-weight: 600;
-            color: var(--text-primary);
+            color: var(--text);
             text-decoration: none;
             display: block;
         }
@@ -798,7 +1048,7 @@
             display: flex;
             justify-content: space-between;
             padding: var(--space-sm) var(--space-md);
-            background: var(--bg-secondary);
+            background: var(--bg-subtle);
             border-bottom: 1px solid var(--border);
             font-size: 0.85rem;
             border-radius: var(--radius) var(--radius) 0 0;
@@ -819,8 +1069,8 @@
             padding: var(--space-sm);
             border: 1px solid var(--border);
             border-radius: var(--radius);
-            background: var(--bg-primary);
-            color: var(--text-primary);
+            background: var(--bg);
+            color: var(--text);
             resize: vertical;
         }
 
@@ -831,8 +1081,8 @@
         }
 
         .btn-secondary {
-            background: var(--bg-secondary);
-            color: var(--text-primary);
+            background: var(--bg-subtle);
+            color: var(--text);
             border: 1px solid var(--border);
             padding: 6px 14px;
             border-radius: var(--radius);
@@ -853,12 +1103,12 @@
             color: var(--text-secondary);
             margin-bottom: var(--space-md);
             padding: var(--space-sm) var(--space-md);
-            background: var(--bg-secondary);
+            background: var(--bg-subtle);
             border-radius: var(--radius);
         }
 
         .patch-branches-hint code {
-            background: var(--bg-primary);
+            background: var(--bg);
             padding: 2px 6px;
             border-radius: 3px;
             font-size: 0.8rem;
@@ -897,12 +1147,15 @@
             fierj
         </a>
         <div class="spacer"></div>
-        <a class="nav-link" href="/new">+ New</a>
+        <a class="nav-link" href="/new">
+            <svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>
+            New
+        </a>
         {{if .User}}
         <a class="nav-link" href="/users">Users</a>
-        <span class="nav-link" style="opacity:0.7;">{{.User}}</span>
-        <form method="post" action="/logout" style="display:inline;">
-            <button type="submit" class="nav-link" style="background:none;border:none;cursor:pointer;font-size:0.85rem;">Sign out</button>
+        <span class="nav-link nav-link-muted">{{.User}}</span>
+        <form method="post" action="/logout" class="nav-form">
+            <button type="submit" class="nav-link">Sign out</button>
         </form>
         {{else}}
         <a class="nav-link" href="/login">Sign in</a>
@@ -920,9 +1173,19 @@
     </main>
     <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
     <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
-    <link rel="stylesheet"
+    <link id="hljs-theme" rel="stylesheet"
         href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css">
     <script>
+        function hljsHref(theme) {
+            return theme === 'light'
+                ? 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css'
+                : 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css';
+        }
+        function resolvedTheme() {
+            const attr = document.documentElement.getAttribute('data-theme');
+            if (attr) return attr;
+            return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+        }
         function toggleTheme() {
             const html = document.documentElement;
             const current = html.getAttribute('data-theme');
@@ -930,10 +1193,12 @@
                 (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark'));
             html.setAttribute('data-theme', next);
             localStorage.setItem('theme', next);
+            document.getElementById('hljs-theme').href = hljsHref(next);
         }
         (function () {
             const saved = localStorage.getItem('theme');
             if (saved) document.documentElement.setAttribute('data-theme', saved);
+            document.getElementById('hljs-theme').href = hljsHref(resolvedTheme());
         })();
         document.querySelectorAll('pre code').forEach(el => hljs.highlightElement(el));
         document.querySelectorAll('.markdown-body').forEach(el => {
diff --git a/templates/login.html b/templates/login.html
index dc515d3..c496a59 100644
--- a/templates/login.html
+++ b/templates/login.html
@@ -1,11 +1,11 @@
 {{template "head" .}}
 {{define "title"}}sign in - fierj{{end}}
 
-<div style="max-width:360px;margin:var(--space-xl) auto;">
-  <h1 style="margin-bottom:var(--space-lg);text-align:center;">Sign in to fierj</h1>
+<div class="auth-card">
+  <h1>Sign in to fierj</h1>
 
   {{if .Error}}
-  <div style="background:var(--danger);color:#fff;padding:var(--space-sm) var(--space-md);border-radius:var(--radius);margin-bottom:var(--space-md);font-size:0.85rem;">
+  <div class="alert-danger">
     {{.Error}}
   </div>
   {{end}}
@@ -19,7 +19,7 @@
       <label for="password">Password</label>
       <input type="password" id="password" name="password" required>
     </div>
-    <button type="submit" class="btn" style="width:100%;">Sign in</button>
+    <button type="submit" class="btn">Sign in</button>
   </form>
 </div>
 {{template "foot" .}}
diff --git a/templates/new_repo.html b/templates/new_repo.html
index c5b2e40..5f88440 100644
--- a/templates/new_repo.html
+++ b/templates/new_repo.html
@@ -1,18 +1,18 @@
 {{template "head" .}}
 {{define "title"}}new repository - fierj{{end}}
-<h1 style="margin-bottom: var(--space-lg);">New Repository</h1>
-<form method="post" style="max-width: 520px;">
+<h1 class="page-header">New Repository</h1>
+<form method="post" class="form-narrow">
   <div class="form-group">
     <label for="import_url">Import from URL (optional)</label>
     <input type="text" id="import_url" name="import_url" placeholder="https://github.com/user/repo.git">
-    <div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: var(--space-xs);">
+    <div class="hint">
       Leave blank to create an empty repository.
     </div>
   </div>
   <div class="form-group">
     <label for="name">Repository name</label>
     <input type="text" id="name" name="name" placeholder="my-project" {{if eq .FromImport "true"}}value="{{.SuggestedName}}"{{end}}>
-    <div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: var(--space-xs);">
+    <div class="hint">
       Required unless importing &mdash; then it's derived from the URL.
     </div>
   </div>
diff --git a/templates/partials.html b/templates/partials.html
index ba54680..9aedaa5 100644
--- a/templates/partials.html
+++ b/templates/partials.html
@@ -6,7 +6,14 @@
             d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" />
     </svg>
     <h1><a href="/{{.Repo}}">{{.Repo}}</a></h1>
+    {{if .IsPrivate}}
+    <span class="visibility visibility-private">
+        <svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"/></svg>
+        Private
+    </span>
+    {{else}}
     <span class="visibility">Public</span>
+    {{end}}
 </div>
 {{if .Description}}<p class="repo-desc">{{.Description}}</p>{{end}}
 {{end}}
@@ -23,21 +30,19 @@
     <a href="/{{.Repo}}/log/{{.Ref}}" {{if eq .ActiveTab "commits" }} class="active" {{end}}>
         <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
             <path
-                d="M1.643 3.143.427 1.927A.25.25 0 0 1 .604 1.5h3.71a.25.25 0 0 1 .177.427L3.275 3.143a3.5 3.5 0 1 0 5.6 1.476.75.75 0 1 1 1.388.568A5 5 0 1 1 1.643 3.143ZM8 4a.75.75 0 0 1 .75.75v2.44l1.53 1.53a.75.75 0 0 1-1.06 1.06l-1.72-1.72V4.75A.75.75 0 0 1 8 4Z" />
+                d="m.427 1.927 1.215 1.215a8.002 8.002 0 1 1-1.6 5.685.75.75 0 1 1 1.493-.154 6.5 6.5 0 1 0 1.18-4.458l1.358 1.358A.25.25 0 0 1 3.896 6H.25A.25.25 0 0 1 0 5.75V2.104a.25.25 0 0 1 .427-.177ZM7.75 4a.75.75 0 0 1 .75.75v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5A.75.75 0 0 1 7.75 4Z" />
         </svg>
         Commits
         {{if .Commits}}<span class="count">{{.Commits}}</span>{{end}}
     </a>
-    {{if or .Branches .Tags}}
     <a href="/{{.Repo}}/refs" {{if eq .ActiveTab "refs" }} class="active" {{end}}>
         <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
             <path
                 d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Z" />
         </svg>
         Refs
-        <span class="count">{{len .Branches}}{{if .Tags}}+{{len .Tags}}{{end}}</span>
+        {{if or .Branches .Tags}}<span class="count">{{len .Branches}}{{if .Tags}}+{{len .Tags}}{{end}}</span>{{end}}
     </a>
-    {{end}}
     <a href="/{{.Repo}}/threads" {{if eq .ActiveTab "threads" }} class="active" {{end}}>
         <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
             <path
@@ -48,22 +53,24 @@
     <a href="/{{.Repo}}/patches" {{if eq .ActiveTab "patches" }} class="active" {{end}}>
         <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
             <path
-                d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354Z" />
+                d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z" />
         </svg>
         Patches
     </a>
+    {{if $.User}}
     <a href="/{{.Repo}}/jobs" {{if eq .ActiveTab "jobs" }} class="active" {{end}}>
         <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
-            <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM4.5 7.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7Z"/>
+            <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"/>
         </svg>
         Jobs
     </a>
     <a href="/{{.Repo}}/settings" {{if eq .ActiveTab "settings" }} class="active" {{end}}>
         <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
-            <path d="M8 0a2 2 0 0 1 2 2v.586l2.293-2.293a1 1 0 1 1 1.414 1.414L11.414 4H12a2 2 0 0 1 2 2v3.5a2.5 2.5 0 0 1-2.5 2.5H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 13.043V12H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h.586L.293 1.707a1 1 0 1 1 1.414-1.414L4 2.586V2a2 2 0 0 1 2-2Z"/>
+            <path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"/>
         </svg>
         Settings
     </a>
+    {{end}}
 </nav>
 {{end}}
 
diff --git a/templates/refs.html b/templates/refs.html
index e2d0cf8..446863d 100644
--- a/templates/refs.html
+++ b/templates/refs.html
@@ -10,10 +10,10 @@
         <div class="thread-list">
             {{range .Branches}}
             <div class="thread-list-item">
-                <svg class="icon thread-icon-open" viewBox="0 0 16 16" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Z"/></svg>
+                <svg class="icon icon-ref" viewBox="0 0 16 16" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Z"/></svg>
                 <div class="thread-list-content">
                     <a href="/{{$.Repo}}/tree/{{.}}" class="thread-title">{{.}}</a>
-                    {{if eq . $.DefaultBranch}}<span class="label label-default">default</span>{{end}}
+                    {{if eq . $.DefaultBranch}}<span class="label-default">default</span>{{end}}
                 </div>
             </div>
             {{end}}
@@ -26,7 +26,7 @@
         <div class="thread-list">
             {{range .Tags}}
             <div class="thread-list-item">
-                <svg class="icon thread-icon-open" viewBox="0 0 16 16" fill="currentColor"><path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"/></svg>
+                <svg class="icon icon-ref" viewBox="0 0 16 16" fill="currentColor"><path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"/></svg>
                 <div class="thread-list-content">
                     <a href="/{{$.Repo}}/tree/{{.}}" class="thread-title">{{.}}</a>
                 </div>
diff --git a/templates/repos.html b/templates/repos.html
index 5816688..cefef3e 100644
--- a/templates/repos.html
+++ b/templates/repos.html
@@ -1,21 +1,26 @@
 {{template "head" .}}
 {{define "title"}}repositories - fierj{{end}}
-<h1 style="margin-bottom: var(--space-lg);">Repositories</h1>
+<h1 class="page-header">Repositories</h1>
 {{if .Repos}}
-<div style="display: grid; gap: var(--space-md);">
+<div class="repo-list">
     {{range .Repos}}
-    <div
-        style="border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-md) var(--space-lg); background: var(--bg);">
-        <div style="display: flex; align-items: center; gap: var(--space-sm);">
-            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="color: var(--text-secondary);">
+    <div class="repo-card">
+        <div class="repo-card-header">
+            <svg class="icon" viewBox="0 0 16 16" fill="currentColor">
                 <path
                     d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" />
             </svg>
-            <a href="/{{.Name}}" style="font-weight: 600; font-size: 1rem;">{{.Name}}</a>
-            <span style="font-size: 0.7rem; border: 1px solid var(--border); border-radius: 2em; padding: 0.1rem 0.5rem; color: var(--text-secondary);">{{if .IsPrivate}}Private{{else}}Public{{end}}</span>
+            <a href="/{{.Name}}">{{.Name}}</a>
+            {{if .IsPrivate}}
+            <span class="visibility visibility-private">
+                <svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"/></svg>
+                Private
+            </span>
+            {{else}}
+            <span class="visibility">Public</span>
+            {{end}}
         </div>
-        {{if .Description}}<p style="color: var(--text-secondary); margin-top: var(--space-xs); font-size: 0.85rem;">
-            {{.Description}}</p>{{end}}
+        {{if .Description}}<p class="repo-card-desc">{{.Description}}</p>{{end}}
     </div>
     {{end}}
 </div>
diff --git a/templates/settings.html b/templates/settings.html
index 298e97d..b791219 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -3,16 +3,16 @@
 {{template "repo-header" .}}
 {{template "repo-tabs" .}}
 
-<h1 style="margin-bottom:var(--space-lg);font-size:1.25rem;">Repository settings</h1>
+<h1 class="page-header" style="font-size:1.25rem;">Repository settings</h1>
 
-<form method="post" style="max-width:600px;">
+<form method="post" class="form-narrow">
   <div class="form-group">
     <label for="description">Description</label>
     <input type="text" id="description" name="description" value="{{.Description}}">
   </div>
 
   <div class="form-group">
-    <label>
+    <label class="form-check">
       <input type="checkbox" name="is_private" value="true" {{if .IsPrivate}}checked{{end}}>
       Private repository (only visible to logged-in users)
     </label>
@@ -20,16 +20,16 @@
 
   <div class="form-group">
     <label for="authorized_keys">Authorized SSH keys (one per line)</label>
-    <textarea id="authorized_keys" name="authorized_keys" rows="6" style="font-family:var(--font-mono);font-size:0.8rem;">{{.AuthorizedKeys}}</textarea>
-    <div style="font-size:0.8rem;color:var(--text-secondary);margin-top:var(--space-xs);">
+    <textarea id="authorized_keys" name="authorized_keys" rows="6" class="mono-input">{{.AuthorizedKeys}}</textarea>
+    <div class="hint">
       Users with these keys can push to this repo. Leave empty to allow anyone with SSH access.
     </div>
   </div>
 
   <div class="form-group">
     <label for="protected_branches">Protected branches (one per line)</label>
-    <textarea id="protected_branches" name="protected_branches" rows="4" placeholder="main&#10;release/*" style="font-family:var(--font-mono);font-size:0.8rem;">{{.ProtectedBranches}}</textarea>
-    <div style="font-size:0.8rem;color:var(--text-secondary);margin-top:var(--space-xs);">
+    <textarea id="protected_branches" name="protected_branches" rows="4" placeholder="main&#10;release/*" class="mono-input">{{.ProtectedBranches}}</textarea>
+    <div class="hint">
       Branches that require authorized keys to push. Supports wildcards (e.g. <code>release/*</code>).
     </div>
   </div>
diff --git a/templates/setup.html b/templates/setup.html
index 113e392..43e8b00 100644
--- a/templates/setup.html
+++ b/templates/setup.html
@@ -1,14 +1,14 @@
 {{template "head" .}}
 {{define "title"}}setup - fierj{{end}}
 
-<div style="max-width:360px;margin:var(--space-xl) auto;">
-  <h1 style="margin-bottom:var(--space-sm);text-align:center;">Welcome to fierj</h1>
-  <p style="text-align:center;color:var(--text-secondary);margin-bottom:var(--space-lg);">
+<div class="auth-card">
+  <h1 style="margin-bottom:var(--space-sm);">Welcome to fierj</h1>
+  <p class="subtitle">
     Create the first admin account to get started.
   </p>
 
   {{if .Error}}
-  <div style="background:var(--danger);color:#fff;padding:var(--space-sm) var(--space-md);border-radius:var(--radius);margin-bottom:var(--space-md);font-size:0.85rem;">
+  <div class="alert-danger">
     {{.Error}}
   </div>
   {{end}}
@@ -26,7 +26,7 @@
       <label for="confirm">Confirm password</label>
       <input type="password" id="confirm" name="confirm" required minlength="6">
     </div>
-    <button type="submit" class="btn" style="width:100%;">Create account</button>
+    <button type="submit" class="btn">Create account</button>
   </form>
 </div>
 {{template "foot" .}}
diff --git a/templates/tree.html b/templates/tree.html
index 795a2df..310d000 100644
--- a/templates/tree.html
+++ b/templates/tree.html
@@ -18,18 +18,18 @@ git push -u origin main</code></pre>{{end}}
         {{.Ref}}
     </span>
     {{if .Breadcrumb}}
-    <span class="breadcrumb" style="font-size:0.85rem;color:var(--text-secondary);">
+    <span class="breadcrumb">
         <a href="/{{.Repo}}">{{.Repo}}</a>
         {{range .Breadcrumb}} / <a href="/{{$.Repo}}/tree/{{$.Ref}}/{{.URL}}">{{.Name}}</a>{{end}}
     </span>
     {{end}}
-    <span class="code-bar spacer"></span>
+    <span class="spacer"></span>
     <span class="clone-btn">
-        <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06Zm6.56 0a.75.75 0 1 0-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 1 0 1.06 1.06l4.25-4.25a.75.75 0 0 0 0-1.06Z"/></svg>
+        <svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06Zm6.56 0a.75.75 0 1 0-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 1 0 1.06 1.06l4.25-4.25a.75.75 0 0 0 0-1.06Z"/></svg>
         {{if .CloneSSH}}{{.CloneSSH}}{{end}}
     </span>
-    <a href="/{{.Repo}}/feed.xml" title="RSS feed" class="clone-btn" style="text-decoration:none;">
-        <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2.002 2.725a.75.75 0 0 1 .797-.699C8.79 2.42 13.58 7.21 13.974 13.201a.75.75 0 0 1-1.497.098 10.502 10.502 0 0 0-9.776-9.776.747.747 0 0 1-.7-.798ZM2.84 7.05h-.002a7.002 7.002 0 0 1 6.113 6.111.75.75 0 0 1-1.5.008 5.501 5.501 0 0 0-4.8-4.8.75.75 0 0 1 .189-1.319ZM2 13a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z"/></svg>
+    <a href="/{{.Repo}}/feed.xml" title="RSS feed" class="clone-btn icon-btn">
+        <svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M2.002 2.725a.75.75 0 0 1 .797-.699C8.79 2.42 13.58 7.21 13.974 13.201a.75.75 0 0 1-1.497.098 10.502 10.502 0 0 0-9.776-9.776.747.747 0 0 1-.7-.798ZM2.84 7.05h-.002a7.002 7.002 0 0 1 6.113 6.111.75.75 0 0 1-1.5.008 5.501 5.501 0 0 0-4.8-4.8.75.75 0 0 1 .189-1.319ZM2 13a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z"/></svg>
     </a>
 </div>
 
diff --git a/templates/users.html b/templates/users.html
index 585b009..c3392fd 100644
--- a/templates/users.html
+++ b/templates/users.html
@@ -1,12 +1,12 @@
 {{template "head" .}}
 {{define "title"}}users - fierj{{end}}
 
-<h1 style="margin-bottom:var(--space-lg);">Users</h1>
+<h1 class="page-header">Users</h1>
 
-<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-xl);">
+<div class="grid-2">
   <!-- Add user -->
-  <div style="border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-lg);">
-    <h2 style="font-size:1rem;margin-bottom:var(--space-md);">Add user</h2>
+  <div class="card">
+    <h2>Add user</h2>
     <form method="post">
       <input type="hidden" name="action" value="add">
       <div class="form-group">
@@ -22,8 +22,8 @@
   </div>
 
   <!-- Change own password -->
-  <div style="border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-lg);">
-    <h2 style="font-size:1rem;margin-bottom:var(--space-md);">Change your password</h2>
+  <div class="card">
+    <h2>Change your password</h2>
     <form method="post">
       <input type="hidden" name="action" value="change-password">
       <div class="form-group">
@@ -36,20 +36,20 @@
 </div>
 
 <!-- User list -->
-<div style="margin-top:var(--space-xl);">
-  <h2 style="font-size:1rem;margin-bottom:var(--space-md);">All users ({{len .Users}})</h2>
-  <div style="border:1px solid var(--border);border-radius:var(--radius);">
+<div class="mt-xl">
+  <h2 class="section-title">All users ({{len .Users}})</h2>
+  <div class="list-box">
     {{range .Users}}
-    <div style="display:flex;align-items:center;justify-content:space-between;padding:var(--space-sm) var(--space-md);border-bottom:1px solid var(--border-subtle);">
+    <div class="list-row">
       <span>{{.}}</span>
       {{if ne . $.User}}
-      <form method="post" style="display:inline;" onsubmit="return confirm('Remove {{.}}?')">
+      <form method="post" onsubmit="return confirm('Remove {{.}}?')">
         <input type="hidden" name="action" value="remove">
         <input type="hidden" name="username" value="{{.}}">
-        <button type="submit" class="btn-subtle" style="padding:2px 10px;font-size:0.8rem;">Remove</button>
+        <button type="submit" class="btn-subtle btn-sm">Remove</button>
       </form>
       {{else}}
-      <span style="font-size:0.8rem;color:var(--text-tertiary);">you</span>
+      <span class="tag-self">you</span>
       {{end}}
     </div>
     {{end}}