Patches 4 active
UI: lighten palette to graphite/grey theme; enrich landing (how-it-works, CLI snippet, FAQ) + SEO/AEO meta and JSON-LD (SoftwareApplication, FAQPage) accepted
admin · 3 files · accepted Jun 14
Fix: public projects viewable anonymously in web UI again (drop blanket [Authorize] on /app read pages; per-project CanRead still gates private) accepted
admin · 9 files · accepted Jun 14
Hardening: crash-safe atomic storage, overlap-detector correctness, transactional accept + BaseHash concurrency, per-project roles, webhook SSRF guard, web-UI authorization; remove orphaned Catena.Web accepted
admin · 80 files · accepted Jun 14
Initial accepted
admin · 168 files · accepted Apr 06
UI: lighten palette to graphite/grey theme; enrich landing (how-it-works, CLI snippet, FAQ) + SEO/AEO meta and JSON-LD (SoftwareApplication, FAQPage)
admin · accepted Jun 14 · base: trunk-head
3 files changed+538 -538 lines
Diff
Files (3)
Review (0)
Overlaps (0)
~ src/Catena.Server/Components/App.razor
-previous content
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <base href="/" />
7 <title>Catena — Patch-based version control</title>
8 <meta name="description" content="Catena is a patch-based version control system — no branches, no merges. Immutable patches with deterministic overlap detection, for teams that ship." />
9 <meta name="theme-color" content="#2a2a2e" />
10 <link rel="icon" type="image/svg+xml" href="favicon.svg" />
11 <HeadOutlet @rendermode="RenderMode.InteractiveServer" />
12 <link rel="stylesheet" href="css/app.css" />
13 <style>
14 #components-reconnect-modal {
15 display:none;position:fixed;inset:0;z-index:9999;
16 background:rgba(26,26,30,.92);backdrop-filter:blur(8px);
17 }
18 #components-reconnect-modal.components-reconnect-show,
19 #components-reconnect-modal.components-reconnect-failed,
20 #components-reconnect-modal.components-reconnect-rejected {
21 display:flex;align-items:center;justify-content:center;
22 }
23 .rc-term {
24 background:#26262a;border:1px solid #4a4a52;border-radius:8px;
25 padding:16px;width:320px;font-family:'JetBrains Mono',monospace;font-size:12px;
26 }
27 .rc-bar { display:flex;gap:6px;margin-bottom:12px }
28 .rc-dot { width:10px;height:10px;border-radius:50% }
29 .rc-line { color:#52525b;margin-bottom:4px;line-height:1.6 }
30 .rc-line .prompt { color:#22c55e }
31 .rc-line .cmd { color:#a1a1aa }
32 .rc-line .err { color:#ef4444 }
33 .rc-typing {
34 color:#22c55e;overflow:hidden;white-space:nowrap;
35 border-right:2px solid #22c55e;width:0;max-width:280px;
36 animation:rctype 2s steps(28) infinite, rcblink .6s step-end infinite alternate;
37 }
38 @@keyframes rctype { 0%{width:0} 50%,100%{width:220px} }
39 @@keyframes rcblink { 50%{border-color:transparent} }
40
41 /* Failed state */
42 .rc-connecting { display:block }
43 .rc-failed { display:none }
44 .components-reconnect-failed .rc-connecting,
45 .components-reconnect-rejected .rc-connecting { display:none }
46 .components-reconnect-failed .rc-failed,
47 .components-reconnect-rejected .rc-failed { display:block }
48 .rc-reload {
49 color:#22c55e;cursor:pointer;text-decoration:none;
50 }
51 .rc-reload:hover { text-decoration:underline }
52 </style>
53</head>
54<body>
55 <div id="components-reconnect-modal">
56 <div class="rc-term">
57 <div class="rc-bar">
58 <div class="rc-dot" style="background:#ef4444"></div>
59 <div class="rc-dot" style="background:#f59e0b"></div>
60 <div class="rc-dot" style="background:#22c55e"></div>
61 </div>
62 <div class="rc-line"><span class="prompt">$</span> <span class="cmd">catena status</span></div>
63 <div class="rc-line"><span class="err">connection lost</span></div>
64 <div class="rc-connecting">
65 <div class="rc-line"><span class="prompt">$</span> <span class="cmd">catena reconnect</span></div>
66 <div class="rc-typing">attempting to reach server...</div>
67 </div>
68 <div class="rc-failed">
69 <div class="rc-line"><span class="err">server unreachable.</span></div>
70 <div class="rc-line"><span class="prompt">$</span> <a class="rc-reload" onclick="location.reload()">catena reload</a></div>
71 </div>
72 </div>
73 </div>
74 <Routes @rendermode="RenderMode.InteractiveServer" />
75 <script src="_framework/blazor.web.js"></script>
76 <script src="js/timeline.js"></script>
77</body>
78</html>
79
~ src/Catena.Server/Components/Pages/Landing.razor
-previous content
1@page "/"
2@layout Catena.Server.Components.Layout.EmptyLayout
3@inject ProjectStore ProjectStore
4@using Microsoft.AspNetCore.Components.Authorization
5@inject AuthenticationStateProvider AuthState
6
7<PageTitle>Catena — Patch-based version control (no branches, no merges)</PageTitle>
8
9<HeadContent>
10 <meta name="description" content="Catena is a patch-based version control system — no branches, no merges. Draft, propose and accept immutable patches with deterministic overlap detection. Built for small teams that ship." />
11 <meta name="keywords" content="patch-based version control, Catena VCS, Git alternative, no branches, overlap detection, online-first version control, trunk-based" />
12 <link rel="canonical" href="https://catena.qsp.app/" />
13 <meta property="og:type" content="website" />
14 <meta property="og:site_name" content="Catena" />
15 <meta property="og:title" content="Catena — Patch-based version control" />
16 <meta property="og:description" content="No branches. No merges. Just immutable patches, applied in order, with deterministic overlap detection." />
17 <meta property="og:url" content="https://catena.qsp.app/" />
18 <meta property="og:image" content="https://catena.qsp.app/favicon.svg" />
19 <meta name="twitter:card" content="summary" />
20 <meta name="twitter:title" content="Catena — Patch-based version control" />
21 <meta name="twitter:description" content="No branches. No merges. Just immutable patches, applied in order." />
22 <script type="application/ld+json">@((MarkupString)StructuredData)</script>
23</HeadContent>
24
25<div class="landing">
26 <header class="landing-hero">
27 <div class="landing-brand">CATENA<span class="dot">.</span></div>
28 <h1 class="landing-tagline">Patch-based version control for teams that ship.</h1>
29 <p class="landing-sub">No branches. No merges. Just patches, applied in order.</p>
30 <div class="landing-actions">
31 @if (_isAuthenticated)
32 {
33 <a href="/app" class="btn-primary">Open Dashboard</a>
34 }
35 else
36 {
37 <a href="/login" class="btn-primary">Sign In</a>
38 }
39 <a href="/docs" class="btn-primary" style="background:transparent;border:1px solid var(--border);color:var(--text2)">How it Works</a>
40 </div>
41 </header>
42
43 <section class="landing-how">
44 <h2>How Catena works</h2>
45 <div class="how-steps">
46 <div class="how-step">
47 <div class="num">01</div>
48 <h3>Draft</h3>
49 <p>Edit your files and capture the change as one immutable patch — author, dependencies and targets included.</p>
50 </div>
51 <div class="how-arrow">&rarr;</div>
52 <div class="how-step">
53 <div class="num">02</div>
54 <h3>Propose</h3>
55 <p>Submit the patch. Catena checks it against every other open patch for overlaps — deterministically, before anything lands.</p>
56 </div>
57 <div class="how-arrow">&rarr;</div>
58 <div class="how-step">
59 <div class="num">03</div>
60 <h3>Accept</h3>
61 <p>A maintainer accepts it and the patch is applied to the trunk. One source of truth, always buildable.</p>
62 </div>
63 </div>
64 <div class="landing-cli">
65 <div><span class="p">$</span> <span class="c">catena draft -m "add login form" --sync</span></div>
66 <div><span class="o"> patch 38091ef2 created · 3 files</span></div>
67 <div><span class="p">$</span> <span class="c">catena propose 38091ef2</span></div>
68 <div><span class="p">$</span> <span class="c">catena accept 38091ef2</span></div>
69 <div><span class="o"> trunk updated.</span></div>
70 </div>
71 </section>
72
73 @if (_publicProjects is not null && _publicProjects.Count > 0)
74 {
75 <section class="landing-projects">
76 <h2>Public Projects</h2>
77 <div class="project-grid">
78 @foreach (var p in _publicProjects)
79 {
80 <a href="/app/projects/@p.Id" class="project-card">
81 <div class="project-card-name">@p.Name</div>
82 <div class="project-card-meta">
83 <code>@p.Id</code>
84 <span>@p.CreatedAt.ToString("MMM dd, yyyy")</span>
85 </div>
86 </a>
87 }
88 </div>
89 </section>
90 }
91
92 <section class="landing-features">
93 <div class="feature">
94 <div class="feature-icon">+</div>
95 <h3>Patch-Chain</h3>
96 <p>Immutable, append-only patches. Every change is a first-class object with author, dependencies, and targets.</p>
97 </div>
98 <div class="feature">
99 <div class="feature-icon">~</div>
100 <h3>Overlap Detection</h3>
101 <p>Deterministic conflict detection before merge. No surprises, no broken builds. Auto-apply when safe, block when not.</p>
102 </div>
103 <div class="feature">
104 <div class="feature-icon">&gt;</div>
105 <h3>Views, not Branches</h3>
106 <p>Trunk is the only truth. Proposed patches are candidates, not parallel universes. Accept what's ready, revert what's not.</p>
107 </div>
108 </section>
109
110 <section class="landing-faq">
111 <h2>Frequently asked</h2>
112 <div class="faq-item">
113 <h3>What is Catena?</h3>
114 <p>Catena is a patch-based version control system. Instead of branches and merges, every change is an immutable patch that moves through a maturity lifecycle — draft, proposed, accepted — and is applied to a single trunk.</p>
115 </div>
116 <div class="faq-item">
117 <h3>How is Catena different from Git?</h3>
118 <p>Git tracks snapshots across branches that you later merge. Catena has no branches and no merge step: proposed patches are checked for overlap deterministically and applied to the trunk in order, so there are no surprise merge conflicts.</p>
119 </div>
120 <div class="faq-item">
121 <h3>Does Catena replace Git?</h3>
122 <p>Catena is an online-first alternative aimed at small teams that want a simpler model. It is operated entirely through the <code>catena</code> CLI and a web UI, with a single server — no branch management to maintain.</p>
123 </div>
124 <div class="faq-item">
125 <h3>Is Catena free?</h3>
126 <p>Yes. Catena is free to self-host and ships as a single Docker container.</p>
127 </div>
128 </section>
129
130 <footer class="landing-footer">
131 <a href="/docs">Docs</a> · <span>Catena VCS</span>
132 </footer>
133</div>
134
135@code {
136 private List<Catena.Shared.Models.Project>? _publicProjects;
137 private bool _isAuthenticated;
138
139 // JSON-LD: a SoftwareApplication entry plus a FAQPage that mirrors the visible FAQ above.
140 // Rendered as raw markup so Razor doesn't try to parse the JSON braces.
141 private const string StructuredData = """
142 [
143 {
144 "@context": "https://schema.org",
145 "@type": "SoftwareApplication",
146 "name": "Catena",
147 "applicationCategory": "DeveloperApplication",
148 "operatingSystem": "Windows, Linux, macOS, Docker",
149 "url": "https://catena.qsp.app/",
150 "description": "Catena is a patch-based version control system with no branches and no merges. Immutable patches move through a draft, proposed and accepted lifecycle and are applied to a single trunk, with deterministic overlap detection.",
151 "offers": { "@type": "Offer", "price": "0", "priceCurrency": "EUR" }
152 },
153 {
154 "@context": "https://schema.org",
155 "@type": "FAQPage",
156 "mainEntity": [
157 {
158 "@type": "Question",
159 "name": "What is Catena?",
160 "acceptedAnswer": { "@type": "Answer", "text": "Catena is a patch-based version control system. Instead of branches and merges, every change is an immutable patch that moves through a maturity lifecycle - draft, proposed, accepted - and is applied to a single trunk." }
161 },
162 {
163 "@type": "Question",
164 "name": "How is Catena different from Git?",
165 "acceptedAnswer": { "@type": "Answer", "text": "Git tracks snapshots across branches that you later merge. Catena has no branches and no merge step: proposed patches are checked for overlap deterministically and applied to the trunk in order, so there are no surprise merge conflicts." }
166 },
167 {
168 "@type": "Question",
169 "name": "Does Catena replace Git?",
170 "acceptedAnswer": { "@type": "Answer", "text": "Catena is an online-first alternative aimed at small teams that want a simpler model. It is operated entirely through the catena CLI and a web UI, with a single server." }
171 },
172 {
173 "@type": "Question",
174 "name": "Is Catena free?",
175 "acceptedAnswer": { "@type": "Answer", "text": "Yes. Catena is free to self-host and ships as a single Docker container." }
176 }
177 ]
178 }
179 ]
180 """;
181
182 protected override async Task OnInitializedAsync()
183 {
184 var projects = await ProjectStore.ListAsync();
185 _publicProjects = projects.Where(p => p.IsPublic).ToList();
186
187 var authState = await AuthState.GetAuthenticationStateAsync();
188 _isAuthenticated = authState.User.Identity?.IsAuthenticated == true;
189 }
190}
191
~ src/Catena.Server/wwwroot/css/app.css
-previous content
1@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
2
3*{margin:0;padding:0;box-sizing:border-box}
4
5:root{
6 /* Lightened graphite theme (was near-black; softened to a Notepad++-style grey) */
7 --bg:#2a2a2e;--surface:#323237;--surface2:#3b3b42;--surface3:#47474f;
8 --border:#4a4a52;--border-h:#63636d;
9 --text:#f5f5f6;--text2:#c9c9d1;--text3:#9b9ba6;
10 --accent:#2ecc71;--accent-dim:#1d3d2b;
11 --proposed:#f5a623;--proposed-dim:#4a3417;
12 --draft:#4f93f7;--draft-dim:#26314f;
13 --release:#b39bfb;--release-dim:#352c54;
14 --hotfix:#fb8332;--hotfix-dim:#4a2e1a;
15 --danger:#f15a5a;--danger-dim:#4a2424;
16 --font:'Outfit',sans-serif;--mono:'JetBrains Mono',monospace;
17 --sidebar-w:220px;
18}
19
20body{background:var(--bg);color:var(--text);font-family:var(--font);font-weight:400;min-height:100vh}
21
22/* ===== LANDING PAGE ===== */
23.landing{min-height:100vh;display:flex;flex-direction:column;overflow:auto}
24.landing-hero{text-align:center;padding:120px 24px 70px;background:radial-gradient(ellipse at 50% 0%,rgba(46,204,113,.10) 0%,transparent 70%)}
25.landing-brand{font-size:4rem;font-weight:700;letter-spacing:-2px;color:var(--text)}
26.landing-brand .dot{color:var(--accent)}
27.landing-tagline{font-size:1.4rem;font-weight:300;color:var(--text2);margin-top:16px;letter-spacing:.5px}
28.landing-sub{font-size:.95rem;color:var(--text3);margin-top:8px;font-family:var(--mono)}
29.landing-actions{margin-top:40px;display:flex;justify-content:center;gap:16px}
30.btn-primary{display:inline-block;padding:12px 32px;background:var(--accent);color:#000;font-weight:600;font-size:.95rem;border-radius:6px;text-decoration:none;transition:opacity .15s}
31.btn-primary:hover{opacity:.9;text-decoration:none}
32.landing-projects{max-width:900px;margin:0 auto;padding:0 24px 60px}
33.landing-projects h2{font-size:.8rem;text-transform:uppercase;letter-spacing:2px;color:var(--text3);margin-bottom:20px}
34.project-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
35.project-card{display:block;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;text-decoration:none;transition:border-color .15s,background .15s}
36.project-card:hover{border-color:var(--border-h);background:var(--surface2);text-decoration:none}
37.project-card-name{font-size:1.1rem;font-weight:500;color:var(--text);margin-bottom:8px}
38.project-card-meta{display:flex;justify-content:space-between;align-items:center}
39.project-card-meta code{font-family:var(--mono);font-size:.75rem;color:var(--text3)}
40.project-card-meta span{font-size:.75rem;color:var(--text3)}
41.landing-features{max-width:900px;margin:0 auto;padding:40px 24px 80px;display:grid;grid-template-columns:repeat(3,1fr);gap:24px;border-top:1px solid var(--border)}
42.feature{padding:24px}
43.feature-icon{width:40px;height:40px;border-radius:8px;background:var(--accent-dim);border:1px solid #166534;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1.2rem;color:var(--accent);margin-bottom:16px}
44.feature h3{font-size:1rem;font-weight:500;margin-bottom:8px}
45.feature p{font-size:.85rem;color:var(--text2);line-height:1.5}
46.landing-footer{text-align:center;padding:24px;color:var(--text3);font-size:.8rem;border-top:1px solid var(--border)}
47.landing-footer a{color:var(--text2);text-decoration:none}
48.landing-footer a:hover{color:var(--accent)}
49
50/* How it works */
51.landing-how{max-width:900px;margin:0 auto;padding:48px 24px 8px;text-align:center}
52.landing-how h2,.landing-faq h2{font-size:.8rem;text-transform:uppercase;letter-spacing:2px;color:var(--text3);margin-bottom:28px}
53.how-steps{display:flex;justify-content:center;align-items:stretch;gap:0;flex-wrap:wrap}
54.how-step{flex:1;min-width:180px;max-width:240px;padding:0 18px;text-align:left}
55.how-step .num{font-family:var(--mono);font-size:.75rem;color:var(--accent);margin-bottom:8px}
56.how-step h3{font-size:1.05rem;font-weight:500;color:var(--text);margin-bottom:6px}
57.how-step p{font-size:.85rem;color:var(--text2);line-height:1.5}
58.how-arrow{display:flex;align-items:center;color:var(--text3);font-family:var(--mono);font-size:1.2rem;padding:0 4px}
59.landing-cli{max-width:620px;margin:36px auto 0;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:18px 20px;text-align:left;font-family:var(--mono);font-size:.82rem;line-height:1.9}
60.landing-cli .c{color:var(--text2)} .landing-cli .p{color:var(--accent);user-select:none} .landing-cli .o{color:var(--text3)}
61
62/* FAQ (also feeds the FAQPage structured data for answer engines) */
63.landing-faq{max-width:760px;margin:0 auto;padding:56px 24px 72px}
64.faq-item{border-top:1px solid var(--border);padding:20px 0}
65.faq-item h3{font-size:1rem;font-weight:500;color:var(--text);margin-bottom:8px}
66.faq-item p{font-size:.9rem;color:var(--text2);line-height:1.6}
67.login-page{max-width:400px;margin:0 auto;padding:120px 24px}
68.login-brand{display:block;font-size:2rem;font-weight:700;letter-spacing:-1px;color:var(--text);text-decoration:none;margin-bottom:40px}
69.login-brand .dot{color:var(--accent)}
70.login-page h1{font-size:1.2rem;font-weight:400;color:var(--text2);margin-bottom:24px}
71
72/* ===== APP SHELL ===== */
73.app-shell{display:flex;height:100vh;overflow:hidden}
74
75/* ===== SIDEBAR ===== */
76.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;height:100vh}
77.sidebar-brand{padding:20px 20px 16px;font-size:15px;font-weight:600;letter-spacing:1px;color:var(--text);border-bottom:1px solid var(--border);text-decoration:none;display:block}
78.sidebar-brand span{color:var(--accent)}
79.project-select{margin:12px 12px 8px;padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;text-decoration:none}
80.project-select::after{content:'';width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--text3)}
81.nav{padding:8px 0;flex:1}
82.nav-item{display:flex;align-items:center;gap:10px;padding:9px 20px;font-size:13px;color:var(--text2);cursor:pointer;transition:all .15s;border-left:3px solid transparent;text-decoration:none}
83.nav-item:hover{background:var(--surface2);color:var(--text);text-decoration:none}
84.nav-item.active{color:var(--text);background:var(--surface2);border-left-color:var(--accent)}
85.nav-item .nav-icon{width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:14px;opacity:.6}
86.nav-item.active .nav-icon{opacity:1}
87.nav-badge{margin-left:auto;font-size:10px;font-family:var(--mono);padding:2px 6px;border-radius:4px;background:var(--danger-dim);color:var(--danger)}
88.sidebar-footer{padding:12px 20px;border-top:1px solid var(--border);font-size:11px;color:var(--text3);display:flex;align-items:center;gap:8px}
89.user-dot{width:24px;height:24px;border-radius:50%;background:var(--accent-dim);border:1.5px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:500;color:var(--accent)}
90
91/* ===== MAIN ===== */
92.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden}
93
94/* ===== TOP BAR ===== */
95.topbar{height:52px;min-height:52px;border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 24px;gap:16px}
96.topbar-title{font-size:15px;font-weight:500}
97.topbar-sub{font-size:12px;color:var(--text3);font-family:var(--mono)}
98.topbar-actions{margin-left:auto;display:flex;gap:8px}
99.btn{font-family:var(--font);font-size:12px;font-weight:500;padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text2);cursor:pointer;transition:all .15s;text-decoration:none;display:inline-block}
100.btn:hover{background:var(--surface3);color:var(--text);border-color:var(--border-h);text-decoration:none}
101.btn-accent{background:var(--accent-dim);color:var(--accent);border-color:#166534}
102.btn-accent:hover{background:#0a3d1a}
103.btn-danger{background:var(--danger-dim);color:var(--danger);border-color:#991b1b}
104
105/* ===== DASHBOARD ===== */
106.dashboard-content{flex:1;overflow-y:auto;padding:24px}
107.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}
108.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px 18px}
109.stat-label{font-size:11px;color:var(--text3);text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px}
110.stat-value{font-size:26px;font-weight:600}
111.stat-value.green{color:var(--accent)}
112.stat-value.amber{color:var(--proposed)}
113.stat-value.blue{color:var(--draft)}
114.stat-value.red{color:var(--danger)}
115.section-title{font-size:12px;font-weight:500;color:var(--text3);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px}
116.activity-list{margin-bottom:24px}
117.activity-item{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid var(--border);font-size:13px}
118.activity-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
119.activity-text{flex:1;color:var(--text2)}
120.activity-text strong{color:var(--text);font-weight:500}
121.activity-time{font-size:11px;color:var(--text3);font-family:var(--mono)}
122
123/* ===== PATCHES ===== */
124.patches-layout{flex:1;display:grid;grid-template-columns:320px minmax(0,1fr);overflow:hidden}
125.patch-list{border-right:1px solid var(--border);overflow-y:auto}
126.patch-filters{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;gap:6px}
127.filter-pill{font-family:var(--font);font-size:11px;padding:4px 10px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text3);cursor:pointer;transition:all .15s}
128.filter-pill:hover{color:var(--text2);border-color:var(--border-h)}
129.filter-pill.active{background:var(--accent-dim);color:var(--accent);border-color:#166534}
130.patch-item{padding:12px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .1s;border-left:3px solid transparent}
131.patch-item:hover{background:var(--surface)}
132.patch-item.selected{background:var(--surface);border-left-color:var(--accent)}
133.patch-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
134.patch-name{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
135.patch-status{font-size:10px;font-family:var(--mono);padding:2px 7px;border-radius:4px}
136.status-accepted{background:var(--accent-dim);color:var(--accent)}
137.status-proposed{background:var(--proposed-dim);color:var(--proposed)}
138.status-draft{background:var(--draft-dim);color:var(--draft)}
139.status-conflict{background:var(--danger-dim);color:var(--danger)}
140.patch-meta{font-size:11px;color:var(--text3)}
141.patch-overlap{font-size:10px;color:var(--danger);margin-top:4px}
142.patch-dep{font-size:10px;color:var(--draft);margin-top:3px}
143.patch-detail{overflow-y:auto;display:flex;flex-direction:column}
144.patch-detail-header{padding:20px 24px;border-bottom:1px solid var(--border)}
145.pd-title{font-size:18px;font-weight:500;margin-bottom:4px}
146.pd-meta{font-size:12px;color:var(--text3);margin-bottom:12px}
147.pd-tags{display:flex;gap:6px;flex-wrap:wrap}
148.pd-tag{font-size:10px;font-family:var(--mono);padding:3px 8px;border-radius:4px}
149.patch-tabs{display:flex;border-bottom:1px solid var(--border)}
150.patch-tab{padding:10px 20px;font-size:13px;color:var(--text3);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
151.patch-tab:hover{color:var(--text2)}
152.patch-tab.active{color:var(--text);border-bottom-color:var(--accent)}
153.patch-tab-content{flex:1;padding:20px 24px;overflow-y:auto}
154
155/* Diff */
156.diff-file{margin-bottom:16px}
157.diff-file-header{font-size:12px;font-weight:500;font-family:var(--mono);padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:8px 8px 0 0;color:var(--text2)}
158.diff-lines{border:1px solid var(--border);border-top:none;border-radius:0 0 8px 8px;overflow:hidden;font-family:var(--mono);font-size:12px;line-height:1.7}
159.diff-line{padding:1px 12px;white-space:pre}
160.diff-line .ln{display:inline-block;width:35px;text-align:right;margin-right:16px;color:var(--text3);user-select:none;opacity:.5}
161.diff-line .ln.ln-clickable{cursor:pointer;transition:opacity .1s}
162.diff-line .ln.ln-clickable:hover{opacity:1;color:var(--accent)}
163.diff-line.ctx{color:var(--text3)}
164.diff-line.add{background:#05200e;color:#6ee7b7}
165.diff-line.del{background:#200505;color:#fca5a5}
166.diff-comment{margin:8px 0 8px 24px;padding:10px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:8px}
167.diff-comment-head{display:flex;align-items:center;gap:8px;margin-bottom:4px}
168.diff-comment-avatar{width:20px;height:20px;border-radius:50%;background:var(--draft-dim);border:1px solid var(--draft);display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:500;color:var(--draft)}
169.diff-comment-author{font-size:12px;font-weight:500}
170.diff-comment-time{font-size:10px;color:var(--text3)}
171.diff-comment-text{font-size:12px;color:var(--text2);font-family:var(--font)}
172
173/* ===== TIMELINE ===== */
174.tl-split{flex:1;display:flex;flex-direction:column;overflow:hidden}
175.tl-wip-zone{flex:0 1 auto;overflow:auto;min-height:200px}
176.tl-divider{height:1px;background:linear-gradient(90deg,transparent,var(--border) 20%,var(--border) 80%,transparent);flex-shrink:0}
177.tl-trunk-zone{flex:1 1 0;overflow-y:auto;overflow-x:hidden;min-height:0}
178.tl-graph{position:relative;padding:20px}
179.tl-edges{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
180.tl-detail-panel{position:fixed;bottom:0;right:0;width:45vw;height:45vh;overflow:auto;background:var(--surface);border-left:1px solid var(--border);border-top:1px solid var(--border);border-radius:8px 0 0 0;z-index:10;padding:20px;box-shadow:-4px -4px 20px rgba(0,0,0,.4)}
181.tl-zone-label{font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--text3);font-weight:400;margin-bottom:8px}
182.tl-node{position:absolute;border-radius:8px;cursor:pointer;z-index:2;display:flex;align-items:center;gap:8px;white-space:nowrap}
183.tl-node:hover{z-index:3;filter:brightness(1.15)}
184.tl-node-accepted{padding:6px 12px;background:var(--accent-dim);border:1.5px solid #166534}
185.tl-node-proposed{padding:8px 12px;background:var(--surface);border:1px solid var(--border)}
186.tl-node-draft{padding:8px 12px;background:var(--surface);border:1px solid var(--border)}
187.tl-node-release{padding:4px 14px;background:var(--release-dim);border:1.5px solid var(--release);border-radius:14px}
188.tl-node-hotfix{padding:6px 12px;background:var(--hotfix-dim);border:1.5px solid #9a3412}
189.tl-node-next{padding:4px 10px;border:1.5px dashed var(--text3);background:transparent;border-radius:6px}
190.tl-nd{width:11px;height:11px;border-radius:50%;flex-shrink:0;border:2px solid}
191.tl-nd-accepted{background:var(--accent-dim);border-color:var(--accent)}
192.tl-nd-proposed{background:var(--amber-dim, #451a03);border-color:var(--amber, #f59e0b)}
193.tl-nd-synced{background:var(--blue-dim, #172554);border-color:var(--blue, #3b82f6);border-style:dashed}
194.tl-nd-shared{background:var(--blue-dim, #172554);border-color:var(--blue, #3b82f6)}
195.tl-nd-hotfix{background:var(--hotfix-dim);border-color:var(--hotfix)}
196.tl-nn{font-size:12px;font-weight:500}
197.tl-ns{font-size:10px;color:var(--text3);display:block;margin-top:1px}
198.tl-ndep{font-size:9px;color:var(--draft);display:block;margin-top:2px}
199.tl-ni{display:flex;flex-direction:column}
200.tl-legend{display:flex;gap:12px;font-size:10px;color:var(--text2);margin-left:auto}
201.tl-legend i{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:middle}
202.tl-svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1}
203.tl-zone-label{position:absolute;top:10px;font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--text3);font-weight:400;z-index:3}
204.tl-node{position:absolute;display:flex;align-items:center;gap:8px;z-index:2;cursor:pointer;transition:transform .1s}
205.tl-node:hover{transform:scale(1.03)}
206.tl-node-left{flex-direction:row-reverse}
207.tl-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0;border:2px solid}
208.dot-accepted{background:var(--accent-dim);border-color:var(--accent)}
209.dot-proposed{background:var(--proposed-dim);border-color:var(--proposed)}
210.dot-synced{background:var(--draft-dim);border-color:var(--draft);border-style:dashed}
211.dot-shared{background:var(--draft-dim);border-color:var(--draft)}
212.dot-hotfix{background:var(--hotfix-dim);border-color:var(--hotfix)}
213.tl-info{display:flex;flex-direction:column;gap:1px}
214.tl-name{font-size:12px;font-weight:500;white-space:nowrap}
215.tl-meta{font-size:10px;color:var(--text3);white-space:nowrap}
216.release-pill{font-size:10px;font-weight:500;font-family:var(--mono);padding:3px 10px;border-radius:10px;background:var(--release-dim);border:1.5px solid var(--release);color:var(--release);white-space:nowrap}
217.tl-wip-card{position:absolute;display:flex;align-items:flex-start;gap:8px;padding:7px 11px;background:var(--surface);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s;z-index:2;max-width:280px}
218.tl-wip-card:hover{border-color:var(--border-h);background:var(--surface2)}
219.tl-wip-info{display:flex;flex-direction:column;gap:1px;flex:1}
220.tl-wip-title{font-size:12px;font-weight:500;white-space:nowrap}
221.tl-wip-sub{font-size:10px;color:var(--text3);white-space:nowrap}
222.tl-wip-overlap-badge{font-size:9px;font-family:var(--mono);padding:2px 6px;border-radius:4px;background:var(--danger-dim);color:var(--danger);position:absolute;top:5px;right:8px}
223
224/* ===== FILES ===== */
225.files-layout{flex:1;display:grid;grid-template-columns:280px minmax(0,1fr);overflow:hidden}
226.file-tree{border-right:1px solid var(--border);overflow-y:auto;padding:12px 0}
227.file-item{padding:6px 20px;font-size:13px;color:var(--text2);cursor:pointer;font-family:var(--mono);display:flex;align-items:center;gap:8px}
228.file-item:hover{background:var(--surface);color:var(--text)}
229.file-item.selected{background:var(--surface);color:var(--text)}
230.file-item.folder{color:var(--text3);font-weight:500;font-family:var(--font)}
231.file-indent{padding-left:40px}
232.file-indent2{padding-left:60px}
233.file-icon{font-size:12px;opacity:.5}
234.file-viewer{overflow-y:auto;padding:20px 24px}
235.file-viewer-header{font-size:14px;font-weight:500;font-family:var(--mono);margin-bottom:4px}
236.file-viewer-meta{font-size:11px;color:var(--text3);margin-bottom:16px}
237.file-code{font-family:var(--mono);font-size:12px;line-height:1.7;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:14px;overflow-x:auto;white-space:pre}
238.file-code .ln{color:var(--text3);display:inline-block;width:30px;text-align:right;margin-right:16px;user-select:none}
239
240/* ===== RELEASES ===== */
241.releases-content{flex:1;overflow-y:auto;padding:24px}
242.release-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:18px 20px;margin-bottom:12px;display:flex;align-items:center;gap:16px}
243.release-version{font-family:var(--mono);font-size:16px;font-weight:500;color:var(--release);min-width:80px}
244.release-info{flex:1}
245.release-date{font-size:12px;color:var(--text3)}
246.release-patches{font-size:12px;color:var(--text2);margin-top:2px}
247.release-tag{font-size:10px;font-family:var(--mono);padding:3px 8px;border-radius:4px;background:var(--release-dim);color:var(--release)}
248
249/* ===== SETTINGS ===== */
250.settings-content{flex:1;overflow-y:auto;padding:24px;max-width:640px}
251.setting-section{margin-bottom:28px}
252.setting-section-title{font-size:14px;font-weight:500;margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
253.setting-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;font-size:13px}
254.setting-label{color:var(--text2)}
255.setting-value{font-family:var(--mono);font-size:12px;color:var(--text3);padding:4px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px}
256.user-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px}
257.user-role{margin-left:auto;font-size:11px;padding:3px 8px;border-radius:4px;font-family:var(--mono)}
258.role-admin{background:var(--danger-dim);color:var(--danger)}
259.role-maintainer{background:var(--proposed-dim);color:var(--proposed)}
260.role-dev{background:var(--accent-dim);color:var(--accent)}
261
262/* ===== FORMS ===== */
263form{display:flex;flex-direction:column;gap:.5rem;max-width:400px}
264label{color:var(--text2);font-size:.85rem}
265input[type="password"],input[type="text"],select{padding:.5rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:var(--font)}
266form button[type="submit"]{padding:.5rem 1rem;background:var(--accent);color:#000;border:none;border-radius:4px;cursor:pointer;font-weight:600}
267.error{color:var(--danger);margin-bottom:.5rem;font-size:.9rem}
268