From 5e5c6d5837ef296a7ccff3cad8da442d447beea4 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 18:10:02 +0000 Subject: [PATCH 1/5] Add web/static/themes/arcade.css --- web/static/themes/arcade.css | 415 +++++++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 web/static/themes/arcade.css diff --git a/web/static/themes/arcade.css b/web/static/themes/arcade.css new file mode 100644 index 0000000..5bfe066 --- /dev/null +++ b/web/static/themes/arcade.css @@ -0,0 +1,415 @@ +/* =============================== + Arcade — 80s Side-Scroller + =============================== */ + +/* Optional pixel fonts (load externally if you like): + - "Press Start 2P", "VT323", "Pixelify Sans" + This theme will fall back to monospace if those aren’t present. +*/ + +html[data-theme="arcade"] { + /* Core palette */ + --arc-ink: #dff5ff; /* UI text on dark */ + --arc-ink-muted: #9bd0e7; + --arc-ink-strong: #ffffff; + + --arc-face: #0a1630; /* panels/cards dark navy */ + --arc-elev: #0f1f45; /* elevated */ + --arc-border: #1c3b77; + --arc-shadow: rgba(0,0,0,.45); + + /* Accent pixels */ + --arc-accent: #1de1ff; /* cyan */ + --arc-accent-600: #10b6d1; /* cyan hover */ + --arc-accent-200: #7ee9ff; + + /* Status */ + --arc-success: #22d36f; + --arc-warning: #f4c542; + --arc-danger: #ff4d6d; + --arc-info: #5ea1ff; + + /* Buttons / geometry */ + --arc-radius: 10px; + --arc-chip-radius: 999px; + + /* Pixel borders (fake 1px “CRT” bevel) */ + --px-light: rgba(255,255,255,.22); + --px-dark: rgba(0,0,0,.6); + + /* Topbar */ + --arc-bar: #08102a; + --arc-bar-ink: #e6f8ff; +} + +/* Base + Animated Background + ---------------------------------------------------------------- */ +html[data-theme="arcade"] body{ + min-height:100vh; + color:var(--arc-ink); + background:#07132a; + font-family: "Press Start 2P","Pixelify Sans","VT323","SFMono-Regular", + Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + letter-spacing:.2px; + position:relative; + overflow-x:hidden; /* prevent any horizontal jiggle */ +} + +/* Parallax stars + floating orbs + moving ground. + All CSS-only; no images. */ +html[data-theme="arcade"] body::before, +html[data-theme="arcade"] body::after{ + content:""; + position:fixed; + inset:0; + pointer-events:none; + z-index:-1; +} + +/* Stars layer */ +html[data-theme="arcade"] body::before{ + background: + radial-gradient(2px 2px at 20% 30%, #ffffff 60%, transparent 61%) repeat, + radial-gradient(2px 2px at 70% 60%, #a4ffff 60%, transparent 61%) repeat, + radial-gradient(1px 1px at 40% 80%, #d0f0ff 60%, transparent 61%) repeat; + background-size: 200px 200px, 260px 260px, 180px 180px; + animation: arc-stars 60s linear infinite; + opacity:.55; +} + +/* Platforms / ground + orbs */ +html[data-theme="arcade"] body::after{ + background: + /* orbs/coins */ + radial-gradient(14px 14px at 15% 25%, #ffdf57 35%, #ff9a3d 36% 55%, transparent 56%) , + radial-gradient(10px 10px at 65% 35%, #ff8bd6 35%, #e24da8 36% 55%, transparent 56%) , + radial-gradient(12px 12px at 80% 70%, #85f766 35%, #2ed158 36% 55%, transparent 56%) , + /* floating platforms (grass on stone) */ + linear-gradient(#2f5836, #214527) 0 72%, /* top edge */ + repeating-linear-gradient( + #223a7a 0 16px, + #1d2f62 16px 32px + ), + /* ground stripe bottom */ + linear-gradient(#0b1a40, #0b1a40) 0 100%; + background-repeat: no-repeat, no-repeat, no-repeat, repeat-x, repeat, repeat-x; + background-size: + 28px 28px, 22px 22px, 24px 24px, + 100% 10px, + 100% 100%, + 100% 8px; + background-position: + 10% 26%, 68% 38%, 82% 72%, + 0 60%, + 0 0, + 0 100%; + animation: arc-floating 14s ease-in-out infinite alternate, + arc-ground 40s linear infinite; + opacity:.9; +} + +/* Respect prefers-reduced-motion */ +@media (prefers-reduced-motion: reduce){ + html[data-theme="arcade"] body::before, + html[data-theme="arcade"] body::after{ + animation: none !important; + } +} + +/* Starfield gentle drift */ +@keyframes arc-stars{ + 0% { background-position: 0 0, 0 0, 0 0; } + 100% { background-position: -400px -200px, -320px -160px, -280px -240px; } +} + +/* Platforms/orbs bobbing */ +@keyframes arc-floating{ + 0% { background-position: 10% 27%, 68% 37%, 82% 71%, 0 60%, 0 0, 0 100%; } + 100% { background-position: 12% 24%, 70% 39%, 80% 74%, 0 61.5%, 0 0, 0 100%; } +} + +/* Slow “ground scroll” illusion */ +@keyframes arc-ground{ + 0% { background-position: 10% 26%, 68% 38%, 82% 72%, + 0 60%, 0 0, 0 100%; } + 100% { background-position: 10% 26%, 68% 38%, 82% 72%, + -800px 60%, 0 0, -1200px 100%; } +} + +/* Selection highlight */ +html[data-theme="arcade"] ::selection{ + background: rgba(29,225,255,.35); + color:#fff; +} + +/* Top bar + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .topbar-wrap{ + background: linear-gradient(180deg, var(--arc-bar) 0%, #0a1a3a 100%); + border-bottom: 1px solid #0f2e66; + box-shadow: 0 8px 26px rgba(0,0,0,.45); +} +html[data-theme="arcade"] .brand-title, +html[data-theme="arcade"] .brand .tagline, +html[data-theme="arcade"] .version-link{ + color: var(--arc-bar-ink) !important; +} + +/* Pixel buttons on the topbar (Find/Create/Insights) */ +html[data-theme="arcade"] .topbar .nav-btn, +html[data-theme="arcade"] .topbar .btn, +html[data-theme="arcade"] .topbar .btn-success{ + display:inline-flex !important; + align-items:center !important; + justify-content:center !important; + height:36px !important; + padding:6px 14px !important; + box-sizing:border-box !important; + + color:#e6fbff !important; + background: #0b254e !important; + border: 2px solid #2f6bd4 !important; + border-radius: 6px !important; + box-shadow: + 0 0 0 2px #071a3a inset, + 0 2px 0 0 rgba(0,0,0,.45); + text-shadow: 0 1px 0 rgba(0,0,0,.6); +} +html[data-theme="arcade"] .topbar .nav-btn:hover, +html[data-theme="arcade"] .topbar .btn:hover{ + background:#113066 !important; + border-color:#4b89ff !important; +} +html[data-theme="arcade"] .topbar .user-chip{ + background:#113066 !important; + border-color:#3a79ef !important; + color:#e6fbff !important; + border-radius:999px !important; +} + +/* Cards / Panels + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .card, +html[data-theme="arcade"] .result-card, +html[data-theme="arcade"] .search-form, +html[data-theme="arcade"] .form-card, +html[data-theme="arcade"] .user-menu, +html[data-theme="arcade"] .mobile-menu-inner{ + background: linear-gradient(180deg, var(--arc-face) 0%, #0b1d42 100%); + border: 1px solid var(--arc-border); + color: var(--arc-ink); + border-radius: var(--arc-radius); + box-shadow: 0 14px 28px var(--arc-shadow); +} + +/* Section headings & muted text */ +html[data-theme="arcade"] .page-title, +html[data-theme="arcade"] .subject-title, +html[data-theme="arcade"] .cc-panel-title{ + color: var(--arc-ink-strong); + text-shadow: 0 2px 0 rgba(0,0,0,.5); +} +html[data-theme="arcade"] .section-label, +html[data-theme="arcade"] .meta-label, +html[data-theme="arcade"] .page-subtitle, +html[data-theme="arcade"] .muted{ + color: var(--arc-ink-muted); +} + +/* Inputs + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .search-input, +html[data-theme="arcade"] .form-control, +html[data-theme="arcade"] .login-input, +html[data-theme="arcade"] .tool-input, +html[data-theme="arcade"] textarea{ + background:#0b1b3d; + border:1px solid #2a4ea0; + color:#dff5ff; + border-radius:8px; + box-shadow: 0 2px 0 rgba(0,0,0,.35) inset, 0 0 0 2px rgba(16,34,82,.4) inset; +} +html[data-theme="arcade"] .search-input::placeholder, +html[data-theme="arcade"] .form-control::placeholder{ + color:#8db5cf; +} +html[data-theme="arcade"] .search-input:focus, +html[data-theme="arcade"] .form-control:focus, +html[data-theme="arcade"] .login-input:focus, +html[data-theme="arcade"] .tool-input:focus{ + border-color: var(--arc-accent); + box-shadow: 0 0 0 3px rgba(29,225,255,.26); + outline:none; +} + +/* Check pills/chips (filters) + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .check-pill{ + background:#0c214a; + border:1px solid #2a4ea0; + color:#c9eaff; + border-radius:8px; +} +html[data-theme="arcade"] .check-pill:hover{ background:#103062; } +html[data-theme="arcade"] .chip, +html[data-theme="arcade"] .chip-link{ + background:#0d2552; + border:1px solid #2a4ea0; + color:#c9eaff; + border-radius: var(--arc-chip-radius); +} +html[data-theme="arcade"] .chip-muted{ + background:#0e2143; + color:#9ec6df; +} + +/* Buttons + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .btn{ + background:#0b254e; + border:2px solid #2f6bd4; + color:#e6fbff; + border-radius:10px; + padding:8px 14px; + text-shadow:0 1px 0 rgba(0,0,0,.6); + box-shadow: + 0 0 0 2px #071a3a inset, + 0 4px 0 0 rgba(0,0,0,.45); +} +html[data-theme="arcade"] .btn:hover{ + background:#113066; + border-color:#4b89ff; +} +html[data-theme="arcade"] .btn-primary{ + background:linear-gradient(180deg,#1de1ff 0%,#13a7be 100%); + border-color:#67f0ff; + color:#06202c; + text-shadow:none; +} +html[data-theme="arcade"] .btn-primary:hover{ + background:linear-gradient(180deg,#57ecff 0%,#18bfd8 100%); + border-color:#a1f7ff; +} +html[data-theme="arcade"] .btn-danger{ + background:#4a0f1e; + border-color:#ff738c; + color:#ffdbe2; +} +html[data-theme="arcade"] .btn-danger:hover{ + background:#5c1426; +} + +/* Messages + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .msg{ + background:#0c1e43; + border:1px solid #2a4ea0; + color:#e3f6ff; +} +html[data-theme="arcade"] .msg.info { border-left:4px solid var(--arc-info); background:#0b1f42; } +html[data-theme="arcade"] .msg.success { border-left:4px solid var(--arc-success); background:#0b2b3a; } +html[data-theme="arcade"] .msg.warning { border-left:4px solid var(--arc-warning); background:#2a240a; } +html[data-theme="arcade"] .msg.error { border-left:4px solid var(--arc-danger); background:#2a0d18; } + +/* Links + ---------------------------------------------------------------- */ +html[data-theme="arcade"] a{ + color:#7ee9ff; +} +html[data-theme="arcade"] a:hover{ + color:#b6f4ff; + text-decoration: underline; +} + +/* Menus + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .user-menu, +html[data-theme="arcade"] .mobile-menu-inner{ + background:#0a1836; + border:1px solid #2a4ea0; +} +html[data-theme="arcade"] .user-menu .menu-item:hover, +html[data-theme="arcade"] .mobile-link:hover{ + background:#0e2a5a; +} + +/* Tables + ---------------------------------------------------------------- */ +html[data-theme="arcade"] table{ border-collapse:collapse; } +html[data-theme="arcade"] th, +html[data-theme="arcade"] td{ + border:1px solid #223f86; + background:#0f2147; + color:#daf2ff; +} + +/* Highlights & Validation + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .mark-hit{ + background:#fffb91; + color:#0a1630; + padding:0 2px; + border:1px solid #f0e276; + border-radius:3px; +} + +html[data-theme="arcade"] .form-control.scripture-valid, +html[data-theme="arcade"] .search-input.scripture-valid{ + border-color: var(--arc-success); + background:#062a1a; + box-shadow:0 0 0 3px rgba(34,211,110,.18); +} +html[data-theme="arcade"] .form-control.scripture-invalid, +html[data-theme="arcade"] .search-input.scripture-invalid{ + border-color: var(--arc-danger); + background:#2a0d18; + box-shadow:0 0 0 3px rgba(255,77,109,.18); +} +html[data-theme="arcade"] .scripture-pill-invalid{ + background:#3a0b18; + border:1px solid #ff8ea2; + color:#ffdbe2; +} +html[data-theme="arcade"] .scripture-pill-wol{ + background:#0d263e; + border:1px solid #7ee9ff; + color:#d9fbff; +} + +/* Command Center accents + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .settings-console .sec-tile{ + background:#0b1d42; + border:1px solid #2a4ea0; + color:#dff5ff; + border-radius:12px; + box-shadow:0 8px 20px rgba(0,0,0,.35); +} +html[data-theme="arcade"] .settings-console .sec-tile:hover{ + transform:translateY(-1px); + box-shadow:0 12px 26px rgba(0,0,0,.45); +} +html[data-theme="arcade"] .settings-console .sec-icon{ + background:#0c2a5a; + color:#9ddcff; + border:1px solid #2a4ea0; +} +html[data-theme="arcade"] .settings-console .swatch{ + border:1px solid #2a4ea0; + background:linear-gradient(135deg,#0d2856,#18428d); +} + +/* Misc polish + ---------------------------------------------------------------- */ +html[data-theme="arcade"] .topbar-wrap.is-scrolled{ + box-shadow:0 10px 28px rgba(0,0,0,.55); + border-bottom-color:#0f2e66; +} + +/* Make subject “chips” look like game powerups when highlighted */ +html[data-theme="arcade"] .chip-subject.chip-hit{ + background:#ffe28c; + border-color:#ffc864; + color:#241300; + box-shadow:0 2px 0 0 rgba(0,0,0,.45); +} \ No newline at end of file -- 2.45.2 From f8e483ba125caaaedf1d555da2eafb45deaaa42e Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 18:12:08 +0000 Subject: [PATCH 2/5] Delete web/static/themes/dawn.css --- web/static/themes/dawn.css | 63 -------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 web/static/themes/dawn.css diff --git a/web/static/themes/dawn.css b/web/static/themes/dawn.css deleted file mode 100644 index 4f50715..0000000 --- a/web/static/themes/dawn.css +++ /dev/null @@ -1,63 +0,0 @@ -/* =============================== - Theme: Dawn (bolder sunrise look) - =============================== */ - -:root { - --bg: #fef9f6; /* very light warm base */ - --card: #ffffff; - --ink: #2c2c2c; /* dark gray text */ - --ink-muted: #6b7280; /* muted gray */ - - --border: #e0d9d5; - - --brand: #e67e5c; /* warm orange */ - --brand-800: #c45d3d; /* darker orange/red */ - - --danger: #c53030; - - --btn-bg: #ffffff; - --btn-border: #e5dedb; - --btn-hover: #f9ece6; -} - -/* ----- Background gradient ----- */ -html[data-theme="dawn"] body { - min-height: 100vh; - background: linear-gradient( - 135deg, - #ffd9a0 0%, /* sunrise peach */ - #ffb6b9 40%, /* rosy pink */ - #a3d5ff 100% /* clear sky blue */ - ); - background-attachment: fixed; - color: var(--ink); -} - -/* Keep cards floating nicely */ -html[data-theme="dawn"] .card, -html[data-theme="dawn"] .result-card, -html[data-theme="dawn"] .search-form, -html[data-theme="dawn"] .form-card { - background: var(--card); - border: 1px solid var(--border); - box-shadow: 0 8px 24px rgba(0,0,0,.06); -} - -/* Translucent topbar over gradient */ -html[data-theme="dawn"] .topbar-wrap { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); - border-bottom: 1px solid rgba(229,231,235,.8); -} - -/* Buttons */ -html[data-theme="dawn"] .btn-primary { - background: var(--brand); - border-color: var(--brand); - color: #fff; -} -html[data-theme="dawn"] .btn-primary:hover { - background: var(--brand-800); - border-color: var(--brand-800); -} \ No newline at end of file -- 2.45.2 From 99434e29f5bf7407aa3a9aa6711e07507eccbe38 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 18:39:57 +0000 Subject: [PATCH 3/5] Update web/core/views.py --- web/core/views.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/web/core/views.py b/web/core/views.py index a2b7f57..a2a478c 100644 --- a/web/core/views.py +++ b/web/core/views.py @@ -25,6 +25,7 @@ from django.contrib.staticfiles.storage import staticfiles_storage from django.db import transaction from . import utils as core_utils from .models_audit import AuditLog +from .scripture_normalizer import normalize_scripture_field # Order + labels used in the Search UI @@ -118,6 +119,9 @@ def search_page(request): - quoted phrases - * and ? wildcards (regex); if regex returns zero, falls back to icontains - AND across tokens, OR across the selected fields + + Special power term: + - 'invalidscripture' -> returns only entries whose scripture would trigger red pills (validation warnings) """ default_fields = { "subject": True, @@ -143,6 +147,56 @@ def search_page(request): q = (request.GET.get("q") or "").strip() if q: + # ✨ Special command: list entries whose Scripture validation would fail (red pills) + if q.lower() == "invalidscripture": + qs = Entry.objects.exclude(scripture_raw="").only("id", "scripture_raw", "date_added") + invalid_ids = [] + # Reuse the same rules your front-end uses + for e in qs.iterator(chunk_size=1000): + _norm, warnings = normalize_scripture_field((e.scripture_raw or "").strip()) + if warnings: + invalid_ids.append(e.id) + + # Keep your standard sort order (newest first, then id desc) + ids = list( + Entry.objects.filter(id__in=invalid_ids) + .order_by("-date_added", "-id") + .values_list("id", flat=True) + ) + + try: + print(f"[search] q='invalidscripture' count={len(ids)}") + except Exception: + pass + + request.session["result_ids"] = ids + request.session["last_search"] = {"q": q, "fields": ["scripture_raw"]} + request.session.modified = True + + count = len(ids) + if count: + entry = Entry.objects.get(pk=ids[0]) + ctx = entry_context(entry, ids) + ctx.update({"from_search": True}) + if request.user.is_staff: + ctx["tts_url"] = reverse("api_tts_for_entry", args=[entry.id]) + return render(request, "entry_view.html", ctx) + + total = Entry.objects.count() + return render( + request, + "search.html", + { + "q": q, + "selected": selected, + "field_options": field_options, + "total": total, + "ran_search": True, + "result_count": 0, + }, + ) + + # --- existing search flow --- tokens = terms(q) fields = [f for f, sel in selected.items() if sel] or ["subject"] -- 2.45.2 From 6a1d3beef9806a029e537b190b3b39033f0ccc4c Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 18:44:10 +0000 Subject: [PATCH 4/5] Update web/core/views.py --- web/core/views.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/web/core/views.py b/web/core/views.py index a2a478c..57bcbf6 100644 --- a/web/core/views.py +++ b/web/core/views.py @@ -121,7 +121,7 @@ def search_page(request): - AND across tokens, OR across the selected fields Special power term: - - 'invalidscripture' -> returns only entries whose scripture would trigger red pills (validation warnings) + - 'invalidscripture' -> entries whose Scripture would show red (invalid) """ default_fields = { "subject": True, @@ -147,17 +147,27 @@ def search_page(request): q = (request.GET.get("q") or "").strip() if q: - # ✨ Special command: list entries whose Scripture validation would fail (red pills) + # ===== SPECIAL POWER TERM ===== if q.lower() == "invalidscripture": - qs = Entry.objects.exclude(scripture_raw="").only("id", "scripture_raw", "date_added") + # A simple server-side validity check that mirrors the front-end idea: + # each piece must look like " " + book_ch_re = re.compile(r"^.+?\s+\d{1,3}(?::\s*.+)?$") + invalid_ids = [] - # Reuse the same rules your front-end uses - for e in qs.iterator(chunk_size=1000): - _norm, warnings = normalize_scripture_field((e.scripture_raw or "").strip()) - if warnings: + qs_all = Entry.objects.exclude(scripture_raw="").only("id", "scripture_raw", "date_added") + for e in qs_all.iterator(chunk_size=1000): + original = (e.scripture_raw or "").strip() + norm, warns = normalize_scripture_field(original) + + # Split into pieces as the UI does + pieces = [p.strip() for p in original.split(";") if p.strip()] + # Invalid if: + # - normalizer produced warnings (e.g., verses but no book), OR + # - any piece fails " " quick check + any_bad_shape = any(not book_ch_re.match(p) for p in pieces) + if warns or any_bad_shape: invalid_ids.append(e.id) - # Keep your standard sort order (newest first, then id desc) ids = list( Entry.objects.filter(id__in=invalid_ids) .order_by("-date_added", "-id") @@ -195,8 +205,8 @@ def search_page(request): "result_count": 0, }, ) + # ===== END SPECIAL TERM ===== - # --- existing search flow --- tokens = terms(q) fields = [f for f, sel in selected.items() if sel] or ["subject"] @@ -233,19 +243,15 @@ def search_page(request): request.session["result_ids"] = ids count = len(ids) - # Ensure highlighter data is available BEFORE navigating to entry_view request.session["last_search"] = {"q": q, "fields": fields} - request.session.modified = True # be explicit so it’s flushed + request.session.modified = True if count: entry = Entry.objects.get(pk=ids[0]) ctx = entry_context(entry, ids) ctx.update({"from_search": True}) - - # 🔽 ADD THIS if request.user.is_staff: ctx["tts_url"] = reverse("api_tts_for_entry", args=[entry.id]) - return render(request, "entry_view.html", ctx) total = Entry.objects.count() -- 2.45.2 From 4e5947545c689e1e837c8724e7cb2d88218342e1 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 18:48:10 +0000 Subject: [PATCH 5/5] Update web/version.txt --- web/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/version.txt b/web/version.txt index b75003b..7fb4b7b 100644 --- a/web/version.txt +++ b/web/version.txt @@ -1 +1 @@ -v4.1.21 \ No newline at end of file +v4.1.22 \ No newline at end of file -- 2.45.2