main 5041353

css.go RawHistoryFolder

1// Package theme provides GTK CSS styling for UI components.
2package theme
3
4import (
5	"fmt"
6	"strings"
7)
8
9// GenerateCSS creates GTK4 CSS using the provided palette with default scale.
10func GenerateCSS(p Palette) string {
11	return GenerateCSSWithScale(p, 1.0)
12}
13
14// GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor.
15// Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings.
16func GenerateCSSWithScale(p Palette, scale float64) string {
17	return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig())
18}
19
20// FontConfig holds font settings for CSS generation.
21type FontConfig struct {
22	SansFont      string
23	MonospaceFont string
24	GtkFont       string
25}
26
27// DefaultFontConfig returns safe font fallbacks.
28func DefaultFontConfig() FontConfig {
29	return FontConfig{
30		SansFont:      "sans-serif",
31		MonospaceFont: "monospace",
32		GtkFont:       DefaultGTKFont(),
33	}
34}
35
36// FontCSSVars generates CSS custom properties for fonts.
37func FontCSSVars(fonts FontConfig) string {
38	var sb strings.Builder
39	// Quote the configured family; include generic fallback.
40	fmt.Fprintf(&sb, "  --font-sans: %q, sans-serif;\n", fonts.SansFont)
41	fmt.Fprintf(&sb, "  --font-mono: %q, monospace;\n", fonts.MonospaceFont)
42	fmt.Fprintf(&sb, "  --font-gtk: %q, sans-serif;\n", fonts.GtkFont)
43	return sb.String()
44}
45
46func generateFontCSS() string {
47	return `/* Font styling */
48window, tooltip, popover {
49	font-family: var(--font-gtk);
50}
51`
52}
53
54const defaultTransitionDurationMs = 120
55
56// GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts.
57// Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings.
58// Uses default mode colors.
59func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string {
60	return GenerateCSSFull(p, scale, fonts, DefaultModeColors())
61}
62
63// GenerateCSSFull creates GTK4 CSS using all provided configuration with the
64// default transition duration.
65// Text scaling is handled via GtkSettings; CSS here stays scale-independent.
66func GenerateCSSFull(p Palette, _ float64, fonts FontConfig, modeColors ModeColors) string {
67	return GenerateCSSFullWithTiming(p, 1.0, fonts, modeColors, defaultTransitionDurationMs)
68}
69
70// GenerateCSSFullWithTiming creates GTK4 CSS using all provided configuration
71// and an explicit transition duration from workspace styling config.
72func GenerateCSSFullWithTiming(p Palette, _ float64, fonts FontConfig, modeColors ModeColors, transitionDurationMs int) string {
73	defaults := DefaultFontConfig()
74	fonts = FontConfig{
75		SansFont:      Coalesce(fonts.SansFont, defaults.SansFont),
76		MonospaceFont: Coalesce(fonts.MonospaceFont, defaults.MonospaceFont),
77		GtkFont:       Coalesce(fonts.GtkFont, defaults.GtkFont),
78	}
79
80	var sb strings.Builder
81
82	// CSS custom properties (variables) - GTK4 uses :root selector
83	sb.WriteString("/* Theme variables */\n")
84	sb.WriteString(":root {\n")
85	sb.WriteString(p.ToCSSVars())
86	sb.WriteString(FontCSSVars(fonts))
87	sb.WriteString(modeColors.ToCSSVars())
88	sb.WriteString("}\n\n")
89
90	// Global font styling
91	sb.WriteString(generateFontCSS())
92	sb.WriteString("\n")
93
94	// Tab bar styling
95	sb.WriteString(generateTabBarCSS(p))
96	sb.WriteString("\n")
97
98	// Omnibox and modal legend styling
99	sb.WriteString(generateOmniboxCSS(p))
100	sb.WriteString(generateModeLegendCSS())
101	sb.WriteString("\n")
102
103	// Find bar styling
104	sb.WriteString(generateFindBarCSS(p))
105	sb.WriteString("\n")
106
107	// Pane styling
108	sb.WriteString(generatePaneCSS(p))
109	sb.WriteString("\n")
110
111	// Stacked pane styling
112	sb.WriteString(generateStackedPaneCSS(p))
113	sb.WriteString("\n")
114
115	// Vim mode styling
116	sb.WriteString(generateVimModeCSS(transitionDurationMs))
117	sb.WriteString("\n")
118
119	// Progress bar styling
120	sb.WriteString(generateProgressBarCSS(p))
121	sb.WriteString("\n")
122
123	// Touchpad history navigation indicator styling
124	sb.WriteString(generateTouchpadNavigationIndicatorCSS(p))
125	sb.WriteString("\n")
126
127	// Toaster styling
128	sb.WriteString(generateToasterCSS(p))
129	sb.WriteString("\n")
130
131	// Link status overlay styling
132	sb.WriteString(generateLinkStatusCSS(p))
133	sb.WriteString("\n")
134
135	// Session manager styling
136	sb.WriteString(generateSessionManagerCSS(p))
137	sb.WriteString("\n")
138
139	// Context menu styling
140	sb.WriteString(generateContextMenuCSS())
141	sb.WriteString("\n")
142
143	// Tab picker styling
144	sb.WriteString(generateTabPickerCSS(p))
145	sb.WriteString("\n")
146
147	// Permission popup styling
148	sb.WriteString(generatePermissionPopupCSS(p))
149	sb.WriteString("\n")
150
151	// Floating pane styling
152	sb.WriteString(generateFloatingPaneCSS(p))
153	sb.WriteString("\n")
154
155	// WebRTC permission activity indicator styling
156	sb.WriteString(generateWebRTCPermissionIndicatorCSS(p))
157	sb.WriteString("\n")
158
159	// Accent picker styling
160	sb.WriteString(generateAccentPickerCSS(p))
161	sb.WriteString("\n")
162
163	appendNativeSidebarCSS(&sb, p)
164
165	return sb.String()
166}
167
168func appendNativeSidebarCSS(sb *strings.Builder, p Palette) {
169	// Native sidebar styling
170	sb.WriteString(generateSidebarCSS(p))
171	sb.WriteString("\n")
172	sb.WriteString(generateHistorySidebarCSS(p))
173	sb.WriteString("\n")
174	sb.WriteString(generateFavoritesSidebarCSS(p))
175}
176
177// generateTabBarCSS creates tab bar styles.
178// Uses em units for scalable UI.
179func generateTabBarCSS(p Palette) string {
180	return `/* Tab bar styling */
181/* Content area inset for overlaid tab bars — uses the same 2em token as
182   .tab-bar min-height and remains static to avoid OSR vertical reallocations. */
183.content-area-tabbar-inset-top {
184	padding-top: 2em;
185}
186
187.content-area-tabbar-inset-bottom {
188	padding-bottom: 2em;
189}
190
191.tab-bar {
192	background-color: var(--surface);
193	border-top: 0.125em solid var(--border);
194	padding: 0;
195	min-height: 2em;
196}
197
198/* Tab button styling */
199button.tab-button {
200	background-color: var(--surface-variant);
201	background-image: none;
202	border: none;
203	border-right: 0.0625em solid var(--border);
204	border-radius: 0;
205	padding: 0.25em 0.5em;
206	transition: background-color 200ms ease-in-out;
207}
208
209button.tab-button:hover {
210	background-color: shade(var(--surface-variant), 1.2);
211}
212
213button.tab-button.tab-button-active {
214	background-color: shade(var(--surface-variant), 1.2);
215	border-bottom: 0.125em solid var(--accent);
216	font-weight: 600;
217}
218
219/* Tab title text */
220.tab-title {
221	font-size: 0.75em;
222	color: var(--text);
223	font-weight: 500;
224}
225`
226}
227
228// generateOmniboxCSS creates omnibox styles.
229// Uses em units for scalable UI.
230func generateOmniboxCSS(p Palette) string {
231	return `/* ===== Omnibox Styling ===== */
232
233window.standalone-omnibox-window {
234	background-color: transparent;
235}
236
237/* Omnibox outer container - for positioning in overlay */
238.omnibox-outer {
239	/* Positioning is handled via SetHalign/SetValign/SetMarginTop in Go */
240}
241
242/* Omnibox main container - the visible popup */
243.omnibox-container {
244	background-color: var(--surface);
245	background-image: none;
246	border: 0.09375em solid alpha(var(--border), 0.9);
247	border-radius: 0.1875em;
248	box-shadow: 0 0.875em 2.5em alpha(black, 0.24), 0 0.0625em 0 alpha(white, 0.05) inset;
249	padding: 0;
250}
251
252/* Header with History/Favorites toggle */
253.omnibox-header {
254	background-color: var(--surface);
255	background-image: none;
256	border-bottom: 0.0625em solid alpha(var(--border), 0.75);
257	padding: 0.375em 0.75em;
258}
259
260.omnibox-header-btn {
261	background-color: alpha(var(--surface), 0.12);
262	background-image: none;
263	border: 0.0625em solid transparent;
264	border-radius: 0.125em;
265	padding: 0.25em 0.75em;
266	margin-right: 0.5em;
267	font-size: 0.8125em;
268	font-weight: 500;
269	color: var(--muted);
270	transition: all 100ms ease-in-out;
271}
272
273.omnibox-header-btn:hover {
274	background-color: alpha(var(--accent), 0.14);
275	border-color: alpha(var(--accent), 0.18);
276	color: var(--text);
277}
278
279.omnibox-header-btn.omnibox-header-active {
280	background-color: alpha(var(--accent), 0.18);
281	border-color: alpha(var(--accent), 0.28);
282	color: var(--accent);
283	font-weight: 600;
284}
285
286/* Search entry field */
287entry.omnibox-entry,
288entry.omnibox-entry > text {
289	background-color: var(--surface-variant);
290	background-image: none;
291	color: var(--control-text);
292	caret-color: var(--accent);
293}
294
295entry.omnibox-entry {
296	border: 0.0625em solid alpha(var(--border), 0.82);
297	border-radius: 0.125em;
298	padding: 0.625em 0.75em;
299	margin: 0.5em 0.75em;
300	font-size: 1em;
301	box-shadow: 0 0.0625em 0 alpha(white, 0.04) inset;
302}
303
304entry.omnibox-entry:focus,
305entry.omnibox-entry:focus-within,
306entry.omnibox-entry:focus-visible,
307entry.omnibox-entry:focus > text,
308entry.omnibox-entry:focus-within > text,
309entry.omnibox-entry:focus-visible > text {
310	border-color: var(--accent);
311	background-color: shade(var(--surface-variant), 1.08);
312	background-image: none;
313	outline-style: none;
314	outline-width: 0px;
315	outline-color: transparent;
316}
317
318/* Override any internal focus styling */
319entry.omnibox-entry > text:focus,
320entry.omnibox-entry > text:focus-visible {
321	outline-style: none;
322	outline-color: transparent;
323}
324
325/* Scrolled window for suggestions */
326.omnibox-scrolled {
327	background-color: var(--surface);
328	border-top: 0.0625em solid alpha(var(--border), 0.65);
329}
330
331/* List box */
332.omnibox-listbox {
333	background-color: transparent;
334}
335
336/* Suggestion rows */
337.omnibox-row {
338	padding: 0.5em 0.75em;
339	margin: 0;
340	border-radius: 0;
341	border-left: 0.1875em solid transparent;
342	border-bottom: 0.0625em solid alpha(var(--border), 0.35);
343	background-color: var(--surface);
344	background-image: none;
345	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
346	min-height: 2.75em;
347}
348
349.omnibox-row:last-child {
350	border-bottom: none;
351}
352
353.omnibox-row:hover {
354	background-color: alpha(var(--accent), 0.12);
355	border-left: 0.1875em solid var(--accent);
356}
357
358.omnibox-row:selected {
359	background-color: alpha(var(--accent), 0.2);
360	border-left: 0.1875em solid var(--accent);
361}
362
363/* Favorite indicator in history mode - keep the row neutral and use the left selection stripe */
364.omnibox-row.omnibox-row-favorite {
365	background-color: transparent;
366	border-left: 0.1875em solid var(--warning);
367}
368
369.omnibox-row.omnibox-row-favorite:hover {
370	background-color: alpha(var(--accent), 0.12);
371	border-left: 0.1875em solid var(--warning);
372}
373
374.omnibox-row.omnibox-row-favorite:selected {
375	background-color: alpha(var(--accent), 0.2);
376	border-left: 0.1875em solid var(--warning);
377}
378
379/* Favicon in omnibox rows */
380.omnibox-favicon {
381	min-width: 1em;
382	min-height: 1em;
383	margin-right: 0.5em;
384}
385
386/* Suggestion title/URL */
387.omnibox-suggestion-title {
388	font-size: 0.875em;
389	color: var(--text);
390	font-weight: 500;
391}
392
393/* Also style labels inside omnibox rows directly */
394.omnibox-row label {
395	color: var(--text);
396}
397
398.omnibox-row:selected .omnibox-suggestion-title {
399	color: var(--text);
400	font-weight: 500;
401}
402
403/* URL text below title - use .omnibox-row prefix for specificity over .omnibox-row label */
404.omnibox-row .omnibox-suggestion-url {
405	font-size: 0.75em;
406	color: alpha(var(--muted), 0.72);
407	font-weight: 400;
408}
409
410.omnibox-row .omnibox-favorite-star {
411	color: mix(var(--warning), var(--muted), 0.45);
412	opacity: 0.75;
413	margin-left: 0.625em;
414	margin-right: 0.125em;
415	font-family: var(--font-mono);
416	font-size: 1em;
417	font-weight: 700;
418}
419
420.omnibox-row:selected .omnibox-favorite-star {
421	color: mix(var(--warning), var(--text), 0.35);
422	opacity: 0.88;
423}
424
425.omnibox-row:selected .omnibox-suggestion-url {
426	color: alpha(var(--muted), 0.75);
427}
428
429/* Keyboard shortcut badge */
430.omnibox-shortcut-badge {
431	background-color: alpha(var(--muted), 0.3);
432	color: var(--muted);
433	border-radius: 0.25em;
434	padding: 0.125em 0.375em;
435	font-size: 0.625em;
436	font-weight: 500;
437	font-family: var(--font-mono);
438	margin-left: 0.5em;
439}
440
441.omnibox-row:hover .omnibox-shortcut-badge {
442	background-color: alpha(var(--accent), 0.2);
443	color: var(--accent);
444}
445
446.omnibox-row:selected .omnibox-shortcut-badge {
447	background-color: alpha(var(--accent), 0.3);
448	color: var(--accent);
449}
450
451/* Bang indicator badge in omnibox header */
452.omnibox-bang-badge {
453	background-color: alpha(var(--accent), 0.18);
454	color: var(--accent);
455	border-radius: 0.25em;
456	padding: 0.125em 0.5em;
457	font-size: 0.6875em;
458	font-weight: 600;
459}
460
461/* Header badge used for omnibox state */
462.omnibox-header-badge {
463	background-color: alpha(var(--surface-variant), 0.65);
464	color: var(--muted);
465	border: 0.0625em solid alpha(var(--border), 0.85);
466	border-radius: 0.1875em;
467	padding: 0.0625em 0.375em;
468	font-size: 0.6875em;
469	font-weight: 500;
470}
471
472/* Zoom indicator in omnibox header */
473.omnibox-zoom-indicator {
474	background-color: alpha(var(--accent), 0.2);
475	color: var(--accent);
476	border-radius: 0.25em;
477	padding: 0.125em 0.5em;
478	font-size: 0.6875em;
479	font-weight: 600;
480	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
481}
482
483entry.omnibox-entry.omnibox-entry-bang-active,
484entry.omnibox-entry.omnibox-entry-bang-active:focus,
485entry.omnibox-entry.omnibox-entry-bang-active:focus-within,
486entry.omnibox-entry.omnibox-entry-bang-active:focus-visible,
487entry.omnibox-entry.omnibox-entry-bang-active > text,
488entry.omnibox-entry.omnibox-entry-bang-active:focus > text,
489entry.omnibox-entry.omnibox-entry-bang-active:focus-within > text,
490entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text {
491	border-color: var(--accent);
492	background-color: shade(var(--surface-variant), 1.08);
493	background-image: none;
494}
495
496.omnibox-row.omnibox-row-bang .omnibox-suggestion-title {
497	color: var(--accent);
498	font-weight: 600;
499}
500
501/* Ghost text autocomplete overlay - positioned to align with entry text */
502.omnibox-ghost {
503	font-size: 1em;
504	color: var(--control-text);
505	margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */
506	background-color: transparent;
507}
508
509/* Multiplexer: segmented status line, square prompt, and strong active rail. */
510.omnibox-container.omnibox-style-multiplexer {
511	border-radius: 0;
512	border-color: var(--border);
513	box-shadow: 0 1.125em 3em alpha(black, 0.34);
514}
515
516.omnibox-style-multiplexer .omnibox-header {
517	padding: 0;
518	min-height: 2em;
519	background-color: shade(var(--surface), 0.94);
520}
521
522.omnibox-style-multiplexer .omnibox-header-btn {
523	margin: 0;
524	padding: 0.375em 1em;
525	border: none;
526	border-right: 0.0625em solid var(--border);
527	border-radius: 0;
528	font-family: var(--font-mono);
529	font-size: 0.75em;
530	font-weight: 600;
531}
532
533.omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active {
534	background-color: var(--accent);
535	color: var(--accent-text);
536}
537
538.omnibox-style-multiplexer .omnibox-header-badge {
539	padding: 0.1875em 0.5em;
540	background-color: transparent;
541	border: none;
542	color: var(--muted);
543	font-family: var(--font-mono);
544	font-size: 0.6875em;
545	font-weight: 600;
546}
547
548.omnibox-style-multiplexer .omnibox-bang-badge,
549.omnibox-style-multiplexer .omnibox-zoom-indicator,
550.omnibox-style-multiplexer .omnibox-shortcut-badge {
551	border-radius: 0;
552	font-family: var(--font-mono);
553}
554
555.omnibox-style-multiplexer entry.omnibox-entry {
556	margin: 0.75em;
557	border-radius: 0;
558	border-left: 0.25em solid var(--accent);
559	font-family: var(--font-mono);
560}
561
562.omnibox-style-multiplexer .omnibox-row {
563	border-left-width: 0.25em;
564}
565
566.omnibox-style-multiplexer .omnibox-row:selected {
567	background-color: alpha(var(--accent), 0.14);
568	box-shadow: 0.1875em 0 0 var(--accent) inset;
569}
570
571.omnibox-style-multiplexer .omnibox-row.omnibox-row-favorite:selected {
572	border-left-color: var(--warning);
573	box-shadow: 0.1875em 0 0 var(--warning) inset;
574}
575
576.omnibox-style-multiplexer .omnibox-favorite-star {
577	min-width: 1.25em;
578	margin: 0 0.375em 0 0.125em;
579	padding: 0;
580	color: var(--warning);
581	opacity: 0.85;
582}
583
584/* Command: a calmer command palette with a floating input and compact tabs. */
585.omnibox-container.omnibox-style-command {
586	border: 0.125em solid alpha(var(--border), 0.95);
587	border-radius: 0.625em;
588	box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset;
589	padding: 0.375em;
590}
591
592.omnibox-style-command .omnibox-header {
593	border-bottom: none;
594	padding: 0.25em 0.375em 0.125em;
595}
596
597.omnibox-style-command .omnibox-header-btn {
598	border-radius: 1em;
599	padding: 0.25em 0.75em;
600	font-size: 0.75em;
601}
602
603.omnibox-style-command .omnibox-header-btn.omnibox-header-active {
604	background-color: alpha(var(--accent), 0.13);
605	border-color: transparent;
606}
607
608.omnibox-style-command .omnibox-header-badge {
609	padding: 0.1875em 0.5em;
610	background-color: transparent;
611	border: none;
612	color: var(--muted);
613	font-family: var(--font-mono);
614	font-size: 0.6875em;
615	font-weight: 600;
616}
617
618.omnibox-style-command entry.omnibox-entry {
619	margin: 0.5em 0.375em 0.75em;
620	padding: 0.75em 0.875em;
621	border-radius: 0.375em;
622	font-family: var(--font-mono);
623	box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12);
624}
625
626.omnibox-style-command .omnibox-scrolled {
627	border-top-color: alpha(var(--border), 0.35);
628}
629
630.omnibox-style-command .omnibox-row {
631	margin: 0.125em 0.25em;
632	border: none;
633	border-radius: 0.25em;
634}
635
636.omnibox-style-command .omnibox-row:selected {
637	background-color: alpha(var(--accent), 0.16);
638	border-left-color: transparent;
639}
640
641.omnibox-style-command .omnibox-row.omnibox-row-favorite:selected {
642	border-left: 0.1875em solid var(--warning);
643}
644
645.omnibox-style-command .omnibox-favorite-star {
646	min-width: 1.25em;
647	margin: 0 0.375em 0 0.125em;
648	padding: 0;
649	color: var(--warning);
650	opacity: 0.85;
651}
652
653/* Minimal: terminal typography and hairline structure with almost no chrome. */
654.omnibox-container.omnibox-style-minimal {
655	border-radius: 0;
656	box-shadow: 0 0.75em 2em alpha(black, 0.2);
657}
658
659.omnibox-style-minimal .omnibox-header {
660	padding: 0.25em 0.75em;
661	background-color: var(--surface);
662}
663
664.omnibox-style-minimal .omnibox-header-btn {
665	margin-right: 1em;
666	padding: 0.25em 0;
667	border: none;
668	border-bottom: 0.125em solid transparent;
669	border-radius: 0;
670	background-color: transparent;
671	font-family: var(--font-mono);
672	font-size: 0.75em;
673}
674
675.omnibox-style-minimal .omnibox-header-btn.omnibox-header-active {
676	background-color: transparent;
677	border-bottom-color: var(--accent);
678}
679
680.omnibox-style-minimal entry.omnibox-entry {
681	margin: 0;
682	padding: 0.75em;
683	border: none;
684	border-bottom: 0.0625em solid var(--border);
685	border-radius: 0;
686	background-color: var(--surface);
687	font-family: var(--font-mono);
688	box-shadow: none;
689}
690
691.omnibox-style-minimal .omnibox-scrolled {
692	border-top: none;
693}
694
695.omnibox-style-minimal .omnibox-row {
696	padding-top: 0.4375em;
697	padding-bottom: 0.4375em;
698	border-left-width: 0.125em;
699	font-family: var(--font-mono);
700}
701
702.omnibox-style-minimal .omnibox-row.omnibox-row-favorite:selected {
703	border-left: 0.125em solid var(--warning);
704}
705
706.omnibox-style-minimal .omnibox-favorite-star {
707	color: var(--warning);
708	opacity: 0.85;
709}
710
711.omnibox-style-minimal .omnibox-header-badge {
712	background-color: transparent;
713	border: none;
714	color: var(--muted);
715	font-family: var(--font-mono);
716}
717
718.omnibox-style-minimal .omnibox-shortcut-badge {
719	background-color: transparent;
720	border: 0.0625em solid alpha(var(--border), 0.7);
721	border-radius: 0;
722}
723`
724}
725
726// generateFindBarCSS creates find bar styles.
727// Uses em units for scalable UI.
728func generateFindBarCSS(p Palette) string {
729	_ = p
730	return `/* ===== Find Bar Styling ===== */
731.find-bar-outer {
732	margin: 0.5em;
733}
734
735.find-bar-container {
736	background-color: var(--surface-variant);
737	border: 0.0625em solid var(--border);
738	border-radius: 0.25em;
739	box-shadow: 0 0.25em 0.75em alpha(black, 0.2);
740	padding: 0.5em;
741	min-width: 20em;
742}
743
744.find-bar-input-row {
745	margin-bottom: 0.25em;
746}
747
748.find-bar-entry {
749	background-color: var(--bg);
750	color: var(--text);
751	border: 0.0625em solid var(--border);
752	border-radius: 0.1875em;
753	padding: 0.4em 0.5em;
754	caret-color: var(--accent);
755}
756
757entry.find-bar-entry:focus,
758entry.find-bar-entry:focus-within,
759entry.find-bar-entry:focus-visible {
760	border-color: var(--accent);
761	outline-style: none;
762	outline-width: 0px;
763	outline-color: transparent;
764}
765
766.find-bar-entry.not-found {
767	background-color: alpha(#ef4444, 0.15);
768	border-color: #ef4444;
769}
770
771.find-bar-count {
772	font-size: 0.8em;
773	color: var(--muted);
774	min-width: 3.5em;
775}
776
777.find-bar-count.find-bar-count-has {
778	color: var(--accent);
779}
780
781.find-bar-nav,
782.find-bar-close,
783.find-bar-toggle {
784	background-image: none;
785	background-color: alpha(var(--bg), 0.6);
786	border: 0.0625em solid var(--border);
787	border-radius: 0.1875em;
788	padding: 0.25em 0.5em;
789	transition: background-color 120ms ease-in-out, border-color 120ms ease-in-out;
790}
791
792.find-bar-nav:hover,
793.find-bar-close:hover,
794.find-bar-toggle:hover {
795	background-color: alpha(var(--accent), 0.15);
796}
797
798.find-bar-toggle:checked {
799	background-color: alpha(var(--accent), 0.2);
800	border-color: var(--accent);
801	color: var(--accent);
802}
803`
804}
805
806// generatePaneCSS creates pane border styles.
807// Uses em units for scalable UI.
808// generateVimModeCSS creates the pane-local Vim Mode accent and pulse styling.
809// It uses --vim-mode-color (from workspace.styling.vim_mode_color).
810func generateVimModeCSS(transitionDurationMs int) string {
811	if transitionDurationMs <= 0 {
812		transitionDurationMs = defaultTransitionDurationMs
813	}
814	normalPulseMs := transitionDurationMs * 3
815	fastPulseMs := transitionDurationMs * 6
816	return fmt.Sprintf(`/* ===== Vim Mode Styling ===== */
817
818/* Vim mode frame on the active pane; pulses use this same widget. */
819.vim-mode-active {
820	box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35);
821	border-radius: 0;
822}
823
824/* Frame pulse keyframes — start/end at the active accent state so the
825   frame keeps its Vim mode border while briefly flaring brighter. The separate
826   -anim-a and -anim-b pairs are required to restart repeated pulse animations. */
827@keyframes vim-mode-overlay-pulse-anim-a {
828	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
829	35%%  { box-shadow: inset 0 0 0 0.2em alpha(var(--vim-mode-color), 0.78); }
830	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
831}
832
833@keyframes vim-mode-overlay-pulse-anim-b {
834	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
835	35%%  { box-shadow: inset 0 0 0 0.2em alpha(var(--vim-mode-color), 0.78); }
836	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
837}
838
839@keyframes vim-mode-overlay-pulse-fast-anim-a {
840	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
841	25%%  { box-shadow: inset 0 0 0 0.26em alpha(var(--vim-mode-color), 0.95); }
842	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
843}
844
845@keyframes vim-mode-overlay-pulse-fast-anim-b {
846	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
847	25%%  { box-shadow: inset 0 0 0 0.26em alpha(var(--vim-mode-color), 0.95); }
848	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
849}
850
851/* Pane overlay scroll pulse — derived from transition_duration. */
852.vim-mode-pulse.vim-mode-pulse-cycle-a {
853	animation: vim-mode-overlay-pulse-anim-a %dms ease-in-out;
854}
855
856.vim-mode-pulse.vim-mode-pulse-cycle-b {
857	animation: vim-mode-overlay-pulse-anim-b %dms ease-in-out;
858}
859
860.vim-mode-pulse-fast.vim-mode-pulse-cycle-a {
861	animation: vim-mode-overlay-pulse-fast-anim-a %dms ease-in-out;
862}
863
864.vim-mode-pulse-fast.vim-mode-pulse-cycle-b {
865	animation: vim-mode-overlay-pulse-fast-anim-b %dms ease-in-out;
866}
867`, normalPulseMs, normalPulseMs, fastPulseMs, fastPulseMs)
868}
869
870func generatePaneCSS(p Palette) string {
871	return `/* ===== Pane Styling ===== */
872
873/* Pane overlay container - theme background prevents white flash */
874.pane-overlay {
875	background-color: var(--bg);
876}
877
878/* WebView widget - theme background prevents white flash during page load */
879.webview-themed {
880	background-color: var(--bg);
881}
882
883/* Loading skeleton - shown until WebView paints */
884.loading-skeleton {
885	background-color: var(--bg);
886}
887
888.loading-skeleton-spinner {
889	-gtk-icon-size: 2.1818em;
890	min-width: 2.1818em;
891	min-height: 2.1818em;
892	color: var(--muted);
893	opacity: 0.4;
894}
895
896.loading-skeleton-logo {
897	opacity: 0.08;
898	min-width: 17.4545em;
899	min-height: 17.4545em;
900}
901
902.loading-skeleton-version {
903	opacity: 0.25;
904	font-size: 0.8864em;
905	color: var(--muted);
906}
907
908/* Pane border - default transparent */
909.pane-border {
910	border: 0.0625em solid transparent;
911}
912
913/* Active pane - accent border */
914.pane-active {
915	border: 0.0625em solid var(--accent);
916}
917
918/* Hide active border when only one pane exists */
919.single-pane .pane-active {
920	border-color: transparent;
921}
922
923/* Mode frame: a click-through overlay drawn above web content. */
924.mode-frame-border {
925	background-color: transparent;
926	border-radius: 0;
927}
928
929.mode-frame-border.pane-mode-active {
930	box-shadow: inset 0 0 0 0.25em var(--pane-mode-color);
931}
932
933.mode-frame-border.tab-mode-active {
934	box-shadow: inset 0 0 0 0.25em var(--tab-mode-color);
935}
936
937.mode-frame-border.resize-mode-active {
938	box-shadow: inset 0 0 0 0.25em var(--resize-mode-color);
939}
940
941.mode-frame-border.prefix-mode-active {
942	box-shadow: inset 0 0 0 0.25em var(--pane-mode-color);
943}
944`
945}
946
947// generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
948// Text uses scalable units while vertical chrome stays compact in device pixels.
949func generateStackedPaneCSS(p Palette) string {
950	return `/* ===== Stacked Pane Styling ===== */
951
952/* Title bar for inactive panes in a stack */
953/* The title bar is now a Box with GestureClick, not wrapped in a button */
954.stacked-pane-titlebar {
955	background-color: var(--surface-variant);
956	color: var(--control-text);
957	border-bottom: 0.0625em solid var(--border);
958	padding: 4px 0.5em;
959	min-height: 24px;
960}
961
962/* Keep the background stable so --control-text retains its contrast.
963 * Indicate hover with an underline and the active pane with an accent border. */
964.stacked-pane-titlebar.active {
965	border-left: 0.1875em solid var(--accent);
966}
967
968/* Favicon image in title bar */
969.stacked-pane-titlebar image {
970	color: var(--control-text);
971	min-width: 1em;
972	min-height: 1em;
973	margin-right: 0.375em;
974}
975
976/* Title text in title bar */
977.stacked-pane-titlebar label {
978	color: var(--control-text);
979	font-size: 0.75em;
980	font-weight: 400;
981}
982
983/* Hover effect on title bar label */
984.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
985	text-decoration-line: underline;
986}
987
988/* Close button in stacked pane title bar.
989 * Its vertical footprint is intentionally smaller than the original scale-1 baseline. */
990.stacked-pane-close-button {
991	background: transparent;
992	border: none;
993	border-radius: 0.2727em;
994	padding: 3px;
995	margin: 0 2px;
996	min-width: 1.3636em;
997	min-height: 15px;
998	opacity: 0.4;
999	transition: opacity 100ms ease-in-out, background-color 100ms ease-in-out;
1000}
1001
1002.stacked-pane-close-button > image {
1003	margin: 0;
1004	padding: 0;
1005}
1006
1007.stacked-pane-close-button:hover {
1008	opacity: 1.0;
1009	background: alpha(var(--destructive), 0.2);
1010}
1011
1012.stacked-pane-close-button:hover > image {
1013	color: var(--destructive);
1014}
1015
1016/* Show close button more prominently when hovering title bar */
1017.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
1018	opacity: 0.7;
1019}
1020
1021.stacked-pane-close-button:focus {
1022	outline: none;
1023	box-shadow: none;
1024}
1025`
1026}
1027
1028// generateProgressBarCSS creates progress bar styles for page loading indication.
1029func generateProgressBarCSS(p Palette) string {
1030	return `/* ===== Progress Bar Styling ===== */
1031
1032/* Native GtkProgressBar with osd class */
1033progressbar.osd {
1034	min-height: 0.2727em;
1035}
1036
1037progressbar.osd trough {
1038	min-height: 0.2727em;
1039	min-width: 0.1364em;
1040	margin: 0;
1041	padding: 0;
1042	background-color: alpha(var(--bg), 0.3);
1043}
1044
1045progressbar.osd progress {
1046	min-height: 0.2727em;
1047	min-width: 0.1364em;
1048	margin: 0;
1049	padding: 0;
1050	background-color: var(--accent);
1051}
1052`
1053}
1054
1055func generateTouchpadNavigationIndicatorCSS(p Palette) string {
1056	_ = p
1057	return `/* ===== Touchpad Navigation Indicator Styling ===== */
1058
1059.touchpad-navigation-indicator {
1060	background-color: alpha(var(--surface), 0.90);
1061	color: var(--text);
1062	border: 0.0625em solid alpha(var(--border), 0.72);
1063	border-radius: 1.125em;
1064	padding: 0.75em 0.875em 0.8125em;
1065	opacity: 1;
1066	filter: blur(0px);
1067	backdrop-filter: blur(0.75em);
1068	box-shadow: 0 1em 2.25em alpha(black, 0.30),
1069	            0 0.0625em 0 alpha(white, 0.06) inset;
1070	transition: opacity 220ms ease-out,
1071	            filter 220ms ease-out,
1072	            backdrop-filter 220ms ease-out,
1073	            background-color 140ms ease-out,
1074	            border-color 140ms ease-out,
1075	            box-shadow 220ms ease-out;
1076}
1077
1078.touchpad-navigation-indicator.back {
1079	border-left: 0.1875em solid alpha(var(--accent), 0.82);
1080}
1081
1082.touchpad-navigation-indicator.forward {
1083	border-right: 0.1875em solid alpha(var(--accent), 0.82);
1084}
1085
1086.touchpad-navigation-indicator.threshold-reached {
1087	border-color: alpha(var(--accent), 0.95);
1088	background-color: alpha(var(--accent), 0.18);
1089	box-shadow: 0 1.125em 2.5em alpha(black, 0.34),
1090	            0 0 0 0.0625em alpha(var(--accent), 0.24) inset;
1091}
1092
1093.touchpad-navigation-indicator.hiding {
1094	opacity: 0;
1095	filter: blur(0.0625em);
1096	box-shadow: 0 0.375em 0.875em alpha(black, 0.10),
1097	            0 0 0 0.0625em alpha(var(--accent), 0.04) inset;
1098}
1099
1100.touchpad-navigation-header {
1101	min-width: 9.75em;
1102}
1103
1104.touchpad-navigation-icon {
1105	background-color: alpha(var(--accent), 0.16);
1106	color: var(--accent);
1107	border-radius: 999px;
1108	font-size: 0.95em;
1109	font-weight: 700;
1110	padding: 0.125em 0.375em;
1111}
1112
1113.touchpad-navigation-indicator.threshold-reached .touchpad-navigation-icon {
1114	background-color: var(--accent);
1115	color: var(--bg);
1116}
1117
1118.touchpad-navigation-label {
1119	color: var(--text);
1120	font-weight: 700;
1121	font-size: 0.92em;
1122	letter-spacing: 0.01em;
1123}
1124
1125progressbar.touchpad-navigation-progress {
1126	min-height: 0.3125em;
1127}
1128
1129progressbar.touchpad-navigation-progress trough {
1130	min-height: 0.3125em;
1131	min-width: 9.75em;
1132	border-radius: 999px;
1133	background-color: alpha(var(--muted), 0.24);
1134	box-shadow: 0 0.0625em 0 alpha(white, 0.06) inset;
1135}
1136
1137progressbar.touchpad-navigation-progress progress {
1138	min-height: 0.3125em;
1139	border-radius: 999px;
1140	background-color: var(--accent);
1141	box-shadow: 0 0 0.35em alpha(var(--accent), 0.35);
1142}
1143`
1144}
1145
1146// generateSessionManagerCSS creates session manager modal styles.
1147// Uses em units for scalable UI, matches omnibox styling patterns.
1148func generateSessionManagerCSS(p Palette) string {
1149	_ = p
1150	return `/* ===== Session Manager Styling ===== */
1151
1152/* Session manager outer container - for positioning in overlay */
1153.session-manager-outer {
1154	/* Positioning is handled via SetHalign/SetValign in Go */
1155}
1156
1157/* Session manager main container - the visible popup */
1158.session-manager-container {
1159	background-color: var(--surface-variant);
1160	border: 0.0625em solid var(--border);
1161	border-radius: 0.1875em;
1162	padding: 0;
1163	/* Width is owned by the Go sizing request so CSS must not force
1164	   growth beyond the percentage viewport limit. */
1165	/* Note: max-width not supported in GTK4 CSS, rely on container constraints */
1166}
1167
1168/* Header with title */
1169.session-manager-header {
1170	background-color: shade(var(--surface-variant), 1.1);
1171	border-bottom: 0.0625em solid var(--border);
1172	padding: 0.5em 0.75em;
1173}
1174
1175.session-manager-title {
1176	font-size: 0.9375em;
1177	font-weight: 600;
1178	color: var(--text);
1179}
1180
1181/* Scrolled window for session list */
1182.session-manager-scrolled {
1183	background-color: shade(var(--surface-variant), 0.95);
1184}
1185
1186/* List box */
1187.session-manager-list {
1188	background-color: transparent;
1189}
1190
1191/* Session rows */
1192.session-manager-row {
1193	padding: 0.5em 0.75em;
1194	margin: 0;
1195	border-radius: 0;
1196	border-left: 0.1875em solid transparent;
1197	border-bottom: 0.0625em solid alpha(var(--border), 0.5);
1198	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
1199	min-height: 2.75em;
1200}
1201
1202.session-manager-row:last-child {
1203	border-bottom: none;
1204}
1205
1206.session-manager-row:hover {
1207	background-color: alpha(var(--accent), 0.12);
1208	border-left: 0.1875em solid var(--accent);
1209}
1210
1211.session-manager-row:selected,
1212row:selected .session-manager-row {
1213	background-color: alpha(var(--accent), 0.2);
1214	border-left: 0.1875em solid var(--accent);
1215}
1216
1217/* Status indicator dot */
1218.session-status {
1219	font-size: 0.875em;
1220	min-width: 1em;
1221	margin-right: 0.5em;
1222}
1223
1224/* Current session - accent color dot */
1225.session-current .session-status {
1226	color: var(--accent);
1227}
1228
1229/* Active session (other instance) - muted dot */
1230.session-active .session-status {
1231	color: var(--muted);
1232}
1233
1234/* Exited session - dimmed appearance with subtle stop icon */
1235.session-exited {
1236	opacity: 0.75;
1237}
1238
1239.session-exited .session-status {
1240	color: var(--error);
1241	opacity: 0.5;
1242}
1243
1244/* Session ID labels */
1245.session-id-short {
1246	font-size: 0.875em;
1247	font-weight: 600;
1248	color: var(--accent);
1249	font-family: var(--font-mono);
1250}
1251
1252.session-id-prefix {
1253	font-size: 0.875em;
1254	font-weight: 400;
1255	color: var(--muted);
1256	font-family: var(--font-mono);
1257}
1258
1259.session-id-badge {
1260	font-size: 0.75em;
1261	font-weight: 500;
1262	color: var(--accent);
1263	margin-left: 0.5em;
1264}
1265
1266/* Tab/pane count label */
1267.session-count {
1268	font-size: 0.75em;
1269	color: var(--muted);
1270}
1271
1272/* Relative time label */
1273.session-time {
1274	font-size: 0.75em;
1275	color: var(--muted);
1276	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
1277}
1278
1279/* Section divider for EXITED sessions */
1280.session-divider {
1281	font-size: 0.6875em;
1282	font-weight: 600;
1283	color: var(--muted);
1284	text-transform: uppercase;
1285	letter-spacing: 0.05em;
1286	padding: 0.5em 0.75em 0.25em 0.75em;
1287	background-color: alpha(var(--border), 0.3);
1288	border-bottom: 0.0625em solid var(--border);
1289}
1290
1291/* Footer with keyboard shortcuts */
1292.session-manager-footer {
1293	background-color: shade(var(--surface-variant), 0.9);
1294	border-top: 0.0625em solid var(--border);
1295	padding: 0.375em 0.75em;
1296	font-size: 0.6875em;
1297	color: var(--muted);
1298	font-family: var(--font-mono);
1299}
1300
1301/* Session mode border */
1302.mode-frame-border.session-mode-active {
1303	box-shadow: inset 0 0 0 0.25em var(--session-mode-color);
1304}
1305`
1306}
1307
1308// generateContextMenuCSS creates shared context menu styles.
1309func generateContextMenuCSS() string {
1310	return `/* ===== Context Menu Styling ===== */
1311
1312popover.context-menu-popover {
1313	background-color: transparent;
1314	background-image: none;
1315}
1316
1317popover.context-menu-popover > contents {
1318	background-color: var(--surface);
1319	background-image: none;
1320	border: 0.0625em solid alpha(var(--border), 0.85);
1321	border-radius: 0.25em;
1322	box-shadow: 0 0.75em 1.75em alpha(var(--bg), 0.28), 0 0.0625em 0 alpha(var(--border), 0.18) inset;
1323	padding: 0;
1324}
1325
1326.context-menu {
1327	padding: 0.125em 0;
1328	min-width: 10.5em;
1329}
1330
1331.context-menu button.context-menu-item {
1332	background-color: transparent;
1333	background-image: none;
1334	border: none;
1335	border-radius: 0;
1336	color: var(--text);
1337	padding: 0.25em 0.75em;
1338	margin: 0;
1339	min-height: 1.75em;
1340	transition: background-color 120ms ease-in-out, color 120ms ease-in-out;
1341}
1342
1343.context-menu button.context-menu-item:hover,
1344.context-menu button.context-menu-item:focus,
1345.context-menu button.context-menu-item:active {
1346	background-color: alpha(var(--surface-variant), 0.9);
1347	color: var(--text);
1348}
1349
1350.context-menu button.context-menu-item:active {
1351	background-color: alpha(var(--accent), 0.16);
1352}
1353
1354.context-menu button.context-menu-item:disabled {
1355	color: var(--muted);
1356	opacity: 0.7;
1357}
1358
1359.context-menu-separator {
1360	background-color: alpha(var(--border), 0.55);
1361	margin: 0.25em 0.5em;
1362	min-height: 0.0625em;
1363}
1364`
1365}
1366
1367// generateToasterCSS creates toast notification styles.
1368// Uses em units for scalable UI.
1369func generateToasterCSS(p Palette) string {
1370	return `/* ===== Toaster Styling ===== */
1371
1372/* Toast notification container */
1373.toast {
1374	background-color: var(--surface-variant);
1375	border-radius: 0.25em;
1376	padding: 0.375em 0.625em;
1377	margin: 0.5em;
1378	font-size: 0.875em;
1379	font-weight: 600;
1380	box-shadow: 0 2px 8px alpha(black, 0.3);
1381	min-width: 2.5em;
1382}
1383
1384/* Toast level: info (default, accent color) */
1385.toast-info {
1386	background-color: alpha(var(--accent), 0.9);
1387	color: var(--accent-text);
1388}
1389
1390/* Toast level: success */
1391.toast-success {
1392	background-color: alpha(var(--success), 0.9);
1393	color: var(--success-text);
1394}
1395
1396/* Toast level: warning */
1397.toast-warning {
1398	background-color: alpha(var(--warning), 0.9);
1399	color: var(--warning-text);
1400}
1401
1402/* Toast level: error */
1403.toast-error {
1404	background-color: alpha(var(--destructive), 0.9);
1405	color: var(--destructive-text);
1406}
1407`
1408}
1409
1410// generateLinkStatusCSS creates link status overlay styles.
1411// Uses CSS transitions for fade-in/fade-out effect.
1412func generateLinkStatusCSS(p Palette) string {
1413	return `/* ===== Link Status Overlay Styling ===== */
1414
1415/* Link status container - bottom left positioning */
1416.link-status {
1417	background-color: alpha(var(--surface-variant), 0.95);
1418	border-radius: 0.25em 0.25em 0 0;
1419	padding: 0.25em 0.5em;
1420	margin: 0;
1421	font-size: 0.75em;
1422	/* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
1423	box-shadow: 0 -1px 4px alpha(black, 0.15);
1424
1425	/* Fade transition - hidden by default */
1426	opacity: 0;
1427	transition: opacity 150ms ease-in-out;
1428}
1429
1430/* Visible state - triggered by adding .visible class */
1431.link-status.visible {
1432	opacity: 1;
1433}
1434
1435/* Link status label text */
1436.link-status label {
1437	color: var(--control-muted);
1438}
1439`
1440}
1441
1442func generateFloatingPaneCSS(p Palette) string {
1443	_ = p
1444	return `/* ===== Floating Pane Styling ===== */
1445.floating-pane-outer {
1446	/* Positioning handled in Go via overlay alignment and margins. */
1447}
1448
1449.floating-pane-container {
1450	background-color: var(--surface-variant);
1451	border: 0.0625em solid var(--border);
1452	border-radius: 0.25em;
1453	box-shadow: 0 0.5em 1.5em alpha(black, 0.25);
1454	opacity: 0;
1455	transition: opacity 110ms ease-out;
1456}
1457
1458.floating-pane-container.floating-pane-visible {
1459	opacity: 1;
1460}
1461
1462.floating-pane-container.pane-active {
1463	border-color: var(--accent);
1464}
1465
1466.floating-pane-omnibox {
1467	background-color: shade(var(--surface-variant), 1.08);
1468	border-bottom: 0.0625em solid var(--border);
1469	padding: 0.375em 0.5em;
1470}
1471
1472.floating-pane-content {
1473	background-color: var(--bg);
1474	min-height: 12em;
1475}
1476`
1477}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close