diff --git a/CLAUDE.md b/CLAUDE.md
index 11bb167..405a70f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -13,9 +13,21 @@ Generate report with defaults:
python generate_report.py
```
-Generate report with custom options:
+Generate report with modern style:
```bash
-python generate_report.py --db pga.db --output report.html --top 10 --background background.png --template templates/platinum.html
+python generate_report.py --style glassmorphism --output report.html
+python generate_report.py --style brutalism --output report.html
+python generate_report.py --style neumorphism --output report.html
+```
+
+Generate report with legacy Platinum template:
+```bash
+python generate_report.py --template templates/platinum.html --output report.html
+```
+
+All options:
+```bash
+python generate_report.py --db pga.db --output report.html --top 10 --background background.png --style glassmorphism
```
## Architecture
@@ -26,13 +38,16 @@ python generate_report.py --db pga.db --output report.html --top 10 --background
- Loads HTML template from `templates/` folder (default: `templates/platinum.html`)
**HTML templates (`templates/`):**
-- **platinum.html**: Mac OS 9 Platinum visual style
-- **brutalism.html**: Bold industrial brutalist design with hard shadows
-- **glassmorphism.html**: Modern frosted glass effect
-- **neumorphism.html**: Soft 3D neumorphic style
+- **modern.html**: Unified template for modern styles (brutalism, glassmorphism, neumorphism)
+- **platinum.html**: Legacy Mac OS 9 Platinum visual style (separate template due to unique structure)
- All templates use Chart.js doughnut charts and dynamic JavaScript filtering
- Placeholder tokens like `__ALL_GAMES__`, `__BACKGROUND_IMAGE__` are replaced at generation time
-- Support light/dark mode with theme toggle button
+- Modern templates support light/dark/auto theme toggle button
+
+**Style system (`styles.py`):**
+- CSS definitions for each modern style (brutalism, glassmorphism, neumorphism)
+- Theme configurations (colors, fonts, chart options) injected via `__THEME_CSS__` and `__THEME_CONFIG__`
+- Use `--style` argument to select a modern style instead of `--template`
**Database schema (`schema.py`):**
- Reference file documenting Lutris database structure
diff --git a/generate_report.py b/generate_report.py
index c76b812..978e79f 100644
--- a/generate_report.py
+++ b/generate_report.py
@@ -21,9 +21,14 @@ import json
import sqlite3
from pathlib import Path
+from styles import get_theme_css, get_theme_config
+
# Directory where this script is located (for finding template.html)
SCRIPT_DIR = Path(__file__).parent
+# Modern styles that use the unified template
+MODERN_STYLES = ["brutalism", "glassmorphism", "neumorphism"]
+
def load_template(template_file: str) -> str:
"""Load the HTML template from the specified file."""
@@ -83,7 +88,7 @@ def get_all_games(db_path: str) -> tuple[list[dict], int]:
return games, total_library
-def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, template_file: str, bg_image_path: str = None) -> None:
+def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, template_file: str, bg_image_path: str = None, style: str = None) -> None:
"""Generate the HTML report."""
all_games, total_library = get_all_games(db_path)
@@ -104,50 +109,67 @@ def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str,
background_image = load_asset_as_base64(assets_path / "Others" / "stripes.png", "image/png")
background_image_custom = "none" # For templates that prefer no default background
- # Load fonts
- font_charcoal = load_asset_as_base64(assets_path / "Charcoal.ttf", "font/truetype")
- font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype")
+ # Check if using modern unified template
+ if style and style in MODERN_STYLES:
+ html = load_template("templates/modern.html")
- # Load images
- titlebar_bg = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png")
- title_stripes = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png")
- close_btn = load_asset_as_base64(assets_path / "Windows" / "close-active.png", "image/png")
- hide_btn = load_asset_as_base64(assets_path / "Windows" / "maximize-active.png", "image/png")
- shade_btn = load_asset_as_base64(assets_path / "Windows" / "shade-active.png", "image/png")
- check_off = load_asset_as_base64(assets_path / "Check-Radio" / "check-normal.png", "image/png")
- check_on = load_asset_as_base64(assets_path / "Check-Radio" / "check-active.png", "image/png")
+ # Inject theme CSS and config
+ theme_css = get_theme_css(style)
+ theme_config = get_theme_config(style)
- # Load scrollbar images
- scrollbar_trough_v = load_asset_as_base64(assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png")
- scrollbar_thumb_v = load_asset_as_base64(assets_path / "Scrollbars" / "slider-vertical.png", "image/png")
- scrollbar_up = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-up.png", "image/png")
- scrollbar_down = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-down.png", "image/png")
+ html = html.replace("__THEME_CSS__", theme_css)
+ html = html.replace("__THEME_CONFIG__", theme_config)
+ html = html.replace("__ALL_GAMES__", json.dumps(all_games))
+ html = html.replace("__TOP_N__", str(top_n))
+ html = html.replace("__TOTAL_LIBRARY__", str(total_library))
+ html = html.replace("__BACKGROUND_IMAGE__", background_image)
+ html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom)
+ else:
+ # Legacy template handling (platinum and others)
+ # Load fonts
+ font_charcoal = load_asset_as_base64(assets_path / "Charcoal.ttf", "font/truetype")
+ font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype")
- # Load tab images
- tab_active = load_asset_as_base64(assets_path / "Tabs" / "tab-top-active.png", "image/png")
- tab_inactive = load_asset_as_base64(assets_path / "Tabs" / "tab-top.png", "image/png")
+ # Load images
+ titlebar_bg = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png")
+ title_stripes = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png")
+ close_btn = load_asset_as_base64(assets_path / "Windows" / "close-active.png", "image/png")
+ hide_btn = load_asset_as_base64(assets_path / "Windows" / "maximize-active.png", "image/png")
+ shade_btn = load_asset_as_base64(assets_path / "Windows" / "shade-active.png", "image/png")
+ check_off = load_asset_as_base64(assets_path / "Check-Radio" / "check-normal.png", "image/png")
+ check_on = load_asset_as_base64(assets_path / "Check-Radio" / "check-active.png", "image/png")
- html = load_template(template_file)
- html = html.replace("__ALL_GAMES__", json.dumps(all_games))
- html = html.replace("__TOP_N__", str(top_n))
- html = html.replace("__TOTAL_LIBRARY__", str(total_library))
- html = html.replace("__FONT_CHARCOAL__", font_charcoal)
- html = html.replace("__FONT_MONACO__", font_monaco)
- html = html.replace("__BACKGROUND_IMAGE__", background_image)
- html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom)
- html = html.replace("__TITLEBAR_BG__", titlebar_bg)
- html = html.replace("__TITLE_STRIPES__", title_stripes)
- html = html.replace("__CLOSE_BTN__", close_btn)
- html = html.replace("__HIDE_BTN__", hide_btn)
- html = html.replace("__SHADE_BTN__", shade_btn)
- html = html.replace("__CHECK_OFF__", check_off)
- html = html.replace("__CHECK_ON__", check_on)
- html = html.replace("__SCROLLBAR_TROUGH_V__", scrollbar_trough_v)
- html = html.replace("__SCROLLBAR_THUMB_V__", scrollbar_thumb_v)
- html = html.replace("__SCROLLBAR_UP__", scrollbar_up)
- html = html.replace("__SCROLLBAR_DOWN__", scrollbar_down)
- html = html.replace("__TAB_ACTIVE__", tab_active)
- html = html.replace("__TAB_INACTIVE__", tab_inactive)
+ # Load scrollbar images
+ scrollbar_trough_v = load_asset_as_base64(assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png")
+ scrollbar_thumb_v = load_asset_as_base64(assets_path / "Scrollbars" / "slider-vertical.png", "image/png")
+ scrollbar_up = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-up.png", "image/png")
+ scrollbar_down = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-down.png", "image/png")
+
+ # Load tab images
+ tab_active = load_asset_as_base64(assets_path / "Tabs" / "tab-top-active.png", "image/png")
+ tab_inactive = load_asset_as_base64(assets_path / "Tabs" / "tab-top.png", "image/png")
+
+ html = load_template(template_file)
+ html = html.replace("__ALL_GAMES__", json.dumps(all_games))
+ html = html.replace("__TOP_N__", str(top_n))
+ html = html.replace("__TOTAL_LIBRARY__", str(total_library))
+ html = html.replace("__FONT_CHARCOAL__", font_charcoal)
+ html = html.replace("__FONT_MONACO__", font_monaco)
+ html = html.replace("__BACKGROUND_IMAGE__", background_image)
+ html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom)
+ html = html.replace("__TITLEBAR_BG__", titlebar_bg)
+ html = html.replace("__TITLE_STRIPES__", title_stripes)
+ html = html.replace("__CLOSE_BTN__", close_btn)
+ html = html.replace("__HIDE_BTN__", hide_btn)
+ html = html.replace("__SHADE_BTN__", shade_btn)
+ html = html.replace("__CHECK_OFF__", check_off)
+ html = html.replace("__CHECK_ON__", check_on)
+ html = html.replace("__SCROLLBAR_TROUGH_V__", scrollbar_trough_v)
+ html = html.replace("__SCROLLBAR_THUMB_V__", scrollbar_thumb_v)
+ html = html.replace("__SCROLLBAR_UP__", scrollbar_up)
+ html = html.replace("__SCROLLBAR_DOWN__", scrollbar_down)
+ html = html.replace("__TAB_ACTIVE__", tab_active)
+ html = html.replace("__TAB_INACTIVE__", tab_inactive)
Path(output_path).write_text(html, encoding="utf-8")
print(f"Report generated: {output_path}")
@@ -189,7 +211,13 @@ def main():
parser.add_argument(
"--template",
default="templates/platinum.html",
- help="HTML template file to use (default: templates/platinum.html)"
+ help="HTML template file to use (default: templates/platinum.html). Ignored if --style is used."
+ )
+ parser.add_argument(
+ "--style",
+ choices=["brutalism", "glassmorphism", "neumorphism"],
+ default=None,
+ help="Modern style to use (brutalism, glassmorphism, neumorphism). Overrides --template."
)
args = parser.parse_args()
@@ -202,12 +230,17 @@ def main():
print(f"Error: Assets directory not found: {args.assets}")
return 1
- template_path = SCRIPT_DIR / args.template
+ # Validate template only if not using modern style
+ if args.style and args.style in MODERN_STYLES:
+ template_path = SCRIPT_DIR / "templates" / "modern.html"
+ else:
+ template_path = SCRIPT_DIR / args.template
+
if not template_path.exists():
print(f"Error: Template file not found: {template_path}")
return 1
- generate_report(args.db, args.output, args.top, args.assets, args.template, args.background)
+ generate_report(args.db, args.output, args.top, args.assets, args.template, args.background, args.style)
return 0
diff --git a/styles.py b/styles.py
new file mode 100644
index 0000000..a1b5dfd
--- /dev/null
+++ b/styles.py
@@ -0,0 +1,1903 @@
+"""CSS styles and theme configurations for modern templates."""
+
+import json
+
+# Theme configurations for Chart.js
+THEME_CONFIGS = {
+ "brutalism": {
+ "colors": [
+ "#ff0000", "#0000ff", "#ffff00", "#00ff00", "#ff00ff",
+ "#00ffff", "#ff8800", "#8800ff", "#0088ff", "#88ff00",
+ "#888888"
+ ],
+ "fontFamily": "'Courier New', monospace",
+ "fontWeight": "bold",
+ "pointStyle": "rect",
+ "textColorLight": "#000000",
+ "textColorDark": "#ffffff",
+ "borderColorLight": "#000000",
+ "borderColorDark": "#ffffff",
+ "borderWidth": 3,
+ "tooltipBg": "#000000",
+ "tooltipTitleColor": "#ffffff",
+ "tooltipBodyColor": "#ffffff",
+ "tooltipBorderColor": "#ffffff",
+ "tooltipBorderWidth": 2,
+ "tooltipCornerRadius": 0,
+ "uppercaseTooltip": True
+ },
+ "glassmorphism": {
+ "colors": [
+ "#6366f1", "#8b5cf6", "#ec4899", "#f43f5e", "#f97316",
+ "#eab308", "#22c55e", "#14b8a6", "#06b6d4", "#3b82f6",
+ "#64748b"
+ ],
+ "fontFamily": "'Inter', sans-serif",
+ "fontWeight": "normal",
+ "pointStyle": "circle",
+ "textColorLight": "#1a1a2e",
+ "textColorDark": "#f0f0f5",
+ "borderColorLight": "rgba(255, 255, 255, 0.2)",
+ "borderColorDark": "rgba(255, 255, 255, 0.2)",
+ "borderWidth": 2,
+ "tooltipBg": "rgba(0, 0, 0, 0.8)",
+ "tooltipTitleColor": "#ffffff",
+ "tooltipBodyColor": "#ffffff",
+ "tooltipBorderColor": "transparent",
+ "tooltipBorderWidth": 0,
+ "tooltipCornerRadius": 8,
+ "uppercaseTooltip": False
+ },
+ "neumorphism": {
+ "colors": [
+ "#6366f1", "#8b5cf6", "#ec4899", "#f43f5e", "#f97316",
+ "#eab308", "#22c55e", "#14b8a6", "#06b6d4", "#3b82f6",
+ "#64748b"
+ ],
+ "fontFamily": "'Inter', sans-serif",
+ "fontWeight": "normal",
+ "pointStyle": "circle",
+ "textColorLight": "#2d3436",
+ "textColorDark": "#f0f0f5",
+ "borderColorLight": "rgba(255, 255, 255, 0.3)",
+ "borderColorDark": "rgba(255, 255, 255, 0.3)",
+ "borderWidth": 3,
+ "tooltipBg": "rgba(0, 0, 0, 0.8)",
+ "tooltipTitleColor": "#ffffff",
+ "tooltipBodyColor": "#ffffff",
+ "tooltipBorderColor": "transparent",
+ "tooltipBorderWidth": 0,
+ "tooltipCornerRadius": 8,
+ "uppercaseTooltip": False
+ }
+}
+
+# CSS styles for each theme
+THEME_CSS = {
+ "brutalism": """
+ :root {
+ --bg-base: #ffffff;
+ --bg-primary: #ffffff;
+ --bg-secondary: #f0f0f0;
+ --bg-tertiary: #e0e0e0;
+ --text-primary: #000000;
+ --text-secondary: #333333;
+ --text-muted: #666666;
+ --border-color: #000000;
+ --border-width: 3px;
+ --accent-color: #ff0000;
+ --accent-secondary: #0000ff;
+ --accent-tertiary: #ffff00;
+ --selection-bg: #ffff00;
+ --selection-text: #000000;
+ --shadow-offset: 6px;
+ }
+
+ [data-theme="dark"] {
+ --bg-base: #000000;
+ --bg-primary: #000000;
+ --bg-secondary: #111111;
+ --bg-tertiary: #222222;
+ --text-primary: #ffffff;
+ --text-secondary: #cccccc;
+ --text-muted: #888888;
+ --border-color: #ffffff;
+ --accent-color: #ff3333;
+ --accent-secondary: #3333ff;
+ --accent-tertiary: #ffff33;
+ --selection-bg: #ff3333;
+ --selection-text: #000000;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --bg-base: #000000;
+ --bg-primary: #000000;
+ --bg-secondary: #111111;
+ --bg-tertiary: #222222;
+ --text-primary: #ffffff;
+ --text-secondary: #cccccc;
+ --text-muted: #888888;
+ --border-color: #ffffff;
+ --accent-color: #ff3333;
+ --accent-secondary: #3333ff;
+ --accent-tertiary: #ffff33;
+ --selection-bg: #ff3333;
+ --selection-text: #000000;
+ }
+ }
+
+ * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 14px;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: var(--bg-base);
+ background-image: __BACKGROUND_IMAGE_CUSTOM__;
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ min-height: 100vh;
+ color: var(--text-primary);
+ line-height: 1.4;
+ }
+
+ /* Theme Toggle Button */
+ .theme-toggle {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ width: 50px;
+ height: 50px;
+ border: var(--border-width) solid var(--border-color);
+ background: var(--bg-primary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);
+ transition: transform 0.1s, box-shadow 0.1s;
+ }
+
+ .theme-toggle:hover {
+ transform: translate(-2px, -2px);
+ box-shadow: calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color);
+ }
+
+ .theme-toggle:active {
+ transform: translate(2px, 2px);
+ box-shadow: calc(var(--shadow-offset) - 2px) calc(var(--shadow-offset) - 2px) 0 var(--border-color);
+ }
+
+ .theme-toggle .icon {
+ line-height: 1;
+ }
+
+ .theme-toggle .icon-auto {
+ position: relative;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .theme-toggle .icon-auto::before,
+ .theme-toggle .icon-auto::after {
+ position: absolute;
+ font-size: 20px;
+ line-height: 1;
+ }
+
+ .theme-toggle .icon-auto::before {
+ content: '\\2600\\FE0F';
+ clip-path: inset(0 50% 0 0);
+ }
+
+ .theme-toggle .icon-auto::after {
+ content: '\\D83C\\DF19';
+ clip-path: inset(0 0 0 50%);
+ }
+
+ /* Card Style */
+ .card {
+ background: var(--bg-primary);
+ border: var(--border-width) solid var(--border-color);
+ box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);
+ margin-bottom: 24px;
+ overflow: hidden;
+ }
+
+ .card-header {
+ padding: 12px 16px;
+ border-bottom: var(--border-width) solid var(--border-color);
+ background: var(--accent-tertiary);
+ }
+
+ .card-title {
+ font-size: 18px;
+ font-weight: 900;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ color: var(--text-primary);
+ }
+
+ [data-theme="dark"] .card-title {
+ color: #000000;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) .card-title {
+ color: #000000;
+ }
+ }
+
+ .card-content {
+ padding: 20px;
+ }
+
+ .summaries-content {
+ padding: 0;
+ padding-top: 16px;
+ }
+
+ /* Filters */
+ .filters {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ .filter-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ user-select: none;
+ padding: 8px 12px;
+ background: var(--bg-secondary);
+ border: 2px solid var(--border-color);
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 12px;
+ transition: transform 0.1s, box-shadow 0.1s;
+ }
+
+ .filter-label:hover {
+ transform: translate(-2px, -2px);
+ box-shadow: 4px 4px 0 var(--border-color);
+ }
+
+ .filter-label input[type="checkbox"] {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--border-color);
+ background: var(--bg-primary);
+ cursor: pointer;
+ position: relative;
+ }
+
+ .filter-label input[type="checkbox"]:checked {
+ background: var(--accent-color);
+ }
+
+ .filter-label input[type="checkbox"]:checked::after {
+ content: '\\2715';
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 14px;
+ font-weight: 900;
+ color: var(--bg-primary);
+ }
+
+ .filter-label .service-name {
+ text-transform: uppercase;
+ font-weight: 700;
+ color: var(--text-primary);
+ }
+
+ .filter-label .service-count {
+ color: var(--text-muted);
+ font-size: 11px;
+ }
+
+ /* Stats */
+ .stats {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ flex-wrap: wrap;
+ }
+
+ .stat {
+ text-align: center;
+ padding: 20px 30px;
+ background: var(--bg-secondary);
+ border: var(--border-width) solid var(--border-color);
+ min-width: 150px;
+ }
+
+ .stat-value {
+ font-size: 36px;
+ font-weight: 900;
+ color: var(--accent-color);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .stat-label {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin-top: 4px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-weight: 700;
+ }
+
+ /* Charts */
+ .charts-wrapper {
+ display: flex;
+ gap: 20px;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .chart-container {
+ flex: 1;
+ min-width: 280px;
+ max-width: 450px;
+ padding: 16px;
+ background: var(--bg-secondary);
+ border: var(--border-width) solid var(--border-color);
+ }
+
+ .chart-title {
+ font-size: 14px;
+ font-weight: 900;
+ text-align: center;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ }
+
+ @media (max-width: 700px) {
+ .charts-wrapper {
+ flex-direction: column;
+ align-items: center;
+ }
+ .chart-container {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+
+ /* Tabs */
+ .tabs {
+ display: flex;
+ gap: 0;
+ padding: 0 16px;
+ margin-bottom: -3px;
+ position: relative;
+ z-index: 1;
+ }
+
+ .tab {
+ padding: 12px 24px;
+ cursor: pointer;
+ color: var(--text-secondary);
+ font-weight: 900;
+ text-transform: uppercase;
+ font-size: 12px;
+ letter-spacing: 1px;
+ background: var(--bg-tertiary);
+ border: var(--border-width) solid var(--border-color);
+ border-bottom: none;
+ margin-right: -3px;
+ transition: none;
+ }
+
+ .tab:hover {
+ color: var(--text-primary);
+ background: var(--bg-secondary);
+ }
+
+ .tab.active {
+ color: var(--accent-color);
+ background: var(--bg-secondary);
+ position: relative;
+ }
+
+ .tab.active::after {
+ content: '';
+ position: absolute;
+ bottom: -3px;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: var(--bg-secondary);
+ }
+
+ .tab-content {
+ background: var(--bg-secondary);
+ border: var(--border-width) solid var(--border-color);
+ padding: 16px;
+ }
+
+ .tab-panel {
+ display: none;
+ }
+
+ .tab-panel.active {
+ display: block;
+ }
+
+ /* Tables */
+ .table-wrapper {
+ overflow-x: auto;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+ }
+
+ th {
+ padding: 12px 16px;
+ text-align: left;
+ font-weight: 900;
+ color: var(--text-primary);
+ border-bottom: var(--border-width) solid var(--border-color);
+ text-transform: uppercase;
+ font-size: 11px;
+ letter-spacing: 1px;
+ background: var(--bg-tertiary);
+ }
+
+ td {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-color);
+ color: var(--text-primary);
+ }
+
+ tr:hover td {
+ background: var(--selection-bg);
+ color: var(--selection-text);
+ }
+
+ tr:hover .service-badge {
+ background: var(--border-color);
+ color: var(--bg-primary);
+ }
+
+ tr:last-child td {
+ border-bottom: none;
+ }
+
+ .time {
+ font-variant-numeric: tabular-nums;
+ font-weight: 700;
+ }
+
+ .percent {
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+ color: var(--text-muted);
+ font-weight: 700;
+ }
+
+ .color-box {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ margin-right: 8px;
+ vertical-align: middle;
+ border: 2px solid var(--border-color);
+ }
+
+ .service-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 2px 6px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-muted);
+ margin-left: 8px;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ }
+
+ .category-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 2px 6px;
+ background: var(--accent-secondary);
+ border: 1px solid var(--border-color);
+ color: var(--bg-primary);
+ margin-left: 4px;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ }
+
+ .no-data {
+ text-align: center;
+ padding: 40px;
+ color: var(--text-muted);
+ font-weight: 700;
+ text-transform: uppercase;
+ }
+
+ /* Others row expansion */
+ .others-row {
+ cursor: pointer;
+ }
+
+ .others-row td:first-child::before {
+ content: '\\25B6';
+ display: inline-block;
+ margin-right: 8px;
+ font-size: 10px;
+ transition: transform 0.1s;
+ }
+
+ .others-row.expanded td:first-child::before {
+ transform: rotate(90deg);
+ }
+
+ .others-detail {
+ display: none;
+ background: var(--bg-tertiary);
+ }
+
+ .others-detail.visible {
+ display: table-row;
+ }
+
+ .others-detail td {
+ padding-left: 32px;
+ }
+
+ /* Scrollbar */
+ ::-webkit-scrollbar {
+ width: 12px;
+ height: 12px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--text-muted);
+ border: 1px solid var(--border-color);
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+ }
+
+ /* Responsive */
+ @media (max-width: 600px) {
+ body {
+ padding: 12px;
+ }
+
+ .stats {
+ gap: 12px;
+ }
+
+ .stat {
+ padding: 16px 20px;
+ min-width: 100px;
+ }
+
+ .stat-value {
+ font-size: 28px;
+ }
+
+ .filter-label {
+ padding: 6px 10px;
+ font-size: 11px;
+ }
+
+ .tabs {
+ padding: 0 12px;
+ }
+
+ .tab {
+ padding: 10px 16px;
+ font-size: 11px;
+ }
+
+ .theme-toggle {
+ width: 44px;
+ height: 44px;
+ font-size: 18px;
+ }
+ }
+
+ /* Scroll to Top Button */
+ .scroll-top {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 1000;
+ width: 50px;
+ height: 50px;
+ border: var(--border-width) solid var(--border-color);
+ background: var(--bg-primary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0.2s, transform 0.1s, box-shadow 0.1s;
+ }
+
+ .scroll-top.visible {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .scroll-top:hover {
+ transform: translate(-2px, -2px);
+ box-shadow: calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color);
+ }
+
+ .scroll-top:active {
+ transform: translate(2px, 2px);
+ box-shadow: calc(var(--shadow-offset) - 2px) calc(var(--shadow-offset) - 2px) 0 var(--border-color);
+ }
+
+ .scroll-top-arrow {
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-bottom: 12px solid var(--text-primary);
+ }
+
+ @media (max-width: 600px) {
+ .scroll-top {
+ width: 44px;
+ height: 44px;
+ }
+
+ .scroll-top-arrow {
+ border-left-width: 8px;
+ border-right-width: 8px;
+ border-bottom-width: 10px;
+ }
+ }
+ """,
+ "glassmorphism": """
+ :root {
+ --bg-primary: rgba(255, 255, 255, 0.25);
+ --bg-secondary: rgba(255, 255, 255, 0.15);
+ --bg-tertiary: rgba(255, 255, 255, 0.1);
+ --text-primary: #1a1a2e;
+ --text-secondary: #4a4a6a;
+ --text-muted: #6a6a8a;
+ --border-color: rgba(255, 255, 255, 0.3);
+ --shadow-color: rgba(0, 0, 0, 0.1);
+ --accent-color: #6366f1;
+ --accent-hover: #4f46e5;
+ --selection-bg: rgba(99, 102, 241, 0.3);
+ --glass-blur: 20px;
+ --card-radius: 16px;
+ }
+
+ [data-theme="dark"] {
+ --bg-primary: rgba(30, 30, 50, 0.6);
+ --bg-secondary: rgba(40, 40, 70, 0.5);
+ --bg-tertiary: rgba(50, 50, 80, 0.4);
+ --text-primary: #f0f0f5;
+ --text-secondary: #c0c0d0;
+ --text-muted: #9090a0;
+ --border-color: rgba(255, 255, 255, 0.1);
+ --shadow-color: rgba(0, 0, 0, 0.3);
+ --accent-color: #818cf8;
+ --accent-hover: #6366f1;
+ --selection-bg: rgba(129, 140, 248, 0.3);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --bg-primary: rgba(30, 30, 50, 0.6);
+ --bg-secondary: rgba(40, 40, 70, 0.5);
+ --bg-tertiary: rgba(50, 50, 80, 0.4);
+ --text-primary: #f0f0f5;
+ --text-secondary: #c0c0d0;
+ --text-muted: #9090a0;
+ --border-color: rgba(255, 255, 255, 0.1);
+ --shadow-color: rgba(0, 0, 0, 0.3);
+ --accent-color: #818cf8;
+ --accent-hover: #6366f1;
+ --selection-bg: rgba(129, 140, 248, 0.3);
+ }
+ }
+
+ * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ font-size: 14px;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #667eea;
+ background-image: url('__BACKGROUND_IMAGE__');
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ min-height: 100vh;
+ color: var(--text-primary);
+ line-height: 1.5;
+ }
+
+ /* Theme Toggle Button */
+ .theme-toggle {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: 1px solid var(--border-color);
+ background: var(--bg-primary);
+ backdrop-filter: blur(var(--glass-blur));
+ -webkit-backdrop-filter: blur(var(--glass-blur));
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ box-shadow: 0 8px 32px var(--shadow-color);
+ transition: all 0.3s ease;
+ }
+
+ .theme-toggle:hover {
+ transform: scale(1.1);
+ background: var(--bg-secondary);
+ }
+
+ .theme-toggle .icon {
+ transition: transform 0.3s ease;
+ }
+
+ .theme-toggle .icon-auto {
+ position: relative;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .theme-toggle .icon-auto::before,
+ .theme-toggle .icon-auto::after {
+ position: absolute;
+ font-size: 20px;
+ line-height: 1;
+ }
+
+ .theme-toggle .icon-auto::before {
+ content: '\\2600\\FE0F';
+ clip-path: inset(0 50% 0 0);
+ }
+
+ .theme-toggle .icon-auto::after {
+ content: '\\D83C\\DF19';
+ clip-path: inset(0 0 0 50%);
+ }
+
+ /* Card Style */
+ .card {
+ background: var(--bg-primary);
+ backdrop-filter: blur(var(--glass-blur));
+ -webkit-backdrop-filter: blur(var(--glass-blur));
+ border-radius: var(--card-radius);
+ border: 1px solid var(--border-color);
+ box-shadow: 0 8px 32px var(--shadow-color);
+ margin-bottom: 24px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .card:hover {
+ box-shadow: 0 12px 40px var(--shadow-color);
+ }
+
+ .card-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
+ }
+
+ .card-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ }
+
+ .card-content {
+ padding: 20px;
+ }
+
+ .summaries-content {
+ padding: 0;
+ padding-top: 16px;
+ }
+
+ /* Filters */
+ .filters {
+ display: flex;
+ justify-content: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ }
+
+ .filter-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ user-select: none;
+ padding: 8px 16px;
+ border-radius: 24px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ transition: all 0.2s ease;
+ }
+
+ .filter-label:hover {
+ background: var(--bg-tertiary);
+ }
+
+ .filter-label input[type="checkbox"] {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 18px;
+ height: 18px;
+ border-radius: 4px;
+ border: 2px solid var(--text-muted);
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+ }
+
+ .filter-label input[type="checkbox"]:checked {
+ background: var(--accent-color);
+ border-color: var(--accent-color);
+ }
+
+ .filter-label input[type="checkbox"]:checked::after {
+ content: '';
+ position: absolute;
+ left: 5px;
+ top: 2px;
+ width: 4px;
+ height: 8px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+ }
+
+ .filter-label .service-name {
+ text-transform: capitalize;
+ font-weight: 500;
+ color: var(--text-primary);
+ }
+
+ .filter-label .service-count {
+ color: var(--text-muted);
+ font-size: 12px;
+ }
+
+ /* Stats */
+ .stats {
+ display: flex;
+ justify-content: center;
+ gap: 24px;
+ flex-wrap: wrap;
+ }
+
+ .stat {
+ text-align: center;
+ padding: 20px 32px;
+ background: var(--bg-secondary);
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+ min-width: 140px;
+ }
+
+ .stat-value {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--accent-color);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-top: 4px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ /* Charts */
+ .charts-wrapper {
+ display: flex;
+ gap: 24px;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .chart-container {
+ flex: 1;
+ min-width: 280px;
+ max-width: 450px;
+ padding: 16px;
+ background: var(--bg-secondary);
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+ }
+
+ .chart-title {
+ font-size: 14px;
+ font-weight: 600;
+ text-align: center;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+ }
+
+ @media (max-width: 700px) {
+ .charts-wrapper {
+ flex-direction: column;
+ align-items: center;
+ }
+ .chart-container {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+
+ /* Tabs */
+ .tabs {
+ display: flex;
+ gap: 8px;
+ padding: 0 20px;
+ margin-bottom: -1px;
+ position: relative;
+ z-index: 1;
+ }
+
+ .tab {
+ padding: 12px 24px;
+ cursor: pointer;
+ color: var(--text-secondary);
+ font-weight: 500;
+ border-radius: 12px 12px 0 0;
+ background: var(--bg-tertiary);
+ border: 1px solid transparent;
+ border-bottom: none;
+ transition: all 0.2s ease;
+ }
+
+ .tab:hover {
+ color: var(--text-primary);
+ background: var(--bg-secondary);
+ }
+
+ .tab.active {
+ color: var(--accent-color);
+ background: var(--bg-secondary);
+ border-color: var(--border-color);
+ }
+
+ .tab-content {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 0 12px 12px 12px;
+ padding: 16px;
+ }
+
+ .tab-panel {
+ display: none;
+ }
+
+ .tab-panel.active {
+ display: block;
+ }
+
+ /* Tables */
+ .table-wrapper {
+ overflow-x: auto;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+ }
+
+ th {
+ padding: 12px 16px;
+ text-align: left;
+ font-weight: 600;
+ color: var(--text-secondary);
+ border-bottom: 2px solid var(--border-color);
+ text-transform: uppercase;
+ font-size: 11px;
+ letter-spacing: 0.5px;
+ }
+
+ td {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-color);
+ color: var(--text-primary);
+ }
+
+ tr:hover td {
+ background: var(--selection-bg);
+ }
+
+ tr:last-child td {
+ border-bottom: none;
+ }
+
+ .time {
+ font-variant-numeric: tabular-nums;
+ font-weight: 500;
+ }
+
+ .percent {
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+ color: var(--text-muted);
+ }
+
+ .color-box {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ margin-right: 8px;
+ vertical-align: middle;
+ border-radius: 3px;
+ }
+
+ .service-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 2px 8px;
+ background: var(--bg-tertiary);
+ border-radius: 12px;
+ color: var(--text-muted);
+ margin-left: 8px;
+ text-transform: capitalize;
+ font-weight: 500;
+ }
+
+ .category-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 2px 8px;
+ background: rgba(99, 102, 241, 0.2);
+ border-radius: 12px;
+ color: var(--accent-color);
+ margin-left: 4px;
+ text-transform: capitalize;
+ font-weight: 500;
+ }
+
+ .no-data {
+ text-align: center;
+ padding: 40px;
+ color: var(--text-muted);
+ }
+
+ /* Others row expansion */
+ .others-row {
+ cursor: pointer;
+ }
+
+ .others-row td:first-child::before {
+ content: '';
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-left: 6px solid var(--text-muted);
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ margin-right: 8px;
+ transition: transform 0.2s ease;
+ }
+
+ .others-row.expanded td:first-child::before {
+ transform: rotate(90deg);
+ }
+
+ .others-detail {
+ display: none;
+ background: var(--bg-tertiary);
+ }
+
+ .others-detail.visible {
+ display: table-row;
+ }
+
+ .others-detail td {
+ padding-left: 32px;
+ }
+
+ /* Scrollbar */
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--text-muted);
+ border-radius: 4px;
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+ }
+
+ /* Responsive */
+ @media (max-width: 600px) {
+ body {
+ padding: 12px;
+ }
+
+ .stats {
+ gap: 12px;
+ }
+
+ .stat {
+ padding: 16px 20px;
+ min-width: 100px;
+ }
+
+ .stat-value {
+ font-size: 22px;
+ }
+
+ .filter-label {
+ padding: 6px 12px;
+ font-size: 13px;
+ }
+
+ .tabs {
+ padding: 0 12px;
+ }
+
+ .tab {
+ padding: 10px 16px;
+ font-size: 13px;
+ }
+
+ .theme-toggle {
+ width: 40px;
+ height: 40px;
+ font-size: 16px;
+ }
+ }
+
+ /* Scroll to Top Button */
+ .scroll-top {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 1000;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: 1px solid var(--border-color);
+ background: var(--bg-primary);
+ backdrop-filter: blur(var(--glass-blur));
+ -webkit-backdrop-filter: blur(var(--glass-blur));
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8px 32px var(--shadow-color);
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .scroll-top.visible {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .scroll-top:hover {
+ transform: scale(1.1);
+ background: var(--bg-secondary);
+ }
+
+ .scroll-top-arrow {
+ width: 0;
+ height: 0;
+ border-left: 8px solid transparent;
+ border-right: 8px solid transparent;
+ border-bottom: 10px solid var(--text-primary);
+ }
+
+ @media (max-width: 600px) {
+ .scroll-top {
+ width: 40px;
+ height: 40px;
+ }
+
+ .scroll-top-arrow {
+ border-left-width: 6px;
+ border-right-width: 6px;
+ border-bottom-width: 8px;
+ }
+ }
+ """,
+ "neumorphism": """
+ :root {
+ --bg-base: #e0e5ec;
+ --bg-primary: #e0e5ec;
+ --bg-secondary: #e0e5ec;
+ --bg-tertiary: #d1d9e6;
+ --text-primary: #2d3436;
+ --text-secondary: #4a5568;
+ --text-muted: #718096;
+ --accent-color: #6366f1;
+ --accent-hover: #4f46e5;
+ --selection-bg: rgba(99, 102, 241, 0.15);
+ --shadow-light: rgba(255, 255, 255, 0.8);
+ --shadow-dark: rgba(163, 177, 198, 0.6);
+ --shadow-inset-light: rgba(255, 255, 255, 0.7);
+ --shadow-inset-dark: rgba(163, 177, 198, 0.5);
+ --card-radius: 20px;
+ --border-color: transparent;
+ }
+
+ [data-theme="dark"] {
+ --bg-base: #2d3436;
+ --bg-primary: #2d3436;
+ --bg-secondary: #2d3436;
+ --bg-tertiary: #353b3d;
+ --text-primary: #f0f0f5;
+ --text-secondary: #b0b8c0;
+ --text-muted: #8090a0;
+ --accent-color: #818cf8;
+ --accent-hover: #6366f1;
+ --selection-bg: rgba(129, 140, 248, 0.2);
+ --shadow-light: rgba(255, 255, 255, 0.05);
+ --shadow-dark: rgba(0, 0, 0, 0.4);
+ --shadow-inset-light: rgba(255, 255, 255, 0.03);
+ --shadow-inset-dark: rgba(0, 0, 0, 0.3);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --bg-base: #2d3436;
+ --bg-primary: #2d3436;
+ --bg-secondary: #2d3436;
+ --bg-tertiary: #353b3d;
+ --text-primary: #f0f0f5;
+ --text-secondary: #b0b8c0;
+ --text-muted: #8090a0;
+ --accent-color: #818cf8;
+ --accent-hover: #6366f1;
+ --selection-bg: rgba(129, 140, 248, 0.2);
+ --shadow-light: rgba(255, 255, 255, 0.05);
+ --shadow-dark: rgba(0, 0, 0, 0.4);
+ --shadow-inset-light: rgba(255, 255, 255, 0.03);
+ --shadow-inset-dark: rgba(0, 0, 0, 0.3);
+ }
+ }
+
+ * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ font-size: 14px;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: var(--bg-base);
+ min-height: 100vh;
+ color: var(--text-primary);
+ line-height: 1.5;
+ }
+
+ /* Theme Toggle Button */
+ .theme-toggle {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: none;
+ background: var(--bg-primary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ box-shadow:
+ 6px 6px 12px var(--shadow-dark),
+ -6px -6px 12px var(--shadow-light);
+ transition: all 0.3s ease;
+ }
+
+ .theme-toggle:hover {
+ transform: scale(1.05);
+ }
+
+ .theme-toggle:active {
+ box-shadow:
+ inset 4px 4px 8px var(--shadow-inset-dark),
+ inset -4px -4px 8px var(--shadow-inset-light);
+ }
+
+ .theme-toggle .icon {
+ transition: transform 0.3s ease;
+ }
+
+ .theme-toggle .icon-auto {
+ position: relative;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .theme-toggle .icon-auto::before,
+ .theme-toggle .icon-auto::after {
+ position: absolute;
+ font-size: 20px;
+ line-height: 1;
+ }
+
+ .theme-toggle .icon-auto::before {
+ content: '\\2600\\FE0F';
+ clip-path: inset(0 50% 0 0);
+ }
+
+ .theme-toggle .icon-auto::after {
+ content: '\\D83C\\DF19';
+ clip-path: inset(0 0 0 50%);
+ }
+
+ /* Card Style */
+ .card {
+ background: var(--bg-primary);
+ border-radius: var(--card-radius);
+ box-shadow:
+ 8px 8px 16px var(--shadow-dark),
+ -8px -8px 16px var(--shadow-light);
+ margin-bottom: 24px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .card-header {
+ padding: 16px 20px;
+ background: var(--bg-tertiary);
+ border-radius: var(--card-radius) var(--card-radius) 0 0;
+ }
+
+ .card-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ }
+
+ .card-content {
+ padding: 20px;
+ }
+
+ .summaries-content {
+ padding: 0;
+ padding-top: 16px;
+ }
+
+ /* Filters */
+ .filters {
+ display: flex;
+ justify-content: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ }
+
+ .filter-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ user-select: none;
+ padding: 10px 18px;
+ border-radius: 30px;
+ background: var(--bg-secondary);
+ box-shadow:
+ 4px 4px 8px var(--shadow-dark),
+ -4px -4px 8px var(--shadow-light);
+ transition: all 0.2s ease;
+ }
+
+ .filter-label:hover {
+ transform: translateY(-2px);
+ }
+
+ .filter-label:active {
+ box-shadow:
+ inset 3px 3px 6px var(--shadow-inset-dark),
+ inset -3px -3px 6px var(--shadow-inset-light);
+ }
+
+ .filter-label input[type="checkbox"] {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 6px;
+ background: var(--bg-secondary);
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+ box-shadow:
+ inset 2px 2px 4px var(--shadow-inset-dark),
+ inset -2px -2px 4px var(--shadow-inset-light);
+ }
+
+ .filter-label input[type="checkbox"]:checked {
+ background: var(--accent-color);
+ box-shadow:
+ inset 2px 2px 4px rgba(0, 0, 0, 0.2),
+ inset -2px -2px 4px rgba(255, 255, 255, 0.1);
+ }
+
+ .filter-label input[type="checkbox"]:checked::after {
+ content: '';
+ position: absolute;
+ left: 6px;
+ top: 3px;
+ width: 5px;
+ height: 9px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+ }
+
+ .filter-label .service-name {
+ text-transform: capitalize;
+ font-weight: 500;
+ color: var(--text-primary);
+ }
+
+ .filter-label .service-count {
+ color: var(--text-muted);
+ font-size: 12px;
+ }
+
+ /* Stats */
+ .stats {
+ display: flex;
+ justify-content: center;
+ gap: 24px;
+ flex-wrap: wrap;
+ }
+
+ .stat {
+ text-align: center;
+ padding: 24px 36px;
+ background: var(--bg-secondary);
+ border-radius: 16px;
+ min-width: 140px;
+ box-shadow:
+ 6px 6px 12px var(--shadow-dark),
+ -6px -6px 12px var(--shadow-light);
+ }
+
+ .stat-value {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--accent-color);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-top: 4px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ /* Charts */
+ .charts-wrapper {
+ display: flex;
+ gap: 24px;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .chart-container {
+ flex: 1;
+ min-width: 280px;
+ max-width: 450px;
+ padding: 20px;
+ background: var(--bg-secondary);
+ border-radius: 16px;
+ box-shadow:
+ inset 4px 4px 8px var(--shadow-inset-dark),
+ inset -4px -4px 8px var(--shadow-inset-light);
+ }
+
+ .chart-title {
+ font-size: 14px;
+ font-weight: 600;
+ text-align: center;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+ }
+
+ @media (max-width: 700px) {
+ .charts-wrapper {
+ flex-direction: column;
+ align-items: center;
+ }
+ .chart-container {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+
+ /* Tabs */
+ .tabs {
+ display: flex;
+ gap: 8px;
+ padding: 0 20px;
+ margin-bottom: -1px;
+ position: relative;
+ z-index: 1;
+ }
+
+ .tab {
+ padding: 12px 24px;
+ cursor: pointer;
+ color: var(--text-secondary);
+ font-weight: 500;
+ border-radius: 16px 16px 0 0;
+ background: var(--bg-tertiary);
+ transition: all 0.2s ease;
+ box-shadow:
+ 4px -4px 8px var(--shadow-dark),
+ -4px -4px 8px var(--shadow-light);
+ }
+
+ .tab:hover {
+ color: var(--text-primary);
+ }
+
+ .tab.active {
+ color: var(--accent-color);
+ background: var(--bg-secondary);
+ box-shadow:
+ inset 2px 2px 4px var(--shadow-inset-dark),
+ inset -2px -2px 4px var(--shadow-inset-light);
+ }
+
+ .tab-content {
+ background: var(--bg-secondary);
+ border-radius: 0 16px 16px 16px;
+ padding: 16px;
+ box-shadow:
+ inset 4px 4px 8px var(--shadow-inset-dark),
+ inset -4px -4px 8px var(--shadow-inset-light);
+ }
+
+ .tab-panel {
+ display: none;
+ }
+
+ .tab-panel.active {
+ display: block;
+ }
+
+ /* Tables */
+ .table-wrapper {
+ overflow-x: auto;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+ }
+
+ th {
+ padding: 12px 16px;
+ text-align: left;
+ font-weight: 600;
+ color: var(--text-secondary);
+ border-bottom: 2px solid var(--bg-tertiary);
+ text-transform: uppercase;
+ font-size: 11px;
+ letter-spacing: 0.5px;
+ }
+
+ td {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--bg-tertiary);
+ color: var(--text-primary);
+ }
+
+ tr:hover td {
+ background: var(--selection-bg);
+ }
+
+ tr:last-child td {
+ border-bottom: none;
+ }
+
+ .time {
+ font-variant-numeric: tabular-nums;
+ font-weight: 500;
+ }
+
+ .percent {
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+ color: var(--text-muted);
+ }
+
+ .color-box {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ margin-right: 8px;
+ vertical-align: middle;
+ border-radius: 4px;
+ box-shadow:
+ 2px 2px 4px var(--shadow-dark),
+ -2px -2px 4px var(--shadow-light);
+ }
+
+ .service-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 3px 10px;
+ background: var(--bg-tertiary);
+ border-radius: 12px;
+ color: var(--text-muted);
+ margin-left: 8px;
+ text-transform: capitalize;
+ font-weight: 500;
+ box-shadow:
+ inset 1px 1px 2px var(--shadow-inset-dark),
+ inset -1px -1px 2px var(--shadow-inset-light);
+ }
+
+ .category-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 3px 10px;
+ background: var(--bg-tertiary);
+ border-radius: 12px;
+ color: var(--accent-color);
+ margin-left: 4px;
+ text-transform: capitalize;
+ font-weight: 500;
+ box-shadow:
+ inset 1px 1px 2px var(--shadow-inset-dark),
+ inset -1px -1px 2px var(--shadow-inset-light);
+ }
+
+ .no-data {
+ text-align: center;
+ padding: 40px;
+ color: var(--text-muted);
+ }
+
+ /* Others row expansion */
+ .others-row {
+ cursor: pointer;
+ }
+
+ .others-row td:first-child::before {
+ content: '';
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-left: 6px solid var(--text-muted);
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ margin-right: 8px;
+ transition: transform 0.2s ease;
+ }
+
+ .others-row.expanded td:first-child::before {
+ transform: rotate(90deg);
+ }
+
+ .others-detail {
+ display: none;
+ background: var(--bg-tertiary);
+ }
+
+ .others-detail.visible {
+ display: table-row;
+ }
+
+ .others-detail td {
+ padding-left: 32px;
+ }
+
+ /* Scrollbar */
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--text-muted);
+ border-radius: 4px;
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+ }
+
+ /* Responsive */
+ @media (max-width: 600px) {
+ body {
+ padding: 12px;
+ }
+
+ .stats {
+ gap: 12px;
+ }
+
+ .stat {
+ padding: 16px 20px;
+ min-width: 100px;
+ }
+
+ .stat-value {
+ font-size: 22px;
+ }
+
+ .filter-label {
+ padding: 8px 14px;
+ font-size: 13px;
+ }
+
+ .tabs {
+ padding: 0 12px;
+ }
+
+ .tab {
+ padding: 10px 16px;
+ font-size: 13px;
+ }
+
+ .theme-toggle {
+ width: 40px;
+ height: 40px;
+ font-size: 16px;
+ }
+ }
+
+ /* Scroll to Top Button */
+ .scroll-top {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 1000;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: none;
+ background: var(--bg-primary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow:
+ 6px 6px 12px var(--shadow-dark),
+ -6px -6px 12px var(--shadow-light);
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .scroll-top.visible {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .scroll-top:hover {
+ transform: scale(1.05);
+ }
+
+ .scroll-top:active {
+ box-shadow:
+ inset 4px 4px 8px var(--shadow-inset-dark),
+ inset -4px -4px 8px var(--shadow-inset-light);
+ }
+
+ .scroll-top-arrow {
+ width: 0;
+ height: 0;
+ border-left: 8px solid transparent;
+ border-right: 8px solid transparent;
+ border-bottom: 10px solid var(--text-primary);
+ }
+
+ @media (max-width: 600px) {
+ .scroll-top {
+ width: 40px;
+ height: 40px;
+ }
+
+ .scroll-top-arrow {
+ border-left-width: 6px;
+ border-right-width: 6px;
+ border-bottom-width: 8px;
+ }
+ }
+ """
+}
+
+
+def get_theme_css(style: str) -> str:
+ """Get the CSS for a given style."""
+ return THEME_CSS.get(style, THEME_CSS["glassmorphism"])
+
+
+def get_theme_config(style: str) -> str:
+ """Get the theme config as JSON string for a given style."""
+ config = THEME_CONFIGS.get(style, THEME_CONFIGS["glassmorphism"])
+ return json.dumps(config)
diff --git a/templates/brutalism.html b/templates/brutalism.html
deleted file mode 100644
index f01d4c1..0000000
--- a/templates/brutalism.html
+++ /dev/null
@@ -1,1335 +0,0 @@
-
-
-
-
-
- Lutris Playtime Report
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
__TOTAL_LIBRARY__
-
Games in Library
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Top Games
-
By Category
-
By Runner
-
-
-
-
-
-
-
- | # |
- Game |
- Playtime |
- % |
-
-
-
-
-
-
-
-
-
-
-
- | # |
- Category |
- Playtime |
- % |
-
-
-
-
-
-
-
-
-
-
-
- | # |
- Runner |
- Playtime |
- % |
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/templates/glassmorphism.html b/templates/modern.html
similarity index 62%
rename from templates/glassmorphism.html
rename to templates/modern.html
index 49701c0..cdda685 100644
--- a/templates/glassmorphism.html
+++ b/templates/modern.html
@@ -6,578 +6,8 @@
Lutris Playtime Report
@@ -591,7 +21,7 @@
-
-
-
-
+
-
+
Top Games
By Category
@@ -718,11 +148,11 @@
themeIcon.className = 'icon icon-auto';
themeToggle.title = 'Theme: Auto (click to change)';
} else if (theme === 'light') {
- themeIcon.textContent = '☀️';
+ themeIcon.textContent = '\u2600\uFE0F';
themeIcon.className = 'icon';
themeToggle.title = 'Theme: Light (click to change)';
} else {
- themeIcon.textContent = '🌙';
+ themeIcon.textContent = '\uD83C\uDF19';
themeIcon.className = 'icon';
themeToggle.title = 'Theme: Dark (click to change)';
}
@@ -765,11 +195,8 @@
const allGames = __ALL_GAMES__;
const topN = __TOP_N__;
- const colors = [
- '#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316',
- '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6',
- '#64748b'
- ];
+ // Theme-specific configuration
+ const themeConfig = __THEME_CONFIG__;
function formatTime(hours) {
const h = Math.floor(hours);
@@ -890,9 +317,16 @@
function getChartTextColor() {
const theme = themes[currentThemeIndex];
- if (theme === 'dark') return '#f0f0f5';
- if (theme === 'light') return '#1a1a2e';
- return getSystemTheme() === 'dark' ? '#f0f0f5' : '#1a1a2e';
+ if (theme === 'dark') return themeConfig.textColorDark;
+ if (theme === 'light') return themeConfig.textColorLight;
+ return getSystemTheme() === 'dark' ? themeConfig.textColorDark : themeConfig.textColorLight;
+ }
+
+ function getChartBorderColor() {
+ const theme = themes[currentThemeIndex];
+ if (theme === 'dark') return themeConfig.borderColorDark;
+ if (theme === 'light') return themeConfig.borderColorLight;
+ return getSystemTheme() === 'dark' ? themeConfig.borderColorDark : themeConfig.borderColorLight;
}
function updateChartColors() {
@@ -932,6 +366,7 @@
}
const textColor = getChartTextColor();
+ const borderColor = getChartBorderColor();
chart = new Chart(ctx, {
type: 'doughnut',
@@ -939,9 +374,9 @@
labels: chartData.map(g => g.name),
datasets: [{
data: chartData.map(g => g.playtime),
- backgroundColor: colors.slice(0, chartData.length),
- borderColor: 'rgba(255, 255, 255, 0.2)',
- borderWidth: 2
+ backgroundColor: themeConfig.colors.slice(0, chartData.length),
+ borderColor: borderColor,
+ borderWidth: themeConfig.borderWidth
}]
},
options: {
@@ -952,29 +387,34 @@
labels: {
color: textColor,
font: {
- family: "'Inter', sans-serif",
- size: 11
+ family: themeConfig.fontFamily,
+ size: 11,
+ weight: themeConfig.fontWeight
},
padding: 12,
usePointStyle: true,
- pointStyle: 'circle'
+ pointStyle: themeConfig.pointStyle
}
},
tooltip: {
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
- titleFont: { weight: 'bold' },
- bodyFont: { weight: 'normal' },
- cornerRadius: 8,
+ backgroundColor: themeConfig.tooltipBg,
+ titleColor: themeConfig.tooltipTitleColor,
+ bodyColor: themeConfig.tooltipBodyColor,
+ borderColor: themeConfig.tooltipBorderColor,
+ borderWidth: themeConfig.tooltipBorderWidth,
+ titleFont: { weight: 'bold', family: themeConfig.fontFamily },
+ bodyFont: { weight: 'normal', family: themeConfig.fontFamily },
+ cornerRadius: themeConfig.tooltipCornerRadius,
padding: 12,
callbacks: {
title: function(context) {
- return context[0].label;
+ return themeConfig.uppercaseTooltip ? context[0].label.toUpperCase() : context[0].label;
},
beforeBody: function(context) {
const index = context[0].dataIndex;
const service = chartData[index].service;
if (service && service !== 'others') {
- return service.charAt(0).toUpperCase() + service.slice(1);
+ return themeConfig.uppercaseTooltip ? service.toUpperCase() : service.charAt(0).toUpperCase() + service.slice(1);
}
return '';
},
@@ -1010,9 +450,9 @@
labels: categoriesChartData.map(c => c.name),
datasets: [{
data: categoriesChartData.map(c => c.playtime),
- backgroundColor: colors.slice(0, categoriesChartData.length),
- borderColor: 'rgba(255, 255, 255, 0.2)',
- borderWidth: 2
+ backgroundColor: themeConfig.colors.slice(0, categoriesChartData.length),
+ borderColor: borderColor,
+ borderWidth: themeConfig.borderWidth
}]
},
options: {
@@ -1023,18 +463,25 @@
labels: {
color: textColor,
font: {
- family: "'Inter', sans-serif",
- size: 11
+ family: themeConfig.fontFamily,
+ size: 11,
+ weight: themeConfig.fontWeight
},
padding: 12,
usePointStyle: true,
- pointStyle: 'circle'
+ pointStyle: themeConfig.pointStyle
}
},
tooltip: {
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
- cornerRadius: 8,
+ backgroundColor: themeConfig.tooltipBg,
+ titleColor: themeConfig.tooltipTitleColor,
+ bodyColor: themeConfig.tooltipBodyColor,
+ borderColor: themeConfig.tooltipBorderColor,
+ borderWidth: themeConfig.tooltipBorderWidth,
+ cornerRadius: themeConfig.tooltipCornerRadius,
padding: 12,
+ titleFont: { family: themeConfig.fontFamily },
+ bodyFont: { family: themeConfig.fontFamily },
callbacks: {
label: function(context) {
const value = context.raw;
@@ -1069,7 +516,7 @@
row.innerHTML = `
${index + 1} |
-
+
${game.name}${serviceBadge}${categoriesBadges}
|
${formatTime(game.playtime)} |
@@ -1123,7 +570,7 @@
row.innerHTML = `
${index + 1} |
-
+
${cat.name} ${cat.gameCount} games
|
${formatTime(cat.playtime)} |
@@ -1142,7 +589,7 @@
othersRow.innerHTML = `
${othersIndex + 1} |
-
+
Others (${otherCategories.length} categories)
|
${formatTime(othersPlaytime)} |
@@ -1188,7 +635,7 @@
row.innerHTML = `
${index + 1} |
-
+
${runner.name} ${runner.gameCount} games
|
${formatTime(runner.playtime)} |
@@ -1207,7 +654,7 @@
othersRow.innerHTML = `
${othersIndex + 1} |
-
+
Others (${otherRunners.length} runners)
|
${formatTime(othersPlaytime)} |
diff --git a/templates/neumorphism.html b/templates/neumorphism.html
deleted file mode 100644
index 6712ba5..0000000
--- a/templates/neumorphism.html
+++ /dev/null
@@ -1,1319 +0,0 @@
-
-
-
-
-
-
Lutris Playtime Report
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
__TOTAL_LIBRARY__
-
Games in Library
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Top Games
-
By Category
-
By Runner
-
-
-
-
-
-
-
- | # |
- Game |
- Playtime |
- % |
-
-
-
-
-
-
-
-
-
-
-
- | # |
- Category |
- Playtime |
- % |
-
-
-
-
-
-
-
-
-
-
-
- | # |
- Runner |
- Playtime |
- % |
-
-
-
-
-
-
-
-
-
-
-
-
-