diff --git a/app/config.py b/app/config.py
index 2383c73..0096d63 100644
--- a/app/config.py
+++ b/app/config.py
@@ -13,8 +13,8 @@ class Settings(BaseSettings):
# Gitea
gitea_url: str = "https://git.dracodev.net"
gitea_token: str = "change-me"
- gitea_oauth_client_id: str = ""
- gitea_oauth_client_secret: str = ""
+ gitea_oauth_client_id: str = "test-id"
+ gitea_oauth_client_secret: str = "test-secret"
gitea_webhook_secret: str = ""
# GitHub
@@ -53,7 +53,12 @@ class Settings(BaseSettings):
if self.database_url == "sqlite:///:memory:":
return Path(":memory:") # Special SQLite in-memory
if self.database_url.startswith("sqlite:///"):
- return Path(self.database_url.replace("sqlite:///", "/"))
+ p = self.database_url.replace("sqlite:///", "", 1)
+ # On Windows, don't prepend / if path starts with a drive letter
+ import re
+ if re.match(r'^[a-zA-Z]:', p):
+ return Path(p)
+ return Path("/" + p)
return Path("/data/flowdeck.db")
diff --git a/app/routers/auth.py b/app/routers/auth.py
index 2019c4f..8d263c2 100644
--- a/app/routers/auth.py
+++ b/app/routers/auth.py
@@ -153,9 +153,11 @@ async def register(request: Request):
if existing:
from fastapi.responses import JSONResponse
return JSONResponse({"error": "Account already exists"}, status_code=409)
- # First user is admin
- user_count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
- is_admin = 1 if user_count == 0 else 0
+ # First real user (excluding default admin with no password) is admin
+ real_user_count = conn.execute(
+ "SELECT COUNT(*) FROM users WHERE password_hash IS NOT NULL AND password_hash != ''"
+ ).fetchone()[0]
+ is_admin = 1 if real_user_count == 0 else 0
conn.execute(
"INSERT INTO users (login, full_name, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)",
(email, name, email, hash_password(password), is_admin),
diff --git a/app/routers/board.py b/app/routers/board.py
index 9b629ae..45f3c20 100644
--- a/app/routers/board.py
+++ b/app/routers/board.py
@@ -926,10 +926,6 @@ async def view_page(request: Request, page_id: int):
return HTMLResponse("
Page not found
", status_code=404)
page = dict(row)
- # ── File viewer for uploaded files ──
- if page.get("content_format") == "file":
- return _render_file_viewer(page, request)
-
ws = page.get("workspace", "")
parts = ws.split("/") if "/" in ws else ["", ""]
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
@@ -941,10 +937,38 @@ async def view_page(request: Request, page_id: int):
fav = conn.execute(
"SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id)
).fetchone()
+
+ # Build page_data, including file metadata for uploaded files
+ page_data = {"id": page_id, "title": page.get("title"), "content_format": page.get("content_format", "blocks"), "content": page.get("content", ""), "favorited": fav is not None, "share_mode": page.get("share_mode", "private"), "published": bool(page.get("published", 0))}
+
+ # For file pages, extract file metadata and add to page_data
+ if page.get("content_format") == "file":
+ import json as _json
+ try:
+ meta = _json.loads(page.get("content", "{}"))
+ except _json.JSONDecodeError:
+ meta = {}
+ file_path = meta.get("file_path", "").replace("\\", "/")
+ mime_type = meta.get("mime_type", "application/octet-stream")
+ file_size = meta.get("size", 0)
+ # Build workspace_id from file_path
+ fp_parts = file_path.split("/")
+ ws_id = ""
+ for p in fp_parts:
+ if p.startswith("workspace_"):
+ ws_id = p.replace("workspace_", "")
+ break
+ filename = fp_parts[-1] if fp_parts else page.get("title", "File")
+ file_url = f"/api/files/{ws_id}/{filename}" if ws_id else ""
+ page_data["file_url"] = file_url
+ page_data["file_mime"] = mime_type
+ page_data["file_size"] = file_size
+ page_data["file_name"] = filename
+
ctx = {**sidebar, "page": page, "page_favorited": fav is not None,
"page_share_mode": page.get("share_mode", "private"),
"page_published": bool(page.get("published", 0)),
- "page_data": {"id": page_id, "title": page.get("title"), "content_format": page.get("content_format", "blocks"), "content": page.get("content", ""), "favorited": fav is not None, "share_mode": page.get("share_mode", "private"), "published": bool(page.get("published", 0))}}
+ "page_data": page_data}
template = env.get_template("page_editor.html")
response = template.render(**ctx)
return HTMLResponse(content=response, headers={"Cache-Control": "no-store, max-age=0"})
diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py
index 5529d27..623e257 100644
--- a/app/routers/dashboard.py
+++ b/app/routers/dashboard.py
@@ -216,7 +216,6 @@ async def view_page_root(request: Request, page_id: int):
"""Render a Markdown page at root level with workspace context — or file viewer."""
from jinja2 import Environment, FileSystemLoader
from app.routers.board import _sidebar_data as board_sidebar
- from app.routers.board import _render_file_viewer
env = Environment(loader=FileSystemLoader("app/templates"))
with get_conn() as conn:
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
@@ -224,9 +223,6 @@ async def view_page_root(request: Request, page_id: int):
return HTMLResponse("Page not found
", status_code=404)
page = dict(row)
- # ── File viewer for uploaded files ──
- if page.get("content_format") == "file":
- return _render_file_viewer(page, request)
ws = page.get("workspace", "")
parts = ws.split("/") if "/" in ws else ["", ""]
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
@@ -237,11 +233,42 @@ async def view_page_root(request: Request, page_id: int):
"SELECT id, title FROM pages WHERE parent_id=? ORDER BY updated_at DESC",
(page_id,),
).fetchall()
+ fav = conn.execute(
+ "SELECT id FROM favorites WHERE user_id=? AND page_id=?",
+ (1, page_id),
+ ).fetchone()
+
+ # Build page_data, including file metadata for uploaded files
+ page_data = {"id": page_id, "title": page.get("title"), "content_format": page.get("content_format", "blocks"), "content": page.get("content", ""), "favorited": fav is not None, "share_mode": page.get("share_mode", "private"), "published": bool(page.get("published", 0))}
+
+ # For file pages, extract file metadata and add to page_data
+ if page.get("content_format") == "file":
+ import json as _json
+ try:
+ meta = _json.loads(page.get("content", "{}"))
+ except _json.JSONDecodeError:
+ meta = {}
+ file_path = meta.get("file_path", "").replace("\\", "/")
+ mime_type = meta.get("mime_type", "application/octet-stream")
+ file_size = meta.get("size", 0)
+ fp_parts = file_path.split("/")
+ ws_id = ""
+ for p in fp_parts:
+ if p.startswith("workspace_"):
+ ws_id = p.replace("workspace_", "")
+ break
+ filename = fp_parts[-1] if fp_parts else page.get("title", "File")
+ file_url = f"/api/files/{ws_id}/{filename}" if ws_id else ""
+ page_data["file_url"] = file_url
+ page_data["file_mime"] = mime_type
+ page_data["file_size"] = file_size
+ page_data["file_name"] = filename
+
ctx = {**sidebar, "page": page, "sub_pages": [dict(s) for s in subs],
- "page_favorited": False,
+ "page_favorited": fav is not None,
"page_share_mode": page.get("share_mode", "private"),
"page_published": bool(page.get("published", 0)),
- "page_data": {"id": page_id, "title": page.get("title"), "content_format": page.get("content_format", "blocks"), "content": page.get("content", ""), "favorited": False, "share_mode": page.get("share_mode", "private"), "published": bool(page.get("published", 0))}}
+ "page_data": page_data}
template = env.get_template("page_editor.html")
response = template.render(**ctx)
return HTMLResponse(content=response, headers={"Cache-Control": "no-store, max-age=0"})
@@ -866,8 +893,11 @@ async def serve_uploaded_file(ws_id: int, filename: str):
"""Serve an uploaded file from disk."""
from pathlib import Path
import mimetypes
- fp = Path(f"/data/uploads/workspace_{ws_id}/{filename}").resolve()
- if not str(fp).startswith(f"/data/uploads/workspace_{ws_id}/"):
+ base_dir = Path(f"/data/uploads/workspace_{ws_id}").resolve()
+ fp = (base_dir / filename).resolve()
+ try:
+ fp.relative_to(base_dir)
+ except ValueError:
return JSONResponse({"error": "Path traversal denied"}, status_code=403)
if not fp.exists():
return JSONResponse({"error": "File not found"}, status_code=404)
diff --git a/app/templates/base.html b/app/templates/base.html
index 13590f9..935cccf 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -1,12 +1,11 @@
-
+
FlowDeck — {% block title_prefix %}Home{% endblock %}
-
-
+
@@ -1049,38 +1048,13 @@
toggleTheme() {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
- html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
- localStorage.setItem('fd_theme', current === 'dark' ? 'light' : 'dark');
- if (current === 'dark') {
- // Switching to light
- html.style.setProperty('--bg-primary', '#FFFFFF');
- html.style.setProperty('--bg-secondary', '#F7F6F3');
- html.style.setProperty('--bg-tertiary', '#EDECE9');
- html.style.setProperty('--text', '#37352F');
- html.style.setProperty('--text-secondary', '#6B6966');
- html.style.setProperty('--text-dim', '#9B9A97');
- html.style.setProperty('--border', '#E0DFDC');
- html.style.setProperty('--border-strong', '#CCCBC8');
- } else {
- // Switching to dark — remove overrides, use CSS defaults
- ['bg-primary','bg-secondary','bg-tertiary','text','text-secondary','text-dim','border','border-strong'].forEach(function(v) {
- html.style.removeProperty('--'+v);
- });
- }
+ const next = current === 'dark' ? 'light' : 'dark';
+ html.setAttribute('data-theme', next);
+ localStorage.setItem('fd_theme', next);
},
initTheme() {
- var saved = localStorage.getItem('fd_theme') || 'dark';
- if (saved === 'light') {
- document.documentElement.setAttribute('data-theme', 'light');
- document.documentElement.style.setProperty('--bg-primary', '#FFFFFF');
- document.documentElement.style.setProperty('--bg-secondary', '#F7F6F3');
- document.documentElement.style.setProperty('--bg-tertiary', '#EDECE9');
- document.documentElement.style.setProperty('--text', '#37352F');
- document.documentElement.style.setProperty('--text-secondary', '#6B6966');
- document.documentElement.style.setProperty('--text-dim', '#9B9A97');
- document.documentElement.style.setProperty('--border', '#E0DFDC');
- document.documentElement.style.setProperty('--border-strong', '#CCCBC8');
- }
+ var saved = localStorage.getItem('fd_theme') || 'light';
+ document.documentElement.setAttribute('data-theme', saved);
}
};
}
@@ -1115,24 +1089,8 @@
// Apply saved theme on page load (AND on bfcache restore)
function applySavedTheme() {
- var saved = localStorage.getItem('fd_theme') || 'dark';
- var h = document.documentElement;
- h.setAttribute('data-theme', saved);
- if (saved === 'light') {
- h.style.setProperty('--bg-primary', '#FFFFFF');
- h.style.setProperty('--bg-secondary', '#F7F6F3');
- h.style.setProperty('--bg-tertiary', '#EDECE9');
- h.style.setProperty('--text', '#37352F');
- h.style.setProperty('--text-secondary', '#6B6966');
- h.style.setProperty('--text-dim', '#9B9A97');
- h.style.setProperty('--border', '#E0DFDC');
- h.style.setProperty('--border-strong', '#CCCBC8');
- } else {
- // Remove light overrides, use CSS defaults
- ['bg-primary','bg-secondary','bg-tertiary','text','text-secondary','text-dim','border','border-strong'].forEach(function(v) {
- h.style.removeProperty('--'+v);
- });
- }
+ var saved = localStorage.getItem('fd_theme') || 'light';
+ document.documentElement.setAttribute('data-theme', saved);
}
applySavedTheme();
// Re-apply when page is restored from bfcache (e.g. browser back button)
diff --git a/app/templates/gitea_workspace.html b/app/templates/gitea_workspace.html
index f4cac8c..f6455e1 100644
--- a/app/templates/gitea_workspace.html
+++ b/app/templates/gitea_workspace.html
@@ -758,7 +758,7 @@ document.addEventListener('alpine:init', () => {
x-show="gwCtx.visible"
:style="'left:' + gwCtx.x + 'px; top:' + gwCtx.y + 'px'"
@click.outside="gwCtx.visible = false"
- style="display:none;position:fixed;background:var(--bg-secondary);border:1px solid var(--border-strong);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:1100;min-width:160px;padding:4px;">
+ style="display:none;position:fixed;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-popover);z-index:1100;min-width:160px;padding:4px;">
✏️ Rename
🗑 Delete
diff --git a/app/templates/library.html b/app/templates/library.html
index 3d4b224..9b58304 100644
--- a/app/templates/library.html
+++ b/app/templates/library.html
@@ -110,7 +110,7 @@
.source-filter-btn.active {
background: var(--bg-primary);
color: var(--text-primary);
- box-shadow: 0 1px 2px rgba(0,0,0,.2);
+ box-shadow: var(--shadow-popover);
}
/* ── Card grid ── */
@@ -171,15 +171,15 @@
}
.source-local {
background: rgba(100, 180, 100, 0.15);
- color: #64b464;
+ color: var(--green);
}
.source-gitea {
background: rgba(100, 160, 220, 0.15);
- color: #64a0dc;
+ color: var(--blue);
}
.source-github {
background: rgba(180, 140, 220, 0.15);
- color: #b48cdc;
+ color: var(--purple);
}
.library-card-workspace {
font-size: 12px;
diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html
index b281808..89ae2e8 100644
--- a/app/templates/local_workspace.html
+++ b/app/templates/local_workspace.html
@@ -67,7 +67,7 @@
.modified-dot.show{display:block;}
/* Offline banner */
-.offline-banner{position:fixed;top:0;left:0;right:0;background:#f85149;color:#fff;text-align:center;padding:6px 12px;font-size:12px;z-index:500;font-weight:500;}
+.offline-banner{position:fixed;top:0;left:0;right:0;background:var(--danger);color:#fff;text-align:center;padding:6px 12px;font-size:12px;z-index:500;font-weight:500;}
/* Pinned indicator */
.pin-icon{font-size:10px;color:var(--text-tertiary);cursor:pointer;margin-left:4px;opacity:0;transition:opacity 150ms;}
@@ -83,6 +83,8 @@
.bulk-bar{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--accent);color:#fff;border-radius:8px;margin-bottom:10px;font-size:13px;font-weight:500;}
.bulk-bar .btn-danger{background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.3);}
.bulk-bar .btn-secondary{background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);}
+[data-theme="light"] .bulk-bar .btn-danger{background:rgba(0,0,0,.08);color:var(--text-primary);border:1px solid var(--border);}
+[data-theme="light"] .bulk-bar .btn-secondary{background:rgba(0,0,0,.05);color:var(--text-primary);border:1px solid var(--border);}
/* Table view */
.ws-table{width:100%;border-collapse:collapse;font-size:13px;}
@@ -94,8 +96,8 @@
.ws-table .table-name:hover{color:var(--accent);}
.ws-table .table-meta{color:var(--text-tertiary);font-size:12px;}
-.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:999;backdrop-filter:blur(4px);}
-.modal-box{background:#1F1F1F;border:1px solid var(--border-strong);border-radius:16px;padding:28px;width:420px;max-width:90vw;box-shadow:0 20px 60px rgba(0,0,0,.5);}
+.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:999;backdrop-filter:blur(4px);}
+.modal-box{background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);padding:28px;width:420px;max-width:90vw;box-shadow:var(--shadow-modal);}
.modal-box h3{font-size:16px;margin-bottom:6px;display:flex;align-items:center;gap:8px;}
.modal-box .modal-sub{font-size:12px;color:var(--text-tertiary);margin-bottom:20px;}
.modal-input{width:100%;padding:12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:14px;outline:none;}
@@ -117,7 +119,7 @@
.drop-overlay .drop-hint{background:var(--bg);border:2px dashed var(--accent);border-radius:16px;padding:40px 60px;text-align:center;color:var(--text);font-size:16px;}
.drop-overlay .drop-hint .icon{font-size:48px;display:block;margin-bottom:12px;}
.drop-target-highlight{outline:2px solid var(--accent)!important;outline-offset:-2px;border-radius:6px;background:rgba(35,131,226,.1)!important;}
-.upload-progress{position:fixed;bottom:24px;right:24px;background:var(--bg);border:1px solid var(--border-strong);border-radius:12px;padding:16px 20px;z-index:200;box-shadow:0 8px 30px rgba(0,0,0,.4);min-width:200px;}
+.upload-progress{position:fixed;bottom:24px;right:24px;background:var(--bg-modal);border:1px solid var(--border);border-radius:12px;padding:16px 20px;z-index:200;box-shadow:var(--shadow-popover);min-width:200px;}
.upload-progress .up-bar{height:4px;background:var(--bg-tertiary);border-radius:2px;margin-top:8px;overflow:hidden;}
.upload-progress .up-bar-fill{height:100%;background:var(--accent);border-radius:2px;transition:width 200ms;}
@@ -153,7 +155,7 @@
.sort-select:focus{border-color:var(--accent);}
/* Undo toast */
-.undo-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg);border:1px solid var(--border-strong);border-radius:12px;padding:10px 20px;z-index:300;box-shadow:0 8px 30px rgba(0,0,0,.5);display:flex;align-items:center;gap:12px;font-size:13px;}
+.undo-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg-modal);border:1px solid var(--border);border-radius:12px;padding:10px 20px;z-index:300;box-shadow:var(--shadow-popover);display:flex;align-items:center;gap:12px;font-size:13px;}
.undo-btn{background:var(--accent);color:#fff;border:none;padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;}
.undo-btn:hover{opacity:.85;}
.undo-dismiss{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:18px;padding:0 4px;}
@@ -171,9 +173,9 @@
.path-go:hover{opacity:.85;}
/* File preview tooltip */
-.file-preview{position:fixed;z-index:300;background:#1a1a1a;background:var(--bg,#1a1a1a);border:1px solid var(--border-strong,#333);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.5);max-width:400px;min-width:200px;pointer-events:auto;}
-.file-preview-header{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary,#888);border-bottom:1px solid var(--border,#333);}
-.file-preview-body{padding:10px 12px;margin:0;font-size:12px;line-height:1.5;color:var(--text,#ddd);max-height:200px;overflow:auto;font-family:'Fira Code','Cascadia Code',monospace;white-space:pre-wrap;word-wrap:break-word;background:#141414;}
+.file-preview{position:fixed;z-index:300;background:var(--bg-modal);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-popover);max-width:400px;min-width:200px;pointer-events:auto;}
+.file-preview-header{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border);}
+.file-preview-body{padding:10px 12px;margin:0;font-size:12px;line-height:1.5;color:var(--text);max-height:200px;overflow:auto;font-family:'Fira Code','Cascadia Code',monospace;white-space:pre-wrap;word-wrap:break-word;background:var(--bg-secondary);}
/* Tag chips */
.tag-chip{display:inline-flex;align-items:center;gap:2px;padding:1px 8px;border-radius:10px;font-size:11px;color:#fff;white-space:nowrap;margin:1px 2px;transition:all 150ms;}
@@ -254,13 +256,13 @@
.view-dropdown-btn{display:flex;align-items:center;gap:6px;padding:5px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:12px;color:var(--text);white-space:nowrap;}
.view-dropdown-btn:hover{background:var(--bg-hover);border-color:var(--accent);}
.dropdown-arrow{font-size:10px;color:var(--text-tertiary);}
-.view-dropdown-menu{position:absolute;top:100%;left:0;margin-top:4px;background:#1E1E1E;border:1px solid var(--border-strong);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.6);z-index:250;min-width:140px;overflow:hidden;}
+.view-dropdown-menu{position:absolute;top:100%;left:0;margin-top:4px;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-popover);z-index:250;min-width:140px;overflow:hidden;}
.view-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;background:none;border:none;color:var(--text);font-size:13px;cursor:pointer;text-align:left;}
.view-menu-item:hover{background:var(--bg-hover);}
.view-menu-item.active{background:rgba(35,131,226,.12);color:var(--accent);font-weight:500;}
/* Context menu */
-.ctx-menu{position:fixed;background:#1E1E1E;border:1px solid var(--border-strong);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.5);min-width:200px;z-index:600;padding:4px 0;max-height:80vh;overflow-y:auto;}
+.ctx-menu{position:fixed;background:var(--bg-modal);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-popover);min-width:200px;z-index:600;padding:4px 0;max-height:80vh;overflow-y:auto;}
.ctx-item{display:flex;align-items:center;gap:8px;padding:7px 14px;font-size:13px;color:var(--text);cursor:pointer;transition:background 100ms;}
.ctx-item:hover{background:var(--bg-hover);}
.ctx-item.ctx-danger{color:var(--danger);}
diff --git a/app/templates/page_editor.html b/app/templates/page_editor.html
index 5d02c08..7dc8ded 100644
--- a/app/templates/page_editor.html
+++ b/app/templates/page_editor.html
@@ -1,8 +1,8 @@
-{% extends "base.html" %} {% block page_icon %}📄{% endblock %} {% block
+{% extends "base.html" %} {% block page_icon %}{% if page.content_format == 'file' %}📎{% else %}📄{% endif %}{% endblock %} {% block
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
-{% set page_icon = "📄" %}
+{% set page_icon = "📄" if page.content_format != 'file' else "📎" %}
{% set page_title = page.title %}
-{% set right_actions = 'Edited ▾' %}
+{% set right_actions = 'Edited ▾' %}
{% include '_header.html' %}
{% endblock %} {% block content %}
@@ -12,7 +12,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
@@ -336,9 +336,9 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
+ style="position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-modal);z-index:1000;width:280px;padding:12px;">
Edited
Created
@@ -349,7 +349,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}