diff --git a/asciinema/compose.yaml b/asciinema/compose.yaml
index a4e74c3..7e68922 100644
--- a/asciinema/compose.yaml
+++ b/asciinema/compose.yaml
@@ -8,6 +8,7 @@ services:
volumes:
- asciinema_data:/var/opt/asciinema
- ./custom.exs:/opt/app/etc/custom.exs:ro
+ - ./theme:/opt/app/etc/theme:ro
environment:
SECRET_KEY_BASE: ${ASCIINEMA_SECRET_KEY}
URL_HOST: ${ASCIINEMA_TRAEFIK_HOST}
diff --git a/asciinema/custom.exs b/asciinema/custom.exs
index dd1b726..a1cc669 100644
--- a/asciinema/custom.exs
+++ b/asciinema/custom.exs
@@ -15,3 +15,58 @@ config :asciinema, Asciinema.Emails.Mailer,
verify: :verify_none,
versions: [:"tlsv1.2", :"tlsv1.3"]
]
+
+# Custom theme configuration - inject custom CSS and favicon
+defmodule AsciinemaWeb.CustomThemePlug do
+ @moduledoc """
+ Plug to inject custom CSS and favicon into HTML responses
+ """
+ import Plug.Conn
+
+ def init(opts), do: opts
+
+ def call(conn, _opts) do
+ register_before_send(conn, fn conn ->
+ if html_response?(conn) do
+ inject_custom_theme(conn)
+ else
+ conn
+ end
+ end)
+ end
+
+ defp html_response?(conn) do
+ case get_resp_header(conn, "content-type") do
+ [content_type | _] -> String.contains?(content_type, "text/html")
+ [] -> false
+ end
+ end
+
+ defp inject_custom_theme(conn) do
+ custom_head = """
+
+
+ """
+
+ case conn.resp_body do
+ body when is_binary(body) ->
+ new_body = String.replace(body, "", "#{custom_head}")
+ %{conn | resp_body: new_body}
+ _ ->
+ conn
+ end
+ end
+end
+
+# Configure Phoenix endpoint to serve custom theme files
+config :asciinema, AsciinemaWeb.Endpoint,
+ # Serve theme files from /opt/app/etc/theme
+ static_dirs: %{
+ at: "/theme",
+ from: "/opt/app/etc/theme",
+ gzip: false
+ }
+
+# Inject the custom theme Plug into the endpoint
+config :asciinema, AsciinemaWeb.Endpoint,
+ plug: AsciinemaWeb.CustomThemePlug
diff --git a/asciinema/theme/custom.css b/asciinema/theme/custom.css
new file mode 100644
index 0000000..9d092f1
--- /dev/null
+++ b/asciinema/theme/custom.css
@@ -0,0 +1,278 @@
+/* Pivoine Rose Custom Theme for Asciinema Server */
+/* Primary Color: #CE275B (Pivoine Rose) */
+/* Background: Gray tones */
+
+:root {
+ --pivoine-rose: #CE275B;
+ --pivoine-rose-dark: #A61E47;
+ --pivoine-rose-light: #E63368;
+ --gray-50: #FAFAFA;
+ --gray-100: #F5F5F5;
+ --gray-200: #EEEEEE;
+ --gray-300: #E0E0E0;
+ --gray-400: #BDBDBD;
+ --gray-500: #9E9E9E;
+ --gray-600: #757575;
+ --gray-700: #616161;
+ --gray-800: #424242;
+ --gray-900: #212121;
+}
+
+/* Override Bootstrap primary color */
+.btn-primary,
+.badge-primary,
+.bg-primary,
+.text-primary,
+a.text-primary:hover,
+a.text-primary:focus {
+ background-color: var(--pivoine-rose) !important;
+ border-color: var(--pivoine-rose) !important;
+ color: white !important;
+}
+
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active {
+ background-color: var(--pivoine-rose-dark) !important;
+ border-color: var(--pivoine-rose-dark) !important;
+}
+
+/* Links */
+a {
+ color: var(--pivoine-rose);
+}
+
+a:hover,
+a:focus {
+ color: var(--pivoine-rose-dark);
+}
+
+/* Body and backgrounds */
+body {
+ background-color: var(--gray-100);
+ color: var(--gray-900);
+}
+
+/* Header/Navbar */
+.navbar {
+ background-color: var(--gray-800) !important;
+ border-bottom: 3px solid var(--pivoine-rose);
+}
+
+.navbar-dark .navbar-brand {
+ color: var(--pivoine-rose) !important;
+ font-weight: 600;
+}
+
+.navbar-dark .navbar-brand:hover {
+ color: var(--pivoine-rose-light) !important;
+}
+
+.navbar-dark .navbar-nav .nav-link {
+ color: var(--gray-200) !important;
+}
+
+.navbar-dark .navbar-nav .nav-link:hover {
+ color: var(--pivoine-rose) !important;
+}
+
+/* Cards and containers */
+.card,
+.list-group-item {
+ background-color: white;
+ border-color: var(--gray-300);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.card-header,
+.list-group-item.active {
+ background-color: var(--gray-800);
+ border-color: var(--gray-700);
+ color: white;
+}
+
+/* Main content area */
+.container,
+.container-fluid {
+ background-color: var(--gray-50);
+ padding: 2rem 1rem;
+}
+
+main {
+ background-color: white;
+ border-radius: 4px;
+ padding: 2rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* Buttons */
+.btn {
+ border-radius: 4px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.btn-outline-primary {
+ color: var(--pivoine-rose);
+ border-color: var(--pivoine-rose);
+}
+
+.btn-outline-primary:hover {
+ background-color: var(--pivoine-rose);
+ border-color: var(--pivoine-rose);
+ color: white;
+}
+
+.btn-secondary {
+ background-color: var(--gray-600);
+ border-color: var(--gray-600);
+}
+
+.btn-secondary:hover {
+ background-color: var(--gray-700);
+ border-color: var(--gray-700);
+}
+
+/* Forms */
+.form-control {
+ border-color: var(--gray-300);
+ border-radius: 4px;
+}
+
+.form-control:focus {
+ border-color: var(--pivoine-rose);
+ box-shadow: 0 0 0 0.2rem rgba(206, 39, 91, 0.25);
+}
+
+/* Alerts */
+.alert-primary {
+ background-color: rgba(206, 39, 91, 0.1);
+ border-color: var(--pivoine-rose);
+ color: var(--pivoine-rose-dark);
+}
+
+/* Tables */
+.table {
+ background-color: white;
+}
+
+.table thead th {
+ background-color: var(--gray-100);
+ border-bottom: 2px solid var(--pivoine-rose);
+ color: var(--gray-800);
+}
+
+.table-striped tbody tr:nth-of-type(odd) {
+ background-color: var(--gray-50);
+}
+
+/* Pagination */
+.pagination .page-item.active .page-link {
+ background-color: var(--pivoine-rose);
+ border-color: var(--pivoine-rose);
+}
+
+.pagination .page-link {
+ color: var(--pivoine-rose);
+}
+
+.pagination .page-link:hover {
+ color: var(--pivoine-rose-dark);
+ background-color: var(--gray-100);
+}
+
+/* Player/Terminal */
+.asciinema-player-wrapper,
+.asciinema-terminal {
+ border-radius: 4px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+/* Footer */
+footer {
+ background-color: var(--gray-800);
+ color: var(--gray-400);
+ border-top: 3px solid var(--pivoine-rose);
+}
+
+footer a {
+ color: var(--gray-300);
+}
+
+footer a:hover {
+ color: var(--pivoine-rose);
+}
+
+/* Badges */
+.badge {
+ border-radius: 3px;
+ font-weight: 500;
+}
+
+.badge-secondary {
+ background-color: var(--gray-600);
+}
+
+/* Dropdowns */
+.dropdown-menu {
+ border-color: var(--gray-300);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.dropdown-item:hover,
+.dropdown-item:focus {
+ background-color: var(--gray-100);
+ color: var(--pivoine-rose);
+}
+
+/* Loading/Progress */
+.progress-bar {
+ background-color: var(--pivoine-rose);
+}
+
+/* Custom recording list */
+.recording-item {
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.recording-item:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* Logo styling */
+.navbar-brand svg,
+.navbar-brand img {
+ filter: brightness(0) saturate(100%) invert(28%) sepia(91%) saturate(2059%) hue-rotate(325deg) brightness(89%) contrast(92%);
+ transition: filter 0.2s ease;
+}
+
+.navbar-brand:hover svg,
+.navbar-brand:hover img {
+ filter: brightness(0) saturate(100%) invert(36%) sepia(96%) saturate(2373%) hue-rotate(329deg) brightness(96%) contrast(87%);
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ main {
+ padding: 1rem;
+ }
+
+ .container,
+ .container-fluid {
+ padding: 1rem 0.5rem;
+ }
+}
+
+/* Smooth animations */
+* {
+ transition-property: background-color, border-color, color;
+ transition-duration: 0.15s;
+ transition-timing-function: ease-in-out;
+}
+
+button,
+.btn,
+a {
+ transition-property: all;
+}
diff --git a/asciinema/theme/favicon.svg b/asciinema/theme/favicon.svg
new file mode 100644
index 0000000..2a96a8b
--- /dev/null
+++ b/asciinema/theme/favicon.svg
@@ -0,0 +1,24 @@
+