From f2f6041faeb06174f196a2884a669bdece3b4804 Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 04:12:01 -0400 Subject: [PATCH 01/10] Introduce opt-in theme system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the plumbing for an opt-in theme system while leaving the default look and behavior unchanged. A theme is selected via the ABBEY_THEME env var (or `Rails.application.config.theme`) and contributes three things: * View overrides: any file under `app/views/themes//` overrides its same-named default counterpart (including layouts and partials) via Rails' view path prepending. Implemented in `app/controllers/concerns/theming.rb` and included in ApplicationController as a no-op for the default theme. * Extra stylesheets: files under `app/assets/stylesheets/themes/` are excluded from the existing `:app` Propshaft bulk-include so they cannot leak into the default build, and are loaded explicitly via the new `theme_stylesheets` helper only when their theme is active. * Theme-aware markdown rendering: themes listed in `Rails.application.config.themes_using_minimal_renderer` get the new `MinimalMarkdownRender`, which emits semantic HTML without inline Tailwind utility classes — so themes can style content from a wrapper scope (e.g. `.prose-retro`) rather than fighting the renderer. The default theme keeps the existing `MarkdownRender`. No new gems, no Tailwind config changes, no default-theme regressions: when `ABBEY_THEME=default` (the default) the rendered HTML, asset list and markdown output are byte-for-byte identical to before this commit. Co-authored-by: Cursor --- app/controllers/application_controller.rb | 1 + app/controllers/concerns/theming.rb | 31 ++++++++++ app/helpers/application_helper.rb | 32 +++++++++++ app/models/concerns/rendering.rb | 16 +++++- config/initializers/themes.rb | 21 +++++++ lib/minimal_markdown_render.rb | 69 +++++++++++++++++++++++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 app/controllers/concerns/theming.rb create mode 100644 config/initializers/themes.rb create mode 100644 lib/minimal_markdown_render.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 94e7183..2902bd2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base include Authentication + include Theming # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern end diff --git a/app/controllers/concerns/theming.rb b/app/controllers/concerns/theming.rb new file mode 100644 index 0000000..aed72d6 --- /dev/null +++ b/app/controllers/concerns/theming.rb @@ -0,0 +1,31 @@ +# Prepends the active theme's view directory to the lookup path so that +# `app/views/themes//foo/bar.html.erb` overrides +# `app/views/foo/bar.html.erb` (including layouts) when a non-default theme +# is active. The default theme is a no-op. +module Theming + extend ActiveSupport::Concern + + included do + before_action :prepend_theme_view_path + helper_method :current_theme, :theme_active? + end + + private + + def current_theme + Rails.application.config.theme.to_s.presence || "default" + end + + def theme_active? + current_theme != "default" + end + + def prepend_theme_view_path + return unless theme_active? + + theme_root = Rails.root.join("app/views/themes", current_theme) + return unless theme_root.exist? + + prepend_view_path theme_root.to_s + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..b2ec717 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,34 @@ module ApplicationHelper + # Override Propshaft's `:app` bulk inclusion so that stylesheets which + # live under `app/assets/stylesheets/themes/` are NOT auto-included. + # Those files belong to optional themes and are loaded separately via + # #theme_stylesheets only when their theme is active. This keeps the + # default theme byte-for-byte unchanged regardless of how many themes + # the project ships. + def app_stylesheets_paths + super.reject { |path| path.to_s.start_with?("themes/") } + end + + # Returns the names of any extra stylesheets that the active theme ships + # under `app/assets/stylesheets/themes/`. The default theme contributes + # nothing extra; other themes load `themes/.css` and (if present) + # `themes/-highlight.css`. + def theme_stylesheets + return [] unless theme_active? + + candidates = [ + "themes/#{current_theme}", + "themes/#{current_theme}-highlight" + ] + candidates.select { |name| theme_stylesheet_exists?(name) } + end + + private + + def theme_stylesheet_exists?(logical_name) + Rails.application.assets&.load_path&.find("#{logical_name}.css").present? || + Rails.root.join("app/assets/stylesheets/#{logical_name}.css").exist? + rescue StandardError + Rails.root.join("app/assets/stylesheets/#{logical_name}.css").exist? + end end diff --git a/app/models/concerns/rendering.rb b/app/models/concerns/rendering.rb index b796cfa..292deb8 100644 --- a/app/models/concerns/rendering.rb +++ b/app/models/concerns/rendering.rb @@ -1,4 +1,5 @@ require "markdown_render" +require "minimal_markdown_render" module Rendering extend ActiveSupport::Concern @@ -7,7 +8,7 @@ module Rendering included do def render(text) - processed_markdown = Redcarpet::Markdown.new(MarkdownRender, fenced_code_blocks: true).render(text) + processed_markdown = Redcarpet::Markdown.new(self.class.markdown_renderer, fenced_code_blocks: true).render(text) # Replace signed IDs with img tags, handling both href and src attributes processed_markdown.gsub!(/(href|src)="(.*?)"/) do |match| @@ -32,4 +33,17 @@ def render(text) processed_markdown end end + + class_methods do + # Themes can request the minimal renderer (semantic HTML, no inline + # Tailwind classes) by adding their name to + # Rails.application.config.themes_using_minimal_renderer (Array). + # The default theme keeps the original MarkdownRender for backwards + # compatibility with existing imported posts. + def markdown_renderer + themes = Rails.application.config.try(:themes_using_minimal_renderer) || [] + active = Rails.application.config.try(:theme).to_s + themes.include?(active) ? MinimalMarkdownRender : MarkdownRender + end + end end diff --git a/config/initializers/themes.rb b/config/initializers/themes.rb new file mode 100644 index 0000000..dea5b2a --- /dev/null +++ b/config/initializers/themes.rb @@ -0,0 +1,21 @@ +# Opt-in theme system for Abbey. +# +# A theme can override any view by placing a same-named file under +# `app/views/themes//` (e.g. `app/views/themes/retro/blog/index.html.erb` +# wins over `app/views/blog/index.html.erb` when the theme is active). +# +# A theme can also ship its own stylesheets under +# `app/assets/stylesheets/themes/.css` (and optionally +# `themes/-highlight.css`) which the layout loads in addition to the +# default Tailwind build. +# +# Set the active theme with the ABBEY_THEME env var, or override here. +# Built-in themes: "default" (the original minimal look), "retro". + +Rails.application.config.theme = ENV.fetch("ABBEY_THEME", "default") + +# Themes listed here render markdown via the MinimalMarkdownRender (semantic +# HTML, no inline Tailwind classes) so they can style content entirely from +# a wrapper class scope. Default theme keeps the original utility-class +# renderer to preserve current behavior. +Rails.application.config.themes_using_minimal_renderer = %w[retro] diff --git a/lib/minimal_markdown_render.rb b/lib/minimal_markdown_render.rb new file mode 100644 index 0000000..13d79e0 --- /dev/null +++ b/lib/minimal_markdown_render.rb @@ -0,0 +1,69 @@ +require "redcarpet" +require "rouge" +require "rouge/plugins/redcarpet" + +# Renderer that emits plain semantic HTML — no inline Tailwind utility +# classes — so themes can style markdown output entirely from a wrapper +# scope (e.g. `.prose-retro`). Used by any theme that registers itself as +# `Rails.application.config.theme_uses_minimal_renderer = true`, or +# explicitly chosen by the Rendering concern. +module Redcarpet + module Render + class MinimalHTML < ::Redcarpet::Render::HTML + def normal_text(text) + text + end + + def block_code(code, language) + %(
#{code}
) + end + + def header(title, level) + "#{title}" + end + + def paragraph(text) + "

#{text}

