diff --git a/app.py b/app.py index b0164e7..143fade 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ from __future__ import annotations import click -from flask import Flask +from flask import Flask, render_template from auth import current_user_id, is_logged_in from config import Config @@ -42,6 +42,14 @@ def create_app(config_overrides: dict | None = None) -> Flask: app.register_blueprint(account_bp) app.register_blueprint(leagues_bp) + @app.errorhandler(404) + def not_found(e): + return render_template("404.html"), 404 + + @app.errorhandler(403) + def forbidden(e): + return render_template("403.html"), 403 + @app.cli.command("init-db") def init_db_command() -> None: if not database_extensions_available() or db is None: diff --git a/config.py b/config.py index 38f0056..1c128a4 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.5.10" +APP_VERSION = "2.5.11" def load_local_env(env_path: Path) -> None: diff --git a/static/css/style.css b/static/css/style.css index 28145ff..efc6a39 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -5604,6 +5604,91 @@ select.control { cursor: pointer; } } } +/* ================================================================ + ERROR PAGES (404, 403) + ================================================================ */ +.page--error { + padding: 0; + max-width: none; +} + +.error-pg { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 62px); + position: relative; + overflow: hidden; + background: + radial-gradient(ellipse 90% 70% at 50% 60%, #1c0e3e 0%, #100820 55%, var(--bg) 100%), + radial-gradient(ellipse 50% 40% at 80% 20%, rgba(155,140,240,.07) 0%, transparent 55%); +} + +/* large suit watermark */ +.error-pg__suit { + position: absolute; + font-size: clamp(320px, 46vw, 660px); + color: rgba(155,140,240,.065); + line-height: 1; + pointer-events: none; + user-select: none; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transition: none; +} +.error-pg--403 .error-pg__suit { + color: rgba(155,140,240,.06); +} + +.error-pg__body { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 0; + padding: clamp(32px, 5vw, 64px) clamp(24px, 5vw, 48px); +} + +.error-pg__code { + font-family: var(--font-mono); + font-size: clamp(80px, 13vw, 148px); + font-weight: 900; + letter-spacing: -.04em; + line-height: 1; + background: linear-gradient(135deg, var(--accent) 0%, rgba(155,140,240,.35) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 8px; +} + +.error-pg__title { + font-size: clamp(28px, 4vw, 52px); + font-weight: 800; + color: var(--text); + letter-spacing: -.04em; + margin: 0 0 16px; + line-height: 1.1; +} + +.error-pg__sub { + font-size: clamp(14px, 1.3vw, 16px); + color: rgba(255,255,255,.42); + max-width: 40ch; + line-height: 1.68; + margin: 0 0 28px; +} + +.error-pg__actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; +} + /* ================================================================ LANDING — hero kicker (replaces pill eyebrow in hero only) ================================================================ */ diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..625f376 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block title %}403 · Access Denied · myboker.org{% endblock %} +{% block page_class %}page--error{% endblock %} +{% block content %} + +