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(--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(--muted);
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 border-bottom: 0.0625em solid var(--border);
749 padding: 0.25em 0.5em;
750 min-height: 1.5em;
751}
752
753/* Clickable title bar - hover styling */
754.stacked-pane-titlebar.stacked-pane-title-clickable {
755 transition: background-color 150ms ease-in-out;
756}
757
758.stacked-pane-titlebar.stacked-pane-title-clickable:hover {
759 background-color: shade(var(--surface-variant), 1.15);
760}
761
762/* Title bar content box */
763.stacked-pane-titlebar.active {
764 background-color: shade(var(--surface-variant), 1.2);
765 border-left: 0.1875em solid var(--accent);
766}
767
768/* Favicon image in title bar */
769.stacked-pane-titlebar image {
770 min-width: 1em;
771 min-height: 1em;
772 margin-right: 0.375em;
773}
774
775/* Title text in title bar */
776.stacked-pane-titlebar label {
777 color: var(--text);
778 font-size: 0.75em;
779 font-weight: 400;
780}
781
782/* Hover effect on title bar label */
783.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
784 color: var(--accent);
785}
786
787/* Close button in stacked pane title bar */
788.stacked-pane-close-button {
789 background: transparent;
790 border: none;
791 border-radius: 4px;
792 padding: 4px;
793 margin: 0 2px;
794 min-width: 20px;
795 min-height: 20px;
796 opacity: 0.4;
797 transition: opacity 100ms ease-in-out, background-color 100ms ease-in-out;
798}
799
800.stacked-pane-close-button > image {
801 margin: 0;
802 padding: 0;
803}
804
805.stacked-pane-close-button:hover {
806 opacity: 1.0;
807 background: alpha(var(--destructive), 0.2);
808}
809
810.stacked-pane-close-button:hover > image {
811 color: var(--destructive);
812}
813
814/* Show close button more prominently when hovering title bar */
815.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
816 opacity: 0.7;
817}
818
819.stacked-pane-close-button:focus {
820 outline: none;
821 box-shadow: none;
822}
823`
824}
825
826// generateProgressBarCSS creates progress bar styles for page loading indication.
827func generateProgressBarCSS(p Palette) string {
828 return `/* ===== Progress Bar Styling ===== */
829
830/* Native GtkProgressBar with osd class */
831progressbar.osd {
832 min-height: 4px;
833}
834
835progressbar.osd trough {
836 min-height: 4px;
837 min-width: 2px;
838 margin: 0;
839 padding: 0;
840 background-color: alpha(var(--bg), 0.3);
841}
842
843progressbar.osd progress {
844 min-height: 4px;
845 min-width: 2px;
846 margin: 0;
847 padding: 0;
848 background-color: var(--accent);
849}
850`
851}
852
853func generateTouchpadNavigationIndicatorCSS(p Palette) string {
854 _ = p
855 return `/* ===== Touchpad Navigation Indicator Styling ===== */
856
857.touchpad-navigation-indicator {
858 background-color: alpha(var(--surface), 0.90);
859 color: var(--text);
860 border: 0.0625em solid alpha(var(--border), 0.72);
861 border-radius: 1.125em;
862 padding: 0.75em 0.875em 0.8125em;
863 opacity: 1;
864 filter: blur(0px);
865 backdrop-filter: blur(0.75em);
866 box-shadow: 0 1em 2.25em alpha(black, 0.30),
867 0 0.0625em 0 alpha(white, 0.06) inset;
868 transition: opacity 220ms ease-out,
869 filter 220ms ease-out,
870 backdrop-filter 220ms ease-out,
871 background-color 140ms ease-out,
872 border-color 140ms ease-out,
873 box-shadow 220ms ease-out;
874}
875
876.touchpad-navigation-indicator.back {
877 border-left: 0.1875em solid alpha(var(--accent), 0.82);
878}
879
880.touchpad-navigation-indicator.forward {
881 border-right: 0.1875em solid alpha(var(--accent), 0.82);
882}
883
884.touchpad-navigation-indicator.threshold-reached {
885 border-color: alpha(var(--accent), 0.95);
886 background-color: alpha(var(--accent), 0.18);
887 box-shadow: 0 1.125em 2.5em alpha(black, 0.34),
888 0 0 0 0.0625em alpha(var(--accent), 0.24) inset;
889}
890
891.touchpad-navigation-indicator.hiding {
892 opacity: 0;
893 filter: blur(0.0625em);
894 box-shadow: 0 0.375em 0.875em alpha(black, 0.10),
895 0 0 0 0.0625em alpha(var(--accent), 0.04) inset;
896}
897
898.touchpad-navigation-header {
899 min-width: 9.75em;
900}
901
902.touchpad-navigation-icon {
903 background-color: alpha(var(--accent), 0.16);
904 color: var(--accent);
905 border-radius: 999px;
906 font-size: 0.95em;
907 font-weight: 700;
908 padding: 0.125em 0.375em;
909}
910
911.touchpad-navigation-indicator.threshold-reached .touchpad-navigation-icon {
912 background-color: var(--accent);
913 color: var(--bg);
914}
915
916.touchpad-navigation-label {
917 color: var(--text);
918 font-weight: 700;
919 font-size: 0.92em;
920 letter-spacing: 0.01em;
921}
922
923progressbar.touchpad-navigation-progress {
924 min-height: 0.3125em;
925}
926
927progressbar.touchpad-navigation-progress trough {
928 min-height: 0.3125em;
929 min-width: 9.75em;
930 border-radius: 999px;
931 background-color: alpha(var(--muted), 0.24);
932 box-shadow: 0 0.0625em 0 alpha(white, 0.06) inset;
933}
934
935progressbar.touchpad-navigation-progress progress {
936 min-height: 0.3125em;
937 border-radius: 999px;
938 background-color: var(--accent);
939 box-shadow: 0 0 0.35em alpha(var(--accent), 0.35);
940}
941`
942}
943
944// generateSessionManagerCSS creates session manager modal styles.
945// Uses em units for scalable UI, matches omnibox styling patterns.
946func generateSessionManagerCSS(p Palette) string {
947 _ = p
948 return `/* ===== Session Manager Styling ===== */
949
950/* Session manager outer container - for positioning in overlay */
951.session-manager-outer {
952 /* Positioning is handled via SetHalign/SetValign in Go */
953}
954
955/* Session manager main container - the visible popup */
956.session-manager-container {
957 background-color: var(--surface-variant);
958 border: 0.0625em solid var(--border);
959 border-radius: 0.1875em;
960 padding: 0;
961 min-width: 28em;
962 /* Note: max-width not supported in GTK4 CSS, rely on container constraints */
963}
964
965/* Header with title */
966.session-manager-header {
967 background-color: shade(var(--surface-variant), 1.1);
968 border-bottom: 0.0625em solid var(--border);
969 padding: 0.5em 0.75em;
970}
971
972.session-manager-title {
973 font-size: 0.9375em;
974 font-weight: 600;
975 color: var(--text);
976}
977
978/* Scrolled window for session list */
979.session-manager-scrolled {
980 background-color: shade(var(--surface-variant), 0.95);
981}
982
983/* List box */
984.session-manager-list {
985 background-color: transparent;
986}
987
988/* Session rows */
989.session-manager-row {
990 padding: 0.5em 0.75em;
991 margin: 0;
992 border-radius: 0;
993 border-left: 0.1875em solid transparent;
994 border-bottom: 0.0625em solid alpha(var(--border), 0.5);
995 transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
996 min-height: 2.75em;
997}
998
999.session-manager-row:last-child {
1000 border-bottom: none;
1001}
1002
1003.session-manager-row:hover {
1004 background-color: alpha(var(--accent), 0.12);
1005 border-left: 0.1875em solid var(--accent);
1006}
1007
1008.session-manager-row:selected,
1009row:selected .session-manager-row {
1010 background-color: alpha(var(--accent), 0.2);
1011 border-left: 0.1875em solid var(--accent);
1012}
1013
1014/* Status indicator dot */
1015.session-status {
1016 font-size: 0.875em;
1017 min-width: 1em;
1018 margin-right: 0.5em;
1019}
1020
1021/* Current session - accent color dot */
1022.session-current .session-status {
1023 color: var(--accent);
1024}
1025
1026/* Active session (other instance) - muted dot */
1027.session-active .session-status {
1028 color: var(--muted);
1029}
1030
1031/* Exited session - dimmed appearance with subtle stop icon */
1032.session-exited {
1033 opacity: 0.75;
1034}
1035
1036.session-exited .session-status {
1037 color: var(--error);
1038 opacity: 0.5;
1039}
1040
1041/* Session ID labels */
1042.session-id-short {
1043 font-size: 0.875em;
1044 font-weight: 600;
1045 color: var(--accent);
1046 font-family: var(--font-mono);
1047}
1048
1049.session-id-prefix {
1050 font-size: 0.875em;
1051 font-weight: 400;
1052 color: var(--muted);
1053 font-family: var(--font-mono);
1054}
1055
1056.session-id-badge {
1057 font-size: 0.75em;
1058 font-weight: 500;
1059 color: var(--accent);
1060 margin-left: 0.5em;
1061}
1062
1063/* Tab/pane count label */
1064.session-count {
1065 font-size: 0.75em;
1066 color: var(--muted);
1067}
1068
1069/* Relative time label */
1070.session-time {
1071 font-size: 0.75em;
1072 color: var(--muted);
1073 /* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
1074}
1075
1076/* Section divider for EXITED sessions */
1077.session-divider {
1078 font-size: 0.6875em;
1079 font-weight: 600;
1080 color: var(--muted);
1081 text-transform: uppercase;
1082 letter-spacing: 0.05em;
1083 padding: 0.5em 0.75em 0.25em 0.75em;
1084 background-color: alpha(var(--border), 0.3);
1085 border-bottom: 0.0625em solid var(--border);
1086}
1087
1088/* Footer with keyboard shortcuts */
1089.session-manager-footer {
1090 background-color: shade(var(--surface-variant), 0.9);
1091 border-top: 0.0625em solid var(--border);
1092 padding: 0.375em 0.75em;
1093 font-size: 0.6875em;
1094 color: var(--muted);
1095 font-family: var(--font-mono);
1096}
1097
1098/* Session mode border */
1099.session-mode-active {
1100 background-color: transparent;
1101 box-shadow: inset 0 0 0 0.25em var(--session-mode-color);
1102 border-radius: 0;
1103}
1104`
1105}
1106
1107// generateContextMenuCSS creates shared context menu styles.
1108func generateContextMenuCSS() string {
1109 return `/* ===== Context Menu Styling ===== */
1110
1111popover.context-menu-popover {
1112 background-color: transparent;
1113 background-image: none;
1114}
1115
1116popover.context-menu-popover > contents {
1117 background-color: var(--surface);
1118 background-image: none;
1119 border: 0.0625em solid alpha(var(--border), 0.85);
1120 border-radius: 0.25em;
1121 box-shadow: 0 0.75em 1.75em alpha(var(--bg), 0.28), 0 0.0625em 0 alpha(var(--border), 0.18) inset;
1122 padding: 0;
1123}
1124
1125.context-menu {
1126 padding: 0.125em 0;
1127 min-width: 10.5em;
1128}
1129
1130.context-menu button.context-menu-item {
1131 background-color: transparent;
1132 background-image: none;
1133 border: none;
1134 border-radius: 0;
1135 color: var(--text);
1136 padding: 0.25em 0.75em;
1137 margin: 0;
1138 min-height: 1.75em;
1139 transition: background-color 120ms ease-in-out, color 120ms ease-in-out;
1140}
1141
1142.context-menu button.context-menu-item:hover,
1143.context-menu button.context-menu-item:focus,
1144.context-menu button.context-menu-item:active {
1145 background-color: alpha(var(--surface-variant), 0.9);
1146 color: var(--text);
1147}
1148
1149.context-menu button.context-menu-item:active {
1150 background-color: alpha(var(--accent), 0.16);
1151}
1152
1153.context-menu button.context-menu-item:disabled {
1154 color: var(--muted);
1155 opacity: 0.7;
1156}
1157
1158.context-menu-separator {
1159 background-color: alpha(var(--border), 0.55);
1160 margin: 0.25em 0.5em;
1161 min-height: 0.0625em;
1162}
1163`
1164}
1165
1166// generateToasterCSS creates toast notification styles.
1167// Uses em units for scalable UI.
1168func generateToasterCSS(p Palette) string {
1169 return `/* ===== Toaster Styling ===== */
1170
1171/* Toast notification container */
1172.toast {
1173 background-color: var(--surface-variant);
1174 border-radius: 0.25em;
1175 padding: 0.375em 0.625em;
1176 margin: 0.5em;
1177 font-size: 0.875em;
1178 font-weight: 600;
1179 box-shadow: 0 2px 8px alpha(black, 0.3);
1180 min-width: 2.5em;
1181}
1182
1183/* Toast level: info (default, accent color) */
1184.toast-info {
1185 background-color: alpha(var(--accent), 0.9);
1186 color: var(--bg);
1187}
1188
1189/* Toast level: success */
1190.toast-success {
1191 background-color: alpha(var(--success), 0.9);
1192 color: var(--bg);
1193}
1194
1195/* Toast level: warning */
1196.toast-warning {
1197 background-color: alpha(var(--warning), 0.9);
1198 color: var(--bg);
1199}
1200
1201/* Toast level: error */
1202.toast-error {
1203 background-color: alpha(var(--destructive), 0.9);
1204 color: var(--bg);
1205}
1206
1207/* Toast with custom styling (mode indicator toasters) */
1208.toast-custom {
1209 color: #ffffff;
1210}
1211
1212/* Mode-specific toast colors */
1213.toast-pane-mode {
1214 background-color: var(--pane-mode-color);
1215 color: #ffffff;
1216}
1217
1218.toast-vim-mode {
1219 background-color: var(--pane-mode-color);
1220 color: #ffffff;
1221}
1222
1223.toast-tab-mode {
1224 background-color: var(--tab-mode-color);
1225 color: #ffffff;
1226}
1227
1228.toast-session-mode {
1229 background-color: var(--session-mode-color);
1230 color: #ffffff;
1231}
1232
1233.toast-resize-mode {
1234 background-color: var(--resize-mode-color);
1235 color: #ffffff;
1236}
1237`
1238}
1239
1240// generateLinkStatusCSS creates link status overlay styles.
1241// Uses CSS transitions for fade-in/fade-out effect.
1242func generateLinkStatusCSS(p Palette) string {
1243 return `/* ===== Link Status Overlay Styling ===== */
1244
1245/* Link status container - bottom left positioning */
1246.link-status {
1247 background-color: alpha(var(--surface-variant), 0.95);
1248 border-radius: 0.25em 0.25em 0 0;
1249 padding: 0.25em 0.5em;
1250 margin: 0;
1251 font-size: 0.75em;
1252 /* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
1253 box-shadow: 0 -1px 4px alpha(black, 0.15);
1254
1255 /* Fade transition - hidden by default */
1256 opacity: 0;
1257 transition: opacity 150ms ease-in-out;
1258}
1259
1260/* Visible state - triggered by adding .visible class */
1261.link-status.visible {
1262 opacity: 1;
1263}
1264
1265/* Link status label text */
1266.link-status label {
1267 color: var(--muted);
1268}
1269`
1270}
1271
1272func generateFloatingPaneCSS(p Palette) string {
1273 _ = p
1274 return `/* ===== Floating Pane Styling ===== */
1275.floating-pane-outer {
1276 /* Positioning handled in Go via overlay alignment and margins. */
1277}
1278
1279.floating-pane-container {
1280 background-color: var(--surface-variant);
1281 border: 0.0625em solid var(--border);
1282 border-radius: 0.25em;
1283 box-shadow: 0 0.5em 1.5em alpha(black, 0.25);
1284 opacity: 0;
1285 transition: opacity 110ms ease-out;
1286}
1287
1288.floating-pane-container.floating-pane-visible {
1289 opacity: 1;
1290}
1291
1292.floating-pane-container.pane-active {
1293 border-color: var(--accent);
1294}
1295
1296.floating-pane-omnibox {
1297 background-color: shade(var(--surface-variant), 1.08);
1298 border-bottom: 0.0625em solid var(--border);
1299 padding: 0.375em 0.5em;
1300}
1301
1302.floating-pane-content {
1303 background-color: var(--bg);
1304 min-height: 12em;
1305}
1306`
1307}