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/* Toast with custom styling (mode indicator toasters) */
1409.toast-custom {
1410 color: #ffffff;
1411}
1412
1413/* Mode-specific toast colors */
1414.toast-pane-mode {
1415 background-color: var(--pane-mode-color);
1416 color: var(--pane-mode-text);
1417}
1418
1419.toast-vim-mode {
1420 background-color: var(--vim-mode-color);
1421 color: var(--vim-mode-text);
1422}
1423
1424.toast-tab-mode {
1425 background-color: var(--tab-mode-color);
1426 color: var(--tab-mode-text);
1427}
1428
1429.toast-session-mode {
1430 background-color: var(--session-mode-color);
1431 color: var(--session-mode-text);
1432}
1433
1434.toast-resize-mode {
1435 background-color: var(--resize-mode-color);
1436 color: var(--resize-mode-text);
1437}
1438
1439.toast-prefix-mode {
1440 background-color: var(--pane-mode-color);
1441 color: var(--pane-mode-text);
1442}
1443`
1444}
1445
1446// generateLinkStatusCSS creates link status overlay styles.
1447// Uses CSS transitions for fade-in/fade-out effect.
1448func generateLinkStatusCSS(p Palette) string {
1449 return `/* ===== Link Status Overlay Styling ===== */
1450
1451/* Link status container - bottom left positioning */
1452.link-status {
1453 background-color: alpha(var(--surface-variant), 0.95);
1454 border-radius: 0.25em 0.25em 0 0;
1455 padding: 0.25em 0.5em;
1456 margin: 0;
1457 font-size: 0.75em;
1458 /* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
1459 box-shadow: 0 -1px 4px alpha(black, 0.15);
1460
1461 /* Fade transition - hidden by default */
1462 opacity: 0;
1463 transition: opacity 150ms ease-in-out;
1464}
1465
1466/* Visible state - triggered by adding .visible class */
1467.link-status.visible {
1468 opacity: 1;
1469}
1470
1471/* Link status label text */
1472.link-status label {
1473 color: var(--control-muted);
1474}
1475`
1476}
1477
1478func generateFloatingPaneCSS(p Palette) string {
1479 _ = p
1480 return `/* ===== Floating Pane Styling ===== */
1481.floating-pane-outer {
1482 /* Positioning handled in Go via overlay alignment and margins. */
1483}
1484
1485.floating-pane-container {
1486 background-color: var(--surface-variant);
1487 border: 0.0625em solid var(--border);
1488 border-radius: 0.25em;
1489 box-shadow: 0 0.5em 1.5em alpha(black, 0.25);
1490 opacity: 0;
1491 transition: opacity 110ms ease-out;
1492}
1493
1494.floating-pane-container.floating-pane-visible {
1495 opacity: 1;
1496}
1497
1498.floating-pane-container.pane-active {
1499 border-color: var(--accent);
1500}
1501
1502.floating-pane-omnibox {
1503 background-color: shade(var(--surface-variant), 1.08);
1504 border-bottom: 0.0625em solid var(--border);
1505 padding: 0.375em 0.5em;
1506}
1507
1508.floating-pane-content {
1509 background-color: var(--bg);
1510 min-height: 12em;
1511}
1512`
1513}