diff --git a/main.css b/main.css index 0e6e559..5fd1b8d 100644 --- a/main.css +++ b/main.css @@ -14,7 +14,7 @@ all: unset; color: var(--foreground); font-size: 12px; - font-family: unset; + font-family: "Rubik", sans-serif; } #corner { diff --git a/modules/launcher/main.py b/modules/launcher/main.py index 7afd05b..171b939 100644 --- a/modules/launcher/main.py +++ b/modules/launcher/main.py @@ -17,16 +17,16 @@ SEARCH_DEBOUNCE_MS = 150 TRIGGER_SEARCH_DEBOUNCE_MS = 50 CURSOR_POSITION_DELAY_MS = 10 SCROLL_PADDING = 10 -DEFAULT_ITEM_HEIGHT = 60 +DEFAULT_ITEM_HEIGHT = 68 PAGE_NAVIGATION_STEP = 5 -LAUNCHER_WIDTH = 550 -LAUNCHER_HEIGHT = 260 +LAUNCHER_WIDTH = 640 +LAUNCHER_HEIGHT = 400 class Launcher(Window): """ Main launcher window with search functionality and plugin system. - Similar to Albert Launcher interface. + Spotlight-style interface. """ # Properties @@ -62,7 +62,7 @@ class Launcher(Window): # Current results and selection self.results: List[Result] = [] self.selected_index = 0 - self.max_results = 10 + self.max_results = 8 # Spotlight typically shows fewer results # Trigger system self.triggered_plugin = None # Currently active triggered plugin @@ -84,10 +84,10 @@ class Launcher(Window): ) self.add(main_box) - # Search entry with overlay for trigger indication + # Search entry with Spotlight-style large text self.search_entry = Entry( name="launcher-search", - placeholder="Type to search...", + placeholder="Spotlight Search", h_expand=True, h_align="fill", notify_text=lambda entry, *_: self._on_search_changed(entry), @@ -106,7 +106,7 @@ class Launcher(Window): main_box.add(self.header_box) - # Results container + # Results container with increased height for Spotlight-style self.results_scroll = ScrolledWindow( name="launcher-results-scroll", h_scrollbar_policy="never", @@ -644,7 +644,7 @@ class Launcher(Window): if getattr(self, "_initializing", True) or not hasattr(self, "results_box"): return - # Update input field with trigger indication (Albert-style) + # Update input field with trigger indication (Spotlight-style) self._update_input_action_text() # Clear existing results @@ -684,7 +684,7 @@ class Launcher(Window): self.results_scroll.hide() def _update_input_action_text(self): - """Update the input field with action text (Albert-style).""" + """Update the input field with action text (Spotlight-style).""" # Check if search_entry is initialized if not hasattr(self, "search_entry") or self.search_entry is None: return @@ -692,51 +692,27 @@ class Launcher(Window): # Get the current input text current_text = self.search_entry.get_text() + # Spotlight-style placeholder text if self.triggered_plugin: if self.results: - # Show the first result as action text - first_result = self.results[0] - action_text = first_result.title - - # For calculator results, show the evaluation - if hasattr(first_result, "data") and "result" in first_result.data: - action_text = str(first_result.data["result"]) - - # Set placeholder to show the action text - if action_text and action_text != current_text: - self.search_entry.set_placeholder_text( - f"{current_text} → {action_text}" - ) - else: - self.search_entry.set_placeholder_text( - f"[{self.active_trigger.strip()}] searching..." - ) + # Show more minimal placeholder for triggered mode + self.search_entry.set_placeholder_text(f"Search {self.active_trigger.strip()}...") else: # In trigger mode but no results yet if current_text == self.active_trigger.strip(): # Just the trigger keyword - self.search_entry.set_placeholder_text( - f"[{self.active_trigger.strip()}] ready - type to search" - ) + self.search_entry.set_placeholder_text(f"Search {self.active_trigger.strip()}...") else: # Searching within trigger - self.search_entry.set_placeholder_text( - f"[{self.active_trigger.strip()}] searching..." - ) + self.search_entry.set_placeholder_text(f"Searching {self.active_trigger.strip()}...") else: - # Not in trigger mode - show trigger help + # Not in trigger mode - show Spotlight-style help if current_text == ":": - self.search_entry.set_placeholder_text( - "Showing all available triggers." - ) + self.search_entry.set_placeholder_text("Available search triggers") elif current_text: - self.search_entry.set_placeholder_text( - "Type trigger keyword (calc, app, file, system...)" - ) + self.search_entry.set_placeholder_text("Spotlight Search") else: - self.search_entry.set_placeholder_text( - "Type trigger keyword: calc, app, file, system..." - ) + self.search_entry.set_placeholder_text("Spotlight Search") def _clear_results(self): """Clear all results.""" @@ -1116,7 +1092,7 @@ class Launcher(Window): # Try to get actual allocation, fallback to estimation try: allocation = selected_child.get_allocation() - item_height = allocation.height if allocation.height > 0 else 60 + item_height = allocation.height if allocation.height > 0 else 68 item_y = allocation.y except: # Fallback to estimation diff --git a/modules/notification/notification.py b/modules/notification/notification.py index c8e36ae..2377158 100644 --- a/modules/notification/notification.py +++ b/modules/notification/notification.py @@ -1,6 +1,6 @@ import os -from gi.repository import Gdk, GdkPixbuf, GLib # type: ignore +from gi.repository import Gdk, GdkPixbuf, GLib, Gtk # type: ignore from loguru import logger import config.data as data @@ -296,11 +296,23 @@ class NotificationRevealer(SlideRevealer): self.parent_window = parent_window self._is_closing = False - # Add swipe detection variables + # Enhanced swipe detection variables for Android-style animation self._drag_start_x = 0 self._drag_start_y = 0 self._is_dragging = False - self._swipe_threshold = 100 # pixels to trigger swipe dismiss + self._swipe_threshold = 80 # Distance to trigger auto-dismiss + self._swipe_velocity_threshold = 150 # Velocity to trigger dismiss even on shorter swipes + self._swipe_in_progress = False + self._current_offset = 0 + self._last_drag_time = 0 + self._drag_velocity = 0 + self._spring_back_duration = 200 # Duration for spring-back animation + self._dismiss_threshold = 0.3 # Dismiss if swiped 30% of width + + # Animation state + self._animation_in_progress = False + self._spring_timer_id = None + self._css_provider = None # Wrap notification in EventBox for swipe detection self.event_box = EventBox( @@ -315,6 +327,7 @@ class NotificationRevealer(SlideRevealer): self.event_box.connect("button-press-event", self._on_button_press) self.event_box.connect("button-release-event", self._on_button_release) self.event_box.connect("motion-notify-event", self._on_motion) + self.event_box.connect("realize", self._on_realize) super().__init__( child=self.event_box, @@ -327,6 +340,148 @@ class NotificationRevealer(SlideRevealer): # Connect our own handler that manages the slide animation self.notification.connect("closed", self.on_resolved) + def _on_realize(self, widget): + """Setup CSS provider when widget is realized""" + try: + self._css_provider = Gtk.CssProvider() + context = widget.get_style_context() + context.add_provider(self._css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + except Exception as e: + logger.warning(f"Could not setup CSS provider for notification animation: {e}") + + def _apply_transform(self, offset_x, opacity=1.0, scale=1.0): + """Apply transform to the notification widget for smooth animation""" + if not self.event_box or not self.event_box.get_realized(): + return + + # Clamp values + offset_x = max(0, offset_x) # Only allow rightward movement + opacity = max(0.1, min(1.0, opacity)) # Keep some visibility + scale = max(0.8, min(1.0, scale)) # Subtle scale effect + + try: + if self._css_provider: + # Apply CSS transform using the provider + css_data = f""" + * {{ + margin-left: {int(offset_x)}px; + opacity: {opacity}; + transform: scale({scale}); + }} + """ + self._css_provider.load_from_data(css_data.encode('utf-8')) + else: + # Fallback: use widget margins for offset + current_margin = self.event_box.get_margin_start() + if current_margin != int(offset_x): + self.event_box.set_margin_start(int(offset_x)) + + # Set opacity directly on the widget + self.event_box.set_opacity(opacity) + + except Exception as e: + # Final fallback: just use margin + try: + self.event_box.set_margin_start(int(offset_x)) + self.event_box.set_opacity(opacity) + except: + pass + + def _animate_spring_back(self, start_offset, target_offset=0, duration=None): + """Animate the notification springing back to its original position""" + if duration is None: + duration = self._spring_back_duration + + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + + start_time = GLib.get_monotonic_time() / 1000 + offset_diff = target_offset - start_offset + + def animate_step(): + current_time = GLib.get_monotonic_time() / 1000 + elapsed = current_time - start_time + progress = min(1.0, elapsed / duration) + + # Use easing function for spring-like animation + eased_progress = self._ease_out_back(progress) + current_offset = start_offset + (offset_diff * eased_progress) + + # Calculate opacity and scale based on offset + max_offset = NOTIFICATION_WIDTH * 0.8 + offset_ratio = abs(current_offset) / max_offset + opacity = 1.0 - (offset_ratio * 0.3) # Fade slightly + scale = 1.0 - (offset_ratio * 0.05) # Slight scale down + + self._apply_transform(current_offset, opacity, scale) + + if progress >= 1.0: + self._animation_in_progress = False + self._current_offset = target_offset + return False + + return True + + self._animation_in_progress = True + self._spring_timer_id = GLib.timeout_add(16, animate_step) # ~60fps + + def _ease_out_back(self, t): + """Easing function for spring-back animation""" + c1 = 1.70158 + c3 = c1 + 1 + return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2) + + def _animate_dismiss(self, start_offset): + """Animate the notification sliding out completely - now much faster""" + target_offset = NOTIFICATION_WIDTH + 50 # Slide completely out of view + duration = 120 # Much faster dismiss animation (was 250) + + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + + start_time = GLib.get_monotonic_time() / 1000 + offset_diff = target_offset - start_offset + + def animate_step(): + current_time = GLib.get_monotonic_time() / 1000 + elapsed = current_time - start_time + progress = min(1.0, elapsed / duration) + + # Use faster easing for snappier exit + eased_progress = progress * progress # Quadratic ease out - faster than cubic + current_offset = start_offset + (offset_diff * eased_progress) + + # Fade out and scale down during dismiss + opacity = 1.0 - (progress * 1.2) # Faster fade out + scale = 1.0 - (progress * 0.15) # Slightly more scale change + + self._apply_transform(current_offset, opacity, scale) + + if progress >= 1.0: + # Trigger the actual notification dismissal immediately + try: + self.notification.close("dismissed-by-user") + except: + pass + return False + + return True + + self._animation_in_progress = True + self._spring_timer_id = GLib.timeout_add(12, animate_step) # Higher fps for smoother animation + + def _calculate_drag_velocity(self, current_x): + """Calculate the velocity of the drag gesture""" + current_time = GLib.get_monotonic_time() / 1000 + + if self._last_drag_time > 0: + time_diff = current_time - self._last_drag_time + if time_diff > 0: + distance_diff = current_x - self._drag_start_x - self._current_offset + self._drag_velocity = abs(distance_diff / time_diff) + + self._last_drag_time = current_time + def _on_animation_complete(self, is_hiding=False): if is_hiding: # Manually destroy the notification widget since we disconnected its handler @@ -346,23 +501,42 @@ class NotificationRevealer(SlideRevealer): self._is_closing = True - # Use left-to-right slide for auto-dismiss (expired), right-to-left for manual close + # Clean up any ongoing animations + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + + # Use different slide directions based on dismiss reason if reason == "expired": - # Left-to-right slide for auto-dismiss + # Left-to-right slide for auto-dismiss (expired) self.set_slide_direction("left") + elif self._swipe_in_progress: + # For swipe dismissals, use immediate hiding without additional slide animation + # The swipe animation already handled the visual feedback + self.duration = 50 # Very fast transition + self.set_slide_direction("right") else: - # Right-to-left slide for manual close + # Right-to-left slide for manual close (button clicks) self.set_slide_direction("right") self.hide() - # Reduced timeout for snappier transitions - GLib.timeout_add(self.duration + 30, lambda: self._on_animation_complete(True)) + # Reduced timeout for snappier transitions, especially for swipe dismissals + timeout_duration = 80 if self._swipe_in_progress else (self.duration + 30) + GLib.timeout_add(timeout_duration, lambda: self._on_animation_complete(True)) def _on_button_press(self, _widget, event): if event.button == 1: self._drag_start_x = event.x self._drag_start_y = event.y self._is_dragging = True + self._swipe_in_progress = False + self._current_offset = 0 + self._last_drag_time = GLib.get_monotonic_time() / 1000 + self._drag_velocity = 0 + + # Stop any ongoing animations + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + self._animation_in_progress = False return False def _on_button_release(self, _widget, event): @@ -378,26 +552,86 @@ class NotificationRevealer(SlideRevealer): elif self._is_dragging and event.button == 1: self._is_dragging = False - - # Calculate swipe distance + + # Calculate final swipe metrics dx = event.x - self._drag_start_x dy = abs(event.y - self._drag_start_y) - - # Left-to-right swipe: dismiss current notification only - if dx > self._swipe_threshold and dy < 50: # 50px vertical tolerance + + # Calculate dismiss threshold based on notification width + dismiss_distance = NOTIFICATION_WIDTH * self._dismiss_threshold + + # Determine if we should dismiss or spring back + should_dismiss = ( + (dx > dismiss_distance and dy < 60) or # Dragged far enough + (dx > 30 and self._drag_velocity > self._swipe_velocity_threshold and dy < 60) # Fast swipe + ) + + if should_dismiss: try: - self.notification.close("dismissed-by-user") - except: - pass # Ignore errors + self._swipe_in_progress = True + # For fast dismissal, if already dragged far enough, close immediately + if dx > NOTIFICATION_WIDTH * 0.6: + # Skip animation and close immediately for very large swipes + self.notification.close("dismissed-by-user") + else: + # Animate the dismiss for smaller swipes + self._animate_dismiss(self._current_offset) + logger.debug(f"Notification dismissed by swipe: dx={dx}, velocity={self._drag_velocity}") + except Exception as e: + logger.error(f"Error dismissing notification by swipe: {e}") + else: + # Spring back to original position + if abs(self._current_offset) > 5: # Only animate if there's noticeable displacement + self._animate_spring_back(self._current_offset, 0) + else: + self._apply_transform(0, 1.0, 1.0) + self._current_offset = 0 return False def _on_motion(self, _widget, event): - if self._is_dragging: - # TODO: Add visual feedback during swipe - # Could add translation or opacity changes here - pass + if self._is_dragging and not self._animation_in_progress: + # Calculate current swipe distance + dx = event.x - self._drag_start_x + dy = abs(event.y - self._drag_start_y) + + # Only respond to primarily horizontal gestures + if dy < 60: # Vertical tolerance + # Calculate velocity for smooth interaction + self._calculate_drag_velocity(event.x) + + # Apply real-time transform - only allow rightward movement + if dx > 0: + self._current_offset = dx + + # Calculate visual feedback based on drag distance + max_offset = NOTIFICATION_WIDTH * 0.8 + offset_ratio = min(1.0, dx / max_offset) + + # Apply diminishing returns for large swipes + adjusted_offset = dx * (1.0 - offset_ratio * 0.3) + + # Calculate opacity and scale + opacity = 1.0 - (offset_ratio * 0.4) # Fade as it's dragged + scale = 1.0 - (offset_ratio * 0.1) # Slight scale down + + self._apply_transform(adjusted_offset, opacity, scale) + + # Visual indication of dismiss threshold + if dx > NOTIFICATION_WIDTH * self._dismiss_threshold: + # Could add visual cue here (like changing color) + pass + else: + # Reset to original position if dragging left + self._current_offset = 0 + self._apply_transform(0, 1.0, 1.0) return False + def destroy(self): + # Clean up CSS provider and timers + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + super().destroy() + class NotificationState: IDLE = 0 @@ -411,7 +645,6 @@ class ModusNoti(Window): self.notifications = Box( v_expand=True, h_expand=True, - title="Notification Center", style="margin: 1px 0px 1px 1px;", orientation="v", spacing=5, @@ -431,6 +664,7 @@ class ModusNoti(Window): anchor="top right", child=self.notifications, layer="overlay", + title="noti", all_visible=True, visible=True, exclusive=False, diff --git a/styles/dock.css b/styles/dock.css index 3966d08..1ea4b61 100644 --- a/styles/dock.css +++ b/styles/dock.css @@ -4,7 +4,7 @@ margin: 8px 8px 5px 8px; border-radius: 16px; /* border: 2px solid var(--surface); */ - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); + /* transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); */ } #dock.vertical { @@ -18,7 +18,7 @@ } #dock-full { - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); + /* transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); */ opacity: 1; } @@ -32,7 +32,7 @@ padding: 4px; border-radius: 24px; /* box-shadow: 0 0 4px alpha(var(--shadow), 0.5); */ - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); + /* transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); */ } #dock-app-button:hover { diff --git a/styles/launcher.css b/styles/launcher.css index f115f2b..5195356 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -1,117 +1,142 @@ #launcher { - background: var(--shadow); - padding: 12px; - border-radius: 34px; - min-width: 550px; - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); + background: rgba(30, 30, 30, 0.95); + padding: 0; + border-radius: 12px; + min-width: 640px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); +} + +#header_box { + padding: 20px 20px 0 20px; } #close-button, #config-button { - background-color: var(--surface); - border-radius: 40px; - padding: 8px; - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); + background-color: transparent; + border-radius: 6px; + padding: 6px; + transition: all 0.15s ease; } #close-button:hover, #close-button:focus, #config-button:hover, #config-button:focus { - background-color: var(--surface-bright); - border-radius: 16px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 6px; } #close-button.focused, #config-button.focused { - background-color: var(--primary); - border-radius: 16px; - box-shadow: 0 0 0 2px var(--primary-bright); + background-color: rgba(0, 122, 255, 0.2); + border-radius: 6px; + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.4); } #close-button.focused #close-label { - color: var(--background); + color: #007AFF; } #config-button.focused #config-label { - color: var(--background); + color: #007AFF; } #close-button:active { - background-color: var(--red-dim); - border-radius: 40px; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 6px; } #close-label { - color: var(--red-dim); - font-size: 24px; + color: rgba(255, 255, 255, 0.7); + font-size: 18px; } #close-button:active #close-label { - color: var(--shadow); + color: rgba(255, 255, 255, 0.9); } #config-button:active { - background-color: var(--primary); - border-radius: 40px; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 6px; } #config-label { - color: var(--primary); - font-size: 24px; + color: rgba(255, 255, 255, 0.7); + font-size: 18px; } #config-button:active #config-label { - color: var(--shadow); + color: rgba(255, 255, 255, 0.9); } #launcher-icon-label { - font-size: 32px; - padding: 8px; + font-size: 20px; + padding: 6px; + color: rgba(255, 255, 255, 0.8); } #launcher-search { - font-weight: bold; - background-color: var(--surface); - color: var(--foreground); - border-radius: 16px; - padding: 10px; + font-weight: 400; + font-size: 36px; + background-color: transparent; + color: rgba(255, 255, 255, 0.95); + border: none; + border-radius: 0; + padding: 12px 0; + margin: 0; +} + +#launcher-search:focus { + background-color: transparent; + box-shadow: none; + border: none; } #launcher-search selection { - color: var(--background); - background-color: var(--primary); + color: white; + background-color: rgba(0, 122, 255, 0.8); } #launcher-results-scroll { - margin-top: 10px; - border-radius: 16px; + margin: 0; + border-radius: 0; + background: transparent; + padding: 0 20px 20px 20px; } #launcher-results-scroll scrollbar { - border-radius: 10px; - background-color: var(--surface); - padding: 4px; - margin-left: 6px; + border-radius: 0; + background-color: transparent; + padding: 0; + margin: 0; + min-width: 0; } -#launcher-results-scroll slider { - border-radius: 8px; - min-width: 10px; - min-height: 38px; - background-color: var(--primary); +#launcher-results-scroll scrollbar slider { + border-radius: 2px; + min-width: 4px; + min-height: 20px; + background-color: rgba(255, 255, 255, 0.3); + margin: 0; +} + +#launcher-results-scroll scrollbar:hover slider { + background-color: rgba(255, 255, 255, 0.5); } #launcher-results { background: transparent; + margin-top: 8px; } #launcher-result-item { - border-radius: 40px; - padding: 0 16px; - animation: loadSlot 0.5s ease; - min-height: 52px; - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); + border-radius: 8px; + padding: 12px 16px; + margin: 2px 0; + min-height: 64px; + transition: all 0.15s ease; + background: transparent; } @keyframes loadSlot { @@ -123,54 +148,80 @@ opacity: 1; } } + #launcher-result-item:focus, #launcher-result-item:selected, #launcher-result-item:hover, #launcher-result-item.selected { - border-radius: 16px; - background-color: var(--surface); - padding-left: 20px; + border-radius: 8px; + background-color: rgba(0, 122, 255, 0.8); + padding: 12px 16px; + margin: 2px 0; +} + +#launcher-result-item.selected #result-item-title { + color: white; + font-weight: 600; +} + +#launcher-result-item.selected #result-item-subtitle { + color: rgba(255, 255, 255, 0.8); +} + +#launcher-result-item.selected #result-item-plugin { + color: rgba(255, 255, 255, 0.6); } #result-item-main { - min-height: 48px; + min-height: 56px; } #launcher-result-item { - min-height: 48px; + min-height: 56px; } #result-item-icon { - min-width: 48px; - min-height: 48px; - margin-right: 12px; + min-width: 56px; + min-height: 56px; + margin-right: 16px; + border-radius: 12px; } #result-item-title { - font-size: 14px; - font-weight: 600; - color: var(--foreground); - margin-top: 8px; + font-size: 18px; + font-weight: 500; + color: rgba(255, 255, 255, 0.95); + margin-top: 4px; margin-bottom: 2px; } #result-item-subtitle { - font-size: 12px; - color: var(--outline); - margin-bottom: 8px; + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 4px; } #result-item-plugin { - font-size: 10px; - color: var(--outline); - font-style: italic; - margin-bottom: 8px; - opacity: 0.7; + font-size: 12px; + color: rgba(255, 255, 255, 0.4); + font-style: normal; + margin-bottom: 4px; + opacity: 1; + font-weight: 400; } #network-password-entry { - border: 1px solid var(--outline); - padding: 8px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.9); + padding: 12px; border-radius: 8px; - margin-bottom: 4px; + margin-bottom: 8px; + font-size: 14px; +} + +#network-password-entry:focus { + border: 1px solid rgba(0, 122, 255, 0.6); + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.2); } diff --git a/styles/notification.css b/styles/notification.css index fdae209..09a7598 100644 --- a/styles/notification.css +++ b/styles/notification.css @@ -1,7 +1,7 @@ #notification { padding: 16px; background-color: alpha(#000, 0.3); - border: 0.5px solid alpha(#111, 0.4); + /* border: 0.5px solid alpha(#111, 0.4); */ /* box-shadow: */ /* inset 0 0 200px 0 alpha(#111, 0.5), */ /* inset 0 0 0 0.5px alpha(#aaa, 0.4); */