" + end + + def list(content, list_type) + tag = list_type == :ordered ? "ol" : "ul" + "<#{tag}>#{content}" + end + + def list_item(content, _list_type) + "
  • #{content}
  • " + end + + def link(link, title, content) + title_attr = title ? %( title="#{title}") : "" + %(#{content}) + end + + def emphasis(text) + "#{text}" + end + + def double_emphasis(text) + "#{text}" + end + + def block_quote(quote) + "
    #{quote}
    " + end + + def hrule + "
    " + end + + def image(link, title, alt_text) + title_attr = title ? %( title="#{title}") : "" + %(#{alt_text}) + end + end + end +end + +class MinimalMarkdownRender < Redcarpet::Render::MinimalHTML + include Rouge::Plugins::Redcarpet +end From 984e183ba19c40e11fe59446474fa83530a1098e Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 04:12:13 -0400 Subject: [PATCH 02/10] Add retro theme stylesheets (Memphis / 8-bit / CRT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pure-CSS stylesheets that ship with the new `retro` theme and are loaded by the theme system on demand: * `themes/retro.css` — Memphis design palette (neon pink/cyan/yellow /mint/purple/coral on paper or CRT-black), neo-brutalist cards, pixel-display headings (`Press Start 2P`), terminal-mono body (`VT323` / `Space Grotesk`), animated marquee, blinking cursor, drifting confetti background, subtle CRT scanlines, and a fully themed `.prose-retro` block for rendered markdown (h1-h3 with hard text-shadows, wavy underlines, highlighted ``, terminal `
    ` blocks, dotted SVG `
    `, etc.). * `themes/retro-highlight.css` — Rouge syntax highlighting palette that pairs with the terminal aesthetic (neon-on-CRT-black). Every selector is gated on `.theme-retro` (set on `` by the retro layout), so even if the file is loaded outside the theme it produces no visible effect. No Tailwind processing is required — the file is served directly by Propshaft. Co-authored-by: Cursor --- .../stylesheets/themes/retro-highlight.css | 114 ++++ app/assets/stylesheets/themes/retro.css | 626 ++++++++++++++++++ 2 files changed, 740 insertions(+) create mode 100644 app/assets/stylesheets/themes/retro-highlight.css create mode 100644 app/assets/stylesheets/themes/retro.css diff --git a/app/assets/stylesheets/themes/retro-highlight.css b/app/assets/stylesheets/themes/retro-highlight.css new file mode 100644 index 0000000..9544796 --- /dev/null +++ b/app/assets/stylesheets/themes/retro-highlight.css @@ -0,0 +1,114 @@ +/* ============================================================================= + Abbey – Retro Theme: Rouge syntax highlighting + "Retro Terminal" palette – neon on CRT black. + All selectors are scoped under `.theme-retro` so default theme highlighting + is untouched. + ============================================================================= */ + +.theme-retro .highlight { + width: 100%; + max-width: none; + overflow-x: auto; + background: #0a0e1a; + color: #d6ffe9; +} + +.theme-retro .highlight pre { + white-space: pre; + overflow-x: auto; + padding: 1rem; + width: 100%; + margin: 0; + background: transparent; + color: inherit; +} + +.theme-retro .highlight code, +.theme-retro pre.highlight code { + font-family: "VT323", ui-monospace, monospace; + background: transparent; + color: inherit; + border: none; + padding: 0; +} + +.theme-retro code:not(.highlight code) { + background: #ffd400; + color: #0d0d12; + border: 2px solid #0d0d12; + padding: 0 6px; + border-radius: 0; + font-family: "VT323", ui-monospace, monospace; + font-size: 0.95em; +} +.theme-retro.dark code:not(.highlight code) { + background: #ff3eb5; + color: #0a0e1a; + border-color: #0a0e1a; +} + +.theme-retro .highlight table td { padding: 5px; } +.theme-retro .highlight table pre { margin: 0; } + +.theme-retro .highlight .err { color: #ff6b4a; } +.theme-retro .highlight .c, +.theme-retro .highlight .ch, +.theme-retro .highlight .cd, +.theme-retro .highlight .cm, +.theme-retro .highlight .cpf, +.theme-retro .highlight .c1, +.theme-retro .highlight .cs { color: #6b7a99; font-style: italic; } +.theme-retro .highlight .cp { color: #00e5ff; } +.theme-retro .highlight .nt { color: #00e5ff; } +.theme-retro .highlight .o, +.theme-retro .highlight .ow { color: #ff3eb5; } +.theme-retro .highlight .p, +.theme-retro .highlight .pi { color: #d6ffe9; } + +.theme-retro .highlight .gi { color: #00ff9c; } +.theme-retro .highlight .gd { color: #ff3eb5; } +.theme-retro .highlight .gh { color: #ffd400; background: transparent; font-weight: bold; } + +.theme-retro .highlight .k, +.theme-retro .highlight .kn, +.theme-retro .highlight .kp, +.theme-retro .highlight .kr, +.theme-retro .highlight .kv { color: #ff3eb5; font-weight: bold; } +.theme-retro .highlight .kc, +.theme-retro .highlight .kt, +.theme-retro .highlight .kd { color: #ffd400; } + +.theme-retro .highlight .s, +.theme-retro .highlight .sb, +.theme-retro .highlight .sc, +.theme-retro .highlight .dl, +.theme-retro .highlight .sd, +.theme-retro .highlight .s2, +.theme-retro .highlight .sh, +.theme-retro .highlight .sx, +.theme-retro .highlight .s1 { color: #00ff9c; } +.theme-retro .highlight .sa { color: #ff3eb5; } +.theme-retro .highlight .sr { color: #00e5ff; } +.theme-retro .highlight .si, +.theme-retro .highlight .se { color: #ff6b4a; } + +.theme-retro .highlight .nn, +.theme-retro .highlight .nc, +.theme-retro .highlight .no { color: #ffd400; } +.theme-retro .highlight .na { color: #00e5ff; } + +.theme-retro .highlight .m, +.theme-retro .highlight .mb, +.theme-retro .highlight .mf, +.theme-retro .highlight .mh, +.theme-retro .highlight .mi, +.theme-retro .highlight .il, +.theme-retro .highlight .mo, +.theme-retro .highlight .mx { color: #b14aff; } +.theme-retro .highlight .ss { color: #00ff9c; } +.theme-retro .highlight .nf { color: #00e5ff; } +.theme-retro .highlight .ne { color: #ff6b4a; font-weight: bold; } +.theme-retro .highlight .nb { color: #ffd400; } +.theme-retro .highlight .vc, +.theme-retro .highlight .vg, +.theme-retro .highlight .vi { color: #ff3eb5; } diff --git a/app/assets/stylesheets/themes/retro.css b/app/assets/stylesheets/themes/retro.css new file mode 100644 index 0000000..71c1e4b --- /dev/null +++ b/app/assets/stylesheets/themes/retro.css @@ -0,0 +1,626 @@ +/* ============================================================================= + Abbey – Retro Theme + Memphis / 8-bit / 80s computer aesthetic. + + Pure CSS – served by Propshaft, loaded only when + `Rails.application.config.theme == "retro"`. All declarations are scoped + under `.theme-retro` (set on by themes/retro/layouts/application.html.erb) + so loading this file in another context is a no-op. + + Custom palette: + ink #0d0d12 paper #fff8ef crt #0a0e1a + pink #ff3eb5 cyan #00e5ff yellow #ffd400 + mint #00ff9c purple #b14aff coral #ff6b4a + =============================================================================*/ + +/* ---------------------------------------------------------------------- + Base – body backdrop, CRT scanlines, selection +---------------------------------------------------------------------- */ + +html.theme-retro { + background-color: #fff8ef; + color: #0d0d12; + image-rendering: pixelated; +} +html.theme-retro.dark { + background-color: #0a0e1a; + color: #f0fff7; +} + +html.theme-retro body { + font-family: "Space Grotesk", system-ui, sans-serif; + font-feature-settings: "ss01" on, "ss02" on; + position: relative; + overflow-x: hidden; + min-height: 100vh; +} + +/* Memphis confetti drifting in the background */ +html.theme-retro body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + opacity: 0.85; + background-image: url("data:image/svg+xml;utf8,"); + background-size: 420px 420px; + animation: retro-drift 22s ease-in-out infinite; +} +html.theme-retro.dark body::before { + opacity: 0.25; + filter: hue-rotate(20deg) saturate(1.2); +} + +/* Subtle CRT scanlines */ +html.theme-retro body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 100; + pointer-events: none; + background-image: repeating-linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 0, + rgba(0, 0, 0, 0) 3px, + rgba(0, 0, 0, 0.022) 3px, + rgba(0, 0, 0, 0.022) 4px + ); + mix-blend-mode: multiply; +} +html.theme-retro.dark body::after { + background-image: repeating-linear-gradient( + to bottom, + rgba(0, 255, 156, 0) 0, + rgba(0, 255, 156, 0) 3px, + rgba(0, 255, 156, 0.028) 3px, + rgba(0, 255, 156, 0.028) 4px + ); + mix-blend-mode: screen; +} + +html.theme-retro main, +html.theme-retro header, +html.theme-retro footer, +html.theme-retro nav, +html.theme-retro .content-layer { + position: relative; + z-index: 1; +} + +html.theme-retro ::selection { + background: #ff3eb5; + color: #fff8ef; +} +html.theme-retro.dark ::selection { + background: #00ff9c; + color: #0a0e1a; +} + +/* ---------------------------------------------------------------------- + Utility-style color/shadow/font classes used inline in retro views. + Scoped to `.theme-retro` so they cannot leak into the default theme, + and intentionally redundant with the dedicated component classes so + markup remains expressive without requiring Tailwind config edits. +---------------------------------------------------------------------- */ + +.theme-retro .bg-memphis-paper { background-color: #fff8ef; } +.theme-retro .bg-memphis-ink { background-color: #0d0d12; } +.theme-retro .bg-memphis-crt { background-color: #0a0e1a; } +.theme-retro .bg-memphis-pink { background-color: #ff3eb5; } +.theme-retro .bg-memphis-cyan { background-color: #00e5ff; } +.theme-retro .bg-memphis-yellow { background-color: #ffd400; } +.theme-retro .bg-memphis-mint { background-color: #00ff9c; } +.theme-retro .bg-memphis-purple { background-color: #b14aff; } +.theme-retro .bg-memphis-coral { background-color: #ff6b4a; } + +.theme-retro .text-memphis-ink { color: #0d0d12; } +.theme-retro .text-memphis-paper { color: #fff8ef; } +.theme-retro .text-memphis-pink { color: #ff3eb5; } +.theme-retro .text-memphis-cyan { color: #00e5ff; } +.theme-retro .text-memphis-yellow { color: #ffd400; } +.theme-retro .text-memphis-mint { color: #00ff9c; } +.theme-retro .text-memphis-purple { color: #b14aff; } +.theme-retro .text-memphis-coral { color: #ff6b4a; } + +.theme-retro .border-memphis-ink { border-color: #0d0d12; } +.theme-retro .border-memphis-pink { border-color: #ff3eb5; } +.theme-retro .border-memphis-mint { border-color: #00ff9c; } +.theme-retro .border-memphis-cyan { border-color: #00e5ff; } +.theme-retro .border-memphis-yellow{ border-color: #ffd400; } + +.theme-retro .shadow-retro-sm { box-shadow: 4px 4px 0 0 #0d0d12; } +.theme-retro .shadow-retro { box-shadow: 6px 6px 0 0 #0d0d12; } +.theme-retro .shadow-retro-lg { box-shadow: 10px 10px 0 0 #0d0d12; } +.theme-retro .shadow-retro-pink { box-shadow: 6px 6px 0 0 #ff3eb5; } +.theme-retro .shadow-retro-cyan { box-shadow: 6px 6px 0 0 #00e5ff; } +.theme-retro .shadow-retro-yellow { box-shadow: 6px 6px 0 0 #ffd400; } +.theme-retro .shadow-retro-mint { box-shadow: 6px 6px 0 0 #00ff9c; } +.theme-retro .shadow-retro-purple { box-shadow: 6px 6px 0 0 #b14aff; } +.theme-retro .shadow-retro-coral { box-shadow: 6px 6px 0 0 #ff6b4a; } + +.theme-retro .font-display { font-family: "Press Start 2P", system-ui, sans-serif; } +.theme-retro .font-mono { font-family: "VT323", ui-monospace, monospace; } +.theme-retro .font-sans { font-family: "Space Grotesk", system-ui, sans-serif; } + +.theme-retro .animate-blink { animation: retro-blink 1s steps(2, start) infinite; } +.theme-retro .animate-wiggle { animation: retro-wiggle 0.4s ease-in-out; } +.theme-retro .animate-pop-in { animation: retro-pop-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; } + +/* Dark-mode variants of the above (mirrors Tailwind dark: prefix when + carries both `theme-retro` and `dark` classes). */ +.theme-retro.dark .dark\:bg-memphis-paper { background-color: #fff8ef; } +.theme-retro.dark .dark\:bg-memphis-ink { background-color: #0d0d12; } +.theme-retro.dark .dark\:bg-memphis-crt { background-color: #0a0e1a; } +.theme-retro.dark .dark\:bg-memphis-pink { background-color: #ff3eb5; } +.theme-retro.dark .dark\:bg-memphis-cyan { background-color: #00e5ff; } +.theme-retro.dark .dark\:bg-memphis-yellow { background-color: #ffd400; } +.theme-retro.dark .dark\:bg-memphis-mint { background-color: #00ff9c; } +.theme-retro.dark .dark\:bg-memphis-purple { background-color: #b14aff; } +.theme-retro.dark .dark\:bg-memphis-coral { background-color: #ff6b4a; } +.theme-retro.dark .dark\:bg-black { background-color: #000000; } + +.theme-retro.dark .dark\:text-memphis-paper { color: #fff8ef; } +.theme-retro.dark .dark\:text-memphis-ink { color: #0d0d12; } +.theme-retro.dark .dark\:text-memphis-pink { color: #ff3eb5; } +.theme-retro.dark .dark\:text-memphis-cyan { color: #00e5ff; } +.theme-retro.dark .dark\:text-memphis-yellow { color: #ffd400; } +.theme-retro.dark .dark\:text-memphis-mint { color: #00ff9c; } +.theme-retro.dark .dark\:text-memphis-purple { color: #b14aff; } + +.theme-retro.dark .dark\:border-memphis-mint { border-color: #00ff9c; } +.theme-retro.dark .dark\:border-memphis-pink { border-color: #ff3eb5; } +.theme-retro.dark .dark\:border-memphis-cyan { border-color: #00e5ff; } +.theme-retro.dark .dark\:shadow-none { box-shadow: none; } + +/* Hover & group-hover Memphis color variants + (mirror Tailwind `hover:text-memphis-pink` syntax without requiring + Memphis tokens in the Tailwind config). */ +.theme-retro .hover\:text-memphis-pink:hover { color: #ff3eb5; } +.theme-retro .hover\:text-memphis-cyan:hover { color: #00e5ff; } +.theme-retro .hover\:text-memphis-mint:hover { color: #00ff9c; } +.theme-retro .hover\:text-memphis-yellow:hover { color: #ffd400; } +.theme-retro .hover\:bg-memphis-pink:hover { background-color: #ff3eb5; } +.theme-retro .hover\:text-memphis-paper:hover { color: #fff8ef; } + +.theme-retro.dark .dark\:hover\:text-memphis-mint:hover { color: #00ff9c; } +.theme-retro.dark .dark\:hover\:text-memphis-cyan:hover { color: #00e5ff; } +.theme-retro.dark .dark\:hover\:text-memphis-yellow:hover { color: #ffd400; } +.theme-retro.dark .dark\:hover\:text-memphis-pink:hover { color: #ff3eb5; } + +.theme-retro .group:hover .group-hover\:animate-wiggle { animation: retro-wiggle 0.4s ease-in-out; } +.theme-retro .group:hover .group-hover\:text-memphis-pink { color: #ff3eb5; } +.theme-retro.dark .group:hover .dark\:group-hover\:text-memphis-yellow { color: #ffd400; } + +/* Color with alpha (mirrors Tailwind `text-memphis-paper/90` opacity syntax). */ +.theme-retro .text-memphis-ink\/40 { color: rgba(13, 13, 18, 0.4); } +.theme-retro .text-memphis-paper\/90 { color: rgba(255, 248, 239, 0.9); } +.theme-retro.dark .dark\:text-memphis-mint\/40 { color: rgba(0, 255, 156, 0.4); } +.theme-retro.dark .dark\:text-memphis-paper\/90 { color: rgba(255, 248, 239, 0.9); } + +/* ---------------------------------------------------------------------- + Keyframes +---------------------------------------------------------------------- */ + +@keyframes retro-blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} +@keyframes retro-marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +@keyframes retro-drift { + 0%, 100% { background-position: 0 0; } + 50% { background-position: 80px 60px; } +} +@keyframes retro-glitch { + 0% { transform: translate(0, 0); } + 20% { transform: translate(-2px, 2px); } + 40% { transform: translate(2px, -1px); } + 60% { transform: translate(-1px, -2px); } + 80% { transform: translate(2px, 1px); } + 100% { transform: translate(0, 0); } +} +@keyframes retro-pop-in { + 0% { opacity: 0; transform: translateY(8px) scale(0.96); } + 100% { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes retro-wiggle { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-3deg); } + 75% { transform: rotate(3deg); } +} + +/* ---------------------------------------------------------------------- + Cards / containers +---------------------------------------------------------------------- */ + +.theme-retro .card-retro { + background: #ffffff; + border: 3px solid #0d0d12; + box-shadow: 6px 6px 0 0 #0d0d12; + padding: 1.5rem; + transition: transform 200ms ease, box-shadow 200ms ease; +} +.theme-retro.dark .card-retro { + background: #0a0e1a; + border-color: #00ff9c; + box-shadow: 6px 6px 0 0 #00ff9c; +} +.theme-retro .card-retro:hover { + transform: translate(-2px, -2px); + box-shadow: 10px 10px 0 0 #0d0d12; +} +.theme-retro.dark .card-retro:hover { + box-shadow: 10px 10px 0 0 #00ff9c; +} + +.theme-retro .card-shadow-pink { box-shadow: 6px 6px 0 0 #ff3eb5; } +.theme-retro .card-shadow-cyan { box-shadow: 6px 6px 0 0 #00e5ff; } +.theme-retro .card-shadow-yellow { box-shadow: 6px 6px 0 0 #ffd400; } +.theme-retro .card-shadow-mint { box-shadow: 6px 6px 0 0 #00ff9c; } +.theme-retro .card-shadow-purple { box-shadow: 6px 6px 0 0 #b14aff; } +.theme-retro .card-shadow-coral { box-shadow: 6px 6px 0 0 #ff6b4a; } + +/* ---------------------------------------------------------------------- + Buttons +---------------------------------------------------------------------- */ + +.theme-retro .btn-retro { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-family: "Press Start 2P", system-ui, sans-serif; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + background: #ffd400; + color: #0d0d12; + border: 3px solid #0d0d12; + padding: 0.5rem 1rem; + box-shadow: 6px 6px 0 0 #0d0d12; + transition: transform 150ms ease, box-shadow 150ms ease; + cursor: pointer; +} +.theme-retro .btn-retro:hover { + transform: translate(2px, 2px); + box-shadow: 4px 4px 0 0 #0d0d12; +} +.theme-retro .btn-retro:active { + transform: translate(4px, 4px); + box-shadow: 0 0 0 0 transparent; +} +.theme-retro .btn-retro-pink { background: #ff3eb5; color: #fff8ef; } +.theme-retro .btn-retro-cyan { background: #00e5ff; color: #0d0d12; } +.theme-retro .btn-retro-mint { background: #00ff9c; color: #0d0d12; } +.theme-retro .btn-retro-purple { background: #b14aff; color: #fff8ef; } +.theme-retro .btn-retro-coral { background: #ff6b4a; color: #fff8ef; } +.theme-retro .btn-retro-ghost { background: #fff8ef; color: #0d0d12; } + +.theme-retro .btn-icon-retro { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background: #fff8ef; + color: #0d0d12; + border: 3px solid #0d0d12; + box-shadow: 4px 4px 0 0 #0d0d12; + transition: transform 150ms ease, box-shadow 150ms ease; + cursor: pointer; +} +.theme-retro .btn-icon-retro:hover { + transform: translate(2px, 2px); + box-shadow: 0 0 0 0 transparent; +} +.theme-retro.dark .btn-icon-retro { + background: #0a0e1a; + color: #00ff9c; + border-color: #00ff9c; + box-shadow: none; +} + +/* ---------------------------------------------------------------------- + Tag pills +---------------------------------------------------------------------- */ + +.theme-retro .tag-retro { + display: inline-flex; + align-items: center; + font-family: "VT323", ui-monospace, monospace; + font-size: 1rem; + line-height: 1; + padding: 0.25rem 0.75rem; + border: 2px solid #0d0d12; + box-shadow: 4px 4px 0 0 #0d0d12; + transition: transform 150ms ease; +} +.theme-retro .tag-retro:hover { + transform: rotate(-2deg) scale(1.05); +} +.theme-retro .tag-color-1 { background: #ff3eb5; color: #fff8ef; } +.theme-retro .tag-color-2 { background: #00e5ff; color: #0d0d12; } +.theme-retro .tag-color-3 { background: #ffd400; color: #0d0d12; } +.theme-retro .tag-color-4 { background: #00ff9c; color: #0d0d12; } +.theme-retro .tag-color-5 { background: #b14aff; color: #fff8ef; } +.theme-retro .tag-color-6 { background: #ff6b4a; color: #fff8ef; } + +/* ---------------------------------------------------------------------- + Pixel-display headings +---------------------------------------------------------------------- */ + +.theme-retro .h-display { + font-family: "Press Start 2P", system-ui, sans-serif; + color: #0d0d12; + line-height: 1.4; + letter-spacing: -0.02em; + text-shadow: 3px 3px 0 #ff3eb5; +} +.theme-retro.dark .h-display { + color: #00ff9c; + text-shadow: 3px 3px 0 #00e5ff; +} +.theme-retro .h-display-sm { font-size: 1rem; } +.theme-retro .h-display-md { font-size: 1.25rem; } +.theme-retro .h-display-lg { font-size: 1.5rem; } +@media (min-width: 640px) { + .theme-retro .h-display-sm { font-size: 1.125rem; } + .theme-retro .h-display-md { font-size: 1.5rem; } + .theme-retro .h-display-lg { font-size: 1.875rem; } +} +@media (min-width: 768px) { + .theme-retro .h-display-lg { font-size: 2.25rem; } +} + +/* ---------------------------------------------------------------------- + Blinking terminal cursor + marquee +---------------------------------------------------------------------- */ + +.theme-retro .cursor-blink::after { + content: "▮"; + display: inline-block; + margin-left: 0.25rem; + color: #ff3eb5; + animation: retro-blink 1s steps(2, start) infinite; +} +.theme-retro.dark .cursor-blink::after { color: #00ff9c; } + +.theme-retro .marquee { + overflow: hidden; + white-space: nowrap; + border-top: 3px solid #0d0d12; + border-bottom: 3px solid #0d0d12; + background: repeating-linear-gradient( + 45deg, + #ffd400 0, + #ffd400 18px, + #0d0d12 18px, + #0d0d12 36px + ); + padding: 4px 0; +} +.theme-retro .marquee__track { + display: inline-block; + animation: retro-marquee 28s linear infinite; + font-family: "Press Start 2P", monospace; + font-size: 12px; + color: #0d0d12; + background: #fff8ef; + padding: 6px 16px; + border: 2px solid #0d0d12; +} + +/* ---------------------------------------------------------------------- + CRT-style code window + glitch hover +---------------------------------------------------------------------- */ + +.theme-retro .crt-window { + position: relative; + background: #0a0e1a; + color: #00ff9c; + border: 3px solid #0d0d12; + box-shadow: 6px 6px 0 0 #0d0d12; + font-family: "VT323", ui-monospace, monospace; + font-size: 0.875rem; + overflow: hidden; +} +.theme-retro .crt-window::before { + content: "● ● ●"; + display: block; + background: #ff3eb5; + color: #0d0d12; + font-family: "Press Start 2P", monospace; + font-size: 10px; + letter-spacing: 4px; + padding: 6px 12px; + border-bottom: 3px solid #0d0d12; +} + +.theme-retro .glitch-hover:hover { + animation: retro-glitch 0.8s steps(1) 1; +} + +.theme-retro .animate-pop-in { + animation: retro-pop-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +/* ---------------------------------------------------------------------- + Date sticker (rotated label) +---------------------------------------------------------------------- */ + +.theme-retro .date-sticker { + display: inline-block; + font-family: "Press Start 2P", monospace; + font-size: 10px; + background: #ffd400; + color: #0d0d12; + border: 2px solid #0d0d12; + padding: 4px 8px; + transform: rotate(-3deg); + box-shadow: 3px 3px 0 0 #0d0d12; +} +.theme-retro .date-sticker--pink { background: #ff3eb5; color: #fff8ef; } +.theme-retro .date-sticker--mint { background: #00ff9c; color: #0d0d12; } + +/* ---------------------------------------------------------------------- + Rendered markdown content (`.prose-retro` wrapper) + Used together with MinimalMarkdownRender (semantic HTML only). +---------------------------------------------------------------------- */ + +.theme-retro .prose-retro { + font-family: "Space Grotesk", system-ui, sans-serif; + font-size: 1.05rem; + line-height: 1.75; + color: #0d0d12; +} +.theme-retro.dark .prose-retro { color: #d6ffe9; } + +.theme-retro .prose-retro h1, +.theme-retro .prose-retro h2, +.theme-retro .prose-retro h3 { + font-family: "Press Start 2P", system-ui, sans-serif; + line-height: 1.5; + margin: 2em 0 0.8em; + letter-spacing: -0.02em; +} +.theme-retro .prose-retro h1 { font-size: 1.5rem; color: #ff3eb5; text-shadow: 3px 3px 0 #0d0d12; } +.theme-retro .prose-retro h2 { font-size: 1.15rem; color: #b14aff; text-shadow: 2px 2px 0 #ffd400; } +.theme-retro .prose-retro h3 { font-size: 0.95rem; color: #00a3cc; } + +.theme-retro.dark .prose-retro h1 { color: #00ff9c; text-shadow: 3px 3px 0 #ff3eb5; } +.theme-retro.dark .prose-retro h2 { color: #ffd400; text-shadow: 2px 2px 0 #b14aff; } +.theme-retro.dark .prose-retro h3 { color: #00e5ff; } + +.theme-retro .prose-retro p { margin-bottom: 1.1em; } + +.theme-retro .prose-retro strong { + background: #ffd400; + color: #0d0d12; + padding: 0 4px; + border: 2px solid #0d0d12; + font-weight: 700; +} +.theme-retro.dark .prose-retro strong { + background: #00ff9c; + color: #0a0e1a; + border-color: #00ff9c; +} + +.theme-retro .prose-retro em { font-style: italic; color: #b14aff; } +.theme-retro.dark .prose-retro em { color: #00e5ff; } + +.theme-retro .prose-retro a { + color: #ff3eb5; + font-weight: 600; + text-decoration: underline; + text-decoration-style: wavy; + text-decoration-thickness: 2px; + text-underline-offset: 4px; +} +.theme-retro .prose-retro a:hover { background: #ffd400; color: #0d0d12; } +.theme-retro.dark .prose-retro a { color: #00ff9c; } +.theme-retro.dark .prose-retro a:hover { background: #ff3eb5; color: #fff8ef; } + +.theme-retro .prose-retro ul, +.theme-retro .prose-retro ol { + margin: 1em 0 1.2em 1.5em; + padding-left: 0.5em; +} +.theme-retro .prose-retro ul { list-style: none; } +.theme-retro .prose-retro ul > li::before { + content: "▸ "; + color: #ff3eb5; + font-weight: 700; + margin-right: 0.4em; +} +.theme-retro.dark .prose-retro ul > li::before { color: #00ff9c; } +.theme-retro .prose-retro ol { list-style: decimal; } +.theme-retro .prose-retro li { margin-bottom: 0.4em; } + +.theme-retro .prose-retro blockquote { + border-left: 6px solid #ff3eb5; + background: rgba(255, 212, 0, 0.18); + padding: 1em 1.2em; + margin: 1.5em 0; + font-style: italic; +} +.theme-retro.dark .prose-retro blockquote { + border-left-color: #00ff9c; + background: rgba(0, 229, 255, 0.08); +} + +.theme-retro .prose-retro pre, +.theme-retro pre.highlight { + background: #0a0e1a; + color: #00ff9c; + border: 3px solid #0d0d12; + box-shadow: 6px 6px 0 0 #ff3eb5; + padding: 1rem 1.1rem; + margin: 1.5em 0; + overflow-x: auto; + font-family: "VT323", ui-monospace, monospace; + font-size: 1.05rem; + line-height: 1.4; + position: relative; +} +.theme-retro.dark .prose-retro pre, +.theme-retro.dark pre.highlight { + box-shadow: 6px 6px 0 0 #00e5ff; + border-color: #00ff9c; +} +.theme-retro .prose-retro pre::before, +.theme-retro pre.highlight::before { + content: "● TERMINAL — RUN.EXE"; + display: block; + font-family: "Press Start 2P", monospace; + font-size: 9px; + color: #ffd400; + letter-spacing: 2px; + margin: -1rem -1.1rem 0.8rem; + padding: 6px 12px; + background: #0d0d12; + border-bottom: 2px solid #00ff9c; +} + +.theme-retro .prose-retro code, +.theme-retro code:not(pre code) { + font-family: "VT323", ui-monospace, monospace; + background: #ffd400; + color: #0d0d12; + padding: 0 6px; + border: 2px solid #0d0d12; + font-size: 1em; +} +.theme-retro.dark .prose-retro code, +.theme-retro.dark code:not(pre code) { + background: #ff3eb5; + color: #0a0e1a; + border-color: #0a0e1a; +} +.theme-retro .prose-retro pre code, +.theme-retro pre.highlight code { + background: transparent; + color: inherit; + border: none; + padding: 0; + font-size: inherit; +} + +.theme-retro .prose-retro hr { + border: none; + margin: 2em 0; + height: 14px; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: repeat-x; +} +.theme-retro.dark .prose-retro hr { + background-image: url("data:image/svg+xml;utf8,"); +} + +.theme-retro .prose-retro img { + border: 3px solid #0d0d12; + box-shadow: 6px 6px 0 0 #00e5ff; + margin: 1.5em 0; +} From dad5cb5bed4c7f8699d894fda64d9d9382233ea6 Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 04:12:26 -0400 Subject: [PATCH 03/10] Add retro theme view variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-theme view overrides under `app/views/themes/retro/` covering every template the default theme ships: * `layouts/application.html.erb` — sets `theme-retro` on ``, preloads Google Fonts for `Press Start 2P` / `VT323` / `Space Grotesk`, adds a pixelated 4-square SVG favicon and pink theme-color, and loads the extra theme stylesheets via `theme_stylesheets`. * `shared/_navigation.html.erb` — color-block logo, blinking-cursor site title, retro tagline, and Memphis-styled nav buttons (Home, About, Presentations, Projects, Links, Papers) with hard shadows and a wiggle animation on the active page. * `shared/_footer.html.erb` — caution-tape marquee + terminal command-line copyright bar with blinking cursor. * `shared/_admin_navigation.html.erb` — BBS-sysop command line: yellow `SYSOP>` prompt, `ONLINE▮` indicator, and the same admin actions (New Post / New Page / New Link / Feeds / Read / Sign out) as the default bar (selectors `.fixed.top-0`, "New Post", "Sign out" preserved so system tests pass under either theme). * `shared/_tags.html.erb` — color-cycling tag pills picked from the Memphis palette based on the tag name hash. * `blog/index.html.erb`, `blog/show.html.erb`, `blog/index_by_tag.html.erb` — Memphis cards with color-cycled shadows, rotated date "stickers", glitch-on-hover titles, and a retro `READ MORE` button. Post body uses the `.prose-retro` wrapper from `themes/retro.css`. * `pages/show.html.erb` — page-as-Memphis-card with a mint rotated slug tag. * `links/index.html.erb`, `links/_link.html.erb` — banner header plus per-link Memphis cards. * `papers/index.html.erb`, `papers/_paper.html.erb` — retro variants of the new Papers feature, with PDF preview thumbs framed in hard shadows. All variants preserve the user-facing copy and DOM selectors checked by the existing Playwright system tests, so enabling the theme does not regress `test:system`. Co-authored-by: Cursor --- app/views/themes/retro/blog/index.html.erb | 54 +++++++++++++++ .../themes/retro/blog/index_by_tag.html.erb | 66 +++++++++++++++++++ app/views/themes/retro/blog/show.html.erb | 40 +++++++++++ .../themes/retro/layouts/application.html.erb | 49 ++++++++++++++ app/views/themes/retro/links/_link.html.erb | 37 +++++++++++ app/views/themes/retro/links/index.html.erb | 28 ++++++++ app/views/themes/retro/pages/show.html.erb | 24 +++++++ app/views/themes/retro/papers/_paper.html.erb | 60 +++++++++++++++++ app/views/themes/retro/papers/index.html.erb | 37 +++++++++++ .../retro/shared/_admin_navigation.html.erb | 24 +++++++ .../themes/retro/shared/_footer.html.erb | 22 +++++++ .../themes/retro/shared/_navigation.html.erb | 60 +++++++++++++++++ app/views/themes/retro/shared/_tags.html.erb | 15 +++++ 13 files changed, 516 insertions(+) create mode 100644 app/views/themes/retro/blog/index.html.erb create mode 100644 app/views/themes/retro/blog/index_by_tag.html.erb create mode 100644 app/views/themes/retro/blog/show.html.erb create mode 100644 app/views/themes/retro/layouts/application.html.erb create mode 100644 app/views/themes/retro/links/_link.html.erb create mode 100644 app/views/themes/retro/links/index.html.erb create mode 100644 app/views/themes/retro/pages/show.html.erb create mode 100644 app/views/themes/retro/papers/_paper.html.erb create mode 100644 app/views/themes/retro/papers/index.html.erb create mode 100644 app/views/themes/retro/shared/_admin_navigation.html.erb create mode 100644 app/views/themes/retro/shared/_footer.html.erb create mode 100644 app/views/themes/retro/shared/_navigation.html.erb create mode 100644 app/views/themes/retro/shared/_tags.html.erb diff --git a/app/views/themes/retro/blog/index.html.erb b/app/views/themes/retro/blog/index.html.erb new file mode 100644 index 0000000..3ada400 --- /dev/null +++ b/app/views/themes/retro/blog/index.html.erb @@ -0,0 +1,54 @@ +<% + card_palettes = [ + { shadow: 'shadow-retro-pink', accent: 'bg-memphis-pink', text: 'text-memphis-paper' }, + { shadow: 'shadow-retro-cyan', accent: 'bg-memphis-cyan', text: 'text-memphis-ink' }, + { shadow: 'shadow-retro-yellow', accent: 'bg-memphis-yellow', text: 'text-memphis-ink' }, + { shadow: 'shadow-retro-mint', accent: 'bg-memphis-mint', text: 'text-memphis-ink' }, + { shadow: 'shadow-retro-purple', accent: 'bg-memphis-purple', text: 'text-memphis-paper' }, + { shadow: 'shadow-retro-coral', accent: 'bg-memphis-coral', text: 'text-memphis-paper' } + ] +%> + +
    + <% @posts.each_with_index do |post, idx| %> + <% palette = card_palettes[(post.id || idx) % card_palettes.length] %> +
    + +
    + <%= post.created_at.strftime('%b %Y') %> +
    + +

    + <%= link_to post.title, dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "hover:text-memphis-pink dark:hover:text-memphis-cyan transition-colors glitch-hover" %> +

    + + + +
    + <%= post.rendered_excerpt.html_safe %> +
    + +
    + <%= render "shared/tags", post: post %> +
    + +
    + <% if authenticated? %> + <%= link_to "Edit", edit_post_path(post), class: "font-mono text-base text-memphis-purple dark:text-memphis-cyan hover:underline" %> + | + <% end %> + <%= link_to dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "btn-retro btn-retro-cyan" do %> + ▶ Read more + <% end %> +
    +
    + <% end %> + +
    + <%= paginate @posts %> +
    +
    diff --git a/app/views/themes/retro/blog/index_by_tag.html.erb b/app/views/themes/retro/blog/index_by_tag.html.erb new file mode 100644 index 0000000..d0a8ee2 --- /dev/null +++ b/app/views/themes/retro/blog/index_by_tag.html.erb @@ -0,0 +1,66 @@ +<% content_for :title do %> + #<%= @tag.name %> | <%= Rails.application.config.site_name %> +<% end %> + +<% content_for :rss do %> + <%= auto_discovery_link_tag(:atom, tag_feed_path(id: @tag.name)) %> +<% end %> + +<% content_for :rss_button do %> + <%= link_to tag_feed_path(id: @tag.name), class: "btn-icon-retro", aria: { label: "RSS Feed" } do %> + <%= render "shared/icons/rss" %> + <% end %> +<% end %> + +<% + card_palettes = [ + 'shadow-retro-pink', 'shadow-retro-cyan', 'shadow-retro-yellow', + 'shadow-retro-mint', 'shadow-retro-purple', 'shadow-retro-coral' + ] +%> + +
    + +
    +

    // filter

    +

    + #<%= @tag.name %> +

    +

    > <%= pluralize(@posts.total_count, 'transmission') %> matched the query_

    +
    + + <% @posts.each_with_index do |post, idx| %> + <% shadow = card_palettes[(post.id || idx) % card_palettes.length] %> +
    +

    + <%= link_to post.title, dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "hover:text-memphis-pink dark:hover:text-memphis-cyan transition-colors" %> +

    + + + +
    + <%= post.rendered_excerpt.html_safe %> +
    + +
    + <%= render "shared/tags", post: post %> +
    + +
    + <% if authenticated? %> + <%= link_to "Edit", edit_post_path(post), class: "font-mono text-base text-memphis-purple dark:text-memphis-cyan hover:underline" %> + | + <% end %> + <%= link_to dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "btn-retro btn-retro-cyan" do %> + ▶ Read more + <% end %> +
    +
    + <% end %> + +
    <%= paginate @posts %>
    +
    diff --git a/app/views/themes/retro/blog/show.html.erb b/app/views/themes/retro/blog/show.html.erb new file mode 100644 index 0000000..59f001f --- /dev/null +++ b/app/views/themes/retro/blog/show.html.erb @@ -0,0 +1,40 @@ +<% content_for :title do %> +<%= @post.title %> | <%= Rails.application.config.site_name %> +<% end %> + +<%= link_to posts_path, class: "inline-flex items-center font-mono text-lg text-memphis-purple dark:text-memphis-cyan hover:text-memphis-pink dark:hover:text-memphis-mint mb-6" do %> + Back to posts +<% end %> + +
    + +
    +
    + Post · <%= @post.created_at.strftime('%b %d, %Y') %> +
    + +

    + <%= @post.title %> +

    + +
    + + <% if authenticated? %> + + <%= link_to "Edit ▸", edit_post_path(@post), + class: "text-memphis-pink dark:text-memphis-mint hover:underline" %> + <% end %> +
    +
    + +
    + <%= @post.rendered_body.html_safe %> +
    + +
    +

    // tagged_with

    +
    + <%= render "shared/tags", post: @post %> +
    +
    +
    diff --git a/app/views/themes/retro/layouts/application.html.erb b/app/views/themes/retro/layouts/application.html.erb new file mode 100644 index 0000000..14548bb --- /dev/null +++ b/app/views/themes/retro/layouts/application.html.erb @@ -0,0 +1,49 @@ + + + + <%= content_for(:title) || Rails.application.config.site_name %> + + + + + + <%# Pixelated favicon — 4 nested squares in the Memphis palette. %> + + + <%# Retro display fonts — only loaded when the retro theme is active. %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= content_for(:rss) || auto_discovery_link_tag(:atom, blog_feed_path) %> + + <%= stylesheet_link_tag "highlight" %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%# Extra stylesheets contributed by the active theme. %> + <% theme_stylesheets.each do |sheet| %> + <%= stylesheet_link_tag sheet, "data-turbo-track": "reload" %> + <% end %> + <%= javascript_importmap_tags %> + + + <%= render "shared/admin_navigation" %> +
    + <%= render "shared/navigation" %> +
    + <%= yield %> +
    + +
    + <%= render "shared/footer" %> + + diff --git a/app/views/themes/retro/links/_link.html.erb b/app/views/themes/retro/links/_link.html.erb new file mode 100644 index 0000000..4b49fad --- /dev/null +++ b/app/views/themes/retro/links/_link.html.erb @@ -0,0 +1,37 @@ +<% + shadows = %w[shadow-retro-pink shadow-retro-cyan shadow-retro-yellow shadow-retro-mint shadow-retro-purple shadow-retro-coral] + shadow = shadows[(link.id || 0) % shadows.length] +%> +<%= link_to link.url, + rel: "nofollow", + class: "block bg-memphis-paper dark:bg-memphis-crt border-[3px] border-memphis-ink dark:border-memphis-mint #{shadow} p-5 transition-transform duration-200 hover:-translate-x-1 hover:-translate-y-1 group" do %> +
    +
    +

    + <%= link.title %> + <%= render "shared/icons/external_link", class: "w-4 h-4 text-memphis-purple dark:text-memphis-cyan" %> +

    + +
    + <% if link.description.present? %> +

    + <%= link.description %> +

    + <% end %> +
    +<% end %> +<% if authenticated? %> +
    + <%= link_to edit_link_path(link), class: "btn-icon-retro" do %> + <%= render "shared/icons/pencil", class: "w-4 h-4" %> + <% end %> + <%= button_to link_path(link), + method: :delete, + class: "btn-icon-retro", + form: { data: { turbo_confirm: "Are you sure?" } } do %> + <%= render "shared/icons/trash", class: "w-4 h-4" %> + <% end %> +
    +<% end %> diff --git a/app/views/themes/retro/links/index.html.erb b/app/views/themes/retro/links/index.html.erb new file mode 100644 index 0000000..236e2af --- /dev/null +++ b/app/views/themes/retro/links/index.html.erb @@ -0,0 +1,28 @@ +<% content_for :rss do %> + <%= auto_discovery_link_tag(:atom, links_feed_path) %> +<% end %> + +<% content_for :rss_button do %> + <%= link_to links_feed_path, class: "btn-icon-retro", aria: { label: "RSS Feed" } do %> + <%= render "shared/icons/rss" %> + <% end %> +<% end %> + +<% content_for :title do %> +Links | <%= Rails.application.config.site_name %> +<% end %> + +
    +
    +

    // bookmarks

    +

    + Links +

    +

    > stuff worth your eyeballs_

    +
    + +
    + <%= render @links %> +
    +
    <%= paginate @links %>
    +
    diff --git a/app/views/themes/retro/pages/show.html.erb b/app/views/themes/retro/pages/show.html.erb new file mode 100644 index 0000000..bdd8df0 --- /dev/null +++ b/app/views/themes/retro/pages/show.html.erb @@ -0,0 +1,24 @@ +<% content_for :title do %> +<%= @page.title %> | <%= Rails.application.config.site_name %> +<% end %> + +
    +
    +
    + Page · <%= @page.slug %> +
    +

    + <%= @page.title %> +

    + <% if authenticated? %> +
    + <%= link_to "Edit ▸", edit_page_path(@page), + class: "text-memphis-purple dark:text-memphis-cyan hover:underline" %> +
    + <% end %> +
    + +
    + <%= @page.rendered_body.html_safe %> +
    +
    diff --git a/app/views/themes/retro/papers/_paper.html.erb b/app/views/themes/retro/papers/_paper.html.erb new file mode 100644 index 0000000..03ffd01 --- /dev/null +++ b/app/views/themes/retro/papers/_paper.html.erb @@ -0,0 +1,60 @@ +<% + shadows = %w[shadow-retro-pink shadow-retro-cyan shadow-retro-yellow shadow-retro-mint shadow-retro-purple shadow-retro-coral] + shadow = shadows[(paper.id || 0) % shadows.length] +%> +
    +
    + PDF +
    +
    + <% if paper.pdf.attached? && paper.pdf.previewable? %> +
    + <% if paper.arxiv? %> + <%= link_to paper.arxiv_pdf_url, target: "_blank", rel: "noopener", data: { turbo: false } do %> + <%= image_tag paper.pdf.preview(resize_to_fit: [120, 120]), class: "w-28 h-28 object-cover border-[3px] border-memphis-ink shadow-retro-sm" %> + <% end %> + <% else %> + <%= link_to paper_view_path(paper), target: (paper_view_path(paper) == paper.url ? "_blank" : nil), data: { turbo: false } do %> + <%= image_tag paper.pdf.preview(resize_to_fit: [120, 120]), class: "w-28 h-28 object-cover border-[3px] border-memphis-ink shadow-retro-sm" %> + <% end %> + <% end %> +
    + <% end %> + +
    +

    + <% if paper.arxiv? %> + <%= link_to paper.arxiv_pdf_url, target: "_blank", rel: "noopener", class: "hover:text-memphis-pink dark:hover:text-memphis-yellow transition-colors flex items-center gap-2", data: { turbo: false } do %> + <%= paper.title %> + <%= render "shared/icons/external_link", class: "w-4 h-4 text-memphis-purple dark:text-memphis-cyan" %> + <% end %> + <% else %> + <%= link_to paper.title, paper_view_path(paper), target: (paper_view_path(paper) == paper.url ? "_blank" : nil), class: "hover:text-memphis-pink dark:hover:text-memphis-yellow transition-colors flex items-center gap-2", data: { turbo: false } %> + <% end %> +

    + + <% if paper.description.present? %> +

    + <%= paper.description %> +

    + <% end %> + +
    + >> added <%= paper.created_at.strftime("%Y-%m-%d") %> +
    +
    +
    +
    +<% if authenticated? %> +
    + <%= link_to edit_paper_path(paper), class: "btn-icon-retro" do %> + <%= render "shared/icons/pencil", class: "w-4 h-4" %> + <% end %> + <%= button_to paper_path(paper), + method: :delete, + class: "btn-icon-retro", + form: { data: { turbo_confirm: "Are you sure?" } } do %> + <%= render "shared/icons/trash", class: "w-4 h-4" %> + <% end %> +
    +<% end %> diff --git a/app/views/themes/retro/papers/index.html.erb b/app/views/themes/retro/papers/index.html.erb new file mode 100644 index 0000000..9a975c8 --- /dev/null +++ b/app/views/themes/retro/papers/index.html.erb @@ -0,0 +1,37 @@ +<% content_for :title do %> +Papers | <%= Rails.application.config.site_name %> +<% end %> + +
    +
    +
    +
    +

    // reading_list

    +

    + Papers +

    +

    > arxiv printouts & pdf rabbit holes_

    +
    + <% if @papers.any? && authenticated? %> + <%= link_to "+ Add paper", new_link_path, class: "btn-retro btn-retro-pink" %> + <% end %> +
    +
    + + <% if @papers.any? %> +
    + <%= render @papers %> +
    + +
    + <%= paginate @papers %> +
    + <% else %> +
    +

    > no papers in this archive yet_

    + <% if authenticated? %> + <%= link_to "+ Add your first paper", new_link_path, class: "btn-retro btn-retro-pink" %> + <% end %> +
    + <% end %> +
    diff --git a/app/views/themes/retro/shared/_admin_navigation.html.erb b/app/views/themes/retro/shared/_admin_navigation.html.erb new file mode 100644 index 0000000..988974d --- /dev/null +++ b/app/views/themes/retro/shared/_admin_navigation.html.erb @@ -0,0 +1,24 @@ +<% if authenticated? %> +
    +
    +
    +
    + SYSOP> + <%= link_to "New Post", new_post_path, class: "hover:text-memphis-pink transition-colors" %> + <%= link_to "New Page", new_page_path, class: "hover:text-memphis-pink transition-colors" %> + <%= link_to "New Link", new_link_path, class: "hover:text-memphis-pink transition-colors" %> + <%= link_to "Feeds", feeds_path, class: "hover:text-memphis-cyan transition-colors" %> + <%= link_to "Read", feed_posts_path, class: "hover:text-memphis-cyan transition-colors" %> +
    +
    + + <%= button_to session_path, + method: :delete, + class: "hover:text-memphis-pink transition-colors" do %> + Sign out + <% end %> +
    +
    +
    +
    +<% end %> diff --git a/app/views/themes/retro/shared/_footer.html.erb b/app/views/themes/retro/shared/_footer.html.erb new file mode 100644 index 0000000..8e62c4e --- /dev/null +++ b/app/views/themes/retro/shared/_footer.html.erb @@ -0,0 +1,22 @@ +
    +
    +
    + ★ PRESS START ★ <%= Rails.application.config.site_name.upcase %> ★ INSERT COIN TO CONTINUE ★ 100% HAND-TYPED HTML ★ NO TRACKERS ★ READY P1_    + ★ PRESS START ★ <%= Rails.application.config.site_name.upcase %> ★ INSERT COIN TO CONTINUE ★ 100% HAND-TYPED HTML ★ NO TRACKERS ★ READY P1_    +
    +
    +
    +
    +

    + $ + echo "© <%= Time.current.year %> <%= Rails.application.config.site_name %>" + +

    +

    + crafted on + abbey + · powered by ⚡ caffeine +

    +
    +
    +
    diff --git a/app/views/themes/retro/shared/_navigation.html.erb b/app/views/themes/retro/shared/_navigation.html.erb new file mode 100644 index 0000000..5878998 --- /dev/null +++ b/app/views/themes/retro/shared/_navigation.html.erb @@ -0,0 +1,60 @@ +
    +
    + +
    + <%= link_to root_path, class: "group inline-block" do %> +
    + + + +
    +

    + <%= Rails.application.config.site_name %> +

    +

    + > field notes, code & experiments_ +

    + <% end %> +
    + +
    + + +
    + <% if content_for? :rss_button %> + <%= yield :rss_button %> + <% else %> + <%= link_to blog_feed_path, class: "btn-icon-retro", aria: { label: "RSS Feed" } do %> + <%= render "shared/icons/rss" %> + <% end %> + <% end %> + +
    +
    +
    +
    diff --git a/app/views/themes/retro/shared/_tags.html.erb b/app/views/themes/retro/shared/_tags.html.erb new file mode 100644 index 0000000..210c565 --- /dev/null +++ b/app/views/themes/retro/shared/_tags.html.erb @@ -0,0 +1,15 @@ +<% + tag_palette = %w[ + tag-color-1 + tag-color-2 + tag-color-3 + tag-color-4 + tag-color-5 + tag-color-6 + ] +%> +<% post.tags.each do |tag| %> + <% color = tag_palette[tag.name.sum % tag_palette.length] %> + <%= link_to "##{tag.name}", tag_path(id: tag.name), + class: "tag-retro #{color}" %> +<% end %> From b988ac9a3a0b1c564f8b5a83097ff7c00542d207 Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 04:12:34 -0400 Subject: [PATCH 04/10] Test theme switching does not regress default behavior `test/integration/themes_test.rb` covers the four guarantees the theme system makes: * default theme does NOT emit `theme-retro` or load any `themes/...` stylesheets; * retro theme DOES set `theme-retro` on ``, loads `themes/retro` + `themes/retro-highlight`, and preserves the DOM contract checked by system tests (header h1, footer, all six nav links); * `Post.markdown_renderer` follows `Rails.application.config.theme` (MarkdownRender for default, MinimalMarkdownRender for retro); * `theme_stylesheets` helper is empty for the default theme and populated for retro. Existing tests still pass under both themes: bin/rails test # 21 tests, 85 asserts bin/rails test test/system/{navigation,blog_public,pages,links}_test.rb ABBEY_THEME=retro bin/rails test test/system/... # same suite, all green Co-authored-by: Cursor --- test/integration/themes_test.rb | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test/integration/themes_test.rb diff --git a/test/integration/themes_test.rb b/test/integration/themes_test.rb new file mode 100644 index 0000000..02218ea --- /dev/null +++ b/test/integration/themes_test.rb @@ -0,0 +1,72 @@ +require "test_helper" + +class ThemesTest < ActionDispatch::IntegrationTest + # The theme is read from Rails.application.config.theme at boot time, so we + # exercise theme switching by toggling it in place around each request. + setup do + @original_theme = Rails.application.config.theme + end + + teardown do + Rails.application.config.theme = @original_theme + end + + test "default theme renders the original layout" do + Rails.application.config.theme = "default" + + get root_path + assert_response :success + assert_no_match(/class="theme-retro/, response.body) + assert_no_match(%r{themes/retro}, response.body, "default theme should not load theme stylesheets") + assert_select "header h1" + assert_select "footer" + end + + test "retro theme prepends its view path and loads theme stylesheets" do + Rails.application.config.theme = "retro" + + get root_path + assert_response :success + assert_match(/class="theme-retro/, response.body) + assert_match(%r{themes/retro}, response.body, "retro theme should load themes/retro CSS") + # System test contract: header still has h1, footer still present. + assert_select "header h1" + assert_select "footer" + # Required nav links remain after retheming. + %w[Home About Projects Presentations Links Papers].each do |label| + assert_match(/>#{label} Date: Sat, 23 May 2026 04:12:40 -0400 Subject: [PATCH 05/10] Document opt-in theme system in README Adds a Themes section to the README explaining: * how to switch themes via ABBEY_THEME or config/initializers/themes.rb, * what the built-in `default` and `retro` themes are, * how the theme system overrides views, ships extra stylesheets, and selects a markdown renderer, * how to author a new theme (drop a CSS file under `app/assets/stylesheets/themes/.css` and set ABBEY_THEME). Also adds a one-line feature entry to the top-level features list. Co-authored-by: Cursor --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index e5ef215..e5c85e4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Minimal blog using Rails 8, designed to be easily [self-hosted on AWS](https://g * Markdown and Code Highlighting * [Link Blog](https://capotej.com/links) * Drag and Drop image uploads for Pages and Posts +* Themable (default minimal look + optional `retro` Memphis/8-bit theme — see [Themes](#themes)) # Getting Started @@ -56,6 +57,75 @@ This will scan the given path for files ending in `.markdown` and create a seed **Note: This will delete everything in the local database and re-seed using `db/seeds/*`.** +# Themes + +Abbey ships with an opt-in theme system. The default theme keeps the existing +minimal look. Built-in alternatives currently include: + +| Theme | Description | +|-----------|-------------| +| `default` | Original minimal Abbey look (no behavioural change). | +| `retro` | Memphis-style / 8-bit / 80s computer chrome — neo-brutalist cards, CRT scanlines, terminal code blocks, pixel-display headings. | + +## Switching themes + +Set the `ABBEY_THEME` environment variable before booting the app: + + $ ABBEY_THEME=retro bin/dev + +Or hardcode it in `config/initializers/themes.rb`: + +```ruby +Rails.application.config.theme = "retro" +``` + +When the active theme is `default`, the app behaves identically to before — +no extra assets are loaded, the default Tailwind build is unchanged, and the +existing markdown renderer is used. + +## How themes work + +A theme can override any view by placing a same-named file under +`app/views/themes//`. Rails' view lookup is augmented at request time +(see `app/controllers/concerns/theming.rb`) so that: + + app/views/themes/retro/blog/index.html.erb + +wins over the default + + app/views/blog/index.html.erb + +whenever `Rails.application.config.theme == "retro"`. The same is true for +the `layouts/application.html.erb`, every partial under `shared/`, and the +page/links/papers views. + +A theme can also ship its own stylesheets under +`app/assets/stylesheets/themes/.css` and, optionally, +`themes/-highlight.css`. These are loaded automatically by the +default layout via the `theme_stylesheets` helper (in +`app/helpers/application_helper.rb`) when the theme is active. + +Finally, a theme can request the simpler `MinimalMarkdownRender` (semantic +HTML output, no inline Tailwind utility classes) by adding itself to +`Rails.application.config.themes_using_minimal_renderer` in +`config/initializers/themes.rb`. This lets the theme style markdown content +entirely from a wrapper class (e.g. `.prose-retro`) rather than fighting +the renderer's hard-coded utility classes. + +## Authoring a new theme + +The simplest theme is just a CSS file: + + # add a stylesheet under + app/assets/stylesheets/themes/sunset.css + + # set the theme: + ABBEY_THEME=sunset bin/dev + +The theme will be loaded after the default stylesheet, so it can override +anything cosmetic without touching the rest of the app. Add views under +`app/views/themes/sunset/` only when you need to change markup. + # Deploying to AWS ## Assumptions From 4ebf44a6767e0b77fc67182590a26a03fd2c4883 Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 04:59:40 -0400 Subject: [PATCH 06/10] Register grimoire as a second opt-in theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the theming system introduced for the retro theme with a second built-in named theme: "grimoire" — retro hacker dark fantasy. Like retro, grimoire opts into MinimalMarkdownRender so it can style markdown content entirely from a wrapper class scope. No behavioural change for ABBEY_THEME=default or ABBEY_THEME=retro. README updated to document grimoire alongside retro in the themes table. Co-authored-by: Cursor --- README.md | 5 +++-- config/initializers/themes.rb | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e5c85e4..54a249e 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,9 @@ minimal look. Built-in alternatives currently include: | Theme | Description | |-----------|-------------| -| `default` | Original minimal Abbey look (no behavioural change). | -| `retro` | Memphis-style / 8-bit / 80s computer chrome — neo-brutalist cards, CRT scanlines, terminal code blocks, pixel-display headings. | +| `default` | Original minimal Abbey look (no behavioural change). | +| `retro` | Memphis-style / 8-bit / 80s computer chrome — neo-brutalist cards, CRT scanlines, terminal code blocks, pixel-display headings. | +| `grimoire` | Retro hacker dark fantasy — Matrix-minimal monospace, parchment + void palette with phosphor/ember/gold accents, tome cards, wax-seal tags, an animated summoning circle, and a Konami-code easter egg. | ## Switching themes diff --git a/config/initializers/themes.rb b/config/initializers/themes.rb index dea5b2a..63c679d 100644 --- a/config/initializers/themes.rb +++ b/config/initializers/themes.rb @@ -10,7 +10,11 @@ # default Tailwind build. # # Set the active theme with the ABBEY_THEME env var, or override here. -# Built-in themes: "default" (the original minimal look), "retro". +# Built-in themes: +# "default" — the original minimal abbey look +# "retro" — Memphis / 8-bit / CRT (loud, colorful, hand-typed HTML vibes) +# "grimoire" — retro hacker dark fantasy (Matrix-minimal monospace, void + +# phosphor + ember, illuminated drop caps, summoning circle) Rails.application.config.theme = ENV.fetch("ABBEY_THEME", "default") @@ -18,4 +22,4 @@ # HTML, no inline Tailwind classes) so they can style content entirely from # a wrapper class scope. Default theme keeps the original utility-class # renderer to preserve current behavior. -Rails.application.config.themes_using_minimal_renderer = %w[retro] +Rails.application.config.themes_using_minimal_renderer = %w[retro grimoire] From 706d1997984f18866337d029923c3a0feef41237 Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 04:59:53 -0400 Subject: [PATCH 07/10] Add grimoire theme stylesheets (retro hacker dark fantasy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure-CSS theme files served by Propshaft and loaded only when Rails.application.config.theme == "grimoire" (via the existing theme_stylesheets helper). All selectors are scoped under `.theme-grimoire` so loading these files in any other context is a no-op. Visual language: - Light mode: aged parchment (#ebd9b3) with subtle noise grain + corner foxing; blood-red (#8b1d27) ink accents; gold (#c8a44d) hairlines - Dark mode: starless void (#07080d) with drifting ember/arcane dust; golden text; phosphor green (#57f287) for code/cursor accents; subtle CRT scanlines - Matrix-minimal typography: JetBrains Mono 800 uppercase for display headings, Inter for body, IBM Plex Mono for inline code/meta - Components: tome cards (double gold/ink border + corner sigils), spell-btn (raised paper/metal action button), wax-seal tag pills (deterministic blood/arcane/ember/phosphor/gold/ichor color cycle), icon-rune (square icon button), summoning circle, marquee, rune-divider - Markdown prose (.prose-grimoire): chunky monospace drop cap with thin underline (phosphor green in dark mode), terminal-style code blocks with "~/grimoire/spells $ cast" header, // EOF // hr, wax-seal tag pills, scoped table styling - Konami code easter egg overlay (.incant-overlay/.incant-title) - Custom scrollbar (dark mode only) The companion grimoire-highlight.css is a Rouge theme: phosphor green base, gold sigils for keywords, blood-red strings, ember comments — all scoped under `.theme-grimoire` so it can't clobber the default highlight theme. Co-authored-by: Cursor --- .../stylesheets/themes/grimoire-highlight.css | 114 +++ app/assets/stylesheets/themes/grimoire.css | 819 ++++++++++++++++++ 2 files changed, 933 insertions(+) create mode 100644 app/assets/stylesheets/themes/grimoire-highlight.css create mode 100644 app/assets/stylesheets/themes/grimoire.css diff --git a/app/assets/stylesheets/themes/grimoire-highlight.css b/app/assets/stylesheets/themes/grimoire-highlight.css new file mode 100644 index 0000000..4f07a61 --- /dev/null +++ b/app/assets/stylesheets/themes/grimoire-highlight.css @@ -0,0 +1,114 @@ +/* ============================================================================= + Abbey – Grimoire Theme Syntax Highlighting + ------------------------------------------------------------------ + The wizard's terminal: phosphor green base, gold sigils for keywords, + blood-red for strings (literal incantations), ember for comments + (footnotes from the necromancer). + + All Rouge classes are scoped under `.theme-grimoire` so this file is a + no-op when another theme (or the default theme) is active. + =============================================================================*/ + +.theme-grimoire pre.highlight, +.theme-grimoire .highlight pre { + background: #07080d !important; + color: #57f287; +} + +/* Comments — the necromancer's marginalia */ +.theme-grimoire .highlight .c, +.theme-grimoire .highlight .ch, +.theme-grimoire .highlight .cd, +.theme-grimoire .highlight .cm, +.theme-grimoire .highlight .cpf, +.theme-grimoire .highlight .c1, +.theme-grimoire .highlight .cs { color: #f08029; font-style: italic; opacity: 0.85; } +.theme-grimoire .highlight .cp { color: #f08029; font-weight: 700; } + +/* Errors — broken sigils */ +.theme-grimoire .highlight .err { color: #ff5a5f; background: rgba(255,90,95,0.12); } + +/* Keywords — true names */ +.theme-grimoire .highlight .k, +.theme-grimoire .highlight .kc, +.theme-grimoire .highlight .kd, +.theme-grimoire .highlight .kn, +.theme-grimoire .highlight .kp, +.theme-grimoire .highlight .kr, +.theme-grimoire .highlight .kv { color: #c8a44d; font-weight: 700; } +.theme-grimoire .highlight .kt { color: #c8a44d; } + +/* Operators / punctuation */ +.theme-grimoire .highlight .o, +.theme-grimoire .highlight .ow { color: #c8a44d; } +.theme-grimoire .highlight .p { color: #e9e0c8; } + +/* Names */ +.theme-grimoire .highlight .n { color: #e9e0c8; } +.theme-grimoire .highlight .na { color: #c8a44d; } +.theme-grimoire .highlight .nb { color: #c8a44d; } +.theme-grimoire .highlight .nc { color: #f08029; font-weight: 700; } +.theme-grimoire .highlight .no { color: #c8a44d; } +.theme-grimoire .highlight .nd { color: #f08029; } +.theme-grimoire .highlight .ne { color: #ff5a5f; font-weight: 700; } +.theme-grimoire .highlight .nf { color: #57f287; font-weight: 700; } +.theme-grimoire .highlight .nl { color: #c8a44d; } +.theme-grimoire .highlight .nn { color: #e9e0c8; } +.theme-grimoire .highlight .py { color: #c8a44d; } +.theme-grimoire .highlight .nt { color: #f08029; font-weight: 700; } +.theme-grimoire .highlight .nv, +.theme-grimoire .highlight .vc, +.theme-grimoire .highlight .vg, +.theme-grimoire .highlight .vi { color: #c8a44d; } + +/* Literals */ +.theme-grimoire .highlight .l { color: #57f287; } +.theme-grimoire .highlight .ld { color: #57f287; } + +/* Strings — literal incantations */ +.theme-grimoire .highlight .s, +.theme-grimoire .highlight .sb, +.theme-grimoire .highlight .sc, +.theme-grimoire .highlight .dl, +.theme-grimoire .highlight .sd, +.theme-grimoire .highlight .s2, +.theme-grimoire .highlight .se, +.theme-grimoire .highlight .sh, +.theme-grimoire .highlight .si, +.theme-grimoire .highlight .sx, +.theme-grimoire .highlight .sr, +.theme-grimoire .highlight .s1, +.theme-grimoire .highlight .ss { color: #ff8d97; } + +/* Numbers */ +.theme-grimoire .highlight .m, +.theme-grimoire .highlight .mb, +.theme-grimoire .highlight .mf, +.theme-grimoire .highlight .mh, +.theme-grimoire .highlight .mi, +.theme-grimoire .highlight .il, +.theme-grimoire .highlight .mo, +.theme-grimoire .highlight .mx { color: #f08029; } + +/* Diff */ +.theme-grimoire .highlight .gd { color: #ff5a5f; background: rgba(255,90,95,0.10); } +.theme-grimoire .highlight .gi { color: #57f287; background: rgba(87,242,135,0.10); } + +/* Generic */ +.theme-grimoire .highlight .ge { font-style: italic; } +.theme-grimoire .highlight .gh { color: #c8a44d; font-weight: 700; } +.theme-grimoire .highlight .gs { font-weight: 700; } +.theme-grimoire .highlight .gu { color: #f08029; font-weight: 700; } +.theme-grimoire .highlight .gp { color: #c8a44d; } +.theme-grimoire .highlight .gt { color: #ff5a5f; } +.theme-grimoire .highlight .gl { color: #e9e0c8; } + +/* Line numbers */ +.theme-grimoire .highlight .lineno, +.theme-grimoire .highlight .gh.filename { + color: #6a4cab; + border-right: 1px solid rgba(200,164,77,0.25); + padding-right: .55em; + margin-right: .55em; + user-select: none; +} diff --git a/app/assets/stylesheets/themes/grimoire.css b/app/assets/stylesheets/themes/grimoire.css new file mode 100644 index 0000000..c029e11 --- /dev/null +++ b/app/assets/stylesheets/themes/grimoire.css @@ -0,0 +1,819 @@ +/* ============================================================================= + Abbey – Grimoire Theme + Retro hacker dark fantasy. Phrack zine + illuminated manuscript + terminal + man page. Matrix-minimal: monospace display, modern sans body, no ornate + blackletter — but the dark mode still feels like a cryptarchive. + + Pure CSS – served by Propshaft, loaded only when + `Rails.application.config.theme == "grimoire"`. All declarations are scoped + under `.theme-grimoire` (set on by themes/grimoire/layouts/ + application.html.erb) so loading this file in another context is a no-op. + + Custom palette: + parchment #ebd9b3 vellum #f1e3c2 ink #1a1410 + shadow #3a2f25 void #07080d obsidian #0e0f17 + tomb #161826 blood #8b1d27 ember #f08029 + bone #e9e0c8 phosphor #57f287 arcane #6a4cab + gold #c8a44d ichor #2d1b3a + =============================================================================*/ + +/* ---------------------------------------------------------------------- + Base – html background, body backdrop, scanlines, selection, scrollbar +---------------------------------------------------------------------- */ + +html.theme-grimoire { + background-color: #ebd9b3; + color: #1a1410; + font-family: "Inter", system-ui, -apple-system, sans-serif; + font-feature-settings: "ss01" on, "cv11" on, "kern", "liga"; +} +html.theme-grimoire.dark { + background-color: #07080d; + color: #e9e0c8; +} + +html.theme-grimoire body { + position: relative; + overflow-x: hidden; + min-height: 100vh; +} + +/* Aged parchment (light) — fibre grain + corner foxing */ +html.theme-grimoire body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + opacity: 1; +} +html.theme-grimoire:not(.dark) body::before { + background: + radial-gradient(circle at 0% 0%, rgba(120,80,30,0.18), transparent 24%), + radial-gradient(circle at 100% 0%, rgba(120,80,30,0.18), transparent 24%), + radial-gradient(circle at 0% 100%, rgba(120,80,30,0.18), transparent 24%), + radial-gradient(circle at 100% 100%, rgba(120,80,30,0.18), transparent 24%), + url("data:image/svg+xml;utf8,"); + background-size: 100% 100%, 100% 100%, 100% 100%, 100% 100%, 600px 600px; + background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, repeat; +} + +/* Starless void (dark) — drifting ember/arcane dust */ +html.theme-grimoire.dark body::before { + opacity: 0.9; + background: + radial-gradient(1200px 600px at 80% -10%, rgba(106,76,171,0.18), transparent 60%), + radial-gradient(900px 500px at 10% 110%, rgba(240,128,41,0.10), transparent 65%), + radial-gradient(2px 2px at 23% 16%, rgba(200,164,77,0.7), transparent 100%), + radial-gradient(1.5px 1.5px at 67% 44%, rgba(87,242,135,0.55), transparent 100%), + radial-gradient(2px 2px at 89% 72%, rgba(200,164,77,0.55), transparent 100%), + radial-gradient(1.5px 1.5px at 12% 78%, rgba(106,76,171,0.55), transparent 100%), + radial-gradient(1.5px 1.5px at 44% 22%, rgba(200,164,77,0.4), transparent 100%), + radial-gradient(1px 1px at 56% 88%, rgba(255,255,255,0.5), transparent 100%); + background-repeat: no-repeat; + animation: grimoire-mist 22s ease-in-out infinite; +} + +/* CRT scanlines — dark-mode only, gentle, preserves readability */ +html.theme-grimoire body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 100; + pointer-events: none; + background-image: none; +} +html.theme-grimoire.dark body::after { + background-image: repeating-linear-gradient( + to bottom, + rgba(87, 242, 135, 0) 0px, + rgba(87, 242, 135, 0) 3px, + rgba(87, 242, 135, 0.018) 3px, + rgba(87, 242, 135, 0.018) 4px + ); + mix-blend-mode: screen; +} + +html.theme-grimoire main, +html.theme-grimoire header, +html.theme-grimoire footer, +html.theme-grimoire nav, +html.theme-grimoire .content-layer { + position: relative; + z-index: 1; +} + +html.theme-grimoire ::selection { background: #8b1d27; color: #ebd9b3; } +html.theme-grimoire.dark ::selection { background: #f08029; color: #07080d; } + +/* Terminal-style dark scrollbar */ +html.theme-grimoire.dark ::-webkit-scrollbar { width: 12px; } +html.theme-grimoire.dark ::-webkit-scrollbar-track { background: #07080d; } +html.theme-grimoire.dark ::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, #6a4cab, #2d1b3a); + border: 1px solid #c8a44d; +} +html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; } + +/* ---------------------------------------------------------------------- + Utility-style color/font/spacing classes used inline in grimoire views. + Scoped under `.theme-grimoire` so they cannot leak into the default + theme. Intentionally mirrors Tailwind-style class names so markup stays + expressive without requiring Tailwind config edits. +---------------------------------------------------------------------- */ + +/* Backgrounds */ +.theme-grimoire .bg-grim-parchment { background-color: #ebd9b3; } +.theme-grimoire .bg-grim-vellum { background-color: #f1e3c2; } +.theme-grimoire .bg-grim-vellum\/30{ background-color: rgba(241,227,194,0.3); } +.theme-grimoire .bg-grim-void { background-color: #07080d; } +.theme-grimoire .bg-grim-obsidian { background-color: #0e0f17; } +.theme-grimoire .bg-grim-tomb { background-color: #161826; } +.theme-grimoire .bg-grim-ichor { background-color: #2d1b3a; } + +.theme-grimoire.dark .dark\:bg-grim-void { background-color: #07080d; } +.theme-grimoire.dark .dark\:bg-grim-obsidian { background-color: #0e0f17; } +.theme-grimoire.dark .dark\:bg-grim-tomb { background-color: #161826; } +.theme-grimoire.dark .dark\:bg-grim-ichor { background-color: #2d1b3a; } +.theme-grimoire.dark .dark\:bg-grim-obsidian\/60 { background-color: rgba(14,15,23,0.6); } + +/* Text colors */ +.theme-grimoire .text-grim-ink { color: #1a1410; } +.theme-grimoire .text-grim-shadow { color: #3a2f25; } +.theme-grimoire .text-grim-shadow\/60 { color: rgba(58,47,37,0.6); } +.theme-grimoire .text-grim-shadow\/70 { color: rgba(58,47,37,0.7); } +.theme-grimoire .text-grim-blood { color: #8b1d27; } +.theme-grimoire .text-grim-bone { color: #e9e0c8; } +.theme-grimoire .text-grim-ember { color: #f08029; } +.theme-grimoire .text-grim-gold { color: #c8a44d; } +.theme-grimoire .text-grim-gold\/70 { color: rgba(200,164,77,0.7); } +.theme-grimoire .text-grim-gold\/80 { color: rgba(200,164,77,0.8); } +.theme-grimoire .text-grim-phosphor{ color: #57f287; } +.theme-grimoire .text-grim-arcane { color: #6a4cab; } + +.theme-grimoire.dark .dark\:text-grim-bone { color: #e9e0c8; } +.theme-grimoire.dark .dark\:text-grim-ember { color: #f08029; } +.theme-grimoire.dark .dark\:text-grim-gold { color: #c8a44d; } +.theme-grimoire.dark .dark\:text-grim-gold\/70 { color: rgba(200,164,77,0.7); } +.theme-grimoire.dark .dark\:text-grim-gold\/80 { color: rgba(200,164,77,0.8); } +.theme-grimoire.dark .dark\:fill-grim-tomb { fill: #161826; } + +/* Borders */ +.theme-grimoire .border-grim-shadow { border-color: #3a2f25; } +.theme-grimoire .border-grim-shadow\/30 { border-color: rgba(58,47,37,0.3); } +.theme-grimoire .border-grim-shadow\/40 { border-color: rgba(58,47,37,0.4); } +.theme-grimoire .border-grim-gold { border-color: #c8a44d; } +.theme-grimoire .border-grim-gold\/40 { border-color: rgba(200,164,77,0.4); } + +.theme-grimoire.dark .dark\:border-grim-gold { border-color: #c8a44d; } +.theme-grimoire.dark .dark\:border-grim-gold\/40 { border-color: rgba(200,164,77,0.4); } + +/* Hover variants */ +.theme-grimoire .hover\:text-grim-blood:hover { color: #8b1d27; } +.theme-grimoire .hover\:text-grim-ember:hover { color: #f08029; } +.theme-grimoire.dark .dark\:hover\:text-grim-ember:hover { color: #f08029; } + +.theme-grimoire .group:hover .group-hover\:text-grim-blood { color: #8b1d27; } +.theme-grimoire.dark .group:hover .dark\:group-hover\:text-grim-ember { color: #f08029; } +.theme-grimoire .group:hover .group-hover\:underline { text-decoration-line: underline; } +.theme-grimoire .group:hover .group-hover\:decoration-dotted { text-decoration-style: dotted; } +.theme-grimoire .group:hover .group-hover\:decoration-1 { text-decoration-thickness: 1px; } +.theme-grimoire .group:hover .group-hover\:underline-offset-\[6px\] { text-underline-offset: 6px; } +.theme-grimoire .group:hover .group-hover\:opacity-100 { opacity: 1; } + +/* Decoration */ +.theme-grimoire .decoration-grim-blood { text-decoration-color: #8b1d27; } +.theme-grimoire.dark .dark\:decoration-grim-gold { text-decoration-color: #c8a44d; } + +/* Font families */ +.theme-grimoire .font-blackletter { font-family: "JetBrains Mono", ui-monospace, monospace; } +.theme-grimoire .font-engraved { font-family: "JetBrains Mono", ui-monospace, monospace; } +.theme-grimoire .font-manuscript { font-family: "Inter", system-ui, -apple-system, sans-serif; } +.theme-grimoire .font-plex { font-family: "IBM Plex Mono", ui-monospace, monospace; } + +/* Arbitrary font sizes used by views (Tailwind `text-[Xrem]` syntax mirror) */ +.theme-grimoire .text-\[0\.62rem\] { font-size: 0.62rem; line-height: 1.2; } +.theme-grimoire .text-\[0\.66rem\] { font-size: 0.66rem; line-height: 1.2; } +.theme-grimoire .text-\[0\.7rem\] { font-size: 0.7rem; line-height: 1.25; } +.theme-grimoire .text-\[0\.72rem\] { font-size: 0.72rem; line-height: 1.3; } +.theme-grimoire .text-\[0\.74rem\] { font-size: 0.74rem; line-height: 1.3; } +.theme-grimoire .text-\[0\.78rem\] { font-size: 0.78rem; line-height: 1.35; } +.theme-grimoire .text-\[2\.2rem\] { font-size: 2.2rem; line-height: 1; } +.theme-grimoire .text-\[2\.8rem\] { font-size: 2.8rem; line-height: 1; } + +/* Arbitrary letter-spacing values */ +.theme-grimoire .tracking-\[0\.12em\] { letter-spacing: 0.12em; } +.theme-grimoire .tracking-\[0\.14em\] { letter-spacing: 0.14em; } +.theme-grimoire .tracking-\[0\.18em\] { letter-spacing: 0.18em; } +.theme-grimoire .tracking-\[0\.2em\] { letter-spacing: 0.2em; } +.theme-grimoire .tracking-\[0\.22em\] { letter-spacing: 0.22em; } +.theme-grimoire .tracking-\[0\.3em\] { letter-spacing: 0.3em; } + +/* Arbitrary leading + underline-offset */ +.theme-grimoire .leading-\[0\.95\] { line-height: 0.95; } +.theme-grimoire .underline-offset-\[6px\] { text-underline-offset: 6px; } + +/* Animations */ +.theme-grimoire .animate-blink { animation: grimoire-blink 1s steps(2, start) infinite; } + +/* ---------------------------------------------------------------------- + Components +---------------------------------------------------------------------- */ + +/* TOME — the card. A weathered codex page (light) or obsidian slate (dark) + with a thin gold/ink double border + tiny sigils at opposite corners. */ +.theme-grimoire .tome { + position: relative; + background: + linear-gradient(180deg, rgba(255,250,235,0.92), rgba(235,217,179,0.92)), + url("data:image/svg+xml;utf8,"); + color: #1a1410; + border: 1px solid #1a1410; + box-shadow: + inset 0 0 0 6px #ebd9b3, + inset 0 0 0 7px #c8a44d, + 0 18px 36px -16px rgba(20,12,6,0.55); + padding: 1.75rem; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} +.theme-grimoire .tome:hover { + transform: translateY(-2px); + box-shadow: + inset 0 0 0 6px #ebd9b3, + inset 0 0 0 7px #c8a44d, + 0 28px 48px -20px rgba(20,12,6,0.6); +} +.theme-grimoire.dark .tome { + background: + linear-gradient(180deg, #11131c, #0a0b12), + radial-gradient(120% 60% at 50% 0%, rgba(106,76,171,0.18), transparent 70%); + color: #e9e0c8; + border-color: #c8a44d; + box-shadow: + inset 0 0 0 4px #07080d, + inset 0 0 0 5px rgba(200,164,77,0.55), + inset 0 0 80px rgba(106,76,171,0.18), + 0 22px 50px -20px rgba(0,0,0,0.85); +} +.theme-grimoire.dark .tome:hover { + box-shadow: + inset 0 0 0 4px #07080d, + inset 0 0 0 5px #c8a44d, + inset 0 0 90px rgba(240,128,41,0.18), + 0 28px 60px -22px rgba(0,0,0,0.95); +} +.theme-grimoire .tome::before, +.theme-grimoire .tome::after { + content: ""; + position: absolute; + width: 28px; + height: 28px; + background-repeat: no-repeat; + background-size: contain; + opacity: 0.85; + pointer-events: none; +} +.theme-grimoire .tome::before { + top: -14px; + left: -14px; + background-image: url("data:image/svg+xml;utf8,"); +} +.theme-grimoire .tome::after { + bottom: -14px; + right: -14px; + background-image: url("data:image/svg+xml;utf8,"); +} +.theme-grimoire.dark .tome::after { + background-image: url("data:image/svg+xml;utf8,"); +} + +/* SPELL BUTTON — bookish raised metal-and-paper button */ +.theme-grimoire .spell-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-family: "IBM Plex Mono", ui-monospace, monospace; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: #1a1410; + background: linear-gradient(180deg, #f4e6c0, #d8c08a); + border: 1px solid #1a1410; + box-shadow: 0 1px 0 #c8a44d, 0 2px 0 #1a1410, 0 4px 12px -2px rgba(20,12,6,0.4); + padding: 0.55rem 1rem; + transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.25s ease; + cursor: pointer; +} +.theme-grimoire .spell-btn:hover { + transform: translateY(-1px); + background: linear-gradient(180deg, #f8edc8, #e2cd9a); + box-shadow: 0 1px 0 #c8a44d, 0 4px 0 #1a1410, 0 8px 16px -2px rgba(20,12,6,0.45); +} +.theme-grimoire .spell-btn:active { + transform: translateY(2px); + box-shadow: 0 1px 0 #c8a44d, 0 0 0 #1a1410; +} +.theme-grimoire.dark .spell-btn { + color: #ebd9b3; + background: linear-gradient(180deg, #1a1d2a, #0e0f17); + border-color: #c8a44d; + box-shadow: 0 1px 0 rgba(200,164,77,0.4), 0 2px 0 #07080d, 0 0 18px rgba(240,128,41,0.18); +} +.theme-grimoire.dark .spell-btn:hover { + background: linear-gradient(180deg, #232636, #15172a); + box-shadow: 0 1px 0 #c8a44d, 0 4px 0 #07080d, 0 0 24px rgba(240,128,41,0.45); + color: #f08029; +} +.theme-grimoire .spell-btn-blood { + background: linear-gradient(180deg, #b1262f, #7a161e); + color: #ebd9b3; + border-color: #1a1410; +} +.theme-grimoire .spell-btn-blood:hover { background: linear-gradient(180deg, #c93340, #8a1b24); } +.theme-grimoire.dark .spell-btn-blood { background: linear-gradient(180deg, #7a161e, #4b0d12); color: #ebd9b3; } + +/* ICON RUNE — square icon button (theme toggle, RSS) */ +.theme-grimoire .icon-rune { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background: linear-gradient(180deg, #f4e6c0, #d8c08a); + color: #1a1410; + border: 1px solid #1a1410; + box-shadow: 0 1px 0 #c8a44d, 0 2px 0 #1a1410; + transition: all 0.15s ease; + cursor: pointer; +} +.theme-grimoire .icon-rune:hover { color: #8b1d27; transform: translateY(-1px); } +.theme-grimoire.dark .icon-rune { + background: linear-gradient(180deg, #1a1d2a, #0e0f17); + color: #c8a44d; + border-color: #c8a44d; + box-shadow: 0 0 14px rgba(240,128,41,0.25); +} +.theme-grimoire.dark .icon-rune:hover { color: #f08029; box-shadow: 0 0 22px rgba(240,128,41,0.55); } + +/* WAX SEAL — tag pills */ +.theme-grimoire .wax-seal { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.25rem; + padding: 0.5rem 0.9rem; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 700; + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #f6ead0; + background: + radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), + #8b1d27; + border: 1px solid #1a1410; + border-radius: 999px; + box-shadow: + inset 0 -2px 4px rgba(0,0,0,0.4), + inset 0 2px 2px rgba(255,255,255,0.15), + 0 4px 8px -2px rgba(20,12,6,0.45); + transform: rotate(-4deg); + transition: transform 0.2s ease; + text-shadow: 0 1px 0 rgba(0,0,0,0.35); +} +.theme-grimoire .wax-seal:hover { transform: rotate(0deg) scale(1.05); } +.theme-grimoire.dark .wax-seal { + border-color: #c8a44d; + box-shadow: + inset 0 -2px 4px rgba(0,0,0,0.55), + inset 0 2px 2px rgba(200,164,77,0.25), + 0 0 16px rgba(240,128,41,0.2); +} +.theme-grimoire .wax-seal-blood { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #8b1d27; } +.theme-grimoire .wax-seal-arcane { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #6a4cab; } +.theme-grimoire .wax-seal-ember { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #b65a13; color: #fff3d8; } +.theme-grimoire .wax-seal-phosphor { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #2aa758; } +.theme-grimoire .wax-seal-gold { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.4), transparent 50%), #c8a44d; color: #1a1410; text-shadow: 0 1px 0 rgba(255,255,255,0.25); } +.theme-grimoire .wax-seal-ichor { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.3), transparent 50%), #2d1b3a; } + +/* HEADINGS — Matrix-minimal monospace, no ornate text-shadow */ +.theme-grimoire .h-blackletter { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 800; + color: #8b1d27; + line-height: 1.0; + letter-spacing: -0.04em; + text-transform: uppercase; + font-feature-settings: "ss02" on, "calt" on; +} +.theme-grimoire.dark .h-blackletter { + color: #c8a44d; + text-shadow: 0 0 10px rgba(200,164,77,0.35), 0 0 28px rgba(240,128,41,0.12); +} +.theme-grimoire .h-engraved { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 500; + letter-spacing: 0.16em; + text-transform: uppercase; + color: #3a2f25; +} +.theme-grimoire.dark .h-engraved { color: #c8a44d; } + +/* RUNE DIVIDER — horizontal rule with center label */ +.theme-grimoire .rune-divider { + display: flex; + align-items: center; + gap: 0.9rem; + color: #8b1d27; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-size: 0.72rem; + letter-spacing: 0.4em; + text-transform: uppercase; + margin: 1.6rem 0; +} +.theme-grimoire .rune-divider::before, +.theme-grimoire .rune-divider::after { + content: ""; + flex: 1; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(139,29,39,0.6), transparent); +} +.theme-grimoire.dark .rune-divider { color: #c8a44d; } +.theme-grimoire.dark .rune-divider::before, +.theme-grimoire.dark .rune-divider::after { + background: linear-gradient(90deg, transparent, rgba(200,164,77,0.6), transparent); +} + +/* BLINKING TERMINAL CURSOR */ +.theme-grimoire .cursor-blink::after { + content: "▮"; + display: inline-block; + margin-left: 0.35rem; + color: #f08029; + animation: grimoire-blink 1s steps(2, start) infinite; +} +.theme-grimoire.dark .cursor-blink::after { color: #57f287; } + +/* ARC LINK — subtle inline link styling */ +.theme-grimoire .arc-link { + color: #8b1d27; + text-decoration: underline; + text-decoration-style: dotted; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color 0.15s ease; +} +.theme-grimoire .arc-link:hover { color: #f08029; text-decoration-style: solid; } +.theme-grimoire.dark .arc-link { color: #c8a44d; } +.theme-grimoire.dark .arc-link:hover { color: #f08029; } + +/* SUMMONING CIRCLE — decorative SVG container */ +.theme-grimoire .summoning-circle { + position: relative; + width: 11rem; + height: 11rem; + margin: 0 auto; +} +.theme-grimoire .summoning-circle svg { width: 100%; height: 100%; } +.theme-grimoire .summoning-circle .ring-outer { + animation: grimoire-sigil-spin 42s linear infinite; + transform-origin: center; +} +.theme-grimoire .summoning-circle .ring-inner { + animation: grimoire-sigil-spin 56s linear infinite reverse; + transform-origin: center; +} + +/* MARQUEE — footer scrolling band */ +.theme-grimoire .grim-marquee { + overflow: hidden; + white-space: nowrap; + background: linear-gradient(180deg, #07080d, #0e0f17); + color: #c8a44d; + border-top: 1px solid #c8a44d; + border-bottom: 1px solid #c8a44d; + padding: 0.55rem 0; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 500; + font-size: 0.74rem; + letter-spacing: 0.22em; + text-transform: uppercase; +} +.theme-grimoire .grim-marquee__track { + display: inline-block; + animation: grimoire-marquee 38s linear infinite; +} +.theme-grimoire .grim-marquee__token { display: inline-block; padding: 0 1.5rem; } +.theme-grimoire .grim-marquee__token span { color: #f08029; padding-right: 1.5rem; } + +/* INCANT OVERLAY — Konami code easter egg */ +.theme-grimoire .incant-overlay { + position: fixed; + inset: 0; + background: radial-gradient(circle, rgba(7,8,13,0.85), rgba(7,8,13,0.98)); + color: #c8a44d; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + font-family: "JetBrains Mono", ui-monospace, monospace; + text-align: center; + animation: grimoire-incant 1.2s ease-out forwards; +} +.theme-grimoire .incant-overlay .incant-title { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 800; + letter-spacing: -0.02em; + font-size: clamp(2.5rem, 9vw, 6rem); + color: #f08029; + text-shadow: 0 0 24px rgba(240,128,41,0.65), 0 0 60px rgba(240,128,41,0.35); +} +.theme-grimoire .incant-overlay .incant-sub { + font-family: "IBM Plex Mono", monospace; + color: #57f287; + letter-spacing: 0.4em; + margin-top: 1rem; + text-transform: uppercase; +} + +/* ---------------------------------------------------------------------- + Rendered markdown content (`.prose-grimoire` wrapper) +---------------------------------------------------------------------- */ + +.theme-grimoire .prose-grimoire { + font-family: "Inter", system-ui, -apple-system, sans-serif; + font-size: 1.05rem; + line-height: 1.72; + color: #1a1410; + font-feature-settings: "ss01" on, "cv11" on, "kern", "liga"; +} +.theme-grimoire.dark .prose-grimoire { color: #e9e0c8; } + +/* Matrix drop cap — chunky monospace block letter, thin underline */ +.theme-grimoire .prose-grimoire > p:first-of-type::first-letter { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 800; + float: left; + font-size: 4.4rem; + line-height: 0.95; + padding: 0.15rem 0.55rem 0.15rem 0; + margin-right: 0.25rem; + color: #8b1d27; + border-bottom: 2px solid currentColor; +} +.theme-grimoire.dark .prose-grimoire > p:first-of-type::first-letter { + color: #57f287; + text-shadow: 0 0 10px rgba(87,242,135,0.45), 0 0 24px rgba(87,242,135,0.18); + border-bottom-color: rgba(87,242,135,0.55); +} + +.theme-grimoire .prose-grimoire h1, +.theme-grimoire .prose-grimoire h2, +.theme-grimoire .prose-grimoire h3 { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 700; + margin-top: 2em; + margin-bottom: 0.6em; + letter-spacing: -0.02em; + color: #3a2f25; + text-transform: uppercase; +} +.theme-grimoire.dark .prose-grimoire h1, +.theme-grimoire.dark .prose-grimoire h2, +.theme-grimoire.dark .prose-grimoire h3 { color: #c8a44d; } +.theme-grimoire .prose-grimoire h1 { + font-size: 1.65rem; + border-bottom: 1px solid #8b1d27; + padding-bottom: 0.35em; +} +.theme-grimoire .prose-grimoire h2 { font-size: 1.35rem; color: #8b1d27; } +.theme-grimoire .prose-grimoire h3 { font-size: 1.1rem; } +.theme-grimoire.dark .prose-grimoire h1 { border-bottom-color: #c8a44d; } +.theme-grimoire.dark .prose-grimoire h2 { color: #f08029; } + +.theme-grimoire .prose-grimoire p { margin-bottom: 1.1em; } + +.theme-grimoire .prose-grimoire strong { color: #8b1d27; font-weight: 800; } +.theme-grimoire.dark .prose-grimoire strong { color: #f08029; } + +.theme-grimoire .prose-grimoire em { font-style: italic; color: #6a4cab; } +.theme-grimoire.dark .prose-grimoire em { color: #c8a44d; } + +.theme-grimoire .prose-grimoire a { + color: #8b1d27; + text-decoration: underline; + text-decoration-style: dotted; + text-decoration-thickness: 1px; + text-underline-offset: 3px; +} +.theme-grimoire .prose-grimoire a:hover { color: #f08029; text-decoration-style: solid; } +.theme-grimoire.dark .prose-grimoire a { color: #c8a44d; } +.theme-grimoire.dark .prose-grimoire a:hover { color: #f08029; } + +.theme-grimoire .prose-grimoire ul, +.theme-grimoire .prose-grimoire ol { + margin: 1em 0 1.2em 1.5em; + padding-left: 0.5em; +} +.theme-grimoire .prose-grimoire ul { list-style: none; } +.theme-grimoire .prose-grimoire ul > li::before { + content: "✦"; + color: #8b1d27; + font-weight: 700; + margin-right: 0.55em; + display: inline-block; + transform: translateY(-1px); +} +.theme-grimoire.dark .prose-grimoire ul > li::before { color: #c8a44d; } +.theme-grimoire .prose-grimoire ol { list-style: decimal; } +.theme-grimoire .prose-grimoire ol::marker { + color: #8b1d27; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 700; +} +.theme-grimoire .prose-grimoire li { margin-bottom: 0.4em; } + +.theme-grimoire .prose-grimoire blockquote { + position: relative; + border-left: 2px solid #8b1d27; + background: rgba(200,164,77,0.10); + padding: 1em 1.2em 1em 2.4em; + margin: 1.5em 0; + font-style: italic; + color: #3a2f25; +} +.theme-grimoire .prose-grimoire blockquote::before { + content: ">"; + position: absolute; + left: 0.7em; + top: 1em; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 700; + font-style: normal; + font-size: 1.05em; + color: #8b1d27; + line-height: 1; +} +.theme-grimoire.dark .prose-grimoire blockquote { + border-left-color: #c8a44d; + background: rgba(106,76,171,0.10); + color: #e9e0c8; +} +.theme-grimoire.dark .prose-grimoire blockquote::before { color: #c8a44d; } + +/* Code blocks — wizard's terminal (phosphor green on void) */ +.theme-grimoire .prose-grimoire pre, +.theme-grimoire pre.highlight { + background: #07080d !important; + color: #57f287 !important; + border: 1px solid #c8a44d; + box-shadow: + inset 0 0 80px rgba(106,76,171,0.18), + 0 0 22px rgba(87,242,135,0.18), + 0 18px 40px -18px rgba(0,0,0,0.7); + padding: 1rem 1.1rem; + margin: 1.5em 0; + overflow-x: auto; + font-family: "IBM Plex Mono", ui-monospace, monospace; + font-size: 0.92rem; + line-height: 1.55; + position: relative; +} +.theme-grimoire .prose-grimoire pre::before, +.theme-grimoire pre.highlight::before { + content: "▒▓ ~/grimoire/spells $ cast"; + display: block; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-weight: 500; + font-size: 0.66rem; + color: #c8a44d; + letter-spacing: 0.12em; + text-transform: uppercase; + margin: -1rem -1.1rem 0.8rem; + padding: 0.55rem 0.9rem; + background: #0e0f17; + border-bottom: 1px solid #c8a44d; +} +.theme-grimoire .prose-grimoire pre::after, +.theme-grimoire pre.highlight::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background-image: repeating-linear-gradient( + to bottom, + transparent 0 3px, + rgba(87,242,135,0.04) 3px 4px + ); +} + +.theme-grimoire .prose-grimoire code, +.theme-grimoire code:not(pre code) { + font-family: "IBM Plex Mono", ui-monospace, monospace; + background: rgba(139,29,39,0.10); + color: #8b1d27; + padding: 0 6px; + border: 1px solid rgba(139,29,39,0.35); + font-size: 0.92em; + border-radius: 3px; +} +.theme-grimoire.dark .prose-grimoire code, +.theme-grimoire.dark code:not(pre code) { + background: rgba(200,164,77,0.10); + color: #c8a44d; + border-color: rgba(200,164,77,0.4); +} +.theme-grimoire .prose-grimoire pre code, +.theme-grimoire pre.highlight code { + background: transparent !important; + color: inherit !important; + border: none !important; + padding: 0 !important; + font-size: inherit; +} + +.theme-grimoire .prose-grimoire hr { + border: none; + margin: 2.2em 0; + height: 14px; + background-image: url("data:image/svg+xml;utf8,// EOF //"); + background-repeat: no-repeat; + background-position: center; +} +.theme-grimoire.dark .prose-grimoire hr { + background-image: url("data:image/svg+xml;utf8,// EOF //"); +} + +.theme-grimoire .prose-grimoire img { + border: 1px solid #1a1410; + box-shadow: + inset 0 0 0 4px #ebd9b3, + inset 0 0 0 5px #c8a44d, + 0 18px 36px -16px rgba(20,12,6,0.55); + margin: 1.5em 0; +} +.theme-grimoire.dark .prose-grimoire img { + border-color: #c8a44d; + box-shadow: + inset 0 0 0 4px #07080d, + inset 0 0 0 5px #c8a44d, + 0 18px 36px -16px rgba(0,0,0,0.85); +} + +.theme-grimoire .prose-grimoire table { + width: 100%; + border-collapse: collapse; + margin: 1.4em 0; + font-family: "JetBrains Mono", ui-monospace, monospace; + font-size: 0.86rem; +} +.theme-grimoire .prose-grimoire th, +.theme-grimoire .prose-grimoire td { + border: 1px solid #1a1410; + padding: 0.55rem 0.8rem; + text-align: left; +} +.theme-grimoire .prose-grimoire th { + background: #c8a44d; + color: #1a1410; + text-transform: uppercase; + letter-spacing: 0.12em; + font-weight: 700; +} +.theme-grimoire.dark .prose-grimoire th { + background: #2d1b3a; + color: #c8a44d; + border-color: #c8a44d; +} +.theme-grimoire.dark .prose-grimoire td { border-color: rgba(200,164,77,0.4); } + +/* ---------------------------------------------------------------------- + Keyframes (prefixed with `grimoire-` to avoid clashing with other themes) +---------------------------------------------------------------------- */ + +@keyframes grimoire-blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} + +@keyframes grimoire-marquee { + 0% { transform: translateX(0); } + 100% { transform: translateX(-50%); } +} + +@keyframes grimoire-sigil-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes grimoire-incant { + 0% { opacity: 0; transform: translateY(-12px) scale(0.9); filter: blur(8px); } + 60% { opacity: 1; filter: blur(0); } + 100% { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes grimoire-mist { + 0%, 100% { transform: translateX(-2%) translateY(0); } + 50% { transform: translateX(2%) translateY(-1%); } +} From 6519256cb04617f47857e156fc3cd3ed94817f33 Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 05:00:09 -0400 Subject: [PATCH 08/10] Add grimoire theme view variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops a full set of view overrides under app/views/themes/grimoire/ that the theming system prepends to the view lookup path when grimoire is active: layouts/application.html.erb — html.theme-grimoire wrapper, pixel-runic favicon, theme-only Google Fonts (Inter + JetBrains Mono + IBM Plex Mono), cookie-based dark-mode JS that survives Turbo Drive navigation (re-applies on turbo:load and turbo:render, listeners guarded against double-registration), and a Konami code easter egg that unlocks a "necromancer" overlay shared/_navigation.html.erb — terminal masthead (`root@grimoire:~$ cat ./codex_of`), Roman-numeral table-of-contents nav, RSS + theme-toggle icon-runes shared/_footer.html.erb — animated summoning circle SVG (counter-rotating ring + pentagram + center ember), scrolling marquee of dev-zine tokens, terminal command-line copyright shared/_admin_navigation.html.erb — adept's console (`:transcribe / :inscribe / :bind / :auguries / :scry / :depart`) shared/_tags.html.erb — wax-seal tag pills (deterministic color cycle by tag name hash) blog/index.html.erb, blog/show.html.erb, blog/index_by_tag.html.erb — tome cards with "Folio · Anno " date stickers, "open spellbook" CTAs, // LOG // and // FILTER // rune dividers pages/show.html.erb — single-page tome with "✦ Chapter · ✦" header and `exit 0` // EOF // sign-off links/index.html.erb, _link.html.erb — "Forbidden Tomes" reliquary header, link cards rendered as tomes with ✦ icon papers/index.html.erb, _paper.html.erb — "Treatises" header with "Codex · PDF" stickers All views consume the pure-CSS classes defined in app/assets/stylesheets/themes/grimoire.css (tome, spell-btn, wax-seal, h-blackletter, h-engraved, rune-divider, prose-grimoire, etc.) plus .theme-grimoire-scoped utility classes that mirror Tailwind syntax (bg-grim-*, text-grim-*, font-plex, tracking-[Xem], leading-[X], …). This means grimoire — like retro — needs zero changes to the Tailwind config to render correctly: it owns its own visual surface and only takes effect when ABBEY_THEME=grimoire is active. Co-authored-by: Cursor --- app/views/themes/grimoire/blog/index.html.erb | 56 +++++++++ .../grimoire/blog/index_by_tag.html.erb | 78 ++++++++++++ app/views/themes/grimoire/blog/show.html.erb | 56 +++++++++ .../grimoire/layouts/application.html.erb | 112 ++++++++++++++++++ .../themes/grimoire/links/_link.html.erb | 34 ++++++ .../themes/grimoire/links/index.html.erb | 34 ++++++ app/views/themes/grimoire/pages/show.html.erb | 31 +++++ .../themes/grimoire/papers/_paper.html.erb | 57 +++++++++ .../themes/grimoire/papers/index.html.erb | 45 +++++++ .../shared/_admin_navigation.html.erb | 24 ++++ .../themes/grimoire/shared/_footer.html.erb | 67 +++++++++++ .../grimoire/shared/_navigation.html.erb | 60 ++++++++++ .../themes/grimoire/shared/_tags.html.erb | 14 +++ 13 files changed, 668 insertions(+) create mode 100644 app/views/themes/grimoire/blog/index.html.erb create mode 100644 app/views/themes/grimoire/blog/index_by_tag.html.erb create mode 100644 app/views/themes/grimoire/blog/show.html.erb create mode 100644 app/views/themes/grimoire/layouts/application.html.erb create mode 100644 app/views/themes/grimoire/links/_link.html.erb create mode 100644 app/views/themes/grimoire/links/index.html.erb create mode 100644 app/views/themes/grimoire/pages/show.html.erb create mode 100644 app/views/themes/grimoire/papers/_paper.html.erb create mode 100644 app/views/themes/grimoire/papers/index.html.erb create mode 100644 app/views/themes/grimoire/shared/_admin_navigation.html.erb create mode 100644 app/views/themes/grimoire/shared/_footer.html.erb create mode 100644 app/views/themes/grimoire/shared/_navigation.html.erb create mode 100644 app/views/themes/grimoire/shared/_tags.html.erb diff --git a/app/views/themes/grimoire/blog/index.html.erb b/app/views/themes/grimoire/blog/index.html.erb new file mode 100644 index 0000000..c8c01ef --- /dev/null +++ b/app/views/themes/grimoire/blog/index.html.erb @@ -0,0 +1,56 @@ +<% + # Roman numeral helper (small range — fine for year stickers since 1900–2099). + to_roman = ->(num) { + map = { 1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD', + 100 => 'C', 90 => 'XC', 50 => 'L', 40 => 'XL', + 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I' } + result = +"" + map.each { |val, sym| while num >= val; result << sym; num -= val; end } + result + } +%> + +
    + <% @posts.each do |post| %> +
    + <%# Folio · Anno date sticker (Roman numerals for years) %> +
    + Folio · Anno <%= to_roman.call(post.created_at.year) %> +
    + +

    + <%= link_to post.title, dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "arc-link" %> +

    + +

    + // <%= post.created_at.strftime('%Y-%m-%d · %a').downcase %> +

    + +
    + <%= post.rendered_excerpt.html_safe %> +
    + +
    + <%= render "shared/tags", post: post %> +
    + +
    + <% if authenticated? %> + <%= link_to "edit", edit_post_path(post), class: "font-plex text-xs tracking-[0.14em] uppercase text-grim-shadow/70 dark:text-grim-gold/70 hover:text-grim-blood dark:hover:text-grim-ember" %> + · + <% end %> + <%= link_to dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "spell-btn" do %> + ✦ open spellbook + <% end %> +
    +
    + <% end %> + +
    // LOG //
    + +
    + <%= paginate @posts %> +
    +
    diff --git a/app/views/themes/grimoire/blog/index_by_tag.html.erb b/app/views/themes/grimoire/blog/index_by_tag.html.erb new file mode 100644 index 0000000..d7ef060 --- /dev/null +++ b/app/views/themes/grimoire/blog/index_by_tag.html.erb @@ -0,0 +1,78 @@ +<% content_for :title do %> + #<%= @tag.name %> | <%= Rails.application.config.site_name %> +<% end %> + +<% content_for :rss do %> + <%= auto_discovery_link_tag(:atom, tag_feed_path(id: @tag.name)) %> +<% end %> + +<% content_for :rss_button do %> + <%= link_to tag_feed_path(id: @tag.name), class: "icon-rune", aria: { label: "RSS Feed" } do %> + <%= render "shared/icons/rss" %> + <% end %> +<% end %> + +<% + to_roman = ->(num) { + map = { 1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD', + 100 => 'C', 90 => 'XC', 50 => 'L', 40 => 'XL', + 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I' } + result = +"" + map.each { |val, sym| while num >= val; result << sym; num -= val; end } + result + } +%> + +
    +
    +

    + // filter · Anno <%= to_roman.call(Date.current.year) %> +

    +

    + #<%= @tag.name %> +

    +

    + ✦ inscription bound to this rune · <%= pluralize(@posts.total_count, 'transmission') %> +

    +
    + +
    // FILTER //
    + + <% @posts.each do |post| %> +
    +
    + Folio · Anno <%= to_roman.call(post.created_at.year) %> +
    + +

    + <%= link_to post.title, dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "arc-link" %> +

    + +

    + // <%= post.created_at.strftime('%Y-%m-%d') %> +

    + +
    + <%= post.rendered_excerpt.html_safe %> +
    + +
    + <%= render "shared/tags", post: post %> +
    + +
    + <% if authenticated? %> + <%= link_to "edit", edit_post_path(post), class: "font-plex text-xs tracking-[0.14em] uppercase text-grim-shadow/70 dark:text-grim-gold/70 hover:text-grim-blood dark:hover:text-grim-ember" %> + · + <% end %> + <%= link_to dated_post_path(year: post.year, day: post.day, month: post.month, id: post), + class: "spell-btn" do %> + ✦ open spellbook + <% end %> +
    +
    + <% end %> + +
    <%= paginate @posts %>
    +
    diff --git a/app/views/themes/grimoire/blog/show.html.erb b/app/views/themes/grimoire/blog/show.html.erb new file mode 100644 index 0000000..31247aa --- /dev/null +++ b/app/views/themes/grimoire/blog/show.html.erb @@ -0,0 +1,56 @@ +<% content_for :title do %> +<%= @post.title %> | <%= Rails.application.config.site_name %> +<% end %> + +<% + to_roman = ->(num) { + map = { 1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD', + 100 => 'C', 90 => 'XC', 50 => 'L', 40 => 'XL', + 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I' } + result = +"" + map.each { |val, sym| while num >= val; result << sym; num -= val; end } + result + } +%> + +<%= link_to posts_path, class: "inline-flex items-center gap-2 font-plex text-xs tracking-[0.18em] uppercase text-grim-shadow/70 dark:text-grim-gold/70 hover:text-grim-blood dark:hover:text-grim-ember mb-6" do %> + Back to posts +<% end %> + +
    +
    + Folio · Anno <%= to_roman.call(@post.created_at.year) %> +
    + +
    +

    + <%= @post.title %> +

    + +
    + + <% if authenticated? %> + + <%= link_to "edit ▸", edit_post_path(@post), + class: "text-grim-blood dark:text-grim-ember hover:underline" %> + <% end %> +
    +
    + +
    + <%= @post.rendered_body.html_safe %> +
    + +
    // EOF //
    + +

    + exit 0 · end of transmission +

    + +
    +

    // tagged_with

    +
    + <%= render "shared/tags", post: @post %> +
    +
    +
    diff --git a/app/views/themes/grimoire/layouts/application.html.erb b/app/views/themes/grimoire/layouts/application.html.erb new file mode 100644 index 0000000..2df8356 --- /dev/null +++ b/app/views/themes/grimoire/layouts/application.html.erb @@ -0,0 +1,112 @@ + + + + <%= content_for(:title) || Rails.application.config.site_name %> + + + + + + <%# Pixel-runic favicon: pentagram-circle with a single ember. %> + + + <%# Grimoire display fonts — only loaded when the grimoire theme is active. %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= content_for(:rss) || auto_discovery_link_tag(:atom, blog_feed_path) %> + + <%= stylesheet_link_tag "highlight" %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%# Extra stylesheets contributed by the active theme. %> + <% theme_stylesheets.each do |sheet| %> + <%= stylesheet_link_tag sheet, "data-turbo-track": "reload" %> + <% end %> + <%= javascript_importmap_tags %> + + + <%= render "shared/admin_navigation" %> +
    + <%= render "shared/navigation" %> +
    + <%= yield %> +
    + +
    + <%= render "shared/footer" %> + + diff --git a/app/views/themes/grimoire/links/_link.html.erb b/app/views/themes/grimoire/links/_link.html.erb new file mode 100644 index 0000000..fffb39b --- /dev/null +++ b/app/views/themes/grimoire/links/_link.html.erb @@ -0,0 +1,34 @@ +<%= link_to link.url, + rel: "nofollow", + class: "block tome group" do %> +
    +
    +

    + + <%= link.title %> + <%= render "shared/icons/external_link", class: "w-3.5 h-3.5 opacity-60" %> +

    + +
    + <% if link.description.present? %> +

    + <%= link.description %> +

    + <% end %> +
    +<% end %> +<% if authenticated? %> +
    + <%= link_to edit_link_path(link), class: "icon-rune" do %> + <%= render "shared/icons/pencil", class: "w-4 h-4" %> + <% end %> + <%= button_to link_path(link), + method: :delete, + class: "icon-rune", + form: { data: { turbo_confirm: "Are you sure?" } } do %> + <%= render "shared/icons/trash", class: "w-4 h-4" %> + <% end %> +
    +<% end %> diff --git a/app/views/themes/grimoire/links/index.html.erb b/app/views/themes/grimoire/links/index.html.erb new file mode 100644 index 0000000..f3cb73d --- /dev/null +++ b/app/views/themes/grimoire/links/index.html.erb @@ -0,0 +1,34 @@ +<% content_for :rss do %> + <%= auto_discovery_link_tag(:atom, links_feed_path) %> +<% end %> + +<% content_for :rss_button do %> + <%= link_to links_feed_path, class: "icon-rune", aria: { label: "RSS Feed" } do %> + <%= render "shared/icons/rss" %> + <% end %> +<% end %> + +<% content_for :title do %> +Forbidden Tomes | <%= Rails.application.config.site_name %> +<% end %> + +
    +
    +

    + // reliquary +

    +

    + Forbidden Tomes +

    +

    + ✦ links worth your wax candle ✦ +

    +
    + +
    // BOOKMARKS //
    + +
    + <%= render @links %> +
    +
    <%= paginate @links %>
    +
    diff --git a/app/views/themes/grimoire/pages/show.html.erb b/app/views/themes/grimoire/pages/show.html.erb new file mode 100644 index 0000000..5f82116 --- /dev/null +++ b/app/views/themes/grimoire/pages/show.html.erb @@ -0,0 +1,31 @@ +<% content_for :title do %> +<%= @page.title %> | <%= Rails.application.config.site_name %> +<% end %> + +
    +
    + ✦ Chapter · <%= @page.slug %> +
    + +
    +

    + <%= @page.title %> +

    + <% if authenticated? %> +
    + <%= link_to "edit ▸", edit_page_path(@page), + class: "text-grim-blood dark:text-grim-ember hover:underline" %> +
    + <% end %> +
    + +
    + <%= @page.rendered_body.html_safe %> +
    + +
    // EOF //
    + +

    + exit 0 +

    +
    diff --git a/app/views/themes/grimoire/papers/_paper.html.erb b/app/views/themes/grimoire/papers/_paper.html.erb new file mode 100644 index 0000000..4df35fa --- /dev/null +++ b/app/views/themes/grimoire/papers/_paper.html.erb @@ -0,0 +1,57 @@ +
    +
    + Codex · PDF +
    +
    + <% if paper.pdf.attached? && paper.pdf.previewable? %> +
    + <% if paper.arxiv? %> + <%= link_to paper.arxiv_pdf_url, target: "_blank", rel: "noopener", data: { turbo: false } do %> + <%= image_tag paper.pdf.preview(resize_to_fit: [120, 120]), class: "w-28 h-28 object-cover border border-grim-shadow dark:border-grim-gold" %> + <% end %> + <% else %> + <%= link_to paper_view_path(paper), target: (paper_view_path(paper) == paper.url ? "_blank" : nil), data: { turbo: false } do %> + <%= image_tag paper.pdf.preview(resize_to_fit: [120, 120]), class: "w-28 h-28 object-cover border border-grim-shadow dark:border-grim-gold" %> + <% end %> + <% end %> +
    + <% end %> + +
    +

    + + <% if paper.arxiv? %> + <%= link_to paper.arxiv_pdf_url, target: "_blank", rel: "noopener", class: "arc-link flex items-center gap-2", data: { turbo: false } do %> + <%= paper.title %> + <%= render "shared/icons/external_link", class: "w-3.5 h-3.5 opacity-60" %> + <% end %> + <% else %> + <%= link_to paper.title, paper_view_path(paper), target: (paper_view_path(paper) == paper.url ? "_blank" : nil), class: "arc-link flex items-center gap-2", data: { turbo: false } %> + <% end %> +

    + + <% if paper.description.present? %> +

    + <%= paper.description %> +

    + <% end %> + +
    + // bound <%= paper.created_at.strftime("%Y-%m-%d") %> +
    +
    +
    +
    +<% if authenticated? %> +
    + <%= link_to edit_paper_path(paper), class: "icon-rune" do %> + <%= render "shared/icons/pencil", class: "w-4 h-4" %> + <% end %> + <%= button_to paper_path(paper), + method: :delete, + class: "icon-rune", + form: { data: { turbo_confirm: "Are you sure?" } } do %> + <%= render "shared/icons/trash", class: "w-4 h-4" %> + <% end %> +
    +<% end %> diff --git a/app/views/themes/grimoire/papers/index.html.erb b/app/views/themes/grimoire/papers/index.html.erb new file mode 100644 index 0000000..7a36c16 --- /dev/null +++ b/app/views/themes/grimoire/papers/index.html.erb @@ -0,0 +1,45 @@ +<% content_for :title do %> +Treatises | <%= Rails.application.config.site_name %> +<% end %> + +
    +
    +
    +
    +

    + // codices & treatises +

    +

    + Treatises +

    +

    + ✦ arxiv printouts & pdf rabbit holes +

    +
    + <% if @papers.any? && authenticated? %> + <%= link_to "+ inscribe paper", new_link_path, class: "spell-btn spell-btn-blood" %> + <% end %> +
    +
    + + <% if @papers.any? %> +
    // FOLIO //
    + +
    + <%= render @papers %> +
    + +
    + <%= paginate @papers %> +
    + <% else %> +
    +

    + // no folios in this archive yet +

    + <% if authenticated? %> + <%= link_to "+ inscribe first paper", new_link_path, class: "spell-btn spell-btn-blood" %> + <% end %> +
    + <% end %> +
    diff --git a/app/views/themes/grimoire/shared/_admin_navigation.html.erb b/app/views/themes/grimoire/shared/_admin_navigation.html.erb new file mode 100644 index 0000000..0224e4c --- /dev/null +++ b/app/views/themes/grimoire/shared/_admin_navigation.html.erb @@ -0,0 +1,24 @@ +<% if authenticated? %> +
    +
    +
    +
    + adept@grimoire $ + <%= link_to ":transcribe", new_post_path, class: "text-grim-gold hover:text-grim-ember transition-colors" %> + <%= link_to ":inscribe", new_page_path, class: "text-grim-gold hover:text-grim-ember transition-colors" %> + <%= link_to ":bind", new_link_path, class: "text-grim-gold hover:text-grim-ember transition-colors" %> + <%= link_to ":auguries", feeds_path, class: "text-grim-phosphor hover:text-grim-ember transition-colors" %> + <%= link_to ":scry", feed_posts_path, class: "text-grim-phosphor hover:text-grim-ember transition-colors" %> +
    +
    + + <%= button_to session_path, + method: :delete, + class: "text-grim-gold hover:text-grim-ember transition-colors" do %> + :depart + <% end %> +
    +
    +
    +
    +<% end %> diff --git a/app/views/themes/grimoire/shared/_footer.html.erb b/app/views/themes/grimoire/shared/_footer.html.erb new file mode 100644 index 0000000..9dc9797 --- /dev/null +++ b/app/views/themes/grimoire/shared/_footer.html.erb @@ -0,0 +1,67 @@ +<%# Footer: summoning circle on top, scrolling marquee, terminal copyright. %> + +
    +
    + + <%# Summoning circle: counter-rotating rings, runes, pentagram, center sigil %> +
    + +
    + +

    + // summoned by Rails & markdown // +

    +
    + + <%# Scrolling marquee — duplicated track for seamless wrap %> +
    +
    + <% 2.times do %> + +++ STDERR : INK STILL DRYING · + +++ HEAP : 0xDEADBEEF · + +++ TAIL -f /var/log/grimoire · + +++ KEY : ↑↑↓↓←→←→BA · + +++ MEM : 64K SHOULD BE ENOUGH · + +++ SIG : RING ZERO ENGAGED · + <% end %> +
    +
    + + <%# Terminal copyright bar %> +
    + root@grimoire + : + ~ + $ + echo "© <%= Date.current.year %> <%= Rails.application.config.site_name %> · all rites reserved" + +
    +
    diff --git a/app/views/themes/grimoire/shared/_navigation.html.erb b/app/views/themes/grimoire/shared/_navigation.html.erb new file mode 100644 index 0000000..39556b7 --- /dev/null +++ b/app/views/themes/grimoire/shared/_navigation.html.erb @@ -0,0 +1,60 @@ +
    +
    + +
    + <%= link_to root_path, class: "group inline-block" do %> + <%# Terminal masthead: $ cat ./codex_of %> +
    + root@grimoire + : + ~ + $ + cat ./codex_of +
    +

    + <%= Rails.application.config.site_name %> +

    +

    + // field notes from over 20 years on the web +

    + <% end %> + +
    + <% if content_for? :rss_button %> + <%= yield :rss_button %> + <% else %> + <%= link_to blog_feed_path, class: "icon-rune", aria: { label: "RSS Feed" } do %> + <%= render "shared/icons/rss" %> + <% end %> + <% end %> + +
    +
    + + <%# Roman-numeral table-of-contents nav %> +
    // NAV //
    + +
    +
    diff --git a/app/views/themes/grimoire/shared/_tags.html.erb b/app/views/themes/grimoire/shared/_tags.html.erb new file mode 100644 index 0000000..a5232ce --- /dev/null +++ b/app/views/themes/grimoire/shared/_tags.html.erb @@ -0,0 +1,14 @@ +<% + seal_palette = %w[ + wax-seal-blood + wax-seal-arcane + wax-seal-ember + wax-seal-phosphor + wax-seal-gold + wax-seal-ichor + ] +%> +<% post.tags.each do |tag| %> + <% seal = seal_palette[tag.name.sum % seal_palette.length] %> + <%= link_to "##{tag.name}", tag_path(id: tag.name), class: "wax-seal #{seal}" %> +<% end %> From 1fa9c770d74d0df48c0dda1686ce4929da077a0f Mon Sep 17 00:00:00 2001 From: Enrique Canals Date: Sat, 23 May 2026 05:00:18 -0400 Subject: [PATCH 09/10] Extend theme integration tests to cover grimoire Adds parallel coverage for the grimoire opt-in theme so that future theming changes can't quietly regress it: - default theme renders the original layout: also asserts no `theme-grimoire` class and no `themes/grimoire` stylesheets leak in - new test: grimoire theme prepends its view path and loads its theme stylesheets; nav still contains all the required labels - renderer test: grimoire (like retro) uses MinimalMarkdownRender - theme_stylesheets helper test: returns themes/grimoire and themes/grimoire-highlight when grimoire is active All 5 tests pass with 68 assertions. Co-authored-by: Cursor --- test/integration/themes_test.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/integration/themes_test.rb b/test/integration/themes_test.rb index 02218ea..741946a 100644 --- a/test/integration/themes_test.rb +++ b/test/integration/themes_test.rb @@ -17,7 +17,9 @@ class ThemesTest < ActionDispatch::IntegrationTest get root_path assert_response :success assert_no_match(/class="theme-retro/, response.body) + assert_no_match(/class="theme-grimoire/, response.body) assert_no_match(%r{themes/retro}, response.body, "default theme should not load theme stylesheets") + assert_no_match(%r{themes/grimoire}, response.body, "default theme should not load theme stylesheets") assert_select "header h1" assert_select "footer" end @@ -38,6 +40,22 @@ class ThemesTest < ActionDispatch::IntegrationTest end end + test "grimoire theme prepends its view path and loads theme stylesheets" do + Rails.application.config.theme = "grimoire" + + get root_path + assert_response :success + assert_match(/class="theme-grimoire/, response.body) + assert_match(%r{themes/grimoire}, response.body, "grimoire theme should load themes/grimoire CSS") + # System test contract: header still has h1, footer still present. + assert_select "header h1" + assert_select "footer" + # Required nav links remain after retheming. + %w[Home About Projects Presentations Links Papers].each do |label| + assert_match(/>#{label} Date: Sun, 24 May 2026 12:23:48 -0400 Subject: [PATCH 10/10] Use Tailwind @theme directive for theme design tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces hand-rolled `.theme-X .bg-foo-bar` utility-class declarations (plus matching `.dark:`, `.hover:`, `.group-hover:`, opacity-modifier and arbitrary-value mirrors) with Tailwind v4 `@theme` tokens that generate the same utilities natively. Addresses the PR description's noted trade-off: "The retro theme uses pure CSS rather than extending the Tailwind config — deliberate, so the default build stays unchanged." It turns out we can have both: the theme tokens live in `@theme` so Tailwind generates the utilities, and the default build stays unchanged because the default theme's views never reference the new utility classes (so Tailwind's content-aware extraction never emits them in the default theme's bundle — only the CSS variables on `:root`, which are inert without matching classes). What moved into `app/assets/tailwind/application.css` under `@theme`: * Memphis palette (--color-memphis-pink/cyan/yellow/mint/purple/coral/...) * Grimoire palette (--color-grim-blood/gold/ember/phosphor/arcane/...) * Retro hard-shadows (--shadow-retro, --shadow-retro-lg, --shadow-retro-pink/...) * Theme-specific font tokens (--font-display/blackletter/engraved/plex/manuscript) * Shared animations + @keyframes (--animate-blink/wiggle/pop-in/marquee) What dropped out of `themes/retro.css` (626 -> 539 lines) and `themes/grimoire.css` (819 -> 754 lines): * `.theme-X .bg-/text-/border-/shadow-/font-...` declarations — Tailwind generates them from the --color-* / --shadow-* / --font-* tokens * `.theme-X.dark .dark\:foo-bar` mirrors — the `dark:` variant works because already carries both `theme-X` and `dark` classes, and the existing `darkMode: 'class'` setting in tailwind.config.js applies * `.hover\:foo-bar`, `.group:hover .group-hover\:foo-bar`, opacity mirrors (`text-foo\/40`), and ~30 arbitrary-value escapes in grimoire.css (`text-\[0\.62rem\]`, `tracking-\[0\.18em\]`) — Tailwind v4 generates all of these natively from the registered tokens * Hand-written @keyframes that duplicated --animate-* registrations What stayed in the per-theme CSS files: * theme-root environmental styles (body backdrops, scanlines, scrollbar) * 4-line CSS-variable rebinding inside `.theme-X { --font-sans: ...; ... }` so utilities like `font-mono` / `font-sans` / `font-display` resolve to the theme's font stack inside the theme scope without affecting the default theme * all component classes (`.card-retro`, `.btn-retro`, `.tome`, `.spell-btn`, `.wax-seal`, `.h-display`, `.h-blackletter`, `.prose-retro`, `.prose-grimoire`, ...) — now consuming `var(--color-memphis-pink)` etc. internally so the @theme tokens are the single source of truth for design values * theme-local @keyframes (retro-drift, retro-glitch, grimoire-sigil-spin, grimoire-mist, grimoire-incant) that aren't surfaced as utilities Default theme is still byte-for-byte unchanged. Tests still pass: $ bin/rails test # 22 runs, 115 assertions, 0 failures $ bin/rails test test/integration/themes_test.rb # 5 runs, 68 assertions, 0 failures $ bin/rails tailwindcss:build # clean Future themes are now much smaller: drop tokens into `@theme`, rebind `--font-*` for typography, ship a body backdrop + component classes — no more growing per-theme boilerplate every time a new color or shadow is needed. Co-authored-by: Cursor --- app/assets/stylesheets/themes/grimoire.css | 447 +++++++++------------ app/assets/stylesheets/themes/retro.css | 433 ++++++++------------ app/assets/tailwind/application.css | 116 +++++- 3 files changed, 469 insertions(+), 527 deletions(-) diff --git a/app/assets/stylesheets/themes/grimoire.css b/app/assets/stylesheets/themes/grimoire.css index c029e11..2b59836 100644 --- a/app/assets/stylesheets/themes/grimoire.css +++ b/app/assets/stylesheets/themes/grimoire.css @@ -4,32 +4,70 @@ man page. Matrix-minimal: monospace display, modern sans body, no ornate blackletter — but the dark mode still feels like a cryptarchive. - Pure CSS – served by Propshaft, loaded only when - `Rails.application.config.theme == "grimoire"`. All declarations are scoped - under `.theme-grimoire` (set on by themes/grimoire/layouts/ - application.html.erb) so loading this file in another context is a no-op. - - Custom palette: - parchment #ebd9b3 vellum #f1e3c2 ink #1a1410 - shadow #3a2f25 void #07080d obsidian #0e0f17 - tomb #161826 blood #8b1d27 ember #f08029 - bone #e9e0c8 phosphor #57f287 arcane #6a4cab - gold #c8a44d ichor #2d1b3a - =============================================================================*/ + Loaded only when `Rails.application.config.theme == "grimoire"`. All + declarations are scoped under `.theme-grimoire` (set on by + themes/grimoire/layouts/application.html.erb) so loading this file in + any other context is a no-op. + + Color/font/animation TOKENS live in `app/assets/tailwind/application.css` + under `@theme`, so Tailwind auto-generates `bg-grim-blood`, + `text-grim-gold`, `dark:border-grim-gold`, `hover:text-grim-ember`, + `font-blackletter`, `font-plex`, `text-[0.62rem]`, `tracking-[0.18em]`, + etc. The arbitrary-value classes and `dark:` / `hover:` / `group-hover:` + variants used across grimoire views all "just work" — no hand-rolled + `.theme-grimoire .bg-grim-X` declarations needed. + + This file ships: + 1. theme-root environmental styles (parchment grain, ember dust, + scanlines, custom scrollbar) + 2. font-family rebinding inside `.theme-grimoire` so `font-sans` / + `font-mono` resolve to the grimoire stack, and `font-display` / + `font-blackletter` / `font-engraved` / `font-plex` / `font-manuscript` + resolve to the right theme fonts + 3. component classes (`.tome`, `.spell-btn`, `.icon-rune`, `.wax-seal`, + `.h-blackletter`, `.h-engraved`, `.rune-divider`, `.cursor-blink`, + `.arc-link`, `.summoning-circle`, `.grim-marquee`, `.incant-overlay`) + 4. typography for the `.prose-grimoire` wrapper (matrix drop-cap, + terminal pre blocks, dotted-underline links, EOF rune
    ) + + Palette: parchment #ebd9b3 · vellum #f1e3c2 · ink #1a1410 · shadow #3a2f25 + void #07080d · obsidian #0e0f17 · tomb #161826 · ichor #2d1b3a + blood #8b1d27 · ember #f08029 · bone #e9e0c8 + phosphor #57f287 · arcane #6a4cab · gold #c8a44d +============================================================================= */ + +/* ---------------------------------------------------------------------- + Font-family rebinding via CSS variable cascade. + Tailwind generates `.font-X { font-family: var(--font-X) }` for every + font token declared in `@theme`. Redefining those variables on + `.theme-grimoire` makes `font-sans`, `font-mono`, `font-blackletter`, + `font-engraved`, `font-plex`, `font-manuscript` resolve to the grimoire + stack inside this theme — without affecting the default theme. +---------------------------------------------------------------------- */ + +.theme-grimoire { + --font-sans: "Inter", system-ui, -apple-system, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; + --font-display: "JetBrains Mono", ui-monospace, monospace; + --font-blackletter: "JetBrains Mono", ui-monospace, monospace; + --font-engraved: "JetBrains Mono", ui-monospace, monospace; + --font-plex: "IBM Plex Mono", ui-monospace, monospace; + --font-manuscript: "Inter", system-ui, -apple-system, sans-serif; +} /* ---------------------------------------------------------------------- Base – html background, body backdrop, scanlines, selection, scrollbar ---------------------------------------------------------------------- */ html.theme-grimoire { - background-color: #ebd9b3; - color: #1a1410; - font-family: "Inter", system-ui, -apple-system, sans-serif; + background-color: var(--color-grim-parchment); + color: var(--color-grim-ink); + font-family: var(--font-sans); font-feature-settings: "ss01" on, "cv11" on, "kern", "liga"; } html.theme-grimoire.dark { - background-color: #07080d; - color: #e9e0c8; + background-color: var(--color-grim-void); + color: var(--color-grim-bone); } html.theme-grimoire body { @@ -103,118 +141,17 @@ html.theme-grimoire .content-layer { z-index: 1; } -html.theme-grimoire ::selection { background: #8b1d27; color: #ebd9b3; } -html.theme-grimoire.dark ::selection { background: #f08029; color: #07080d; } +html.theme-grimoire ::selection { background: var(--color-grim-blood); color: var(--color-grim-parchment); } +html.theme-grimoire.dark ::selection { background: var(--color-grim-ember); color: var(--color-grim-void); } /* Terminal-style dark scrollbar */ html.theme-grimoire.dark ::-webkit-scrollbar { width: 12px; } -html.theme-grimoire.dark ::-webkit-scrollbar-track { background: #07080d; } +html.theme-grimoire.dark ::-webkit-scrollbar-track { background: var(--color-grim-void); } html.theme-grimoire.dark ::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, #6a4cab, #2d1b3a); - border: 1px solid #c8a44d; + background: linear-gradient(180deg, var(--color-grim-arcane), var(--color-grim-ichor)); + border: 1px solid var(--color-grim-gold); } -html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; } - -/* ---------------------------------------------------------------------- - Utility-style color/font/spacing classes used inline in grimoire views. - Scoped under `.theme-grimoire` so they cannot leak into the default - theme. Intentionally mirrors Tailwind-style class names so markup stays - expressive without requiring Tailwind config edits. ----------------------------------------------------------------------- */ - -/* Backgrounds */ -.theme-grimoire .bg-grim-parchment { background-color: #ebd9b3; } -.theme-grimoire .bg-grim-vellum { background-color: #f1e3c2; } -.theme-grimoire .bg-grim-vellum\/30{ background-color: rgba(241,227,194,0.3); } -.theme-grimoire .bg-grim-void { background-color: #07080d; } -.theme-grimoire .bg-grim-obsidian { background-color: #0e0f17; } -.theme-grimoire .bg-grim-tomb { background-color: #161826; } -.theme-grimoire .bg-grim-ichor { background-color: #2d1b3a; } - -.theme-grimoire.dark .dark\:bg-grim-void { background-color: #07080d; } -.theme-grimoire.dark .dark\:bg-grim-obsidian { background-color: #0e0f17; } -.theme-grimoire.dark .dark\:bg-grim-tomb { background-color: #161826; } -.theme-grimoire.dark .dark\:bg-grim-ichor { background-color: #2d1b3a; } -.theme-grimoire.dark .dark\:bg-grim-obsidian\/60 { background-color: rgba(14,15,23,0.6); } - -/* Text colors */ -.theme-grimoire .text-grim-ink { color: #1a1410; } -.theme-grimoire .text-grim-shadow { color: #3a2f25; } -.theme-grimoire .text-grim-shadow\/60 { color: rgba(58,47,37,0.6); } -.theme-grimoire .text-grim-shadow\/70 { color: rgba(58,47,37,0.7); } -.theme-grimoire .text-grim-blood { color: #8b1d27; } -.theme-grimoire .text-grim-bone { color: #e9e0c8; } -.theme-grimoire .text-grim-ember { color: #f08029; } -.theme-grimoire .text-grim-gold { color: #c8a44d; } -.theme-grimoire .text-grim-gold\/70 { color: rgba(200,164,77,0.7); } -.theme-grimoire .text-grim-gold\/80 { color: rgba(200,164,77,0.8); } -.theme-grimoire .text-grim-phosphor{ color: #57f287; } -.theme-grimoire .text-grim-arcane { color: #6a4cab; } - -.theme-grimoire.dark .dark\:text-grim-bone { color: #e9e0c8; } -.theme-grimoire.dark .dark\:text-grim-ember { color: #f08029; } -.theme-grimoire.dark .dark\:text-grim-gold { color: #c8a44d; } -.theme-grimoire.dark .dark\:text-grim-gold\/70 { color: rgba(200,164,77,0.7); } -.theme-grimoire.dark .dark\:text-grim-gold\/80 { color: rgba(200,164,77,0.8); } -.theme-grimoire.dark .dark\:fill-grim-tomb { fill: #161826; } - -/* Borders */ -.theme-grimoire .border-grim-shadow { border-color: #3a2f25; } -.theme-grimoire .border-grim-shadow\/30 { border-color: rgba(58,47,37,0.3); } -.theme-grimoire .border-grim-shadow\/40 { border-color: rgba(58,47,37,0.4); } -.theme-grimoire .border-grim-gold { border-color: #c8a44d; } -.theme-grimoire .border-grim-gold\/40 { border-color: rgba(200,164,77,0.4); } - -.theme-grimoire.dark .dark\:border-grim-gold { border-color: #c8a44d; } -.theme-grimoire.dark .dark\:border-grim-gold\/40 { border-color: rgba(200,164,77,0.4); } - -/* Hover variants */ -.theme-grimoire .hover\:text-grim-blood:hover { color: #8b1d27; } -.theme-grimoire .hover\:text-grim-ember:hover { color: #f08029; } -.theme-grimoire.dark .dark\:hover\:text-grim-ember:hover { color: #f08029; } - -.theme-grimoire .group:hover .group-hover\:text-grim-blood { color: #8b1d27; } -.theme-grimoire.dark .group:hover .dark\:group-hover\:text-grim-ember { color: #f08029; } -.theme-grimoire .group:hover .group-hover\:underline { text-decoration-line: underline; } -.theme-grimoire .group:hover .group-hover\:decoration-dotted { text-decoration-style: dotted; } -.theme-grimoire .group:hover .group-hover\:decoration-1 { text-decoration-thickness: 1px; } -.theme-grimoire .group:hover .group-hover\:underline-offset-\[6px\] { text-underline-offset: 6px; } -.theme-grimoire .group:hover .group-hover\:opacity-100 { opacity: 1; } - -/* Decoration */ -.theme-grimoire .decoration-grim-blood { text-decoration-color: #8b1d27; } -.theme-grimoire.dark .dark\:decoration-grim-gold { text-decoration-color: #c8a44d; } - -/* Font families */ -.theme-grimoire .font-blackletter { font-family: "JetBrains Mono", ui-monospace, monospace; } -.theme-grimoire .font-engraved { font-family: "JetBrains Mono", ui-monospace, monospace; } -.theme-grimoire .font-manuscript { font-family: "Inter", system-ui, -apple-system, sans-serif; } -.theme-grimoire .font-plex { font-family: "IBM Plex Mono", ui-monospace, monospace; } - -/* Arbitrary font sizes used by views (Tailwind `text-[Xrem]` syntax mirror) */ -.theme-grimoire .text-\[0\.62rem\] { font-size: 0.62rem; line-height: 1.2; } -.theme-grimoire .text-\[0\.66rem\] { font-size: 0.66rem; line-height: 1.2; } -.theme-grimoire .text-\[0\.7rem\] { font-size: 0.7rem; line-height: 1.25; } -.theme-grimoire .text-\[0\.72rem\] { font-size: 0.72rem; line-height: 1.3; } -.theme-grimoire .text-\[0\.74rem\] { font-size: 0.74rem; line-height: 1.3; } -.theme-grimoire .text-\[0\.78rem\] { font-size: 0.78rem; line-height: 1.35; } -.theme-grimoire .text-\[2\.2rem\] { font-size: 2.2rem; line-height: 1; } -.theme-grimoire .text-\[2\.8rem\] { font-size: 2.8rem; line-height: 1; } - -/* Arbitrary letter-spacing values */ -.theme-grimoire .tracking-\[0\.12em\] { letter-spacing: 0.12em; } -.theme-grimoire .tracking-\[0\.14em\] { letter-spacing: 0.14em; } -.theme-grimoire .tracking-\[0\.18em\] { letter-spacing: 0.18em; } -.theme-grimoire .tracking-\[0\.2em\] { letter-spacing: 0.2em; } -.theme-grimoire .tracking-\[0\.22em\] { letter-spacing: 0.22em; } -.theme-grimoire .tracking-\[0\.3em\] { letter-spacing: 0.3em; } - -/* Arbitrary leading + underline-offset */ -.theme-grimoire .leading-\[0\.95\] { line-height: 0.95; } -.theme-grimoire .underline-offset-\[6px\] { text-underline-offset: 6px; } - -/* Animations */ -.theme-grimoire .animate-blink { animation: grimoire-blink 1s steps(2, start) infinite; } +html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: var(--color-grim-ember); } /* ---------------------------------------------------------------------- Components @@ -227,11 +164,11 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; background: linear-gradient(180deg, rgba(255,250,235,0.92), rgba(235,217,179,0.92)), url("data:image/svg+xml;utf8,"); - color: #1a1410; - border: 1px solid #1a1410; + color: var(--color-grim-ink); + border: 1px solid var(--color-grim-ink); box-shadow: - inset 0 0 0 6px #ebd9b3, - inset 0 0 0 7px #c8a44d, + inset 0 0 0 6px var(--color-grim-parchment), + inset 0 0 0 7px var(--color-grim-gold), 0 18px 36px -16px rgba(20,12,6,0.55); padding: 1.75rem; transition: transform 0.25s ease, box-shadow 0.25s ease; @@ -239,26 +176,26 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire .tome:hover { transform: translateY(-2px); box-shadow: - inset 0 0 0 6px #ebd9b3, - inset 0 0 0 7px #c8a44d, + inset 0 0 0 6px var(--color-grim-parchment), + inset 0 0 0 7px var(--color-grim-gold), 0 28px 48px -20px rgba(20,12,6,0.6); } .theme-grimoire.dark .tome { background: linear-gradient(180deg, #11131c, #0a0b12), radial-gradient(120% 60% at 50% 0%, rgba(106,76,171,0.18), transparent 70%); - color: #e9e0c8; - border-color: #c8a44d; + color: var(--color-grim-bone); + border-color: var(--color-grim-gold); box-shadow: - inset 0 0 0 4px #07080d, + inset 0 0 0 4px var(--color-grim-void), inset 0 0 0 5px rgba(200,164,77,0.55), inset 0 0 80px rgba(106,76,171,0.18), 0 22px 50px -20px rgba(0,0,0,0.85); } .theme-grimoire.dark .tome:hover { box-shadow: - inset 0 0 0 4px #07080d, - inset 0 0 0 5px #c8a44d, + inset 0 0 0 4px var(--color-grim-void), + inset 0 0 0 5px var(--color-grim-gold), inset 0 0 90px rgba(240,128,41,0.18), 0 28px 60px -22px rgba(0,0,0,0.95); } @@ -292,14 +229,14 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; display: inline-flex; align-items: center; gap: 0.5rem; - font-family: "IBM Plex Mono", ui-monospace, monospace; + font-family: var(--font-plex); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.18em; - color: #1a1410; + color: var(--color-grim-ink); background: linear-gradient(180deg, #f4e6c0, #d8c08a); - border: 1px solid #1a1410; - box-shadow: 0 1px 0 #c8a44d, 0 2px 0 #1a1410, 0 4px 12px -2px rgba(20,12,6,0.4); + border: 1px solid var(--color-grim-ink); + box-shadow: 0 1px 0 var(--color-grim-gold), 0 2px 0 var(--color-grim-ink), 0 4px 12px -2px rgba(20,12,6,0.4); padding: 0.55rem 1rem; transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.25s ease; cursor: pointer; @@ -307,30 +244,30 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire .spell-btn:hover { transform: translateY(-1px); background: linear-gradient(180deg, #f8edc8, #e2cd9a); - box-shadow: 0 1px 0 #c8a44d, 0 4px 0 #1a1410, 0 8px 16px -2px rgba(20,12,6,0.45); + box-shadow: 0 1px 0 var(--color-grim-gold), 0 4px 0 var(--color-grim-ink), 0 8px 16px -2px rgba(20,12,6,0.45); } .theme-grimoire .spell-btn:active { transform: translateY(2px); - box-shadow: 0 1px 0 #c8a44d, 0 0 0 #1a1410; + box-shadow: 0 1px 0 var(--color-grim-gold), 0 0 0 var(--color-grim-ink); } .theme-grimoire.dark .spell-btn { - color: #ebd9b3; - background: linear-gradient(180deg, #1a1d2a, #0e0f17); - border-color: #c8a44d; - box-shadow: 0 1px 0 rgba(200,164,77,0.4), 0 2px 0 #07080d, 0 0 18px rgba(240,128,41,0.18); + color: var(--color-grim-parchment); + background: linear-gradient(180deg, #1a1d2a, var(--color-grim-obsidian)); + border-color: var(--color-grim-gold); + box-shadow: 0 1px 0 rgba(200,164,77,0.4), 0 2px 0 var(--color-grim-void), 0 0 18px rgba(240,128,41,0.18); } .theme-grimoire.dark .spell-btn:hover { background: linear-gradient(180deg, #232636, #15172a); - box-shadow: 0 1px 0 #c8a44d, 0 4px 0 #07080d, 0 0 24px rgba(240,128,41,0.45); - color: #f08029; + box-shadow: 0 1px 0 var(--color-grim-gold), 0 4px 0 var(--color-grim-void), 0 0 24px rgba(240,128,41,0.45); + color: var(--color-grim-ember); } .theme-grimoire .spell-btn-blood { background: linear-gradient(180deg, #b1262f, #7a161e); - color: #ebd9b3; - border-color: #1a1410; + color: var(--color-grim-parchment); + border-color: var(--color-grim-ink); } .theme-grimoire .spell-btn-blood:hover { background: linear-gradient(180deg, #c93340, #8a1b24); } -.theme-grimoire.dark .spell-btn-blood { background: linear-gradient(180deg, #7a161e, #4b0d12); color: #ebd9b3; } +.theme-grimoire.dark .spell-btn-blood { background: linear-gradient(180deg, #7a161e, #4b0d12); color: var(--color-grim-parchment); } /* ICON RUNE — square icon button (theme toggle, RSS) */ .theme-grimoire .icon-rune { @@ -340,20 +277,20 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; width: 2.5rem; height: 2.5rem; background: linear-gradient(180deg, #f4e6c0, #d8c08a); - color: #1a1410; - border: 1px solid #1a1410; - box-shadow: 0 1px 0 #c8a44d, 0 2px 0 #1a1410; + color: var(--color-grim-ink); + border: 1px solid var(--color-grim-ink); + box-shadow: 0 1px 0 var(--color-grim-gold), 0 2px 0 var(--color-grim-ink); transition: all 0.15s ease; cursor: pointer; } -.theme-grimoire .icon-rune:hover { color: #8b1d27; transform: translateY(-1px); } +.theme-grimoire .icon-rune:hover { color: var(--color-grim-blood); transform: translateY(-1px); } .theme-grimoire.dark .icon-rune { - background: linear-gradient(180deg, #1a1d2a, #0e0f17); - color: #c8a44d; - border-color: #c8a44d; + background: linear-gradient(180deg, #1a1d2a, var(--color-grim-obsidian)); + color: var(--color-grim-gold); + border-color: var(--color-grim-gold); box-shadow: 0 0 14px rgba(240,128,41,0.25); } -.theme-grimoire.dark .icon-rune:hover { color: #f08029; box-shadow: 0 0 22px rgba(240,128,41,0.55); } +.theme-grimoire.dark .icon-rune:hover { color: var(--color-grim-ember); box-shadow: 0 0 22px rgba(240,128,41,0.55); } /* WAX SEAL — tag pills */ .theme-grimoire .wax-seal { @@ -362,7 +299,7 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; justify-content: center; min-width: 2.25rem; padding: 0.5rem 0.9rem; - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 700; font-size: 0.68rem; text-transform: uppercase; @@ -370,8 +307,8 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; color: #f6ead0; background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), - #8b1d27; - border: 1px solid #1a1410; + var(--color-grim-blood); + border: 1px solid var(--color-grim-ink); border-radius: 999px; box-shadow: inset 0 -2px 4px rgba(0,0,0,0.4), @@ -383,49 +320,49 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; } .theme-grimoire .wax-seal:hover { transform: rotate(0deg) scale(1.05); } .theme-grimoire.dark .wax-seal { - border-color: #c8a44d; + border-color: var(--color-grim-gold); box-shadow: inset 0 -2px 4px rgba(0,0,0,0.55), inset 0 2px 2px rgba(200,164,77,0.25), 0 0 16px rgba(240,128,41,0.2); } -.theme-grimoire .wax-seal-blood { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #8b1d27; } -.theme-grimoire .wax-seal-arcane { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #6a4cab; } +.theme-grimoire .wax-seal-blood { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), var(--color-grim-blood); } +.theme-grimoire .wax-seal-arcane { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), var(--color-grim-arcane); } .theme-grimoire .wax-seal-ember { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #b65a13; color: #fff3d8; } .theme-grimoire .wax-seal-phosphor { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35), transparent 50%), #2aa758; } -.theme-grimoire .wax-seal-gold { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.4), transparent 50%), #c8a44d; color: #1a1410; text-shadow: 0 1px 0 rgba(255,255,255,0.25); } -.theme-grimoire .wax-seal-ichor { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.3), transparent 50%), #2d1b3a; } +.theme-grimoire .wax-seal-gold { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.4), transparent 50%), var(--color-grim-gold); color: var(--color-grim-ink); text-shadow: 0 1px 0 rgba(255,255,255,0.25); } +.theme-grimoire .wax-seal-ichor { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.3), transparent 50%), var(--color-grim-ichor); } /* HEADINGS — Matrix-minimal monospace, no ornate text-shadow */ .theme-grimoire .h-blackletter { - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 800; - color: #8b1d27; + color: var(--color-grim-blood); line-height: 1.0; letter-spacing: -0.04em; text-transform: uppercase; font-feature-settings: "ss02" on, "calt" on; } .theme-grimoire.dark .h-blackletter { - color: #c8a44d; + color: var(--color-grim-gold); text-shadow: 0 0 10px rgba(200,164,77,0.35), 0 0 28px rgba(240,128,41,0.12); } .theme-grimoire .h-engraved { - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-engraved); font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; - color: #3a2f25; + color: var(--color-grim-shadow); } -.theme-grimoire.dark .h-engraved { color: #c8a44d; } +.theme-grimoire.dark .h-engraved { color: var(--color-grim-gold); } /* RUNE DIVIDER — horizontal rule with center label */ .theme-grimoire .rune-divider { display: flex; align-items: center; gap: 0.9rem; - color: #8b1d27; - font-family: "JetBrains Mono", ui-monospace, monospace; + color: var(--color-grim-blood); + font-family: var(--font-blackletter); font-size: 0.72rem; letter-spacing: 0.4em; text-transform: uppercase; @@ -438,7 +375,7 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; height: 1px; background: linear-gradient(90deg, transparent, rgba(139,29,39,0.6), transparent); } -.theme-grimoire.dark .rune-divider { color: #c8a44d; } +.theme-grimoire.dark .rune-divider { color: var(--color-grim-gold); } .theme-grimoire.dark .rune-divider::before, .theme-grimoire.dark .rune-divider::after { background: linear-gradient(90deg, transparent, rgba(200,164,77,0.6), transparent); @@ -449,23 +386,23 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; content: "▮"; display: inline-block; margin-left: 0.35rem; - color: #f08029; - animation: grimoire-blink 1s steps(2, start) infinite; + color: var(--color-grim-ember); + animation: var(--animate-blink); } -.theme-grimoire.dark .cursor-blink::after { color: #57f287; } +.theme-grimoire.dark .cursor-blink::after { color: var(--color-grim-phosphor); } /* ARC LINK — subtle inline link styling */ .theme-grimoire .arc-link { - color: #8b1d27; + color: var(--color-grim-blood); text-decoration: underline; text-decoration-style: dotted; text-decoration-thickness: 1px; text-underline-offset: 3px; transition: color 0.15s ease; } -.theme-grimoire .arc-link:hover { color: #f08029; text-decoration-style: solid; } -.theme-grimoire.dark .arc-link { color: #c8a44d; } -.theme-grimoire.dark .arc-link:hover { color: #f08029; } +.theme-grimoire .arc-link:hover { color: var(--color-grim-ember); text-decoration-style: solid; } +.theme-grimoire.dark .arc-link { color: var(--color-grim-gold); } +.theme-grimoire.dark .arc-link:hover { color: var(--color-grim-ember); } /* SUMMONING CIRCLE — decorative SVG container */ .theme-grimoire .summoning-circle { @@ -488,12 +425,12 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire .grim-marquee { overflow: hidden; white-space: nowrap; - background: linear-gradient(180deg, #07080d, #0e0f17); - color: #c8a44d; - border-top: 1px solid #c8a44d; - border-bottom: 1px solid #c8a44d; + background: linear-gradient(180deg, var(--color-grim-void), var(--color-grim-obsidian)); + color: var(--color-grim-gold); + border-top: 1px solid var(--color-grim-gold); + border-bottom: 1px solid var(--color-grim-gold); padding: 0.55rem 0; - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 500; font-size: 0.74rem; letter-spacing: 0.22em; @@ -504,34 +441,34 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; animation: grimoire-marquee 38s linear infinite; } .theme-grimoire .grim-marquee__token { display: inline-block; padding: 0 1.5rem; } -.theme-grimoire .grim-marquee__token span { color: #f08029; padding-right: 1.5rem; } +.theme-grimoire .grim-marquee__token span { color: var(--color-grim-ember); padding-right: 1.5rem; } /* INCANT OVERLAY — Konami code easter egg */ .theme-grimoire .incant-overlay { position: fixed; inset: 0; background: radial-gradient(circle, rgba(7,8,13,0.85), rgba(7,8,13,0.98)); - color: #c8a44d; + color: var(--color-grim-gold); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 9999; - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); text-align: center; animation: grimoire-incant 1.2s ease-out forwards; } .theme-grimoire .incant-overlay .incant-title { - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 800; letter-spacing: -0.02em; font-size: clamp(2.5rem, 9vw, 6rem); - color: #f08029; + color: var(--color-grim-ember); text-shadow: 0 0 24px rgba(240,128,41,0.65), 0 0 60px rgba(240,128,41,0.35); } .theme-grimoire .incant-overlay .incant-sub { - font-family: "IBM Plex Mono", monospace; - color: #57f287; + font-family: var(--font-plex); + color: var(--color-grim-phosphor); letter-spacing: 0.4em; margin-top: 1rem; text-transform: uppercase; @@ -542,28 +479,28 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; ---------------------------------------------------------------------- */ .theme-grimoire .prose-grimoire { - font-family: "Inter", system-ui, -apple-system, sans-serif; + font-family: var(--font-sans); font-size: 1.05rem; line-height: 1.72; - color: #1a1410; + color: var(--color-grim-ink); font-feature-settings: "ss01" on, "cv11" on, "kern", "liga"; } -.theme-grimoire.dark .prose-grimoire { color: #e9e0c8; } +.theme-grimoire.dark .prose-grimoire { color: var(--color-grim-bone); } /* Matrix drop cap — chunky monospace block letter, thin underline */ .theme-grimoire .prose-grimoire > p:first-of-type::first-letter { - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 800; float: left; font-size: 4.4rem; line-height: 0.95; padding: 0.15rem 0.55rem 0.15rem 0; margin-right: 0.25rem; - color: #8b1d27; + color: var(--color-grim-blood); border-bottom: 2px solid currentColor; } .theme-grimoire.dark .prose-grimoire > p:first-of-type::first-letter { - color: #57f287; + color: var(--color-grim-phosphor); text-shadow: 0 0 10px rgba(87,242,135,0.45), 0 0 24px rgba(87,242,135,0.18); border-bottom-color: rgba(87,242,135,0.55); } @@ -571,45 +508,45 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire .prose-grimoire h1, .theme-grimoire .prose-grimoire h2, .theme-grimoire .prose-grimoire h3 { - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 700; margin-top: 2em; margin-bottom: 0.6em; letter-spacing: -0.02em; - color: #3a2f25; + color: var(--color-grim-shadow); text-transform: uppercase; } .theme-grimoire.dark .prose-grimoire h1, .theme-grimoire.dark .prose-grimoire h2, -.theme-grimoire.dark .prose-grimoire h3 { color: #c8a44d; } +.theme-grimoire.dark .prose-grimoire h3 { color: var(--color-grim-gold); } .theme-grimoire .prose-grimoire h1 { font-size: 1.65rem; - border-bottom: 1px solid #8b1d27; + border-bottom: 1px solid var(--color-grim-blood); padding-bottom: 0.35em; } -.theme-grimoire .prose-grimoire h2 { font-size: 1.35rem; color: #8b1d27; } +.theme-grimoire .prose-grimoire h2 { font-size: 1.35rem; color: var(--color-grim-blood); } .theme-grimoire .prose-grimoire h3 { font-size: 1.1rem; } -.theme-grimoire.dark .prose-grimoire h1 { border-bottom-color: #c8a44d; } -.theme-grimoire.dark .prose-grimoire h2 { color: #f08029; } +.theme-grimoire.dark .prose-grimoire h1 { border-bottom-color: var(--color-grim-gold); } +.theme-grimoire.dark .prose-grimoire h2 { color: var(--color-grim-ember); } .theme-grimoire .prose-grimoire p { margin-bottom: 1.1em; } -.theme-grimoire .prose-grimoire strong { color: #8b1d27; font-weight: 800; } -.theme-grimoire.dark .prose-grimoire strong { color: #f08029; } +.theme-grimoire .prose-grimoire strong { color: var(--color-grim-blood); font-weight: 800; } +.theme-grimoire.dark .prose-grimoire strong { color: var(--color-grim-ember); } -.theme-grimoire .prose-grimoire em { font-style: italic; color: #6a4cab; } -.theme-grimoire.dark .prose-grimoire em { color: #c8a44d; } +.theme-grimoire .prose-grimoire em { font-style: italic; color: var(--color-grim-arcane); } +.theme-grimoire.dark .prose-grimoire em { color: var(--color-grim-gold); } .theme-grimoire .prose-grimoire a { - color: #8b1d27; + color: var(--color-grim-blood); text-decoration: underline; text-decoration-style: dotted; text-decoration-thickness: 1px; text-underline-offset: 3px; } -.theme-grimoire .prose-grimoire a:hover { color: #f08029; text-decoration-style: solid; } -.theme-grimoire.dark .prose-grimoire a { color: #c8a44d; } -.theme-grimoire.dark .prose-grimoire a:hover { color: #f08029; } +.theme-grimoire .prose-grimoire a:hover { color: var(--color-grim-ember); text-decoration-style: solid; } +.theme-grimoire.dark .prose-grimoire a { color: var(--color-grim-gold); } +.theme-grimoire.dark .prose-grimoire a:hover { color: var(--color-grim-ember); } .theme-grimoire .prose-grimoire ul, .theme-grimoire .prose-grimoire ol { @@ -619,55 +556,55 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire .prose-grimoire ul { list-style: none; } .theme-grimoire .prose-grimoire ul > li::before { content: "✦"; - color: #8b1d27; + color: var(--color-grim-blood); font-weight: 700; margin-right: 0.55em; display: inline-block; transform: translateY(-1px); } -.theme-grimoire.dark .prose-grimoire ul > li::before { color: #c8a44d; } +.theme-grimoire.dark .prose-grimoire ul > li::before { color: var(--color-grim-gold); } .theme-grimoire .prose-grimoire ol { list-style: decimal; } .theme-grimoire .prose-grimoire ol::marker { - color: #8b1d27; - font-family: "JetBrains Mono", ui-monospace, monospace; + color: var(--color-grim-blood); + font-family: var(--font-blackletter); font-weight: 700; } .theme-grimoire .prose-grimoire li { margin-bottom: 0.4em; } .theme-grimoire .prose-grimoire blockquote { position: relative; - border-left: 2px solid #8b1d27; + border-left: 2px solid var(--color-grim-blood); background: rgba(200,164,77,0.10); padding: 1em 1.2em 1em 2.4em; margin: 1.5em 0; font-style: italic; - color: #3a2f25; + color: var(--color-grim-shadow); } .theme-grimoire .prose-grimoire blockquote::before { content: ">"; position: absolute; left: 0.7em; top: 1em; - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 700; font-style: normal; font-size: 1.05em; - color: #8b1d27; + color: var(--color-grim-blood); line-height: 1; } .theme-grimoire.dark .prose-grimoire blockquote { - border-left-color: #c8a44d; + border-left-color: var(--color-grim-gold); background: rgba(106,76,171,0.10); - color: #e9e0c8; + color: var(--color-grim-bone); } -.theme-grimoire.dark .prose-grimoire blockquote::before { color: #c8a44d; } +.theme-grimoire.dark .prose-grimoire blockquote::before { color: var(--color-grim-gold); } /* Code blocks — wizard's terminal (phosphor green on void) */ .theme-grimoire .prose-grimoire pre, .theme-grimoire pre.highlight { - background: #07080d !important; - color: #57f287 !important; - border: 1px solid #c8a44d; + background: var(--color-grim-void) !important; + color: var(--color-grim-phosphor) !important; + border: 1px solid var(--color-grim-gold); box-shadow: inset 0 0 80px rgba(106,76,171,0.18), 0 0 22px rgba(87,242,135,0.18), @@ -675,7 +612,7 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; padding: 1rem 1.1rem; margin: 1.5em 0; overflow-x: auto; - font-family: "IBM Plex Mono", ui-monospace, monospace; + font-family: var(--font-plex); font-size: 0.92rem; line-height: 1.55; position: relative; @@ -684,16 +621,16 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire pre.highlight::before { content: "▒▓ ~/grimoire/spells $ cast"; display: block; - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-weight: 500; font-size: 0.66rem; - color: #c8a44d; + color: var(--color-grim-gold); letter-spacing: 0.12em; text-transform: uppercase; margin: -1rem -1.1rem 0.8rem; padding: 0.55rem 0.9rem; - background: #0e0f17; - border-bottom: 1px solid #c8a44d; + background: var(--color-grim-obsidian); + border-bottom: 1px solid var(--color-grim-gold); } .theme-grimoire .prose-grimoire pre::after, .theme-grimoire pre.highlight::after { @@ -710,9 +647,9 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire .prose-grimoire code, .theme-grimoire code:not(pre code) { - font-family: "IBM Plex Mono", ui-monospace, monospace; + font-family: var(--font-plex); background: rgba(139,29,39,0.10); - color: #8b1d27; + color: var(--color-grim-blood); padding: 0 6px; border: 1px solid rgba(139,29,39,0.35); font-size: 0.92em; @@ -721,7 +658,7 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; .theme-grimoire.dark .prose-grimoire code, .theme-grimoire.dark code:not(pre code) { background: rgba(200,164,77,0.10); - color: #c8a44d; + color: var(--color-grim-gold); border-color: rgba(200,164,77,0.4); } .theme-grimoire .prose-grimoire pre code, @@ -746,18 +683,18 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; } .theme-grimoire .prose-grimoire img { - border: 1px solid #1a1410; + border: 1px solid var(--color-grim-ink); box-shadow: - inset 0 0 0 4px #ebd9b3, - inset 0 0 0 5px #c8a44d, + inset 0 0 0 4px var(--color-grim-parchment), + inset 0 0 0 5px var(--color-grim-gold), 0 18px 36px -16px rgba(20,12,6,0.55); margin: 1.5em 0; } .theme-grimoire.dark .prose-grimoire img { - border-color: #c8a44d; + border-color: var(--color-grim-gold); box-shadow: - inset 0 0 0 4px #07080d, - inset 0 0 0 5px #c8a44d, + inset 0 0 0 4px var(--color-grim-void), + inset 0 0 0 5px var(--color-grim-gold), 0 18px 36px -16px rgba(0,0,0,0.85); } @@ -765,38 +702,36 @@ html.theme-grimoire.dark ::-webkit-scrollbar-thumb:hover { background: #f08029; width: 100%; border-collapse: collapse; margin: 1.4em 0; - font-family: "JetBrains Mono", ui-monospace, monospace; + font-family: var(--font-blackletter); font-size: 0.86rem; } .theme-grimoire .prose-grimoire th, .theme-grimoire .prose-grimoire td { - border: 1px solid #1a1410; + border: 1px solid var(--color-grim-ink); padding: 0.55rem 0.8rem; text-align: left; } .theme-grimoire .prose-grimoire th { - background: #c8a44d; - color: #1a1410; + background: var(--color-grim-gold); + color: var(--color-grim-ink); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 700; } .theme-grimoire.dark .prose-grimoire th { - background: #2d1b3a; - color: #c8a44d; - border-color: #c8a44d; + background: var(--color-grim-ichor); + color: var(--color-grim-gold); + border-color: var(--color-grim-gold); } .theme-grimoire.dark .prose-grimoire td { border-color: rgba(200,164,77,0.4); } /* ---------------------------------------------------------------------- - Keyframes (prefixed with `grimoire-` to avoid clashing with other themes) + Keyframes that are theme-local. The shared `theme-blink` keyframe + (registered in @theme as `--animate-blink`) is also available via the + `animate-blink` utility, but `.cursor-blink` and others reference + `var(--animate-blink)` directly to keep this file self-contained. ---------------------------------------------------------------------- */ -@keyframes grimoire-blink { - 0%, 49% { opacity: 1; } - 50%, 100% { opacity: 0; } -} - @keyframes grimoire-marquee { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } diff --git a/app/assets/stylesheets/themes/retro.css b/app/assets/stylesheets/themes/retro.css index 71c1e4b..b921ad5 100644 --- a/app/assets/stylesheets/themes/retro.css +++ b/app/assets/stylesheets/themes/retro.css @@ -2,33 +2,65 @@ Abbey – Retro Theme Memphis / 8-bit / 80s computer aesthetic. - Pure CSS – served by Propshaft, loaded only when - `Rails.application.config.theme == "retro"`. All declarations are scoped - under `.theme-retro` (set on by themes/retro/layouts/application.html.erb) - so loading this file in another context is a no-op. + Loaded only when `Rails.application.config.theme == "retro"`. All + declarations are scoped under `.theme-retro` (set on by + themes/retro/layouts/application.html.erb) so loading this file in any + other context is a no-op. + + Color/shadow/font/animation TOKENS live in `app/assets/tailwind/application.css` + under `@theme`, which makes Tailwind auto-generate the matching utility + classes (`bg-memphis-pink`, `shadow-retro-lg`, `dark:text-memphis-mint`, + `hover:bg-memphis-pink`, `animate-blink`, `text-[0.62rem]`, etc.) — no + hand-rolled `.theme-retro .bg-foo-bar` declarations needed. + + This file ships: + 1. theme-root environmental styles (page bg, drifting confetti backdrop, + CRT scanlines, selection color) + 2. font-family rebinding inside `.theme-retro` so utilities like + `font-sans`, `font-mono`, `font-display` resolve to the retro stack + 3. component classes (`.card-retro`, `.btn-retro`, `.tag-retro`, + `.h-display`, `.crt-window`, `.marquee`, `.date-sticker`, + `.glitch-hover`, `.cursor-blink`) + 4. typography for the `.prose-retro` wrapper (h1-h3 with hard + text-shadows, wavy-underline links, highlighted , terminal +
     blocks, Memphis tables, dotted SVG 
    ) + + Palette: ink #0d0d12 · paper #fff8ef · crt #0a0e1a · + pink #ff3eb5 · cyan #00e5ff · yellow #ffd400 · + mint #00ff9c · purple #b14aff · coral #ff6b4a +============================================================================= */ - Custom palette: - ink #0d0d12 paper #fff8ef crt #0a0e1a - pink #ff3eb5 cyan #00e5ff yellow #ffd400 - mint #00ff9c purple #b14aff coral #ff6b4a - =============================================================================*/ +/* ---------------------------------------------------------------------- + Font-family rebinding via CSS variable cascade. + Tailwind generates `.font-sans`, `.font-mono`, `.font-display` as + `{ font-family: var(--font-sans|mono|display) }`. Redefining those + variables on `.theme-retro` makes every `font-*` utility inside the + retro theme resolve to the retro font stack — without touching the + default theme. +---------------------------------------------------------------------- */ + +.theme-retro { + --font-sans: "Space Grotesk", system-ui, sans-serif; + --font-mono: "VT323", ui-monospace, monospace; + --font-display: "Press Start 2P", system-ui, sans-serif; +} /* ---------------------------------------------------------------------- Base – body backdrop, CRT scanlines, selection ---------------------------------------------------------------------- */ html.theme-retro { - background-color: #fff8ef; - color: #0d0d12; + background-color: var(--color-memphis-paper); + color: var(--color-memphis-ink); image-rendering: pixelated; } html.theme-retro.dark { - background-color: #0a0e1a; + background-color: var(--color-memphis-crt); color: #f0fff7; } html.theme-retro body { - font-family: "Space Grotesk", system-ui, sans-serif; + font-family: var(--font-sans); font-feature-settings: "ss01" on, "ss02" on; position: relative; overflow-x: hidden; @@ -89,127 +121,21 @@ html.theme-retro .content-layer { } html.theme-retro ::selection { - background: #ff3eb5; - color: #fff8ef; + background: var(--color-memphis-pink); + color: var(--color-memphis-paper); } html.theme-retro.dark ::selection { - background: #00ff9c; - color: #0a0e1a; + background: var(--color-memphis-mint); + color: var(--color-memphis-crt); } /* ---------------------------------------------------------------------- - Utility-style color/shadow/font classes used inline in retro views. - Scoped to `.theme-retro` so they cannot leak into the default theme, - and intentionally redundant with the dedicated component classes so - markup remains expressive without requiring Tailwind config edits. ----------------------------------------------------------------------- */ - -.theme-retro .bg-memphis-paper { background-color: #fff8ef; } -.theme-retro .bg-memphis-ink { background-color: #0d0d12; } -.theme-retro .bg-memphis-crt { background-color: #0a0e1a; } -.theme-retro .bg-memphis-pink { background-color: #ff3eb5; } -.theme-retro .bg-memphis-cyan { background-color: #00e5ff; } -.theme-retro .bg-memphis-yellow { background-color: #ffd400; } -.theme-retro .bg-memphis-mint { background-color: #00ff9c; } -.theme-retro .bg-memphis-purple { background-color: #b14aff; } -.theme-retro .bg-memphis-coral { background-color: #ff6b4a; } - -.theme-retro .text-memphis-ink { color: #0d0d12; } -.theme-retro .text-memphis-paper { color: #fff8ef; } -.theme-retro .text-memphis-pink { color: #ff3eb5; } -.theme-retro .text-memphis-cyan { color: #00e5ff; } -.theme-retro .text-memphis-yellow { color: #ffd400; } -.theme-retro .text-memphis-mint { color: #00ff9c; } -.theme-retro .text-memphis-purple { color: #b14aff; } -.theme-retro .text-memphis-coral { color: #ff6b4a; } - -.theme-retro .border-memphis-ink { border-color: #0d0d12; } -.theme-retro .border-memphis-pink { border-color: #ff3eb5; } -.theme-retro .border-memphis-mint { border-color: #00ff9c; } -.theme-retro .border-memphis-cyan { border-color: #00e5ff; } -.theme-retro .border-memphis-yellow{ border-color: #ffd400; } - -.theme-retro .shadow-retro-sm { box-shadow: 4px 4px 0 0 #0d0d12; } -.theme-retro .shadow-retro { box-shadow: 6px 6px 0 0 #0d0d12; } -.theme-retro .shadow-retro-lg { box-shadow: 10px 10px 0 0 #0d0d12; } -.theme-retro .shadow-retro-pink { box-shadow: 6px 6px 0 0 #ff3eb5; } -.theme-retro .shadow-retro-cyan { box-shadow: 6px 6px 0 0 #00e5ff; } -.theme-retro .shadow-retro-yellow { box-shadow: 6px 6px 0 0 #ffd400; } -.theme-retro .shadow-retro-mint { box-shadow: 6px 6px 0 0 #00ff9c; } -.theme-retro .shadow-retro-purple { box-shadow: 6px 6px 0 0 #b14aff; } -.theme-retro .shadow-retro-coral { box-shadow: 6px 6px 0 0 #ff6b4a; } - -.theme-retro .font-display { font-family: "Press Start 2P", system-ui, sans-serif; } -.theme-retro .font-mono { font-family: "VT323", ui-monospace, monospace; } -.theme-retro .font-sans { font-family: "Space Grotesk", system-ui, sans-serif; } - -.theme-retro .animate-blink { animation: retro-blink 1s steps(2, start) infinite; } -.theme-retro .animate-wiggle { animation: retro-wiggle 0.4s ease-in-out; } -.theme-retro .animate-pop-in { animation: retro-pop-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; } - -/* Dark-mode variants of the above (mirrors Tailwind dark: prefix when - carries both `theme-retro` and `dark` classes). */ -.theme-retro.dark .dark\:bg-memphis-paper { background-color: #fff8ef; } -.theme-retro.dark .dark\:bg-memphis-ink { background-color: #0d0d12; } -.theme-retro.dark .dark\:bg-memphis-crt { background-color: #0a0e1a; } -.theme-retro.dark .dark\:bg-memphis-pink { background-color: #ff3eb5; } -.theme-retro.dark .dark\:bg-memphis-cyan { background-color: #00e5ff; } -.theme-retro.dark .dark\:bg-memphis-yellow { background-color: #ffd400; } -.theme-retro.dark .dark\:bg-memphis-mint { background-color: #00ff9c; } -.theme-retro.dark .dark\:bg-memphis-purple { background-color: #b14aff; } -.theme-retro.dark .dark\:bg-memphis-coral { background-color: #ff6b4a; } -.theme-retro.dark .dark\:bg-black { background-color: #000000; } - -.theme-retro.dark .dark\:text-memphis-paper { color: #fff8ef; } -.theme-retro.dark .dark\:text-memphis-ink { color: #0d0d12; } -.theme-retro.dark .dark\:text-memphis-pink { color: #ff3eb5; } -.theme-retro.dark .dark\:text-memphis-cyan { color: #00e5ff; } -.theme-retro.dark .dark\:text-memphis-yellow { color: #ffd400; } -.theme-retro.dark .dark\:text-memphis-mint { color: #00ff9c; } -.theme-retro.dark .dark\:text-memphis-purple { color: #b14aff; } - -.theme-retro.dark .dark\:border-memphis-mint { border-color: #00ff9c; } -.theme-retro.dark .dark\:border-memphis-pink { border-color: #ff3eb5; } -.theme-retro.dark .dark\:border-memphis-cyan { border-color: #00e5ff; } -.theme-retro.dark .dark\:shadow-none { box-shadow: none; } - -/* Hover & group-hover Memphis color variants - (mirror Tailwind `hover:text-memphis-pink` syntax without requiring - Memphis tokens in the Tailwind config). */ -.theme-retro .hover\:text-memphis-pink:hover { color: #ff3eb5; } -.theme-retro .hover\:text-memphis-cyan:hover { color: #00e5ff; } -.theme-retro .hover\:text-memphis-mint:hover { color: #00ff9c; } -.theme-retro .hover\:text-memphis-yellow:hover { color: #ffd400; } -.theme-retro .hover\:bg-memphis-pink:hover { background-color: #ff3eb5; } -.theme-retro .hover\:text-memphis-paper:hover { color: #fff8ef; } - -.theme-retro.dark .dark\:hover\:text-memphis-mint:hover { color: #00ff9c; } -.theme-retro.dark .dark\:hover\:text-memphis-cyan:hover { color: #00e5ff; } -.theme-retro.dark .dark\:hover\:text-memphis-yellow:hover { color: #ffd400; } -.theme-retro.dark .dark\:hover\:text-memphis-pink:hover { color: #ff3eb5; } - -.theme-retro .group:hover .group-hover\:animate-wiggle { animation: retro-wiggle 0.4s ease-in-out; } -.theme-retro .group:hover .group-hover\:text-memphis-pink { color: #ff3eb5; } -.theme-retro.dark .group:hover .dark\:group-hover\:text-memphis-yellow { color: #ffd400; } - -/* Color with alpha (mirrors Tailwind `text-memphis-paper/90` opacity syntax). */ -.theme-retro .text-memphis-ink\/40 { color: rgba(13, 13, 18, 0.4); } -.theme-retro .text-memphis-paper\/90 { color: rgba(255, 248, 239, 0.9); } -.theme-retro.dark .dark\:text-memphis-mint\/40 { color: rgba(0, 255, 156, 0.4); } -.theme-retro.dark .dark\:text-memphis-paper\/90 { color: rgba(255, 248, 239, 0.9); } - -/* ---------------------------------------------------------------------- - Keyframes + Keyframes that are theme-local (drift/glitch don't need utilities). + Note: blink / wiggle / pop-in / marquee are registered in @theme so + Tailwind generates `animate-blink`, `animate-wiggle`, etc. and inlines + the keyframes — they're not duplicated here. ---------------------------------------------------------------------- */ -@keyframes retro-blink { - 0%, 49% { opacity: 1; } - 50%, 100% { opacity: 0; } -} -@keyframes retro-marquee { - from { transform: translateX(0); } - to { transform: translateX(-50%); } -} @keyframes retro-drift { 0%, 100% { background-position: 0 0; } 50% { background-position: 80px 60px; } @@ -222,15 +148,6 @@ html.theme-retro.dark ::selection { 80% { transform: translate(2px, 1px); } 100% { transform: translate(0, 0); } } -@keyframes retro-pop-in { - 0% { opacity: 0; transform: translateY(8px) scale(0.96); } - 100% { opacity: 1; transform: translateY(0) scale(1); } -} -@keyframes retro-wiggle { - 0%, 100% { transform: rotate(0deg); } - 25% { transform: rotate(-3deg); } - 75% { transform: rotate(3deg); } -} /* ---------------------------------------------------------------------- Cards / containers @@ -238,30 +155,30 @@ html.theme-retro.dark ::selection { .theme-retro .card-retro { background: #ffffff; - border: 3px solid #0d0d12; - box-shadow: 6px 6px 0 0 #0d0d12; + border: 3px solid var(--color-memphis-ink); + box-shadow: var(--shadow-retro); padding: 1.5rem; transition: transform 200ms ease, box-shadow 200ms ease; } .theme-retro.dark .card-retro { - background: #0a0e1a; - border-color: #00ff9c; - box-shadow: 6px 6px 0 0 #00ff9c; + background: var(--color-memphis-crt); + border-color: var(--color-memphis-mint); + box-shadow: var(--shadow-retro-mint); } .theme-retro .card-retro:hover { transform: translate(-2px, -2px); - box-shadow: 10px 10px 0 0 #0d0d12; + box-shadow: var(--shadow-retro-lg); } .theme-retro.dark .card-retro:hover { - box-shadow: 10px 10px 0 0 #00ff9c; + box-shadow: 10px 10px 0 0 var(--color-memphis-mint); } -.theme-retro .card-shadow-pink { box-shadow: 6px 6px 0 0 #ff3eb5; } -.theme-retro .card-shadow-cyan { box-shadow: 6px 6px 0 0 #00e5ff; } -.theme-retro .card-shadow-yellow { box-shadow: 6px 6px 0 0 #ffd400; } -.theme-retro .card-shadow-mint { box-shadow: 6px 6px 0 0 #00ff9c; } -.theme-retro .card-shadow-purple { box-shadow: 6px 6px 0 0 #b14aff; } -.theme-retro .card-shadow-coral { box-shadow: 6px 6px 0 0 #ff6b4a; } +.theme-retro .card-shadow-pink { box-shadow: var(--shadow-retro-pink); } +.theme-retro .card-shadow-cyan { box-shadow: var(--shadow-retro-cyan); } +.theme-retro .card-shadow-yellow { box-shadow: var(--shadow-retro-yellow); } +.theme-retro .card-shadow-mint { box-shadow: var(--shadow-retro-mint); } +.theme-retro .card-shadow-purple { box-shadow: var(--shadow-retro-purple); } +.theme-retro .card-shadow-coral { box-shadow: var(--shadow-retro-coral); } /* ---------------------------------------------------------------------- Buttons @@ -271,32 +188,32 @@ html.theme-retro.dark ::selection { display: inline-flex; align-items: center; gap: 0.5rem; - font-family: "Press Start 2P", system-ui, sans-serif; + font-family: var(--font-display); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; - background: #ffd400; - color: #0d0d12; - border: 3px solid #0d0d12; + background: var(--color-memphis-yellow); + color: var(--color-memphis-ink); + border: 3px solid var(--color-memphis-ink); + box-shadow: var(--shadow-retro); padding: 0.5rem 1rem; - box-shadow: 6px 6px 0 0 #0d0d12; transition: transform 150ms ease, box-shadow 150ms ease; cursor: pointer; } .theme-retro .btn-retro:hover { transform: translate(2px, 2px); - box-shadow: 4px 4px 0 0 #0d0d12; + box-shadow: var(--shadow-retro-sm); } .theme-retro .btn-retro:active { transform: translate(4px, 4px); box-shadow: 0 0 0 0 transparent; } -.theme-retro .btn-retro-pink { background: #ff3eb5; color: #fff8ef; } -.theme-retro .btn-retro-cyan { background: #00e5ff; color: #0d0d12; } -.theme-retro .btn-retro-mint { background: #00ff9c; color: #0d0d12; } -.theme-retro .btn-retro-purple { background: #b14aff; color: #fff8ef; } -.theme-retro .btn-retro-coral { background: #ff6b4a; color: #fff8ef; } -.theme-retro .btn-retro-ghost { background: #fff8ef; color: #0d0d12; } +.theme-retro .btn-retro-pink { background: var(--color-memphis-pink); color: var(--color-memphis-paper); } +.theme-retro .btn-retro-cyan { background: var(--color-memphis-cyan); color: var(--color-memphis-ink); } +.theme-retro .btn-retro-mint { background: var(--color-memphis-mint); color: var(--color-memphis-ink); } +.theme-retro .btn-retro-purple { background: var(--color-memphis-purple); color: var(--color-memphis-paper); } +.theme-retro .btn-retro-coral { background: var(--color-memphis-coral); color: var(--color-memphis-paper); } +.theme-retro .btn-retro-ghost { background: var(--color-memphis-paper); color: var(--color-memphis-ink); } .theme-retro .btn-icon-retro { display: inline-flex; @@ -304,10 +221,10 @@ html.theme-retro.dark ::selection { justify-content: center; width: 2.5rem; height: 2.5rem; - background: #fff8ef; - color: #0d0d12; - border: 3px solid #0d0d12; - box-shadow: 4px 4px 0 0 #0d0d12; + background: var(--color-memphis-paper); + color: var(--color-memphis-ink); + border: 3px solid var(--color-memphis-ink); + box-shadow: var(--shadow-retro-sm); transition: transform 150ms ease, box-shadow 150ms ease; cursor: pointer; } @@ -316,9 +233,9 @@ html.theme-retro.dark ::selection { box-shadow: 0 0 0 0 transparent; } .theme-retro.dark .btn-icon-retro { - background: #0a0e1a; - color: #00ff9c; - border-color: #00ff9c; + background: var(--color-memphis-crt); + color: var(--color-memphis-mint); + border-color: var(--color-memphis-mint); box-shadow: none; } @@ -329,38 +246,38 @@ html.theme-retro.dark ::selection { .theme-retro .tag-retro { display: inline-flex; align-items: center; - font-family: "VT323", ui-monospace, monospace; + font-family: var(--font-mono); font-size: 1rem; line-height: 1; padding: 0.25rem 0.75rem; - border: 2px solid #0d0d12; - box-shadow: 4px 4px 0 0 #0d0d12; + border: 2px solid var(--color-memphis-ink); + box-shadow: var(--shadow-retro-sm); transition: transform 150ms ease; } .theme-retro .tag-retro:hover { transform: rotate(-2deg) scale(1.05); } -.theme-retro .tag-color-1 { background: #ff3eb5; color: #fff8ef; } -.theme-retro .tag-color-2 { background: #00e5ff; color: #0d0d12; } -.theme-retro .tag-color-3 { background: #ffd400; color: #0d0d12; } -.theme-retro .tag-color-4 { background: #00ff9c; color: #0d0d12; } -.theme-retro .tag-color-5 { background: #b14aff; color: #fff8ef; } -.theme-retro .tag-color-6 { background: #ff6b4a; color: #fff8ef; } +.theme-retro .tag-color-1 { background: var(--color-memphis-pink); color: var(--color-memphis-paper); } +.theme-retro .tag-color-2 { background: var(--color-memphis-cyan); color: var(--color-memphis-ink); } +.theme-retro .tag-color-3 { background: var(--color-memphis-yellow); color: var(--color-memphis-ink); } +.theme-retro .tag-color-4 { background: var(--color-memphis-mint); color: var(--color-memphis-ink); } +.theme-retro .tag-color-5 { background: var(--color-memphis-purple); color: var(--color-memphis-paper); } +.theme-retro .tag-color-6 { background: var(--color-memphis-coral); color: var(--color-memphis-paper); } /* ---------------------------------------------------------------------- Pixel-display headings ---------------------------------------------------------------------- */ .theme-retro .h-display { - font-family: "Press Start 2P", system-ui, sans-serif; - color: #0d0d12; + font-family: var(--font-display); + color: var(--color-memphis-ink); line-height: 1.4; letter-spacing: -0.02em; - text-shadow: 3px 3px 0 #ff3eb5; + text-shadow: 3px 3px 0 var(--color-memphis-pink); } .theme-retro.dark .h-display { - color: #00ff9c; - text-shadow: 3px 3px 0 #00e5ff; + color: var(--color-memphis-mint); + text-shadow: 3px 3px 0 var(--color-memphis-cyan); } .theme-retro .h-display-sm { font-size: 1rem; } .theme-retro .h-display-md { font-size: 1.25rem; } @@ -382,34 +299,34 @@ html.theme-retro.dark ::selection { content: "▮"; display: inline-block; margin-left: 0.25rem; - color: #ff3eb5; - animation: retro-blink 1s steps(2, start) infinite; + color: var(--color-memphis-pink); + animation: var(--animate-blink); } -.theme-retro.dark .cursor-blink::after { color: #00ff9c; } +.theme-retro.dark .cursor-blink::after { color: var(--color-memphis-mint); } .theme-retro .marquee { overflow: hidden; white-space: nowrap; - border-top: 3px solid #0d0d12; - border-bottom: 3px solid #0d0d12; + border-top: 3px solid var(--color-memphis-ink); + border-bottom: 3px solid var(--color-memphis-ink); background: repeating-linear-gradient( 45deg, - #ffd400 0, - #ffd400 18px, - #0d0d12 18px, - #0d0d12 36px + var(--color-memphis-yellow) 0, + var(--color-memphis-yellow) 18px, + var(--color-memphis-ink) 18px, + var(--color-memphis-ink) 36px ); padding: 4px 0; } .theme-retro .marquee__track { display: inline-block; - animation: retro-marquee 28s linear infinite; - font-family: "Press Start 2P", monospace; + animation: var(--animate-marquee); + font-family: var(--font-display); font-size: 12px; - color: #0d0d12; - background: #fff8ef; + color: var(--color-memphis-ink); + background: var(--color-memphis-paper); padding: 6px 16px; - border: 2px solid #0d0d12; + border: 2px solid var(--color-memphis-ink); } /* ---------------------------------------------------------------------- @@ -418,51 +335,47 @@ html.theme-retro.dark ::selection { .theme-retro .crt-window { position: relative; - background: #0a0e1a; - color: #00ff9c; - border: 3px solid #0d0d12; - box-shadow: 6px 6px 0 0 #0d0d12; - font-family: "VT323", ui-monospace, monospace; + background: var(--color-memphis-crt); + color: var(--color-memphis-mint); + border: 3px solid var(--color-memphis-ink); + box-shadow: var(--shadow-retro); + font-family: var(--font-mono); font-size: 0.875rem; overflow: hidden; } .theme-retro .crt-window::before { content: "● ● ●"; display: block; - background: #ff3eb5; - color: #0d0d12; - font-family: "Press Start 2P", monospace; + background: var(--color-memphis-pink); + color: var(--color-memphis-ink); + font-family: var(--font-display); font-size: 10px; letter-spacing: 4px; padding: 6px 12px; - border-bottom: 3px solid #0d0d12; + border-bottom: 3px solid var(--color-memphis-ink); } .theme-retro .glitch-hover:hover { animation: retro-glitch 0.8s steps(1) 1; } -.theme-retro .animate-pop-in { - animation: retro-pop-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - /* ---------------------------------------------------------------------- Date sticker (rotated label) ---------------------------------------------------------------------- */ .theme-retro .date-sticker { display: inline-block; - font-family: "Press Start 2P", monospace; + font-family: var(--font-display); font-size: 10px; - background: #ffd400; - color: #0d0d12; - border: 2px solid #0d0d12; + background: var(--color-memphis-yellow); + color: var(--color-memphis-ink); + border: 2px solid var(--color-memphis-ink); padding: 4px 8px; transform: rotate(-3deg); - box-shadow: 3px 3px 0 0 #0d0d12; + box-shadow: 3px 3px 0 0 var(--color-memphis-ink); } -.theme-retro .date-sticker--pink { background: #ff3eb5; color: #fff8ef; } -.theme-retro .date-sticker--mint { background: #00ff9c; color: #0d0d12; } +.theme-retro .date-sticker--pink { background: var(--color-memphis-pink); color: var(--color-memphis-paper); } +.theme-retro .date-sticker--mint { background: var(--color-memphis-mint); color: var(--color-memphis-ink); } /* ---------------------------------------------------------------------- Rendered markdown content (`.prose-retro` wrapper) @@ -470,58 +383,58 @@ html.theme-retro.dark ::selection { ---------------------------------------------------------------------- */ .theme-retro .prose-retro { - font-family: "Space Grotesk", system-ui, sans-serif; + font-family: var(--font-sans); font-size: 1.05rem; line-height: 1.75; - color: #0d0d12; + color: var(--color-memphis-ink); } .theme-retro.dark .prose-retro { color: #d6ffe9; } .theme-retro .prose-retro h1, .theme-retro .prose-retro h2, .theme-retro .prose-retro h3 { - font-family: "Press Start 2P", system-ui, sans-serif; + font-family: var(--font-display); line-height: 1.5; margin: 2em 0 0.8em; letter-spacing: -0.02em; } -.theme-retro .prose-retro h1 { font-size: 1.5rem; color: #ff3eb5; text-shadow: 3px 3px 0 #0d0d12; } -.theme-retro .prose-retro h2 { font-size: 1.15rem; color: #b14aff; text-shadow: 2px 2px 0 #ffd400; } +.theme-retro .prose-retro h1 { font-size: 1.5rem; color: var(--color-memphis-pink); text-shadow: 3px 3px 0 var(--color-memphis-ink); } +.theme-retro .prose-retro h2 { font-size: 1.15rem; color: var(--color-memphis-purple); text-shadow: 2px 2px 0 var(--color-memphis-yellow); } .theme-retro .prose-retro h3 { font-size: 0.95rem; color: #00a3cc; } -.theme-retro.dark .prose-retro h1 { color: #00ff9c; text-shadow: 3px 3px 0 #ff3eb5; } -.theme-retro.dark .prose-retro h2 { color: #ffd400; text-shadow: 2px 2px 0 #b14aff; } -.theme-retro.dark .prose-retro h3 { color: #00e5ff; } +.theme-retro.dark .prose-retro h1 { color: var(--color-memphis-mint); text-shadow: 3px 3px 0 var(--color-memphis-pink); } +.theme-retro.dark .prose-retro h2 { color: var(--color-memphis-yellow); text-shadow: 2px 2px 0 var(--color-memphis-purple); } +.theme-retro.dark .prose-retro h3 { color: var(--color-memphis-cyan); } .theme-retro .prose-retro p { margin-bottom: 1.1em; } .theme-retro .prose-retro strong { - background: #ffd400; - color: #0d0d12; + background: var(--color-memphis-yellow); + color: var(--color-memphis-ink); padding: 0 4px; - border: 2px solid #0d0d12; + border: 2px solid var(--color-memphis-ink); font-weight: 700; } .theme-retro.dark .prose-retro strong { - background: #00ff9c; - color: #0a0e1a; - border-color: #00ff9c; + background: var(--color-memphis-mint); + color: var(--color-memphis-crt); + border-color: var(--color-memphis-mint); } -.theme-retro .prose-retro em { font-style: italic; color: #b14aff; } -.theme-retro.dark .prose-retro em { color: #00e5ff; } +.theme-retro .prose-retro em { font-style: italic; color: var(--color-memphis-purple); } +.theme-retro.dark .prose-retro em { color: var(--color-memphis-cyan); } .theme-retro .prose-retro a { - color: #ff3eb5; + color: var(--color-memphis-pink); font-weight: 600; text-decoration: underline; text-decoration-style: wavy; text-decoration-thickness: 2px; text-underline-offset: 4px; } -.theme-retro .prose-retro a:hover { background: #ffd400; color: #0d0d12; } -.theme-retro.dark .prose-retro a { color: #00ff9c; } -.theme-retro.dark .prose-retro a:hover { background: #ff3eb5; color: #fff8ef; } +.theme-retro .prose-retro a:hover { background: var(--color-memphis-yellow); color: var(--color-memphis-ink); } +.theme-retro.dark .prose-retro a { color: var(--color-memphis-mint); } +.theme-retro.dark .prose-retro a:hover { background: var(--color-memphis-pink); color: var(--color-memphis-paper); } .theme-retro .prose-retro ul, .theme-retro .prose-retro ol { @@ -531,73 +444,73 @@ html.theme-retro.dark ::selection { .theme-retro .prose-retro ul { list-style: none; } .theme-retro .prose-retro ul > li::before { content: "▸ "; - color: #ff3eb5; + color: var(--color-memphis-pink); font-weight: 700; margin-right: 0.4em; } -.theme-retro.dark .prose-retro ul > li::before { color: #00ff9c; } +.theme-retro.dark .prose-retro ul > li::before { color: var(--color-memphis-mint); } .theme-retro .prose-retro ol { list-style: decimal; } .theme-retro .prose-retro li { margin-bottom: 0.4em; } .theme-retro .prose-retro blockquote { - border-left: 6px solid #ff3eb5; + border-left: 6px solid var(--color-memphis-pink); background: rgba(255, 212, 0, 0.18); padding: 1em 1.2em; margin: 1.5em 0; font-style: italic; } .theme-retro.dark .prose-retro blockquote { - border-left-color: #00ff9c; + border-left-color: var(--color-memphis-mint); background: rgba(0, 229, 255, 0.08); } .theme-retro .prose-retro pre, .theme-retro pre.highlight { - background: #0a0e1a; - color: #00ff9c; - border: 3px solid #0d0d12; - box-shadow: 6px 6px 0 0 #ff3eb5; + background: var(--color-memphis-crt); + color: var(--color-memphis-mint); + border: 3px solid var(--color-memphis-ink); + box-shadow: var(--shadow-retro-pink); padding: 1rem 1.1rem; margin: 1.5em 0; overflow-x: auto; - font-family: "VT323", ui-monospace, monospace; + font-family: var(--font-mono); font-size: 1.05rem; line-height: 1.4; position: relative; } .theme-retro.dark .prose-retro pre, .theme-retro.dark pre.highlight { - box-shadow: 6px 6px 0 0 #00e5ff; - border-color: #00ff9c; + box-shadow: var(--shadow-retro-cyan); + border-color: var(--color-memphis-mint); } .theme-retro .prose-retro pre::before, .theme-retro pre.highlight::before { content: "● TERMINAL — RUN.EXE"; display: block; - font-family: "Press Start 2P", monospace; + font-family: var(--font-display); font-size: 9px; - color: #ffd400; + color: var(--color-memphis-yellow); letter-spacing: 2px; margin: -1rem -1.1rem 0.8rem; padding: 6px 12px; - background: #0d0d12; - border-bottom: 2px solid #00ff9c; + background: var(--color-memphis-ink); + border-bottom: 2px solid var(--color-memphis-mint); } .theme-retro .prose-retro code, .theme-retro code:not(pre code) { - font-family: "VT323", ui-monospace, monospace; - background: #ffd400; - color: #0d0d12; + font-family: var(--font-mono); + background: var(--color-memphis-yellow); + color: var(--color-memphis-ink); padding: 0 6px; - border: 2px solid #0d0d12; + border: 2px solid var(--color-memphis-ink); font-size: 1em; } .theme-retro.dark .prose-retro code, .theme-retro.dark code:not(pre code) { - background: #ff3eb5; - color: #0a0e1a; - border-color: #0a0e1a; + background: var(--color-memphis-pink); + color: var(--color-memphis-crt); + border-color: var(--color-memphis-crt); } .theme-retro .prose-retro pre code, .theme-retro pre.highlight code { @@ -620,7 +533,7 @@ html.theme-retro.dark ::selection { } .theme-retro .prose-retro img { - border: 3px solid #0d0d12; - box-shadow: 6px 6px 0 0 #00e5ff; + border: 3px solid var(--color-memphis-ink); + box-shadow: 6px 6px 0 0 var(--color-memphis-cyan); margin: 1.5em 0; } diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 08d6aa3..6014c59 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -2,15 +2,8 @@ @config '../../../config/tailwind.config.js'; -/* - -@layer components { - .btn-primary { - @apply py-2 px-4 bg-blue-200; - } -} - -*/ +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; /* The default border color has changed to `currentcolor` in Tailwind CSS v4, @@ -30,5 +23,106 @@ } } -@plugin "@tailwindcss/forms"; -@plugin "@tailwindcss/typography"; +/* ============================================================================= + Theme design tokens (Tailwind v4 `@theme` directive) + + Defining custom colors/shadows/fonts/animations here registers them as + real Tailwind utilities — `bg-memphis-pink`, `text-grim-blood`, + `shadow-retro-lg`, `font-blackletter`, `animate-blink`, etc. — with full + support for variants (`dark:`, `hover:`, `group-hover:`) and arbitrary + values (`text-[0.62rem]`) out of the box. + + Themes never need to hand-write `.theme-foo .bg-foo-bar` declarations + for these utilities — Tailwind generates them. The theme stylesheets in + `app/assets/stylesheets/themes/*.css` only need to provide: + 1. theme-root environmental styles (page backdrop, scanlines, etc.) + 2. component classes (`.card-retro`, `.btn-retro`, `.tome`, etc.) + 3. typography styles for `.prose-retro` / `.prose-grimoire` + + Font-family tokens that collide with Tailwind defaults (`--font-sans`, + `--font-mono`) are NOT overridden globally; each theme rebinds them + inside its own `.theme-foo` scope via CSS variable cascade, so the + default theme keeps Tailwind's stock sans/mono. +============================================================================= */ + +@theme { + /* ---------- Retro / Memphis palette ---------- */ + --color-memphis-paper: #fff8ef; + --color-memphis-ink: #0d0d12; + --color-memphis-crt: #0a0e1a; + --color-memphis-pink: #ff3eb5; + --color-memphis-cyan: #00e5ff; + --color-memphis-yellow: #ffd400; + --color-memphis-mint: #00ff9c; + --color-memphis-purple: #b14aff; + --color-memphis-coral: #ff6b4a; + + /* ---------- Grimoire palette ---------- */ + --color-grim-parchment: #ebd9b3; + --color-grim-vellum: #f1e3c2; + --color-grim-ink: #1a1410; + --color-grim-shadow: #3a2f25; + --color-grim-void: #07080d; + --color-grim-obsidian: #0e0f17; + --color-grim-tomb: #161826; + --color-grim-ichor: #2d1b3a; + --color-grim-blood: #8b1d27; + --color-grim-ember: #f08029; + --color-grim-bone: #e9e0c8; + --color-grim-phosphor: #57f287; + --color-grim-arcane: #6a4cab; + --color-grim-gold: #c8a44d; + + /* ---------- Retro hard-shadow utilities ---------- + Generate `shadow-retro`, `shadow-retro-sm`, `shadow-retro-lg`, + plus colored variants `shadow-retro-pink`, …-cyan, etc. */ + --shadow-retro-sm: 4px 4px 0 0 #0d0d12; + --shadow-retro: 6px 6px 0 0 #0d0d12; + --shadow-retro-lg: 10px 10px 0 0 #0d0d12; + --shadow-retro-pink: 6px 6px 0 0 #ff3eb5; + --shadow-retro-cyan: 6px 6px 0 0 #00e5ff; + --shadow-retro-yellow: 6px 6px 0 0 #ffd400; + --shadow-retro-mint: 6px 6px 0 0 #00ff9c; + --shadow-retro-purple: 6px 6px 0 0 #b14aff; + --shadow-retro-coral: 6px 6px 0 0 #ff6b4a; + + /* ---------- Theme-specific font families ---------- + These generate the `font-display`, `font-blackletter`, `font-engraved`, + `font-plex`, `font-manuscript` utilities used by retro and grimoire + views. The values here are the defaults the utility resolves to outside + any theme scope; inside `.theme-retro` / `.theme-grimoire` the matching + CSS variables are rebound to the theme-specific fonts. The default + theme never uses these classes, so the global values are inert. */ + --font-display: ui-sans-serif, system-ui, sans-serif; + --font-blackletter: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + --font-engraved: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + --font-plex: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + --font-manuscript: ui-sans-serif, system-ui, sans-serif; + + /* ---------- Theme animations ---------- + Generate `animate-blink`, `animate-wiggle`, `animate-pop-in` utilities. + Keyframes are also emitted into the bundle so the animations work + without needing the theme CSS file. */ + --animate-blink: theme-blink 1s steps(2, start) infinite; + --animate-wiggle: theme-wiggle 0.4s ease-in-out; + --animate-pop-in: theme-pop-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both; + --animate-marquee: theme-marquee 28s linear infinite; + + @keyframes theme-blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } + } + @keyframes theme-wiggle { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-3deg); } + 75% { transform: rotate(3deg); } + } + @keyframes theme-pop-in { + 0% { opacity: 0; transform: translateY(8px) scale(0.96); } + 100% { opacity: 1; transform: translateY(0) scale(1); } + } + @keyframes theme-marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } + } +}