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