diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..7aba38c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,43 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+Lutris Year in Review is a Python script that generates a static HTML playtime report from a Lutris gaming platform SQLite database. The report displays game playtime statistics with interactive Chart.js visualizations.
+
+## Commands
+
+Generate report with defaults:
+```bash
+python generate_report.py
+```
+
+Generate report with custom options:
+```bash
+python generate_report.py --db pga.db --output report.html --top 10 --background background.png
+```
+
+## Architecture
+
+**Single-file generator (`generate_report.py`):**
+- Reads Lutris SQLite database (`pga.db`) containing games, categories, and playtime data
+- Embeds all data (games JSON, background image as base64) directly into a self-contained HTML file
+- HTML template with Chart.js doughnut chart and dynamic JavaScript filtering is embedded as a string constant (`HTML_TEMPLATE`)
+
+**Database schema (`schema.py`):**
+- Reference file documenting Lutris database structure
+- Key tables: `games` (with `playtime`, `service` fields), `categories`, `games_categories` (many-to-many join)
+
+**Generated output (`report.html`):**
+- Fully static, can be hosted on any web server
+- Client-side filtering by service (Steam, GOG, itch.io, local)
+- Expandable "Others" row in games table
+- Light/dark mode support via CSS `prefers-color-scheme`
+
+## Key Data Relationships
+
+- Games have a `service` field (steam, gog, itchio, humblebundle, or NULL for local)
+- Games link to categories via `games_categories` join table
+- Categories like `.hidden`, `favorite`, `Horny` are filtered out in the report
+- `playtime` is cumulative hours (REAL), not per-session data
diff --git a/generate_report.py b/generate_report.py
new file mode 100644
index 0000000..4cf4495
--- /dev/null
+++ b/generate_report.py
@@ -0,0 +1,673 @@
+#!/usr/bin/env python3
+"""Generate an HTML playtime report from a Lutris SQLite database."""
+
+####################################################################################################
+# Copyright (C) 2026 by WallyHackenslacker wallyhackenslacker@noreply.git.hackenslacker.space #
+# #
+# Permission to use, copy, modify, and/or distribute this software for any purpose with or without #
+# fee is hereby granted. #
+# #
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS #
+# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE #
+# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, #
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE #
+# OF THIS SOFTWARE. #
+####################################################################################################
+
+import argparse
+import base64
+import json
+import sqlite3
+from pathlib import Path
+
+HTML_TEMPLATE = """
+
+
+
+
+ Lutris Playtime Report
+
+
+
+
+
+
Lutris Playtime Report
+
+
+
+
+
+
+
+
+
+
+
Top Games
+
+
+
+
+ | # |
+ Game |
+ Playtime |
+ % |
+
+
+
+
+
+
+
+
By Category
+
+
+
+
+ | # |
+ Category |
+ Playtime |
+ % |
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+
+def get_all_games(db_path: str) -> list[dict]:
+ """Query the database and return all games with playtime and categories."""
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor()
+
+ # Get games with playtime
+ cursor.execute("""
+ SELECT id, name, playtime, COALESCE(service, 'local') as service
+ FROM games
+ WHERE playtime > 0
+ ORDER BY playtime DESC
+ """)
+ games_rows = cursor.fetchall()
+
+ # Get categories for each game
+ cursor.execute("""
+ SELECT gc.game_id, c.name
+ FROM games_categories gc
+ JOIN categories c ON gc.category_id = c.id
+ """)
+ categories_rows = cursor.fetchall()
+ conn.close()
+
+ # Build game_id -> categories mapping
+ game_categories = {}
+ for game_id, category in categories_rows:
+ if game_id not in game_categories:
+ game_categories[game_id] = []
+ game_categories[game_id].append(category)
+
+ return [
+ {
+ "name": row[1],
+ "playtime": row[2],
+ "service": row[3],
+ "categories": game_categories.get(row[0], [])
+ }
+ for row in games_rows
+ ]
+
+
+def generate_report(db_path: str, output_path: str, top_n: int, bg_image_path: str = None) -> None:
+ """Generate the HTML report."""
+ all_games = get_all_games(db_path)
+
+ if not all_games:
+ print("No games with playtime found in the database.")
+ return
+
+ total_playtime = sum(g["playtime"] for g in all_games)
+ total_games = len(all_games)
+
+ # Load background image as base64
+ bg_data_url = ""
+ if bg_image_path and Path(bg_image_path).exists():
+ with open(bg_image_path, "rb") as f:
+ bg_base64 = base64.b64encode(f.read()).decode("utf-8")
+ bg_data_url = f"data:image/png;base64,{bg_base64}"
+
+ html = HTML_TEMPLATE.replace("__ALL_GAMES__", json.dumps(all_games))
+ html = html.replace("__TOP_N__", str(top_n))
+ html = html.replace("__BACKGROUND_IMAGE__", bg_data_url)
+
+ Path(output_path).write_text(html, encoding="utf-8")
+ print(f"Report generated: {output_path}")
+ print(f"Total games with playtime: {total_games}")
+ print(f"Total playtime: {total_playtime:.1f} hours")
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Generate an HTML playtime report from a Lutris database."
+ )
+ parser.add_argument(
+ "--db",
+ default="pga.db",
+ help="Path to the Lutris SQLite database (default: pga.db)"
+ )
+ parser.add_argument(
+ "--output",
+ default="report.html",
+ help="Output HTML file path (default: report.html)"
+ )
+ parser.add_argument(
+ "--top",
+ type=int,
+ default=10,
+ help="Number of top games to show individually (default: 10)"
+ )
+ parser.add_argument(
+ "--background",
+ default="background.png",
+ help="Path to background image (default: background.png)"
+ )
+
+ args = parser.parse_args()
+
+ if not Path(args.db).exists():
+ print(f"Error: Database file not found: {args.db}")
+ return 1
+
+ generate_report(args.db, args.output, args.top, args.background)
+ return 0
+
+
+if __name__ == "__main__":
+ exit(main())