diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 460ecda..6915d73 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -103,46 +103,7 @@ var basePage = template.Must(template.New("base").Parse(`
{{.Title}} — Gitea Mobile
-
+
diff --git a/internal/templates/.gitkeep b/internal/templates/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/internal/templates/create_issue.html b/internal/templates/create_issue.html
new file mode 100644
index 0000000..ac1c6f7
--- /dev/null
+++ b/internal/templates/create_issue.html
@@ -0,0 +1,42 @@
+{{define "content"}}
+Create Issue
+
+
+
+
+{{end}}
diff --git a/internal/templates/dashboard.html b/internal/templates/dashboard.html
new file mode 100644
index 0000000..102c008
--- /dev/null
+++ b/internal/templates/dashboard.html
@@ -0,0 +1,26 @@
+{{define "content"}}
+Dashboard
+
+{{if .Error}}
+{{.Error}}
+{{else if not .Items}}
+No items need attention. Nice work!
+{{else}}
+
+ {{range .Items}}
+
+
+ {{if eq .Type "pull"}}PR{{else}}issue{{end}}
+ {{.Title}}
+
+
+ {{.RepoOwner}}/{{.RepoName}} #{{.Number}}
+ {{range .Labels}}
+ {{.}}
+ {{end}}
+
+
+ {{end}}
+
+{{end}}
+{{end}}
diff --git a/internal/templates/issue_detail.html b/internal/templates/issue_detail.html
new file mode 100644
index 0000000..5ec2b62
--- /dev/null
+++ b/internal/templates/issue_detail.html
@@ -0,0 +1,43 @@
+{{define "content"}}
+{{.Issue.Title}}
+
+
+
+ {{.Issue.State}}
+ {{.Issue.RepoOwner}}/{{.Issue.RepoName}} #{{.Issue.Number}}
+ {{range .Issue.Labels}}
+ {{.Name}}
+ {{end}}
+
+ {{if .Issue.Body}}
+
{{.Issue.Body}}
+ {{end}}
+
+
+{{if .Comments}}
+Comments
+{{range .Comments}}
+
+{{end}}
+{{end}}
+
+
+
Actions
+
+
+{{end}}
diff --git a/internal/templates/issues.html b/internal/templates/issues.html
new file mode 100644
index 0000000..49451ac
--- /dev/null
+++ b/internal/templates/issues.html
@@ -0,0 +1,44 @@
+{{define "content"}}
+Issues
+
+
+
+
+
+
+{{if .Error}}
+{{.Error}}
+{{else if not .Issues}}
+No issues found.
+{{else}}
+
+ {{range .Issues}}
+
+
{{.Title}}
+
+ {{.RepoOwner}}/{{.RepoName}} #{{.Number}}
+ {{range .Labels}}
+ {{.Name}}
+ {{end}}
+ {{if .Assignee}}
+ {{.Assignee.Login}}
+ {{end}}
+
+
+ {{end}}
+ {{if .HasMore}}
+
+ {{end}}
+
+{{end}}
+{{end}}
diff --git a/internal/templates/layout.html b/internal/templates/layout.html
new file mode 100644
index 0000000..3acdfb4
--- /dev/null
+++ b/internal/templates/layout.html
@@ -0,0 +1,45 @@
+{{define "layout"}}
+
+
+
+
+
+
+
+
+
+ {{.Title}} — Gitea Mobile
+
+
+
+
+
+ {{template "content" .}}
+
+
+
+
+{{end}}
diff --git a/internal/templates/pull_detail.html b/internal/templates/pull_detail.html
new file mode 100644
index 0000000..3f4a5b9
--- /dev/null
+++ b/internal/templates/pull_detail.html
@@ -0,0 +1,47 @@
+{{define "content"}}
+{{.Pull.Title}}
+
+
+
+ PR
+ {{.Pull.State}}
+ {{.Pull.RepoOwner}}/{{.Pull.RepoName}} #{{.Pull.Number}}
+ {{range .Pull.Labels}}
+ {{.Name}}
+ {{end}}
+
+
+ +{{.Pull.Additions}}
+ -{{.Pull.Deletions}}
+ {{if .Pull.Mergeable}}Mergeable{{end}}
+
+ {{if .Pull.Body}}
+
{{.Pull.Body}}
+ {{end}}
+
+
+
+{{end}}
diff --git a/internal/templates/pulls.html b/internal/templates/pulls.html
new file mode 100644
index 0000000..762b230
--- /dev/null
+++ b/internal/templates/pulls.html
@@ -0,0 +1,38 @@
+{{define "content"}}
+Pull Requests
+
+
+
+
+
+{{if .Error}}
+{{.Error}}
+{{else if not .Pulls}}
+No open pull requests found.
+{{else}}
+
+ {{range .Pulls}}
+
+
+ PR
+ {{.Title}}
+
+
+ {{.RepoOwner}}/{{.RepoName}} #{{.Number}}
+ {{range .Labels}}
+ {{.Name}}
+ {{end}}
+ +{{.Additions}}
+ -{{.Deletions}}
+ {{if .Mergeable}}mergeable{{end}}
+
+
+ {{end}}
+
+{{end}}
+{{end}}
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..e867918
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,477 @@
+/* Gitea Mobile — Mobile-first CSS (~5KB target) */
+
+/* Reset */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+/* CSS Variables */
+:root {
+ --bg-primary: #0d1117;
+ --bg-secondary: #161b22;
+ --bg-tertiary: #21262d;
+ --border: #30363d;
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --text-link: #58a6ff;
+ --accent-green: #3fb950;
+ --accent-red: #f85149;
+ --accent-yellow: #d29922;
+ --accent-blue: #58a6ff;
+ --accent-purple: #bc8cff;
+ --spacing-xs: 0.25rem;
+ --spacing-sm: 0.5rem;
+ --spacing-md: 0.75rem;
+ --spacing-lg: 1rem;
+ --radius: 8px;
+ --radius-sm: 6px;
+ --radius-pill: 10px;
+ --nav-height: 56px;
+ --font-sm: 0.75rem;
+ --font-base: 0.875rem;
+ --font-lg: 1rem;
+ --font-xl: 1.25rem;
+}
+
+/* Base */
+html {
+ font-size: 16px;
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.5;
+ padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom));
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Content area */
+.content {
+ padding: var(--spacing-lg);
+ padding-top: max(var(--spacing-lg), env(safe-area-inset-top));
+ max-width: 640px;
+ margin: 0 auto;
+}
+
+/* Typography */
+h1 {
+ font-size: var(--font-xl);
+ font-weight: 700;
+ margin-bottom: var(--spacing-lg);
+}
+
+h2 {
+ font-size: var(--font-lg);
+ font-weight: 600;
+ margin-bottom: var(--spacing-md);
+}
+
+a {
+ color: var(--text-link);
+ text-decoration: none;
+}
+
+a:active {
+ opacity: 0.7;
+}
+
+/* Cards */
+.card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: var(--spacing-md);
+ margin-bottom: var(--spacing-sm);
+ transition: background 0.15s ease;
+}
+
+.card:active {
+ background: var(--bg-tertiary);
+}
+
+.card-title {
+ font-weight: 600;
+ font-size: var(--font-base);
+ margin-bottom: var(--spacing-xs);
+ line-height: 1.3;
+}
+
+.card-meta {
+ font-size: var(--font-sm);
+ color: var(--text-secondary);
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.card-body {
+ font-size: var(--font-base);
+ color: var(--text-secondary);
+ margin-top: var(--spacing-sm);
+ line-height: 1.5;
+}
+
+/* Labels / badges */
+.label {
+ display: inline-block;
+ font-size: 0.7rem;
+ padding: 1px 6px;
+ border-radius: var(--radius-pill);
+ font-weight: 500;
+ line-height: 1.5;
+ white-space: nowrap;
+}
+
+.type-badge {
+ font-size: 0.65rem;
+ text-transform: uppercase;
+ font-weight: 700;
+ padding: 1px 5px;
+ border-radius: 4px;
+ white-space: nowrap;
+}
+
+.type-issue {
+ background: rgba(31, 111, 235, 0.13);
+ color: var(--accent-blue);
+ border: 1px solid rgba(31, 111, 235, 0.27);
+}
+
+.type-pull {
+ background: rgba(35, 134, 54, 0.13);
+ color: var(--accent-green);
+ border: 1px solid rgba(35, 134, 54, 0.27);
+}
+
+.state-open {
+ color: var(--accent-green);
+}
+
+.state-closed {
+ color: var(--accent-red);
+}
+
+.state-merged {
+ color: var(--accent-purple);
+}
+
+/* Priority labels */
+.priority-p1 { color: var(--accent-red); border-color: var(--accent-red); }
+.priority-p2 { color: var(--accent-yellow); border-color: var(--accent-yellow); }
+.priority-p3 { color: var(--accent-blue); border-color: var(--accent-blue); }
+
+/* Diff stats */
+.diff-add { color: var(--accent-green); font-size: var(--font-sm); }
+.diff-del { color: var(--accent-red); font-size: var(--font-sm); }
+
+/* Bottom navigation */
+.bottom-nav {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border);
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ height: var(--nav-height);
+ padding-bottom: env(safe-area-inset-bottom);
+ z-index: 100;
+}
+
+.bottom-nav a {
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 0.7rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 4px 0;
+ min-width: 64px;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.bottom-nav a.active {
+ color: var(--accent-blue);
+}
+
+.bottom-nav svg {
+ width: 22px;
+ height: 22px;
+ margin-bottom: 2px;
+}
+
+/* Filter bar */
+.filter-bar {
+ display: flex;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-lg);
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ padding-bottom: var(--spacing-xs);
+}
+
+.filter-bar::-webkit-scrollbar {
+ display: none;
+}
+
+.filter-bar select,
+.filter-bar input {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: var(--font-base);
+ min-width: 0;
+ flex-shrink: 0;
+ appearance: none;
+ -webkit-appearance: none;
+}
+
+.filter-bar select:focus,
+.filter-bar input:focus {
+ outline: none;
+ border-color: var(--accent-blue);
+}
+
+/* Forms */
+.form-group {
+ margin-bottom: var(--spacing-lg);
+}
+
+.form-group label {
+ display: block;
+ font-size: var(--font-sm);
+ color: var(--text-secondary);
+ margin-bottom: var(--spacing-sm);
+}
+
+.form-group input,
+.form-group textarea,
+.form-group select {
+ width: 100%;
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: var(--font-lg);
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-family: inherit;
+}
+
+.form-group textarea {
+ min-height: 120px;
+ resize: vertical;
+}
+
+.form-group input:focus,
+.form-group textarea:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: var(--accent-blue);
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--spacing-md) var(--spacing-lg);
+ font-size: var(--font-lg);
+ font-weight: 600;
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ transition: background 0.15s ease;
+}
+
+.btn-primary {
+ background: #238636;
+ color: #fff;
+ width: 100%;
+}
+
+.btn-primary:active {
+ background: #2ea043;
+}
+
+.btn-secondary {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border);
+ width: 100%;
+}
+
+.btn-secondary:active {
+ background: var(--border);
+}
+
+.btn-danger {
+ background: #da363322;
+ color: var(--accent-red);
+ border: 1px solid #da363366;
+ width: 100%;
+}
+
+/* Review form */
+.review-options {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-lg);
+}
+
+.review-option {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-sm);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+}
+
+.review-option input[type="radio"] {
+ accent-color: var(--accent-blue);
+}
+
+/* Comments thread */
+.comment {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ margin-bottom: var(--spacing-sm);
+ overflow: hidden;
+}
+
+.comment-header {
+ padding: var(--spacing-sm) var(--spacing-md);
+ background: var(--bg-tertiary);
+ font-size: var(--font-sm);
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.comment-body {
+ padding: var(--spacing-md);
+ font-size: var(--font-base);
+ line-height: 1.6;
+}
+
+/* Avatar */
+.avatar {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+/* Empty state */
+.empty {
+ text-align: center;
+ color: var(--text-secondary);
+ padding: 3rem var(--spacing-lg);
+ font-size: var(--font-base);
+}
+
+/* Messages */
+.message {
+ padding: var(--spacing-md);
+ border-radius: var(--radius-sm);
+ margin-bottom: var(--spacing-lg);
+ font-size: var(--font-base);
+}
+
+.message.success {
+ background: #0d2818;
+ border: 1px solid #238636;
+ color: var(--accent-green);
+}
+
+.message.error {
+ background: #2d1117;
+ border: 1px solid #da3633;
+ color: var(--accent-red);
+}
+
+.message.info {
+ background: #0c1d2e;
+ border: 1px solid #1f6feb;
+ color: var(--accent-blue);
+}
+
+/* Loading indicator */
+.htmx-indicator {
+ display: none;
+}
+
+.htmx-request .htmx-indicator {
+ display: inline-block;
+}
+
+.spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border);
+ border-top-color: var(--accent-blue);
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Infinite scroll sentinel */
+.scroll-sentinel {
+ height: 1px;
+ margin-bottom: var(--spacing-lg);
+}
+
+/* Tablet breakpoint: 2-column grid */
+@media (min-width: 640px) {
+ .content {
+ max-width: 960px;
+ }
+
+ .card-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--spacing-sm);
+ }
+
+ .card-grid .card {
+ margin-bottom: 0;
+ }
+}
+
+/* Respect reduced motion preference */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Dark mode is default; light mode override if needed */
+@media (prefers-color-scheme: light) {
+ :root {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f6f8fa;
+ --bg-tertiary: #eaeef2;
+ --border: #d0d7de;
+ --text-primary: #1f2328;
+ --text-secondary: #656d76;
+ --text-link: #0969da;
+ }
+}