Initial commit — TANINUX (camel): management app + distro packaging

- app: GTK System Settings (tsettings) + Software Hub (thub) + TUI
- distro/: camel.toml manifest + MANIFEST.md (Arch + [tanin] repo model)
- packaging/: taninux, tanin-desktop (niri metapackage), tanin-greet,
  tanin-libadwaita, tanin-setup
- docs/, data/, LICENSE (GPL-3.0-or-later)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 20:18:30 +02:00
commit 10b88a67bc
167 changed files with 19624 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Maintainer: Karim <karim@gabrielevarano.ch>
#
# tanin-greet — TANINUX custom GTK4 greeter for greetd. Black top bar like the
# eww topbar, wallpaper background, user+password centered, session in a corner.
# After install: systemctl enable greetd (disable any other DM first)
pkgname=tanin-greet
pkgver=0.1.0
pkgrel=1
pkgdesc="TANINUX login — custom GTK4 greeter for greetd (mirrors the topbar)"
arch=('any')
url="https://taninux.kgva.ch"
license=('GPL-3.0-or-later')
depends=('greetd' 'cage' 'gtk4' 'python' 'python-gobject'
'adwaita-icon-theme' 'inter-font')
optdepends=('tanin-backgrounds: default login wallpaper at /usr/share/backgrounds/tanin/login.jpg')
backup=('etc/greetd/config.toml' 'etc/greetd/tanin-greet.css')
package() {
install -Dm755 "$startdir/tanin-greet" "$pkgdir/usr/bin/tanin-greet"
install -Dm644 "$startdir/tanin-greet.css" "$pkgdir/etc/greetd/tanin-greet.css"
install -Dm644 "$startdir/config.toml" "$pkgdir/etc/greetd/config.toml"
# The greeter reads its CSS from /etc/greetd by default; no wallpaper is
# shipped here (provide one at /usr/share/backgrounds/tanin/login.jpg, e.g.
# via a tanin-backgrounds package — the greeter falls back to a dark scrim).
}
+47
View File
@@ -0,0 +1,47 @@
# tanin-greet
TANINUX's custom **GTK4 greeter** for greetd. Built because ReGreet's layout is
fixed — this gives full control to match the design:
- a **black top bar** like the eww topbar (`TANINUX` · clock · power)
- a **wallpaper** background under a dark scrim
- **user + password** centered as pills (Fuji violet focus ring, Enter submits)
- the **session selector** tucked tiny in the **bottom-right corner**
It speaks the greetd IPC directly (create_session → auth → start_session) and is
hosted by `cage`.
## Files
- `tanin-greet``/usr/bin/tanin-greet` — the GTK4 app (PyGObject)
- `tanin-greet.css``/etc/greetd/tanin-greet.css` — the look (mirrors `eww.scss`)
- `config.toml``/etc/greetd/config.toml` — greetd; `cage -s -- tanin-greet`
- `dev/` → preview helper + greetd mock (not packaged)
## Preview (no logout, no real auth)
```sh
./dev/preview.sh # uses your current wallpaper path
./dev/preview.sh /path/to/wall.jpg
# screenshot lands at /tmp/tanin-greet.png ; override output with GREET_OUTPUT=HDMI-A-1
```
The greeter renders even without greetd; `dev/mockgreetd.py` provides a fake
socket so the password prompt and login flow work for a full dry-run.
## Env overrides (used by the preview)
- `TANIN_GREET_CSS` stylesheet path (default `/etc/greetd/tanin-greet.css`)
- `TANIN_GREET_BG` wallpaper path (default `/usr/share/backgrounds/tanin/login.jpg`)
- `TANIN_GREET_SESSIONS` sessions dir (default `/usr/share/wayland-sessions`)
- `TANIN_GREET_BRAND` top-bar text (default `TANINUX`)
## Install (on the distro)
```sh
makepkg -si
sudo systemctl enable greetd # disable any other DM first
```
Drop a login wallpaper at `/usr/share/backgrounds/tanin/login.jpg` (or ship a
`tanin-backgrounds` package). Without one, the greeter shows a dark scrim.
## Notes
- Sessions are read from `/usr/share/wayland-sessions/*.desktop`; Niri is the
default selection. Users are read from `/etc/passwd` (uid 100065533, real shell).
- Icons (`avatar-default-symbolic`, `system-shutdown-symbolic`) come from
`adwaita-icon-theme`.
+8
View File
@@ -0,0 +1,8 @@
# greetd config for tanin-greet (TANINUX custom GTK4 greeter).
# The greeter is a GTK app, so it's hosted by `cage` (one-window kiosk compositor).
[terminal]
vt = 1
[default_session]
command = "cage -s -- tanin-greet"
user = "greeter"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
# Minimal greetd IPC mock — just enough for tanin-greet to render and dry-run a
# login WITHOUT real auth. greetd framing: native-endian u32 length + JSON.
# Usage: python3 mockgreetd.py /tmp/mock-greetd.sock
import json, os, socket, struct, sys
SOCK = sys.argv[1] if len(sys.argv) > 1 else "/tmp/mock-greetd.sock"
try:
os.unlink(SOCK)
except FileNotFoundError:
pass
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(SOCK)
srv.listen(8)
print(f"mock greetd listening on {SOCK}", flush=True)
def recv(conn):
hdr = conn.recv(4)
if len(hdr) < 4:
return None
n = struct.unpack("=I", hdr)[0]
buf = b""
while len(buf) < n:
chunk = conn.recv(n - len(buf))
if not chunk:
break
buf += chunk
return json.loads(buf)
def send(conn, obj):
data = json.dumps(obj).encode()
conn.sendall(struct.pack("=I", len(data)) + data)
while True:
conn, _ = srv.accept()
try:
while True:
req = recv(conn)
if req is None:
break
if req.get("type") == "create_session":
send(conn, {"type": "auth_message",
"auth_message_type": "secret",
"auth_message": "Password:"})
else: # post_auth_message_response / start_session / cancel_session
send(conn, {"type": "success"})
except Exception as e: # noqa: BLE001
print("conn error:", e, flush=True)
finally:
conn.close()
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Preview tanin-greet from the repo in a nested cage window (no logout, no real
# auth) and screenshot it. Uses the mock greetd socket so the UI fully renders.
# ./dev/preview.sh [wallpaper.jpg]
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SOCK=/tmp/mock-greetd.sock
SHOT=/tmp/tanin-greet.png
export GREETD_SOCK="$SOCK"
export TANIN_GREET_CSS="$HERE/tanin-greet.css"
export TANIN_GREET_BG="${1:-$HOME/Downloads/karen-z-OW5F_DMfb0E-unsplash.jpg}"
python3 "$HERE/dev/mockgreetd.py" "$SOCK" >/tmp/mockgreetd.log 2>&1 &
MOCK=$!
trap 'kill "$MOCK" 2>/dev/null; pkill -x cage 2>/dev/null' EXIT
sleep 0.4
timeout 8 cage -s -- python3 "$HERE/tanin-greet" >/tmp/tanin-greet.log 2>&1 &
sleep 4.5
grim -o "${GREET_OUTPUT:-DP-3}" "$SHOT" && echo "screenshot: $SHOT"
pkill -x cage 2>/dev/null || true
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Install tanin-greet as the greetd greeter on THIS machine. Run with sudo:
# sudo bash packaging/tanin-greet/install-local.sh
# Reversible: the current greetd config is backed up first.
set -euo pipefail
SRC="$(cd "$(dirname "$0")" && pwd)"
ts="$(date +%Y%m%d-%H%M%S)"
# 1) greeter binary + stylesheet
install -Dm755 "$SRC/tanin-greet" /usr/bin/tanin-greet
install -Dm644 "$SRC/tanin-greet.css" /etc/greetd/tanin-greet.css
# 2) optional login wallpaper (greeter falls back to a dark scrim if absent)
if [ -f "${TANIN_LOGIN_BG:-}" ]; then
install -Dm644 "$TANIN_LOGIN_BG" /usr/share/backgrounds/tanin/login.jpg
fi
# 3) point greetd at tanin-greet (back up the working config first)
[ -f /etc/greetd/config.toml ] && cp -a /etc/greetd/config.toml "/etc/greetd/config.toml.bak-$ts"
install -Dm644 "$SRC/config.toml" /etc/greetd/config.toml
cat <<EOF
tanin-greet installed.
greeter: /usr/bin/tanin-greet
css: /etc/greetd/tanin-greet.css
greetd: /etc/greetd/config.toml -> cage -s -- tanin-greet
backup: /etc/greetd/config.toml.bak-$ts
greetd is already enabled — it shows on the NEXT login. To see it now: log out
(or 'sudo systemctl restart greetd' from a TTY — Ctrl+Alt+F2).
If anything is wrong, switch to a TTY and revert:
sudo cp /etc/greetd/config.toml.bak-$ts /etc/greetd/config.toml
EOF
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""tanin-greet — TANINUX login greeter (GTK4 / PyGObject).
Custom layout: a black top bar like the eww topbar (brand · clock · power), a
wallpaper background, user + password centered as pills, and a small session
selector tucked in the bottom-right corner. Talks to greetd over GREETD_SOCK.
Run by greetd via cage: cage -s -- tanin-greet
Preview without greetd: just run it — the UI renders; only login needs the
socket (set GREETD_SOCK via a mock for a full dry-run).
Env overrides (handy for previewing from the repo):
TANIN_GREET_CSS path to the stylesheet (default /etc/greetd/tanin-greet.css)
TANIN_GREET_BG wallpaper image path (default /usr/share/backgrounds/tanin/login.jpg)
TANIN_GREET_SESSIONS wayland-sessions dir (default /usr/share/wayland-sessions)
"""
import gi, os, sys, json, socket, struct, shlex, glob, pwd
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, GLib, Gdk, Gio # noqa: E402
CSS_PATH = os.environ.get("TANIN_GREET_CSS", "/etc/greetd/tanin-greet.css")
WALLPAPER = os.environ.get("TANIN_GREET_BG", "/usr/share/backgrounds/tanin/login.jpg")
SESSION_DIR = os.environ.get("TANIN_GREET_SESSIONS", "/usr/share/wayland-sessions")
BRAND = os.environ.get("TANIN_GREET_BRAND", "TANINUX")
# --------------------------------------------------------------------------- #
# greetd IPC client — native-endian u32 length prefix + JSON payload.
# --------------------------------------------------------------------------- #
class Greetd:
def __init__(self):
path = os.environ.get("GREETD_SOCK")
self.sock = None
if path:
try:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path)
except OSError:
# No reachable greetd (e.g. preview without a daemon). Render the
# UI anyway; login() will report the missing socket on submit.
self.sock = None
def _recvn(self, n):
buf = b""
while len(buf) < n:
chunk = self.sock.recv(n - len(buf))
if not chunk:
raise RuntimeError("greetd closed the connection")
buf += chunk
return buf
def _send(self, obj):
data = json.dumps(obj).encode()
self.sock.sendall(struct.pack("=I", len(data)) + data)
def _recv(self):
n = struct.unpack("=I", self._recvn(4))[0]
return json.loads(self._recvn(n))
def login(self, username, password, cmd_argv):
"""Authenticate and start the session. Raises on failure."""
if self.sock is None:
raise RuntimeError("GREETD_SOCK not set (preview mode — cannot log in)")
self._send({"type": "create_session", "username": username})
while True:
r = self._recv()
t = r.get("type")
if t == "auth_message":
amt = r.get("auth_message_type")
if amt in ("secret", "visible"):
self._send({"type": "post_auth_message_response", "response": password})
else: # info / error — acknowledge with no response
self._send({"type": "post_auth_message_response"})
elif t == "success":
break
elif t == "error":
self._send({"type": "cancel_session"})
raise RuntimeError(r.get("description", "authentication failed"))
else:
raise RuntimeError(f"unexpected greetd reply: {r}")
# authenticated → launch the chosen session
self._send({"type": "start_session", "cmd": cmd_argv, "env": []})
r = self._recv()
if r.get("type") != "success":
self._send({"type": "cancel_session"})
raise RuntimeError(r.get("description", "failed to start session"))
# --------------------------------------------------------------------------- #
# System enumeration
# --------------------------------------------------------------------------- #
def list_users():
users = []
for p in pwd.getpwall():
if 1000 <= p.pw_uid < 65534 and p.pw_shell not in ("/usr/bin/nologin", "/sbin/nologin", "/bin/false"):
users.append(p.pw_name)
return users or [os.environ.get("USER", "user")]
def list_sessions():
sessions = [] # (name, exec_argv)
for f in sorted(glob.glob(os.path.join(SESSION_DIR, "*.desktop"))):
name = exec_ = None
try:
for line in open(f, encoding="utf-8"):
if line.startswith("Name=") and name is None:
name = line[5:].strip()
elif line.startswith("Exec=") and exec_ is None:
exec_ = line[5:].strip()
except OSError:
continue
if name and exec_:
sessions.append((name, shlex.split(exec_)))
return sessions or [("Niri", ["niri-session"])]
# --------------------------------------------------------------------------- #
# UI
# --------------------------------------------------------------------------- #
class Greeter(Gtk.Application):
def __init__(self):
super().__init__(application_id="ch.gabrielevarano.TaninGreet")
self.greetd = Greetd()
self.users = list_users()
self.sessions = list_sessions()
def do_activate(self):
self._load_css()
Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True)
win = Gtk.ApplicationWindow(application=self)
win.set_decorated(False)
win.fullscreen()
win.add_css_class("greeter")
overlay = Gtk.Overlay()
win.set_child(overlay)
# background image (falls back to the scrim colour if missing)
if os.path.exists(WALLPAPER):
bg = Gtk.Picture.new_for_filename(WALLPAPER)
bg.set_content_fit(Gtk.ContentFit.COVER)
overlay.set_child(bg)
else:
overlay.set_child(Gtk.Box())
# dark scrim so text stays readable over any wallpaper
scrim = Gtk.Box()
scrim.add_css_class("scrim")
scrim.set_hexpand(True)
scrim.set_vexpand(True)
overlay.add_overlay(scrim)
# main column: top bar + centered login
col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
col.append(self._topbar())
center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
center.set_vexpand(True)
center.set_valign(Gtk.Align.CENTER)
center.set_halign(Gtk.Align.CENTER)
center.append(self._login_card())
col.append(center)
col.append(self._footer()) # TANINUX wordmark, bottom center
overlay.add_overlay(col)
win.present()
self.password.grab_focus()
# debug: auto-open the session dropdown so previews can screenshot the
# popover without a click (TANIN_GREET_POPUP=1). No effect in production.
if os.environ.get("TANIN_GREET_POPUP"):
def _open():
btn = self.session_dd.get_first_child()
try:
btn.set_active(True)
except Exception: # noqa: BLE001
pass
return False
GLib.timeout_add(900, _open)
# ---- pieces ----------------------------------------------------------- #
def _topbar(self):
bar = Gtk.CenterBox()
bar.add_css_class("topbar")
# top-left corner: session dropdown
bar.set_start_widget(self._session_dd())
# center: clock — time · weekday, date
self.clock = Gtk.Label()
self.clock.add_css_class("clock")
bar.set_center_widget(self.clock)
self._tick()
GLib.timeout_add_seconds(1, self._tick)
# top-right corner: power
bar.set_end_widget(self._power_button())
return bar
def _session_dd(self):
self.session_dd = Gtk.DropDown.new_from_strings([s[0] for s in self.sessions])
self.session_dd.add_css_class("session-dd")
for i, (name, _) in enumerate(self.sessions):
if name.lower().startswith("niri"):
self.session_dd.set_selected(i)
break
return self.session_dd
def _footer(self):
box = Gtk.Box()
box.set_halign(Gtk.Align.CENTER)
box.set_valign(Gtk.Align.END)
box.set_margin_bottom(18)
lbl = Gtk.Label(label=BRAND)
lbl.add_css_class("brand")
box.append(lbl)
return box
def _power_button(self):
btn = Gtk.MenuButton()
btn.add_css_class("power")
btn.set_icon_name("system-shutdown-symbolic")
pop = Gtk.Popover()
pop.add_css_class("menu")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
for label, argv in (("Reboot", ["systemctl", "reboot"]),
("Power Off", ["systemctl", "poweroff"])):
b = Gtk.Button(label=label)
b.add_css_class("menuitem")
b.connect("clicked", lambda _w, a=argv: self._run(a))
box.append(b)
pop.set_child(box)
btn.set_popover(pop)
return btn
def _login_card(self):
card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14)
card.add_css_class("logincard") # NOT "card" — libadwaita's .card adds a bg+shadow
card.set_halign(Gtk.Align.CENTER)
avatar = Gtk.Image.new_from_icon_name("avatar-default-symbolic")
avatar.set_pixel_size(64)
avatar.add_css_class("avatar")
avatar.set_halign(Gtk.Align.CENTER)
card.append(avatar)
# user row — just the NAME as text, with a small dropdown chevron on the
# right edge (aligned with where the password pill ends). Not a pill.
name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
name_row.add_css_class("namerow")
name_row.set_halign(Gtk.Align.CENTER)
self.name_label = Gtk.Label(label=self.users[0])
self.name_label.add_css_class("username")
self.name_label.set_xalign(0.5) # centered
self.name_label.set_hexpand(True)
name_row.append(self.name_label)
self.user_btn = Gtk.MenuButton()
self.user_btn.add_css_class("userswitch")
self.user_btn.set_icon_name("pan-down-symbolic")
self.user_btn.set_visible(len(self.users) > 1) # no point if there's one user
upop = Gtk.Popover()
upop.add_css_class("menu")
ubox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
for u in self.users:
b = Gtk.Button(label=u)
b.add_css_class("menuitem")
b.connect("clicked", lambda _w, name=u: (self.name_label.set_text(name),
self.user_btn.popdown()))
ubox.append(b)
upop.set_child(ubox)
self.user_btn.set_popover(upop)
name_row.append(self.user_btn)
card.append(name_row)
# password pill
self.password = Gtk.PasswordEntry()
self.password.set_show_peek_icon(True)
self.password.set_property("placeholder-text", "Password")
self.password.add_css_class("pill")
self.password.connect("activate", self._on_submit)
card.append(self.password)
self.error = Gtk.Label()
self.error.add_css_class("error")
self.error.set_visible(False)
card.append(self.error)
return card
# ---- behaviour -------------------------------------------------------- #
def _tick(self):
now = GLib.DateTime.new_now_local()
self.clock.set_text(now.format("%H:%M · %A, %d %B %Y"))
return True
def _on_submit(self, _entry):
user = self.name_label.get_text()
pw = self.password.get_text()
cmd = self.sessions[self.session_dd.get_selected()][1]
try:
self.greetd.login(user, pw, cmd)
self.quit() # greetd takes over from here
except Exception as e: # noqa: BLE001
self.password.set_text("")
self.error.set_text(str(e))
self.error.set_visible(True)
def _run(self, argv):
try:
Gio.Subprocess.new(argv, Gio.SubprocessFlags.NONE)
except GLib.Error as e:
self.error.set_text(str(e))
self.error.set_visible(True)
def _load_css(self):
if not os.path.exists(CSS_PATH):
return
provider = Gtk.CssProvider()
provider.load_from_path(CSS_PATH)
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
if __name__ == "__main__":
sys.exit(Greeter().run(None))
+176
View File
@@ -0,0 +1,176 @@
/* =========================================================================
* tanin-greet — stylesheet. Mirrors the eww topbar (eww.scss): Fuji palette,
* monochrome, pill shapes, Inter Medium. Accent is the brand Fuji violet.
* ========================================================================= */
@define-color bg #0f0f0f;
@define-color bg_alt #191919;
@define-color bg_hi #242424;
@define-color fg #f0f0f0;
@define-color yuki #f0f0f0; /* snow white — pill fill */
@define-color muted #8a8a8a;
@define-color accent #a39ec4;
/* No font-family override — inherit the system UI font. */
.greeter {
font-weight: 500;
color: @fg;
}
* { text-shadow: none; -gtk-icon-shadow: none; outline: none; }
/* dark scrim over the wallpaper — keeps text readable, keeps it moody */
.scrim { background-color: alpha(black, 0.55); }
/* ---- top bar: the eww bar look (opaque #0f0f0f strip) ------------------- *
* left = session dropdown · center = clock (time/day/date) · right = power. */
.topbar {
background-color: @bg;
min-height: 34px;
padding: 0 12px;
}
.topbar .clock {
color: @fg;
font-size: 13px;
}
/* session dropdown, top-left corner — quiet on the black bar, no box */
.session-dd,
.session-dd > button {
background: transparent;
background-image: none;
border: none;
box-shadow: none;
min-height: 0;
padding: 2px 6px;
color: @muted;
font-size: 12px;
}
.session-dd > button:hover { color: @fg; }
/* power button — NO background at all, just the glyph */
.topbar .power,
.topbar .power > button {
background: transparent;
background-image: none;
border: none;
box-shadow: none;
min-height: 0;
min-width: 0;
padding: 2px 6px;
color: @muted;
}
.topbar .power:hover,
.topbar .power > button:hover { color: @accent; background: transparent; }
/* TANINUX wordmark — bottom center, understated */
.brand {
color: @fg;
font-weight: 700;
font-size: 12px;
letter-spacing: 3px;
opacity: 0.8;
}
/* ---- centered login card (flat, no heavy frame — minimal) --------------- *
* NB: class is .logincard, never .card — libadwaita's built-in .card recipe
* paints an elevated background + shadow (the "solid box, too big" bug). */
.logincard { padding: 8px; background: transparent; box-shadow: none; }
.avatar {
color: @fg;
background-color: alpha(@accent, 0.14);
border-radius: 999px;
padding: 16px;
margin-bottom: 6px;
}
/* username row — plain name + chevron on the right, same width as the pill so
* the chevron sits where the password pill ends. No background (just the name). */
.namerow { min-width: 300px; }
.username {
color: @yuki;
font-size: 17px;
font-weight: 600;
}
.userswitch {
background: transparent;
border: none;
box-shadow: none;
min-height: 0;
min-width: 0;
padding: 2px 4px;
color: @muted;
}
.userswitch:hover { color: @fg; }
/* password pill — snow white (yuki), dark text, accent focus ring.
* border-radius 999px like the bar's workspaces/clock. */
.pill {
min-width: 300px;
min-height: 0;
background-color: alpha(@bg_alt, 0.45); /* greyish, slightly transparent */
color: @fg;
border: 1.5px solid alpha(@fg, 0.22); /* grey border (unfocused) */
border-radius: 999px;
padding: 11px 18px;
box-shadow: none;
}
entry.pill { caret-color: @yuki; }
entry.pill text { color: @fg; }
entry.pill text > placeholder { color: alpha(@fg, 0.45); }
entry.pill image { color: alpha(@fg, 0.6); } /* peek-eye icon */
/* clicked / focused → white border */
.pill:focus-within {
border-color: @yuki;
background-color: alpha(@bg_alt, 0.55);
box-shadow: none;
}
/* popup lists (session dropdown, user switcher).
* IMPORTANT: only style `popover > contents` — the outer `popover` node also
* covers the reserved shadow/margin area, so a solid bg there paints a huge
* solid box. Keep the outer node fully transparent. */
popover {
background: transparent;
background-image: none;
border: none;
box-shadow: none;
padding: 0;
}
popover > contents {
background-color: @bg_alt;
color: @fg;
border: 1px solid alpha(@fg, 0.06);
border-radius: 12px;
box-shadow: 0 8px 24px alpha(black, 0.45);
padding: 6px;
min-width: 0;
}
/* kill the inner list's OWN themed background (the lighter nested box) so the
* whole popover reads as one uniform surface. */
popover listview,
popover scrolledwindow,
popover scrolledwindow > viewport,
popover .view {
background: transparent;
background-color: transparent;
color: @fg;
}
popover listview > row,
popover .menuitem { border-radius: 8px; padding: 5px 10px; }
popover listview > row:selected,
popover listview > row:hover,
popover row:selected,
popover .menuitem:hover { background-color: alpha(@accent, 0.14); color: @accent; }
.error { color: #e0a3a3; font-size: 12px; margin-top: 4px; }
/* power popover buttons */
.menuitem {
background: transparent;
border: none;
box-shadow: none;
border-radius: 8px;
padding: 6px 14px;
color: @fg;
}
.menuitem:hover { background-color: alpha(@accent, 0.14); color: @accent; }