diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..be1a490
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,33 @@
+# ============================================================
+# Website Checker — Environment Variables
+# Copy this file to .env and fill in your values.
+# NEVER commit .env to version control.
+# ============================================================
+
+# Flask secret key — generate with: python -c "import secrets; print(secrets.token_hex(32))"
+SECRET_KEY=change_me_to_a_random_secret_key
+
+# MySQL connection
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_NAME=webchecker
+DB_USER=webchecker_user
+DB_PASSWORD=change_me_strong_db_password
+
+# Fernet encryption key for website credentials
+# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
+CRYPTO_SECRET=change_me_to_a_fernet_key
+
+# Groq AI (optional — used by AI Summary feature)
+GROQ_API_KEY=
+
+# Email / SMTP (optional — configured via Admin > Settings UI)
+# These are the defaults; values can also be saved in the app_settings DB table.
+SMTP_HOST=
+SMTP_PORT=587
+SMTP_USER=
+SMTP_PASSWORD=
+SMTP_FROM=
+
+# Environment: development | production
+FLASK_ENV=production
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..bfee15a
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,254 @@
+# CLAUDE.md — AI Developer Context
+
+This file gives Claude (or any AI assistant) the context needed to continue development on this project without re-reading the entire codebase from scratch.
+
+---
+
+## What This Project Is
+
+**Website Checker Web** is a Flask web application that is a direct port of a Tkinter desktop application. Both apps share the **same MySQL database** — this is the single most important constraint. Any schema change, query, or encryption logic must remain compatible with what the desktop app writes and reads.
+
+The app helps a small team at LT Services Inc. (Falls Church, VA) track government procurement websites across scheduled shifts, manage bid/opportunity follow-up, and run AI-powered solicitation document analysis.
+
+---
+
+## Architecture at a Glance
+
+```
+Browser → Nginx (reverse proxy) → Gunicorn (4 workers) → Flask app
+ │
+ MySQL (shared with desktop)
+```
+
+- **Entry point:** `wsgi.py` → `app.py::create_app()`
+- **No ORM** — all DB access is raw SQL via `mysql-connector-python` in `models.py`
+- **No frontend framework** — vanilla JS, no build step, no npm
+- **Blueprints:** one file per feature area in `routes/`
+- **Templates:** Jinja2, all extend `base.html`
+- **Static assets:** single `style.css` + `app.js` — no preprocessor
+
+---
+
+## Critical Constraints
+
+### 1. Shared Database
+Never rename columns, drop tables, or change column types without verifying the desktop app still works. Key schema facts:
+- `activity_log` timestamp column is **`logged_at`** (not `created_at`)
+- `app_log` timestamp column is **`logged_at`**
+- All other tables generally use `created_at`
+- The desktop app writes `bid_tracker`, `bid_updates`, `ai_analysis_log`, `ai_criteria`, `app_settings`, `users`, `websites`, `website_credentials`, `shifts`, `shift_users`, `shift_websites`, `shift_checks`, `login_attempts`
+
+### 2. Credential Encryption
+`utils/crypto.py` must stay byte-for-byte compatible with the desktop's `utils/crypto.py`:
+- `_APP_SECRET = b"WebsiteChecker-v1-CredentialKey"` — never change
+- `_ITERATIONS = 100_000` — never change
+- Salt stored as **base64** in `app_settings` under key `"crypto.salt"`
+- Ciphertext has **`enc:`** prefix; values without this prefix are legacy plaintext and returned as-is
+- Calling `reset_fernet()` forces key reload if the salt changes
+
+### 3. No Inline JS with Dynamic Jinja Values
+All button `onclick` handlers that need dynamic data (site ID, site name, note text) **must use `data-*` attributes** on the HTML element and read them in a delegated event listener. Direct `onclick="fn({{ value }})"` breaks when the value contains quotes, apostrophes, or backslashes.
+
+Example of the correct pattern:
+```html
+
+```
+```js
+document.getElementById('list').addEventListener('click', function(e) {
+ const btn = e.target.closest('.js-check');
+ if (btn) openCheckModal(btn.dataset.id, btn.dataset.name);
+});
+```
+
+### 4. Jinja Macros and `{% extends %}`
+Jinja macros defined in the same file as `{% extends "base.html" %}` cannot be called with `{{ macro_name(...) }}` before the macro definition is reached. **Do not use macros in child templates.** Inline the HTML directly or use JS to build dynamic content.
+
+### 5. CSS Specificity — Local `
+{% endblock %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..9ce21b5
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,68 @@
+
+
+