diff --git a/config/dock/style.css b/config/dock/style.css
new file mode 100644
index 0000000..e4769d5
--- /dev/null
+++ b/config/dock/style.css
@@ -0,0 +1,85 @@
+window#classroom-dock,
+window#classroom-clock {
+ background-color: transparent;
+ background-image: none;
+ border: none;
+ box-shadow: none;
+}
+
+/* Main floating dock */
+.dock {
+ background-image: none;
+ background-color: rgba(34, 38, 50, 0.62);
+
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 24px;
+
+ padding: 8px 10px;
+
+ /* Important: no shadow here.
+ GTK was clipping it against the rectangular window edge. */
+ box-shadow: none;
+}
+
+button.dock-button {
+ color: white;
+
+ background-image: none;
+ background-color: rgba(255, 255, 255, 0.05);
+
+ border: none;
+ border-radius: 16px;
+
+ padding: 6px;
+ margin: 0;
+
+ min-width: 44px;
+ min-height: 44px;
+
+ box-shadow: none;
+ outline: none;
+}
+
+button.dock-button:hover {
+ background-image: none;
+ background-color: rgba(70, 75, 90, 0.85);
+ box-shadow: none;
+}
+
+button.dock-button:active {
+ background-image: none;
+ background-color: rgba(85, 90, 108, 0.90);
+ box-shadow: none;
+}
+
+button.dock-button:focus {
+ background-image: none;
+ background-color: rgba(255, 255, 255, 0.08);
+ box-shadow: none;
+ outline: none;
+}
+
+button.dock-button image {
+ color: white;
+ -gtk-icon-effect: none;
+}
+
+
+/* Clock */
+
+.clock {
+ color: white;
+
+ background-image: none;
+ background-color: rgba(34, 38, 50, 0.62);
+
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 16px;
+
+ padding: 7px 12px;
+
+ font-size: 17px;
+ font-weight: 600;
+
+ box-shadow: none;
+}
diff --git a/config/icons/browser.svg b/config/icons/browser.svg
new file mode 100644
index 0000000..51c93ed
--- /dev/null
+++ b/config/icons/browser.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/config/icons/dvd.svg b/config/icons/dvd.svg
new file mode 100644
index 0000000..b571d06
--- /dev/null
+++ b/config/icons/dvd.svg
@@ -0,0 +1,67 @@
+
+
diff --git a/config/icons/live-off.svg b/config/icons/live-off.svg
new file mode 100644
index 0000000..538ae88
--- /dev/null
+++ b/config/icons/live-off.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/config/icons/live.svg b/config/icons/live.svg
new file mode 100644
index 0000000..942dfe6
--- /dev/null
+++ b/config/icons/live.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/config/sway/config b/config/sway/config
index 03b4ea3..1d2b62c 100644
--- a/config/sway/config
+++ b/config/sway/config
@@ -4,10 +4,10 @@
set $mod Mod4
# Wallpaper
-output * bg #202020 solid_color
+output * bg /usr/share/backgrounds/ekklesia/classroom-default.png fill
-# Waybar
-exec --no-startup-id waybar -c /etc/ekklesia-classroom/waybar/config.jsonc -s /etc/ekklesia-classroom/waybar/style.css
+# Ekklesia Classroom dock
+exec --no-startup-id /usr/local/bin/classroom-dock
# Keep the interface intentionally minimal.
# Additional classroom controls are added explicitly by Ekklesia Classroom.
diff --git a/config/wallpapers/classroom-default.png b/config/wallpapers/classroom-default.png
new file mode 100644
index 0000000..3cce003
Binary files /dev/null and b/config/wallpapers/classroom-default.png differ
diff --git a/packaging/debian/control.template b/packaging/debian/control.template
index c9a640b..7d38f88 100644
--- a/packaging/debian/control.template
+++ b/packaging/debian/control.template
@@ -4,6 +4,6 @@ Section: misc
Priority: optional
Architecture: amd64
Maintainer: Ekklesia
-Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups
+Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, python3-gi, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin
Description: Ekklesia managed classroom environment
A minimal Wayland classroom environment for Ekklesia-managed church computers.
diff --git a/scripts/classroom-dock b/scripts/classroom-dock
new file mode 100755
index 0000000..134decf
--- /dev/null
+++ b/scripts/classroom-dock
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+
+import subprocess
+from pathlib import Path
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("GtkLayerShell", "0.1")
+
+from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, GtkLayerShell
+
+
+CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf")
+CSS_FILE = Path("/etc/ekklesia-classroom/dock/style.css")
+ICON_DIR = Path("/etc/ekklesia-classroom/icons")
+
+
+def read_device_config():
+ config = {}
+
+ if not CONFIG_FILE.exists():
+ return config
+
+ for raw_line in CONFIG_FILE.read_text().splitlines():
+ line = raw_line.strip()
+
+ if not line or line.startswith("#") or "=" not in line:
+ continue
+
+ key, value = line.split("=", 1)
+ config[key.strip()] = value.strip()
+
+ return config
+
+
+def enabled(config, key, default=False):
+ if key not in config:
+ return default
+
+ return config[key].lower() in {
+ "1",
+ "true",
+ "yes",
+ "on",
+ "enabled",
+ }
+
+
+def launch(command):
+ subprocess.Popen(
+ command,
+ start_new_session=True,
+ )
+
+
+def notify(title, message):
+ launch([
+ "notify-send",
+ "--app-name=Ekklesia Classroom",
+ title,
+ message,
+ ])
+
+
+def themed_icon(*names):
+ theme = Gtk.IconTheme.get_default()
+
+ for name in names:
+ if theme.has_icon(name):
+ return Gtk.Image.new_from_icon_name(name, Gtk.IconSize.DIALOG)
+
+ return Gtk.Image.new_from_icon_name(
+ "application-x-executable",
+ Gtk.IconSize.DIALOG,
+ )
+
+
+def custom_icon(filename, size):
+ path = ICON_DIR / filename
+
+ if path.exists():
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
+ str(path),
+ size,
+ size,
+ True,
+ )
+ return Gtk.Image.new_from_pixbuf(pixbuf)
+
+ return None
+
+
+class ClassroomDock(Gtk.Window):
+ def __init__(self):
+ super().__init__()
+ screen = self.get_screen()
+ visual = screen.get_rgba_visual()
+
+ if visual is not None:
+ self.set_visual(visual)
+
+ self.set_app_paintable(True)
+ self.config = read_device_config()
+
+ self.set_name("classroom-dock")
+ self.set_decorated(False)
+ self.set_resizable(False)
+
+ GtkLayerShell.init_for_window(self)
+ GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
+
+ GtkLayerShell.set_anchor(
+ self,
+ GtkLayerShell.Edge.BOTTOM,
+ True,
+ )
+
+ GtkLayerShell.set_margin(
+ self,
+ GtkLayerShell.Edge.BOTTOM,
+ 12,
+ )
+
+ GtkLayerShell.set_keyboard_interactivity(self, False)
+
+ dock = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ spacing=10,
+ )
+ dock.get_style_context().add_class("dock")
+
+ dock.pack_start(
+ self.make_button(
+ self.browser_icon(),
+ "Browser",
+ lambda *_: launch(
+ ["/usr/local/bin/classroom-browser"]
+ ),
+ ),
+ False,
+ False,
+ 0,
+ )
+
+ if enabled(self.config, "DVD_ENABLED", False):
+ dock.pack_start(
+ self.make_button(
+ self.dvd_icon(),
+ "DVD",
+ lambda *_: notify(
+ "DVD",
+ "DVD support is being prepared.",
+ ),
+ ),
+ False,
+ False,
+ 0,
+ )
+
+ dock.pack_start(
+ self.make_button(
+ self.sanctuary_icon(),
+ "Sanctuary",
+ lambda *_: notify(
+ "Sanctuary",
+ "Sanctuary live view is being prepared.",
+ ),
+ ),
+ False,
+ False,
+ 0,
+ )
+
+ self.add(dock)
+
+ def browser_icon(self):
+ image = custom_icon("browser.svg", 28)
+ if image is not None:
+ return image
+
+ image = themed_icon("web-browser-symbolic", "internet-web-browser-symbolic")
+ image.set_pixel_size(28)
+ return image
+
+ def dvd_icon(self):
+ image = custom_icon("dvd.svg", 40)
+
+ if image is not None:
+ return image
+
+ image = themed_icon(
+ "media-optical-symbolic",
+ "media-optical",
+ )
+ image.set_pixel_size(28)
+ return image
+
+ def sanctuary_icon(self):
+ is_live = enabled(
+ self.config,
+ "SANCTUARY_LIVE",
+ False,
+ )
+
+ filename = "live.svg" if is_live else "live-off.svg"
+
+ image = custom_icon(filename, 38)
+
+ if image is not None:
+ return image
+
+ image = themed_icon(
+ "video-display-symbolic",
+ "video-display",
+ )
+ image.set_pixel_size(28)
+
+ return image
+
+ def make_button(self, image, tooltip, callback):
+ button = Gtk.Button()
+ button.set_relief(Gtk.ReliefStyle.NONE)
+ button.get_style_context().add_class("dock-button")
+
+ button.add(image)
+ button.connect("clicked", callback)
+
+ return button
+
+class ClassroomClock(Gtk.Window):
+ def __init__(self):
+ super().__init__()
+ screen = self.get_screen()
+ visual = screen.get_rgba_visual()
+
+ if visual is not None:
+ self.set_visual(visual)
+
+ self.set_app_paintable(True)
+
+ self.set_name("classroom-clock")
+ self.set_decorated(False)
+ self.set_resizable(False)
+
+ GtkLayerShell.init_for_window(self)
+ GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
+
+ GtkLayerShell.set_anchor(
+ self,
+ GtkLayerShell.Edge.BOTTOM,
+ True,
+ )
+
+ GtkLayerShell.set_anchor(
+ self,
+ GtkLayerShell.Edge.RIGHT,
+ True,
+ )
+
+ GtkLayerShell.set_margin(
+ self,
+ GtkLayerShell.Edge.BOTTOM,
+ 16,
+ )
+
+ GtkLayerShell.set_margin(
+ self,
+ GtkLayerShell.Edge.RIGHT,
+ 20,
+ )
+
+ GtkLayerShell.set_keyboard_interactivity(self, False)
+
+ self.label = Gtk.Label()
+ self.label.get_style_context().add_class("clock")
+ self.add(self.label)
+
+ self.update_time()
+ GLib.timeout_add_seconds(15, self.update_time)
+
+ def update_time(self):
+ from datetime import datetime
+
+ self.label.set_text(
+ datetime.now().strftime("%-I:%M %p")
+ )
+
+ return True
+
+def load_css():
+ provider = Gtk.CssProvider()
+ provider.load_from_path(str(CSS_FILE))
+
+ screen = Gdk.Screen.get_default()
+
+ Gtk.StyleContext.add_provider_for_screen(
+ screen,
+ provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
+ )
+
+
+def main():
+ load_css()
+
+ dock = ClassroomDock()
+ dock.connect("destroy", Gtk.main_quit)
+ dock.show_all()
+
+ clock = ClassroomClock()
+ clock.show_all()
+
+ Gtk.main()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/tools/build-release.sh b/tools/build-release.sh
index a8bf95f..7453fef 100755
--- a/tools/build-release.sh
+++ b/tools/build-release.sh
@@ -54,6 +54,18 @@ install -Dm755 \
"$ROOT/scripts/classroom-browser" \
"$STAGE/usr/local/bin/classroom-browser"
+install -Dm644 \
+ "$ROOT/config/wallpapers/classroom-default.png" \
+ "$STAGE/usr/share/backgrounds/ekklesia/classroom-default.png"
+
+install -Dm755 \
+ "$ROOT/scripts/classroom-dock" \
+ "$STAGE/usr/local/bin/classroom-dock"
+
+install -Dm644 \
+ "$ROOT/config/dock/style.css" \
+ "$STAGE/etc/ekklesia-classroom/dock/style.css"
+
install -Dm755 \
"$ROOT/scripts/ekklesia-classroom-update" \
"$STAGE/usr/local/bin/ekklesia-classroom-update"
@@ -71,6 +83,22 @@ install -Dm644 \
"$ROOT/systemd/updater/ekklesia-classroom-update.timer" \
"$STAGE/etc/systemd/system/ekklesia-classroom-update.timer"
+install -Dm644 \
+ "$ROOT/config/icons/dvd.svg" \
+ "$STAGE/etc/ekklesia-classroom/icons/dvd.svg"
+
+install -Dm644 \
+ "$ROOT/config/icons/live.svg" \
+ "$STAGE/etc/ekklesia-classroom/icons/live.svg"
+
+install -Dm644 \
+ "$ROOT/config/icons/live-off.svg" \
+ "$STAGE/etc/ekklesia-classroom/icons/live-off.svg"
+
+install -Dm644 \
+ "$ROOT/config/icons/browser.svg" \
+ "$STAGE/etc/ekklesia-classroom/icons/browser.svg"
+
printf '%s\n' "$VERSION" > "$ROOT/VERSION"
dpkg-deb \