bible270 1.10.2 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfe9cdd0fb7b5bc4a42b10a4c5a7dd25fe585d37b9178f5ffe367fa628824dc8
4
- data.tar.gz: 92d8d22a9c0572082d756cc797a74c957e816a4d226fe59ad3435874edee59ff
3
+ metadata.gz: 278f587d3e7ee3670bbed00a42ebb91477cc29adfcf72280ff023e1702a5acdf
4
+ data.tar.gz: 642a9fa5623de02a5cdee5f8d50184dfb371a3ac15e88ee6b91fb29bef76a2e9
5
5
  SHA512:
6
- metadata.gz: 12c01200bc093c99d11e1ee90773d633d49986001bf70c1673964d6302484c0e47cd9a1e88241ae3644769c0ed2de0dfe33874f98236a7cf48d511a052791a09
7
- data.tar.gz: 59989b7ae98e4c9e796ab8be529755cbc1737cd9cb954e230bfce627d07e21b6d8dd1b448b7167c90c5fa901b53af6722aac6f321eae49c603de99a669059290
6
+ metadata.gz: 230466a6011d77944bd7994e07ea20e67b4b20e34cc364a1347d780f9bfddaa07bc83b0e534f76282c4b9ac07e8f610d57e120ffab8edf954bafae324e2701c2
7
+ data.tar.gz: 23ae33b11fc8fc92b795b240d3e38dd4a3492b66dc7ac8b6d4765cfd15a8aa01af964e12d31dfe2f74c315a33b4fe743394cd7d62f2e7860a1443329b8dbc852
data/CHANGELOG.md CHANGED
@@ -4,7 +4,41 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.10.1...main)**
7
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.11.0...main)**
8
+
9
+ ## [1.11.0] - 2026-10-22
10
+
11
+ ### Added
12
+
13
+ - add Meadow and Ocean light themes alongside Papyrus, Cedar, Forest, and Moonlight
14
+ - add muted artwork for each theme: textured papyrus with ragged edges and curled corners, a sunrise meadow, an afternoon coast with gulls, cedar grain, layered forest silhouettes, and a moonlit lake
15
+ - add shared Hebrew and Greek scripture backgrounds, with rippled reflections in Moonlight
16
+ - add a compact theme selector beside the app title on every signed-in page
17
+
18
+ ### Changed
19
+
20
+ - save and apply theme selections immediately from the header, Profile, and Admin without submitting unrelated edits
21
+ - synchronize controls for the same reader while keeping an admin's own theme independent of another reader's setting
22
+ - use short theme names, list light themes first, and retain Light/Dark descriptions in Profile and Admin
23
+ - keep the header icon, title, spacing, and theme selector consistently compact across desktop and mobile, without wrapping the navigation
24
+ - let System follow the device appearance using Papyrus for light and Cedar for dark, including their artwork
25
+
26
+ ### Fixed
27
+
28
+ - restore the previous theme selection and show an error when saving fails; prevent overlapping saves for the same reader
29
+
30
+ Existing saved theme preferences are preserved; no additional migration is required for installations with display preferences already enabled.
31
+
32
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.10.2...v1.11.0)**
33
+
34
+ ## [1.10.2] - 2026-10-18
35
+
36
+ ### Changed
37
+
38
+ - show each Admin reader row in that reader's selected theme, with matching text and link colors
39
+ - let System-themed rows follow the viewing browser's light or dark preference
40
+
41
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.10.1...v1.10.2)**
8
42
 
9
43
  ## [1.10.1] - 2026-10-18
10
44
 
@@ -863,7 +897,9 @@ This is the initial public release.
863
897
 
864
898
  * Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
865
899
 
866
- [Unreleased]: https://github.com/avonderluft/bible270/compare/v1.10.1...HEAD
900
+ [Unreleased]: https://github.com/avonderluft/bible270/compare/v1.11.0...HEAD
901
+ [1.11.0]: https://github.com/avonderluft/bible270/releases/tag/v1.11.0
902
+ [1.10.2]: https://github.com/avonderluft/bible270/releases/tag/v1.10.2
867
903
  [1.10.1]: https://github.com/avonderluft/bible270/releases/tag/v1.10.1
868
904
  [1.10.0]: https://github.com/avonderluft/bible270/releases/tag/v1.10.0
869
905
  [1.9.1]: https://github.com/avonderluft/bible270/releases/tag/v1.9.1
data/README.md CHANGED
@@ -272,7 +272,8 @@ Admins get an extra "Admin" link in the header. The broadcast form can send an i
272
272
  message either to every reader with an email address or only to readers recognized as administrators.
273
273
  From a reader's page you can:
274
274
 
275
- - **set their theme** — Parchment, Earth, Forest, Midnight, or System, independently of your own theme;
275
+ - **set their theme** — Papyrus (Light), Meadow (Light), Ocean (Light), Cedar (Dark),
276
+ Forest (Dark), Moonlight (Dark), or System, independently of your own theme;
276
277
 
277
278
  - **move them to a day** — "put them on day 42 as of today" back-dates the start date; check-offs are
278
279
  keyed to day numbers, so nothing in their history moves;
@@ -373,15 +374,21 @@ Psalm 78: [20, 39, 55]
373
374
 
374
375
  ## Theme
375
376
 
376
- Readers can choose **Parchment** (light), **Earth** (dark brown), **Forest** (dark green),
377
- **Midnight** (dark blue), or **System** under **Profile → Theme**, then save. The preference is stored
377
+ Readers can choose **Papyrus (Light)**, **Meadow (Light)**, **Ocean (Light)**, **Cedar (Dark)**,
378
+ **Forest (Dark)**, **Moonlight (Dark)**, or **System** under **Profile → Theme**. Signed-in readers can also use
379
+ the compact selector beside the app title on every page; it shows the short theme names without Light/Dark suffixes.
380
+ Selecting a theme saves and applies it immediately without submitting other profile edits. The header and Profile
381
+ selectors stay synchronized, and a failed save restores the previous selection and displays an error.
382
+ Admin theme selections also save automatically;
383
+ changing another reader's theme does not change the admin's own theme. With JavaScript disabled, use Save.
384
+ The preference is stored
378
385
  with their account and applies throughout Bible270 on every device. System is the default for existing
379
- and new readers, follows the device's current appearance using Parchment for light or Earth for dark,
386
+ and new readers, follows the device's current appearance using Papyrus for light or Cedar for dark,
380
387
  and responds to changes without reloading. Signed-out visitors also use System.
381
388
  The completion dove retains its bright colors in every theme.
382
389
 
383
- Existing preferences are preserved under the new names. Forest and Midnight require no additional
384
- migration once display preferences are installed.
390
+ Existing preferences and persisted values are unchanged under the new names. Meadow, Ocean, Cedar, Forest, and Moonlight
391
+ require no additional migration once display preferences are installed.
385
392
 
386
393
  Upgrading hosts must install and run migration `20260101000023`:
387
394
 
@@ -250,18 +250,21 @@ module Bible270
250
250
 
251
251
  def update_theme
252
252
  unless Reader.display_mode_column?
253
- return redirect_to admin_reader_path(@reader),
254
- alert: 'Run the pending Bible270 database migration before changing the theme.'
253
+ return render_theme_error('Run the pending Bible270 database migration before changing the theme.')
255
254
  end
256
255
 
257
256
  mode = params[:display_mode].to_s
258
- unless Reader::DISPLAY_MODES.include?(mode)
259
- return redirect_to admin_reader_path(@reader), alert: 'Choose a theme from the list.'
260
- end
257
+ return render_theme_error('Choose a theme from the list.') unless Reader::DISPLAY_MODES.include?(mode)
261
258
 
262
259
  @reader.reload unless @reader.has_attribute?(:display_mode)
263
260
  @reader.update!(display_mode: mode)
264
- redirect_to admin_reader_path(@reader), notice: 'Theme updated.'
261
+ respond_to do |format|
262
+ format.json { render json: { display_mode: @reader.display_mode } }
263
+ format.html { redirect_to admin_reader_path(@reader), notice: 'Theme updated.' }
264
+ end
265
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
266
+ message = e.record ? e.record.errors.full_messages.to_sentence : nil
267
+ render_theme_error("The theme could not be saved. #{message.presence || 'Please try again.'}")
265
268
  end
266
269
 
267
270
  def update_notifications
@@ -355,6 +358,13 @@ module Bible270
355
358
 
356
359
  private
357
360
 
361
+ def render_theme_error(message)
362
+ respond_to do |format|
363
+ format.json { render json: { error: message }, status: :unprocessable_entity }
364
+ format.html { redirect_to admin_reader_path(@reader), alert: message }
365
+ end
366
+ end
367
+
358
368
  def comment_notifications_available_for?(reader)
359
369
  return false unless Bible270.config.mention_notifications && Reader.comment_notification_columns?
360
370
 
@@ -388,6 +398,9 @@ module Bible270
388
398
  def load_reader
389
399
  @reader = Reader.find_by(id: params[:id])
390
400
  return if @reader
401
+ if action_name == 'update_theme' && request.format.json?
402
+ return render_theme_error('Reader not found.')
403
+ end
391
404
 
392
405
  redirect_to admin_path, alert: 'Reader not found.'
393
406
  end
@@ -43,6 +43,25 @@ module Bible270
43
43
  render_profile_error("Your profile could not be saved. #{message.presence || 'Please try again.'}")
44
44
  end
45
45
 
46
+ def update_theme
47
+ unless Reader.display_mode_column?
48
+ return render_theme_error('Run the pending Bible270 database migration before changing the theme.')
49
+ end
50
+
51
+ mode = params[:display_mode].to_s
52
+ return render_theme_error('Choose a theme from the list.') unless Reader::DISPLAY_MODES.include?(mode)
53
+
54
+ current_reader.reload unless current_reader.has_attribute?(:display_mode)
55
+ current_reader.update!(display_mode: mode)
56
+ respond_to do |format|
57
+ format.json { render json: { display_mode: current_reader.display_mode } }
58
+ format.html { redirect_to profile_path, notice: 'Theme updated.' }
59
+ end
60
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
61
+ message = e.record ? e.record.errors.full_messages.to_sentence : nil
62
+ render_theme_error("Your theme could not be saved. #{message.presence || 'Please try again.'}")
63
+ end
64
+
46
65
  def remove_avatar
47
66
  current_reader.remove_avatar!
48
67
  redirect_to profile_path, notice: 'Your picture has been removed.'
@@ -50,6 +69,13 @@ module Bible270
50
69
 
51
70
  private
52
71
 
72
+ def render_theme_error(message)
73
+ respond_to do |format|
74
+ format.json { render json: { error: message }, status: :unprocessable_entity }
75
+ format.html { redirect_to profile_path, alert: message }
76
+ end
77
+ end
78
+
53
79
  def display_mode_problem
54
80
  return unless Reader.display_mode_column?
55
81
  return if Reader::DISPLAY_MODES.include?(@display_mode)
@@ -143,6 +169,9 @@ module Bible270
143
169
  # visitor who isn't signed in, so send them to sign in rather than 404.
144
170
  def require_signed_in_reader
145
171
  return if signed_in?
172
+ if action_name == 'update_theme' && request.format.json?
173
+ return render_theme_error('Please sign in first.')
174
+ end
146
175
 
147
176
  redirect_to sign_in_path(origin: request.fullpath), alert: 'Please sign in first.'
148
177
  end
@@ -29,7 +29,16 @@ module Bible270
29
29
 
30
30
  PASSAGE_SOURCES = %w[bible_com bible_gateway blue_letter_bible].freeze
31
31
  DEFAULT_PASSAGE_SOURCE = 'bible_com'
32
- DISPLAY_MODES = %w[light dark dark_green dark_blue system].freeze
32
+ DISPLAY_OPTIONS = {
33
+ 'Papyrus (Light)' => 'light',
34
+ 'Meadow (Light)' => 'sage',
35
+ 'Ocean (Light)' => 'mist',
36
+ 'Cedar (Dark)' => 'dark',
37
+ 'Forest (Dark)' => 'dark_green',
38
+ 'Moonlight (Dark)' => 'dark_blue',
39
+ 'System' => 'system'
40
+ }.freeze
41
+ DISPLAY_MODES = DISPLAY_OPTIONS.values.freeze
33
42
  DEFAULT_DISPLAY_MODE = 'system'
34
43
  DAILY_REMINDER_TIME_FORMAT = %r{\A(?:[01]\d|2[0-3]):[0-5]\d\z}
35
44
  DAILY_REMINDER_MINUTES = %w[00 15 30 45].freeze
@@ -82,13 +82,15 @@
82
82
  <% if Bible270::Reader.display_mode_column? %>
83
83
  <%= form_with url: admin_reader_theme_path(@reader), method: :patch, class: "b270-emailform" do %>
84
84
  <%= select_tag :display_mode,
85
- options_for_select([["Parchment", "light"], ["Earth", "dark"], ["Forest", "dark_green"],
86
- ["Midnight", "dark_blue"], ["System", "system"]], @reader.effective_display_mode),
87
- class: "b270-select", aria: { labelledby: "b270-theme-heading", describedby: "b270-theme-hint" } %>
85
+ options_for_select(Bible270::Reader::DISPLAY_OPTIONS, @reader.effective_display_mode),
86
+ class: "b270-select", aria: { labelledby: "b270-theme-heading", describedby: "b270-theme-hint b270-theme-status" },
87
+ data: { b270_theme_url: admin_reader_theme_path(@reader), b270_theme_apply: @reader == current_reader,
88
+ b270_saved_theme: @reader.effective_display_mode } %>
88
89
  <p id="b270-theme-hint" class="b270-hint" style="margin:0">
89
- System follows the reader's device setting: Parchment for light, Earth for dark.
90
+ System follows the reader's device setting: Papyrus for light, Cedar for dark.
90
91
  </p>
91
- <%= submit_tag "Save", class: "b270-btn" %>
92
+ <p id="b270-theme-status" class="b270-hint" role="status" aria-live="polite" style="margin:0"></p>
93
+ <%= submit_tag "Save", class: "b270-btn", data: { b270_theme_fallback: true } %>
92
94
  <% end %>
93
95
  <% else %>
94
96
  <p class="b270-flash alert" style="margin:0">
@@ -97,6 +99,7 @@
97
99
  <% end %>
98
100
  </div>
99
101
 
102
+
100
103
  <div class="b270-panel">
101
104
  <h2 class="b270-subhead">Email notifications</h2>
102
105
  <% unless Bible270.config.mention_notifications %>
@@ -48,11 +48,13 @@
48
48
  <label class="b270-field">
49
49
  <span>Theme</span>
50
50
  <%= select_tag :display_mode,
51
- options_for_select([["Parchment", "light"], ["Earth", "dark"], ["Forest", "dark_green"],
52
- ["Midnight", "dark_blue"], ["System", "system"]], @display_mode),
53
- class: "b270-select", aria: { describedby: "b270-display-hint" } %>
54
- <span id="b270-display-hint" class="b270-hint" style="margin:0">System follows your device's setting: Parchment for light, Earth for dark.</span>
51
+ options_for_select(Bible270::Reader::DISPLAY_OPTIONS, @display_mode),
52
+ class: "b270-select", aria: { describedby: "b270-display-hint b270-theme-status" },
53
+ data: { b270_theme_url: profile_theme_path, b270_theme_apply: true,
54
+ b270_saved_theme: current_reader.effective_display_mode } %>
55
+ <span id="b270-display-hint" class="b270-hint" style="margin:0">System follows your device's setting: Papyrus for light, Cedar for dark.</span>
55
56
  </label>
57
+ <p id="b270-theme-status" class="b270-hint" role="status" aria-live="polite" style="margin:0"></p>
56
58
  <% else %>
57
59
  <p class="b270-flash alert" style="margin:0">
58
60
  Theme settings are temporarily unavailable while this installation finishes a database update.
@@ -4,6 +4,19 @@
4
4
  <%= b270_mark %>
5
5
  <span class="b270-title"><%= Bible270.config.app_name %></span>
6
6
  <% end %>
7
+ <% if current_reader && Bible270::Reader.display_mode_column? %>
8
+ <%= form_with url: profile_theme_path, method: :patch, class: "b270-header-theme" do %>
9
+ <%= select_tag :display_mode,
10
+ options_for_select(Bible270::Reader::DISPLAY_OPTIONS.map { |label, value| [label.sub(/ \((?:Light|Dark)\)\z/, ""), value] }, current_reader.effective_display_mode),
11
+ id: "b270-header-theme", class: "b270-header-theme-select",
12
+ aria: { label: "Theme", describedby: "b270-header-theme-status" },
13
+ data: { b270_theme_url: profile_theme_path, b270_theme_apply: true,
14
+ b270_saved_theme: current_reader.effective_display_mode,
15
+ b270_theme_status: "b270-header-theme-status" } %>
16
+ <%= submit_tag "Save", class: "b270-btn", data: { b270_theme_fallback: true } %>
17
+ <span id="b270-header-theme-status" class="b270-header-theme-status" role="status" aria-live="polite"></span>
18
+ <% end %>
19
+ <% end %>
7
20
  </div>
8
21
  <nav class="b270-nav" aria-label="Primary navigation">
9
22
  <div class="b270-desktopnav">
@@ -18,10 +18,46 @@
18
18
  margin:0; background:radial-gradient(120% 80% at 100% 0%,rgba(165,129,44,.06),transparent 55%),var(--paper);
19
19
  color:var(--ink); font-family:"Inter",system-ui,sans-serif; line-height:1.2; -webkit-font-smoothing:antialiased;
20
20
  }
21
- /* Explicit light keeps native controls light even when the OS is dark.
22
- Only system mode follows the media query, including live OS changes. */
21
+ /* Explicit light also opts out of automatic browser darkening. System still follows the OS. */
22
+ :root[data-b270-display-mode="light"],
23
+ :root[data-b270-display-mode="sage"],
24
+ :root[data-b270-display-mode="mist"]{color-scheme:only light}
25
+ :root[data-b270-display-mode="dark"],
26
+ :root[data-b270-display-mode="dark_green"],
27
+ :root[data-b270-display-mode="dark_blue"]{color-scheme:dark}
28
+ :root[data-b270-display-mode="system"]{color-scheme:light dark}
23
29
  .b270-reader-theme[data-display-mode="light"],
24
- .b270[data-display-mode="light"]{color-scheme:light}
30
+ .b270[data-display-mode="light"]{color-scheme:only light}
31
+ .b270-reader-theme[data-display-mode="sage"],
32
+ .b270[data-display-mode="sage"]{
33
+ color-scheme:only light;
34
+ background-image:none;
35
+ --paper:#e1e8df; --card:#edf0e9; --ink:#29382d; --muted:#52634f; --faint:#5b6c58;
36
+ --line:#cbd6c7; --line-soft:#dce4d8; --teal:#356b60; --oxblood:#905466; --gold:#776023;
37
+ --gold-lite:#776023; --shadow:0 1px 0 rgba(41,56,45,.04),0 10px 30px -18px rgba(41,56,45,.25);
38
+ --control:#fcfdf9; --control-line:#768572; --task-surface:rgba(255,255,255,.35); --button-hover:#3b5040;
39
+ --on-accent:#ffffff; --complete-ink:#ffffff; --complete-dot:#edf0e9;
40
+ --button-surface:#29382d; --button-ink:#edf0e9;
41
+ --teal-fill:#356b60; --oxblood-fill:#905466; --gold-fill:#776023;
42
+ --teal-mark:#356b60; --oxblood-mark:#905466; --gold-mark:#776023;
43
+ --alert-surface:#f5e7e9; --alert-ink:#783f4c; --alert-line:#d9b7c0;
44
+ --notice-surface:#e3eee0; --notice-ink:#375b37; --notice-line:#b8d1b1;
45
+ }
46
+ .b270-reader-theme[data-display-mode="mist"],
47
+ .b270[data-display-mode="mist"]{
48
+ color-scheme:only light;
49
+ background-image:none;
50
+ --paper:#e2e7ed; --card:#ecf0f5; --ink:#293746; --muted:#4f6074; --faint:#58687b;
51
+ --line:#ced7e2; --line-soft:#dfe5ed; --teal:#316977; --oxblood:#86566f; --gold:#7b602b;
52
+ --gold-lite:#7b602b; --shadow:0 1px 0 rgba(41,55,70,.04),0 10px 30px -18px rgba(41,55,70,.25);
53
+ --control:#fcfdff; --control-line:#758496; --task-surface:rgba(255,255,255,.35); --button-hover:#3c5065;
54
+ --on-accent:#ffffff; --complete-ink:#ffffff; --complete-dot:#ecf0f5;
55
+ --button-surface:#293746; --button-ink:#ecf0f5;
56
+ --teal-fill:#316977; --oxblood-fill:#86566f; --gold-fill:#7b602b;
57
+ --teal-mark:#316977; --oxblood-mark:#86566f; --gold-mark:#7b602b;
58
+ --alert-surface:#f3e7ee; --alert-ink:#754357; --alert-line:#d7bbc9;
59
+ --notice-surface:#e3eeea; --notice-ink:#355c50; --notice-line:#b7d0c7;
60
+ }
25
61
  .b270-reader-theme[data-display-mode="dark"],
26
62
  .b270[data-display-mode="dark"]{
27
63
  color-scheme:dark;
@@ -88,7 +124,80 @@
88
124
  --notice-surface:#25382a; --notice-ink:#b6dfb6; --notice-line:#6d956e;
89
125
  }
90
126
  }
127
+ /* A fixed SVG avoids mobile background-attachment quirks and stays behind all content. */
128
+ .b270-theme-scenery{display:none;--b270-scene-height:clamp(280px,52vh,560px);position:fixed;inset:auto 0 0;width:100%;height:var(--b270-scene-height);z-index:-1;pointer-events:none}
129
+ .b270{isolation:isolate}
130
+ .b270 > .b270-theme-scripture{display:block;color:var(--gold)}
131
+ .b270[data-display-mode="dark_blue"] > .b270-theme-scripture{color:#c6d6e9}
132
+ .b270[data-display-mode="mist"] > .b270-theme-scenery[data-scenery="mist"],
133
+ .b270[data-display-mode="mist"] > .b270-theme-scenery[data-scenery="mist_sky"],
134
+ .b270[data-display-mode="mist"] > .b270-theme-scenery[data-scenery="ocean_gulls"],
135
+ .b270[data-display-mode="sage"] > .b270-theme-scenery[data-scenery="sage"],
136
+ .b270[data-display-mode="sage"] > .b270-theme-scenery[data-scenery="sage_sky"],
137
+ .b270[data-display-mode="dark"] > .b270-theme-scenery[data-scenery="dark"],
138
+ .b270[data-display-mode="dark_green"] > .b270-theme-scenery[data-scenery="dark_green"],
139
+ .b270[data-display-mode="dark_blue"] > .b270-theme-scenery[data-scenery="dark_blue"],
140
+ .b270[data-display-mode="dark_blue"] > .b270-theme-scenery[data-scenery="hebrew_water"],
141
+ .b270[data-display-mode="light"] > .b270-theme-scenery[data-scenery="light"],
142
+ .b270[data-display-mode="system"] > .b270-theme-scenery[data-scenery="light"]{display:block}
143
+ .b270-theme-scenery[data-scenery="light"],.b270-theme-scripture{font-size:clamp(12px,2.2vw,30px)}
144
+ .b270-theme-scripture{height:clamp(380px,76vh,820px)}
145
+ .b270-theme-scenery[data-scenery="light"]{height:100%;overflow:hidden}
146
+ .b270-papyrus-grid{transform-box:view-box;transform:translateY(calc(100% - clamp(380px,76vh,820px) * .7))}
147
+ .b270-papyrus-opposite{transform-box:view-box;transform-origin:0 0;transform:translate(100%,100%) rotate(180deg)}
148
+ @media (max-width:600px){
149
+ .b270-papyrus-corner{transform:scale(.55)}
150
+ }
151
+ .b270-theme-scenery[data-scenery="dark"]{height:100%}
152
+ .b270-theme-scenery[data-scenery="sage_sky"]{height:100%}
153
+ .b270-theme-scenery[data-scenery="mist_sky"]{height:100%}
154
+ .b270-ocean-gulls{transform-box:view-box;transform:translate(calc(100% - 115px),calc(100% - 35px))}
155
+ /* Match the moon's 90-unit right inset in its 1600 × 440, right-aligned slice viewport. */
156
+ .b270-scripture-letters [lang="el"]{transform-box:view-box;transform:translateX(calc(100% - max(5.625%, var(--b270-scene-height) * 90 / 440)))}
157
+ .b270-scripture-letters [lang="el"] tspan{font-size:.9em}
158
+ /* Scale reflected Hebrew with the lake artwork, allowing for its smaller letterforms. */
159
+ .b270-hebrew-water{font-size:max(2vw, var(--b270-scene-height) * 32 / 440);transform-box:view-box;transform:translate(10%,calc(100% - max(12.5vw, var(--b270-scene-height) * 200 / 440)))}
160
+ @media (prefers-color-scheme:dark){
161
+ .b270[data-display-mode="system"] > .b270-theme-scenery[data-scenery="light"]{display:none}
162
+ .b270[data-display-mode="system"] > .b270-theme-scenery[data-scenery="dark"]{display:block}
163
+ }
164
+ .b270-mobile-scenery{display:none}
165
+ /* Phone artwork is a quiet accent below the reading area, not a cropped desktop scene. */
166
+ @media (max-width:600px){
167
+ .b270-theme-scenery{--b270-scene-height:clamp(180px,30vh,260px)}
168
+ .b270[data-display-mode] > .b270-theme-scripture,
169
+ .b270[data-display-mode] > .b270-theme-scenery[data-scenery="hebrew_water"]{display:none}
170
+ .b270-mobile-scenery{display:block}
171
+ .b270-papyrus-grid{opacity:.2}
172
+ .b270-papyrus-edges{opacity:.55}
173
+ .b270-theme-scenery[data-scenery="sage_sky"]{opacity:.3}
174
+ #b270-meadow-clouds{opacity:.5}
175
+ #b270-meadow-clouds path:nth-child(even),
176
+ #b270-meadow-grass-distant,
177
+ #b270-meadow-grass-near path:not(:nth-child(3n + 1)){display:none}
178
+ #b270-meadow-grass-near{opacity:.15}
179
+ .b270-theme-scenery[data-scenery="sage"] [fill="url(#b270-meadow-rays)"]{opacity:.25}
180
+ .b270-theme-scenery[data-scenery="mist_sky"]{opacity:.2}
181
+ .b270-theme-scenery[data-scenery="mist"]{opacity:.45}
182
+ #b270-ocean-clouds,
183
+ #b270-ocean-distant-waves,
184
+ #b270-ocean-glints,
185
+ #b270-ocean-sand-detail,
186
+ #b270-ocean-surf path:not(:first-child){display:none}
187
+ .b270-theme-scenery[data-scenery="dark"]{height:clamp(240px,42vh,380px);opacity:.45}
188
+ #b270-cedar-fine-grain path:not(:first-child),
189
+ #b270-cedar-knots path:nth-child(n + 4){display:none}
190
+ #b270-spruce-distant path:nth-of-type(even),
191
+ #b270-spruce-near path:nth-of-type(even),
192
+ #b270-spruce-distant path:nth-of-type(n + 22),
193
+ #b270-spruce-near path:nth-of-type(n + 22),
194
+ #b270-lake-reflection{display:none}
195
+ }
196
+ @media print{
197
+ .b270[data-display-mode] > .b270-theme-scenery[data-scenery]{display:none}
198
+ }
91
199
  .b270 *{box-sizing:border-box}
200
+ .b270 [data-b270-theme-fallback][hidden]{display:none}
92
201
  /* Blanket anchor colour. NB: this is (0,1,1), so any single-class rule
93
202
  colouring an <a> (.b270-btn, .b270-cell, .b270-arrow ...) must be
94
203
  qualified with .b270 to outrank it, or the link inherits body colour. */
@@ -96,14 +205,26 @@
96
205
  .b270 :where(a,button,input,select,textarea,summary):focus-visible{outline:2px solid var(--gold);outline-offset:2px}
97
206
  .b270-wrap{max-width:820px;margin:0 auto;padding:clamp(14px,3.5vw,36px) clamp(14px,4vw,40px) 64px}
98
207
 
99
- .b270-topbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding-bottom:20px;border-bottom:1px solid var(--line-soft);margin-bottom:26px}
100
- .b270-brand{display:flex;margin-right:auto}
208
+ .b270-topbar{display:flex;align-items:center;gap:12px;flex-wrap:nowrap;padding-bottom:20px;border-bottom:1px solid var(--line-soft);margin-bottom:26px}
209
+ .b270-brand{display:flex;align-items:baseline;gap:14px;margin-right:auto;min-width:0}
210
+ .b270-header-theme{position:relative;display:flex;align-items:baseline;gap:4px;margin:0;flex:none}
211
+ .b270-header-theme-select{font:inherit;font-size:12px;width:6.5em;height:28px;padding:2px;margin:0;
212
+ border:1px solid var(--line);border-radius:6px;background:var(--card);color:var(--ink);cursor:pointer}
213
+ @supports (field-sizing:content){
214
+ .b270-header-theme-select{field-sizing:content;width:auto}
215
+ }
216
+ .b270-header-theme-select:disabled{cursor:wait;opacity:.7}
217
+ .b270-header-theme-status:not([data-error="true"]){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
218
+ clip:rect(0,0,0,0);white-space:nowrap;border:0}
219
+ .b270-header-theme-status[data-error="true"]{position:absolute;z-index:30;right:0;top:calc(100% + 6px);width:220px;max-width:calc(100vw - 28px);
220
+ padding:8px 10px;border:1px solid var(--alert-line);border-radius:8px;background:var(--alert-surface);
221
+ color:var(--alert-ink);font-size:12px;line-height:1.5;box-shadow:var(--shadow)}
101
222
  /* The whole brand is one link, so the mark and the words highlight together. */
102
- .b270-brandlink{display:flex;align-items:center;gap:12px;text-decoration:none}
103
- .b270-mark{display:flex;align-items:center;flex:none;line-height:0}
104
- .b270-mark svg,.b270-mark img{display:block;border-radius:8px}
105
- .b270-title{font-family:"Spectral",Georgia,serif;font-weight:600;font-size:22px;text-decoration:none}
106
- .b270-nav{display:flex;align-items:center;font-size:14px}
223
+ .b270-brandlink{display:flex;align-items:baseline;gap:6px;text-decoration:none;min-width:0}
224
+ .b270-mark{display:flex;align-items:center;align-self:center;flex:none;line-height:0}
225
+ .b270-mark svg,.b270-mark img,.b270-brandlink > img.b270-mark{display:block;width:28px;height:28px;border-radius:8px}
226
+ .b270-title{font-family:"Spectral",Georgia,serif;font-weight:600;font-size:18px;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
227
+ .b270-nav{display:flex;align-items:center;font-size:14px;flex:none;white-space:nowrap}
107
228
  .b270-desktopnav{display:flex;align-items:center;gap:16px}
108
229
  .b270-navmenu{display:none}
109
230
  .b270-nav a{color:var(--muted);text-decoration:none}
@@ -475,9 +596,10 @@
475
596
  .b270-footer p{margin:0;line-height:1.5}
476
597
  details.b270-index summary{cursor:pointer;font-family:"Spectral",serif;font-size:18px;font-weight:600;list-style:none;margin-top:30px}
477
598
  details.b270-index summary::-webkit-details-marker{display:none}
478
- @media (max-width:620px){
479
- .b270-topbar{flex-wrap:nowrap;align-items:center}
480
- .b270-brand{min-width:0}
599
+ /* Switch to the compact menu before the full navigation can crowd the theme control. */
600
+ @media (max-width:820px){
601
+ .b270-topbar{flex-wrap:nowrap;align-items:center;gap:8px}
602
+
481
603
  .b270-nav{position:relative;flex:none}
482
604
  .b270-desktopnav{display:none}
483
605
  .b270-navmenu{display:block}
@@ -497,6 +619,7 @@
497
619
  .b270-navlinks a:hover,.b270-nav .b270-linkbtn:hover{background:var(--paper)}
498
620
  .b270-nav form{display:block;width:100%}
499
621
  }
622
+
500
623
  @media (max-width:480px){
501
624
  .b270-tm{grid-template-columns:104px 1fr auto}
502
625
  .b270-avatarrow{align-items:flex-start;flex-wrap:wrap}
@@ -0,0 +1,474 @@
1
+ <%# Kept in the DOM so changing themes can reveal the artwork without a page reload. %>
2
+ <%# Keep the saved "mist" preference compatible while presenting it as Ocean. %>
3
+ <svg class="b270-theme-scenery" data-scenery="mist_sky" viewBox="0 0 1600 1000" preserveAspectRatio="none"
4
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
5
+ <defs>
6
+ <linearGradient id="b270-ocean-sky" x1="0" y1="0" x2="0" y2="1">
7
+ <stop offset="0" stop-color="#b9d2e5" stop-opacity=".7" />
8
+ <stop offset=".5" stop-color="#dce6e9" stop-opacity=".45" />
9
+ <stop offset=".75" stop-color="#eee5d3" stop-opacity=".25" />
10
+ <stop offset="1" stop-color="#e2e7ed" stop-opacity="0" />
11
+ </linearGradient>
12
+ <radialGradient id="b270-ocean-afternoon-glow">
13
+ <stop offset="0" stop-color="#fff1ce" stop-opacity=".4" />
14
+ <stop offset="1" stop-color="#fff1ce" stop-opacity="0" />
15
+ </radialGradient>
16
+ </defs>
17
+ <rect width="1600" height="1000" fill="url(#b270-ocean-sky)" />
18
+ <ellipse cx="1420" cy="180" rx="460" ry="340" fill="url(#b270-ocean-afternoon-glow)" />
19
+ <g id="b270-ocean-clouds" fill="#fffdf7" opacity=".25">
20
+ <path d="M45 148Q154 127 242 130T409 115Q343 143 245 144T45 148Z" />
21
+ <path d="M948 87Q1050 69 1141 73T1322 57Q1237 84 1144 86T948 87Z" />
22
+ </g>
23
+ </svg>
24
+
25
+ <svg class="b270-theme-scenery" data-scenery="mist" viewBox="0 0 1600 440" preserveAspectRatio="xMidYMax slice"
26
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
27
+ <defs>
28
+ <linearGradient id="b270-ocean-water" x1="0" y1="0" x2="0" y2="1">
29
+ <stop offset="0" stop-color="#91b2c6" stop-opacity=".65" />
30
+ <stop offset=".55" stop-color="#a3c6ce" stop-opacity=".75" />
31
+ <stop offset="1" stop-color="#b9d4cf" stop-opacity=".7" />
32
+ </linearGradient>
33
+ <linearGradient id="b270-ocean-sand" x1="0" y1="0" x2="0" y2="1">
34
+ <stop offset="0" stop-color="#c8c9b6" stop-opacity=".8" />
35
+ <stop offset=".35" stop-color="#d9cfb9" stop-opacity=".85" />
36
+ <stop offset="1" stop-color="#e6ddcb" stop-opacity=".9" />
37
+ </linearGradient>
38
+ <linearGradient id="b270-ocean-shimmer" x1="0" y1="0" x2="1" y2="0">
39
+ <stop offset="0" stop-color="#fff6de" stop-opacity="0" />
40
+ <stop offset=".65" stop-color="#fff6de" stop-opacity=".35" />
41
+ <stop offset="1" stop-color="#fff6de" stop-opacity=".1" />
42
+ </linearGradient>
43
+ </defs>
44
+ <rect x="0" y="135" width="1600" height="305" fill="url(#b270-ocean-water)" />
45
+ <path id="b270-ocean-horizon" d="M0 135H1600" fill="none" stroke="#7f9eae" stroke-width="1" opacity=".3" />
46
+ <g id="b270-ocean-distant-waves" fill="none" stroke="#f5f5eb" stroke-linecap="round" opacity=".35">
47
+ <path d="M22 154H178M235 158H442M532 151H708M811 159H1032M1119 153H1385M1460 160H1590" stroke-width="1" />
48
+ <path d="M0 181Q150 174 294 181T601 184M728 182Q874 175 1028 181T1370 180M1442 183H1600" stroke-width="1.4" />
49
+ <path d="M43 216Q193 203 353 214T691 213M821 220Q970 208 1128 218T1510 215" stroke-width="2" />
50
+ </g>
51
+ <g id="b270-ocean-glints" fill="none" stroke="url(#b270-ocean-shimmer)" stroke-linecap="round">
52
+ <path d="M880 170H1360M808 196H1434M751 230H1507" stroke-width="3" />
53
+ <path d="M685 269Q1050 253 1560 272" stroke-width="5" />
54
+ </g>
55
+ <path id="b270-ocean-beach" fill="url(#b270-ocean-sand)"
56
+ d="M0 365C230 338 396 388 617 361S981 304 1190 322 1450 346 1600 310V440H0Z" />
57
+ <path id="b270-ocean-wash" fill="#f9f8ed" opacity=".24"
58
+ d="M0 343C230 318 396 367 617 340S981 283 1190 301 1450 325 1600 289V310C1450 346 1399 340 1190 322S838 334 617 361 230 338 0 365Z" />
59
+ <g id="b270-ocean-surf" fill="none" stroke="#fffdf2" stroke-linecap="round">
60
+ <path d="M0 343C230 318 396 367 617 340S981 283 1190 301 1450 325 1600 289" stroke-width="2.8" opacity=".6" />
61
+ <path d="M0 292C208 270 381 310 593 289S973 254 1181 266 1444 288 1600 257" stroke-width="2" opacity=".38" />
62
+ <path d="M50 365Q174 352 282 366M370 381Q499 389 611 374M745 352Q887 326 1012 328M1194 339Q1335 359 1450 344" stroke-width="1.4" opacity=".38" />
63
+ </g>
64
+ <g id="b270-ocean-sand-detail" fill="none" stroke="#b4a88c" stroke-linecap="round" opacity=".17">
65
+ <path d="M105 414Q228 401 347 413M530 424Q669 412 777 417M1007 386Q1110 379 1227 391M1337 420Q1450 406 1562 411" />
66
+ </g>
67
+ </svg>
68
+
69
+ <svg class="b270-theme-scenery" data-scenery="ocean_gulls"
70
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
71
+ <%# A viewport-based layer keeps both birds on the lower-right sand when the coast is cropped on phones. %>
72
+ <defs>
73
+ <g id="b270-ocean-gull">
74
+ <ellipse cx="0" cy="12" rx="17" ry="2.5" fill="#968e78" opacity=".2" />
75
+ <path d="M-3 3-4 11-8 12M4 3 6 11 10 12" fill="none" stroke="#a18a60" stroke-width="1.2" stroke-linecap="round" />
76
+ <path d="M-19-5-11-4Q-6-11 4-8L8-15Q11-19 15-15Q18-11 13-8L11-1Q7 7-5 4Q-12 2-19-5Z" fill="#f7f5eb" />
77
+ <path d="M-17-5Q-5-11 5-6Q2 1-9 0Z" fill="#8898a0" />
78
+ <path d="M-17-5-10-3-7 0-12-1Z" fill="#52656c" />
79
+ <path d="M15-14 22-11 15-10Z" fill="#b49a63" />
80
+ <circle cx="13" cy="-14" r=".8" fill="#53636a" />
81
+ </g>
82
+ </defs>
83
+ <g class="b270-ocean-gulls" opacity=".65">
84
+ <use href="#b270-ocean-gull" transform="translate(0 -8) scale(.95 .95)" />
85
+ <use href="#b270-ocean-gull" transform="translate(60 3) scale(-1.15 1.15)" />
86
+ </g>
87
+ </svg>
88
+
89
+ <svg class="b270-theme-scenery" data-scenery="sage_sky" viewBox="0 0 1600 1000" preserveAspectRatio="none"
90
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
91
+ <defs>
92
+ <linearGradient id="b270-meadow-blue-sky" x1="0" y1="0" x2="0" y2="1">
93
+ <stop offset="0" stop-color="#bdd8eb" stop-opacity=".8" />
94
+ <stop offset=".4" stop-color="#d1e2ea" stop-opacity=".55" />
95
+ <stop offset=".75" stop-color="#e1e8df" stop-opacity="0" />
96
+ </linearGradient>
97
+ <linearGradient id="b270-meadow-cloud-fade" x1="0" y1="0" x2="1" y2="0">
98
+ <stop offset="0" stop-color="#fffdf5" stop-opacity="0" />
99
+ <stop offset=".35" stop-color="#fffdf5" stop-opacity=".5" />
100
+ <stop offset=".7" stop-color="#fffdf5" stop-opacity=".35" />
101
+ <stop offset="1" stop-color="#fffdf5" stop-opacity="0" />
102
+ </linearGradient>
103
+ </defs>
104
+ <rect width="1600" height="1000" fill="url(#b270-meadow-blue-sky)" />
105
+ <g id="b270-meadow-clouds" fill="url(#b270-meadow-cloud-fade)">
106
+ <path d="M20 105Q105 99 162 84T275 81Q322 82 376 72Q317 94 253 94T142 106Q78 111 20 105Z" />
107
+ <path d="M368 51Q448 45 497 34T586 36Q635 37 690 26Q628 46 562 47T458 52Q407 55 368 51Z" />
108
+ <path d="M1002 99Q1098 90 1160 74T1265 73Q1329 76 1400 61Q1328 86 1247 87T1130 99Q1065 105 1002 99Z" />
109
+ <path d="M1270 161Q1352 153 1405 139T1502 140Q1553 141 1610 128Q1546 151 1481 152T1371 163Q1315 166 1270 161Z" />
110
+ </g>
111
+ </svg>
112
+
113
+ <svg class="b270-theme-scenery" data-scenery="sage" viewBox="0 0 1600 440" preserveAspectRatio="xMaxYMax slice"
114
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
115
+ <defs>
116
+ <clipPath id="b270-meadow-sky">
117
+ <path d="M0 280C160 180 360 180 560 250S900 260 1040 210 1400 200 1600 270V0H0Z" />
118
+ </clipPath>
119
+ <radialGradient id="b270-meadow-glow">
120
+ <stop offset="0" stop-color="#f8e8b6" stop-opacity=".55" />
121
+ <stop offset=".5" stop-color="#f6e8c4" stop-opacity=".25" />
122
+ <stop offset="1" stop-color="#f6e8c4" stop-opacity="0" />
123
+ </radialGradient>
124
+ <linearGradient id="b270-meadow-rays" x1="0" y1="0" x2="0" y2="1">
125
+ <stop offset="0" stop-color="#f8e8b6" stop-opacity="0" />
126
+ <stop offset=".45" stop-color="#f8e8b6" stop-opacity=".22" />
127
+ <stop offset="1" stop-color="#f8e8b6" stop-opacity=".4" />
128
+ </linearGradient>
129
+ <linearGradient id="b270-meadow-distant" x1="0" y1="0" x2="0" y2="1">
130
+ <stop offset="0" stop-color="#c2cfb7" />
131
+ <stop offset="1" stop-color="#e1e8df" />
132
+ </linearGradient>
133
+ <linearGradient id="b270-meadow-middle" x1="0" y1="0" x2="0" y2="1">
134
+ <stop offset="0" stop-color="#afc1a3" />
135
+ <stop offset="1" stop-color="#e1e8df" />
136
+ </linearGradient>
137
+ <linearGradient id="b270-meadow-near" x1="0" y1="0" x2="0" y2="1">
138
+ <stop offset="0" stop-color="#9fb693" />
139
+ <stop offset="1" stop-color="#e1e8df" />
140
+ </linearGradient>
141
+ </defs>
142
+ <g clip-path="url(#b270-meadow-sky)">
143
+ <ellipse cx="1560" cy="245" rx="440" ry="200" fill="url(#b270-meadow-glow)" />
144
+ <path fill="url(#b270-meadow-rays)"
145
+ d="M1560 260 860 0 1020 0Z M1560 260 1180 0 1295 0Z M1560 260 1435 0 1515 0Z M1560 260 1605 0 1685 0Z M1560 260 1825 0 1940 0Z M1560 260 2100 0 2260 0Z" />
146
+ <circle cx="1560" cy="260" r="61" fill="#f6e5b0" opacity=".55" />
147
+ </g>
148
+ <path fill="url(#b270-meadow-distant)" opacity=".65"
149
+ d="M0 280C160 180 360 180 560 250S900 260 1040 210 1400 200 1600 270V440H0Z" />
150
+ <path fill="url(#b270-meadow-middle)" opacity=".55"
151
+ d="M0 320C240 260 350 260 550 300S850 340 1090 290 1420 260 1600 320V440H0Z" />
152
+ <path fill="url(#b270-meadow-near)" opacity=".45"
153
+ d="M0 370C200 340 430 350 640 375S1030 340 1220 360 1480 400 1600 370V440H0Z" />
154
+ <g id="b270-meadow-grass-distant" fill="none" stroke="#7e9672" stroke-width="1" stroke-linecap="round" opacity=".16">
155
+ <path d="M232 287Q232 275 226 269M232 287Q236 277 242 274" />
156
+ <path d="M405 301Q408 286 402 280M405 301Q399 292 393 290" />
157
+ <path d="M708 317Q706 305 711 299M708 317Q714 309 720 307" />
158
+ <path d="M1015 302Q1015 287 1009 281M1015 302Q1020 291 1026 289" />
159
+ <path d="M1278 287Q1280 273 1274 266M1278 287Q1284 278 1290 275" />
160
+ <path d="M1540 320Q1538 306 1544 299M1540 320Q1534 310 1528 308" />
161
+ </g>
162
+ <g id="b270-meadow-grass-near" fill="none" stroke="#7e9672" stroke-width="1.6" stroke-linecap="round" opacity=".24">
163
+ <path d="M110 409Q112 380 99 364M110 409Q119 387 130 380M110 409Q101 393 91 389" />
164
+ <path d="M153 415Q150 394 159 378M153 415Q145 402 136 398" />
165
+ <path d="M1434 418Q1433 392 1446 375M1434 418Q1424 399 1412 393" />
166
+ <path d="M1480 411Q1482 381 1470 366M1480 411Q1491 387 1505 380M1480 411Q1471 397 1463 393" />
167
+ <path d="M285 388Q287 367 279 356M285 388Q293 372 301 368M285 388Q278 378 271 375" />
168
+ <path d="M451 420Q448 398 457 385M451 420Q442 407 435 404" />
169
+ <path d="M635 408Q637 385 629 374M635 408Q644 391 653 387" />
170
+ <path d="M850 383Q849 363 855 354M850 383Q842 369 833 366" />
171
+ <path d="M1064 409Q1066 384 1057 371M1064 409Q1074 390 1084 386M1064 409Q1056 397 1047 393" />
172
+ <path d="M1227 425Q1224 403 1232 391M1227 425Q1218 410 1209 407" />
173
+ <path d="M1570 404Q1572 383 1564 371M1570 404Q1578 389 1587 385" />
174
+ </g>
175
+ </svg>
176
+
177
+ <svg class="b270-theme-scenery" data-scenery="light"
178
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
179
+ <defs>
180
+ <%# CSS anchors the full-page grid to the unchanged scripture baseline. %>
181
+ <pattern id="b270-papyrus-fibres" width="4em" height="3.6em"
182
+ viewBox="0 0 120 108" patternUnits="userSpaceOnUse">
183
+ <g fill="none" stroke="#a48b60" stroke-width=".85" opacity=".23">
184
+ <path d="M0 12H120M0 48H120M0 84H120" />
185
+ <path d="M18 0Q16 28 19 60T18 108M55 0Q58 35 54 72T55 108M96 0Q94 30 97 67T96 108" />
186
+ </g>
187
+ <path d="M8 25H43M78 52H111M32 76H67" stroke="#fff9ed" stroke-width="1" opacity=".3" />
188
+ </pattern>
189
+ <pattern id="b270-papyrus-edge-top" width="240" height="12" patternUnits="userSpaceOnUse">
190
+ <path d="M0 0H240V5L228 7 218 4 203 6 191 3 175 7 162 5 149 8 132 5 118 6 103 3 91 7 77 5 61 8 49 4 35 6 22 3 10 7 0 5Z" fill="#c9bda4" />
191
+ <path d="M0 5L10 7 22 3 35 6 49 4 61 8 77 5 91 7 103 3 118 6 132 5 149 8 162 5 175 7 191 3 203 6 218 4 228 7 240 5" fill="none" stroke="#a48b60" stroke-opacity=".32" />
192
+ </pattern>
193
+ <pattern id="b270-papyrus-edge-side" width="12" height="240" patternUnits="userSpaceOnUse">
194
+ <path d="M0 0H5L7 14 4 28 6 43 3 59 7 74 5 89 8 105 4 121 6 137 3 152 7 168 5 186 8 203 4 221 5 240H0Z" fill="#c9bda4" />
195
+ <path d="M5 0L7 14 4 28 6 43 3 59 7 74 5 89 8 105 4 121 6 137 3 152 7 168 5 186 8 203 4 221 5 240" fill="none" stroke="#a48b60" stroke-opacity=".32" />
196
+ </pattern>
197
+ <linearGradient id="b270-papyrus-curl" x1="0" y1="0" x2="1" y2="1">
198
+ <stop offset="0" stop-color="#bbaa89" />
199
+ <stop offset=".45" stop-color="#dfd3b9" />
200
+ <stop offset=".78" stop-color="#fff7e5" />
201
+ <stop offset="1" stop-color="#c5b38f" />
202
+ </linearGradient>
203
+ <radialGradient id="b270-papyrus-curl-shadow">
204
+ <stop offset="0" stop-color="#705b37" stop-opacity=".25" />
205
+ <stop offset="1" stop-color="#705b37" stop-opacity="0" />
206
+ </radialGradient>
207
+ <g id="b270-papyrus-corner">
208
+ <ellipse cx="29" cy="31" rx="43" ry="31" fill="url(#b270-papyrus-curl-shadow)" transform="rotate(-42 29 31)" />
209
+ <path d="M0 0H64Q31 14 0 64Z" fill="#c9bda4" />
210
+ <path d="M0 64Q15 27 64 0Q48 24 54 49Q29 43 0 64Z" fill="url(#b270-papyrus-curl)" stroke="#a48b60" stroke-opacity=".3" stroke-width=".7" />
211
+ <path d="M3 61Q26 43 54 49" fill="none" stroke="#fff9ed" stroke-opacity=".65" />
212
+ </g>
213
+ </defs>
214
+ <g class="b270-papyrus-grid">
215
+ <rect y="-100%" width="100%" height="200%" fill="url(#b270-papyrus-fibres)" />
216
+ </g>
217
+ <g class="b270-papyrus-edges">
218
+ <rect width="100%" height="12" fill="url(#b270-papyrus-edge-top)" />
219
+ <rect width="12" height="100%" fill="url(#b270-papyrus-edge-side)" />
220
+ </g>
221
+ <g class="b270-papyrus-edges b270-papyrus-opposite">
222
+ <rect width="100%" height="12" fill="url(#b270-papyrus-edge-top)" />
223
+ <rect width="12" height="100%" fill="url(#b270-papyrus-edge-side)" />
224
+ </g>
225
+ <use class="b270-papyrus-corner" href="#b270-papyrus-corner" />
226
+ <g class="b270-papyrus-opposite">
227
+ <use class="b270-papyrus-corner" href="#b270-papyrus-corner" />
228
+ </g>
229
+ </svg>
230
+
231
+ <svg class="b270-theme-scenery" data-scenery="dark"
232
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
233
+ <defs>
234
+ <pattern id="b270-cedar-grain" width="600" height="900" patternUnits="userSpaceOnUse">
235
+ <g fill="#20190f" opacity=".32">
236
+ <path d="M44 0 38 92 49 166 35 246 43 325 30 412 40 492 29 578 42 659 34 744 48 825 44 900H57L61 824 48 743 57 658 43 578 53 492 44 413 56 326 48 248 62 166 51 92 57 0Z" />
237
+ <path d="M204 0 215 116 201 210 210 294 195 378 207 469 194 559 205 648 193 735 208 819 204 900H217L220 818 207 735 218 648 207 559 220 468 209 378 223 295 215 210 227 116 217 0Z" />
238
+ <path d="M372 0 359 104 371 193 356 283 369 369 358 460 372 547 359 635 373 723 362 815 372 900H385L375 814 387 723 373 635 386 547 372 459 383 369 370 282 385 193 373 105 385 0Z" />
239
+ <path d="M529 0 538 92 523 184 536 276 522 362 533 451 518 542 531 636 520 726 534 813 529 900H540L547 813 533 726 544 636 532 542 546 452 535 362 549 276 537 184 551 92 540 0Z" />
240
+ </g>
241
+ <g id="b270-cedar-fine-grain" fill="none" stroke="#ad8359" stroke-linecap="round" opacity=".17">
242
+ <path stroke-width="2" d="M75 0C51 134 85 209 65 330S79 525 62 653 82 811 75 900M241 0C260 112 231 202 250 314S229 511 247 629 225 802 241 900M402 0C383 127 413 223 393 345S418 534 398 665 414 810 402 900M565 0C581 123 552 223 574 348S552 548 571 677 552 807 565 900" />
243
+ <path d="M97 51 88 147 99 232M95 468 84 556 94 631 85 710M272 79 264 164 276 245 267 320M272 502 261 588 273 667 264 757 272 830M455 43 446 132 458 219 449 295M471 408 459 486 471 568M472 767 461 842" />
244
+ <path d="M163 47C150 163 167 221 150 271M150 416C165 481 151 539 160 617M316 171 307 262 318 347 308 429M311 671 302 753 313 845M491 109 481 195 492 281 482 359" />
245
+ </g>
246
+ <g id="b270-cedar-knots" fill="none" stroke="#9b744e" stroke-width="1.5" opacity=".2">
247
+ <path d="M143 261C113 290 108 342 126 385L143 425C158 390 175 346 162 302Z" />
248
+ <path d="M142 288C126 310 124 340 135 365L144 387C152 359 158 329 149 307Z" />
249
+ <path d="M141 315Q134 335 144 352Q150 332 141 315Z" />
250
+ <path d="M433 576C410 608 405 650 424 695L434 731C449 694 463 655 452 614Z" />
251
+ <path d="M433 604C421 627 420 650 430 677L435 695C443 671 448 646 440 624Z" />
252
+ </g>
253
+ </pattern>
254
+ <linearGradient id="b270-cedar-fade" x1="0" y1="0" x2="0" y2="1">
255
+ <stop offset="0" stop-color="white" stop-opacity="0" />
256
+ <stop offset=".28" stop-color="white" stop-opacity=".5" />
257
+ <stop offset=".7" stop-color="white" stop-opacity=".9" />
258
+ <stop offset="1" stop-color="white" />
259
+ </linearGradient>
260
+ <mask id="b270-cedar-mask">
261
+ <rect width="100%" height="100%" fill="url(#b270-cedar-fade)" />
262
+ </mask>
263
+ </defs>
264
+ <rect width="100%" height="100%" fill="url(#b270-cedar-grain)" mask="url(#b270-cedar-mask)" />
265
+ </svg>
266
+
267
+ <%# Direct paths avoid SVG use-instance repainting when a hidden theme is revealed. %>
268
+ <% spruce_tree_path = "M0 0-9 29-5 27-20 55-11 52-29 85-18 81-39 119-26 113-48 154-32 146-57 194-6 181-6 218H6V181L57 194 32 146 48 154 26 113 39 119 18 81 29 85 11 52 20 55 5 27 9 29Z" %>
269
+ <svg class="b270-theme-scenery" data-scenery="dark_green" viewBox="0 0 1600 440" preserveAspectRatio="xMidYMax slice"
270
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
271
+ <defs>
272
+ <linearGradient id="b270-spruce-fade" x1="0" y1="0" x2="0" y2="1">
273
+ <stop offset="0" stop-color="white" />
274
+ <stop offset=".75" stop-color="white" stop-opacity=".9" />
275
+ <stop offset="1" stop-color="white" stop-opacity="0" />
276
+ </linearGradient>
277
+ <mask id="b270-spruce-mask" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="440">
278
+ <rect width="1600" height="440" fill="url(#b270-spruce-fade)" />
279
+ </mask>
280
+ </defs>
281
+ <g id="b270-spruce-treeline" transform="translate(0 -24)">
282
+ <g id="b270-spruce-distant" fill="#66816b" opacity=".19" mask="url(#b270-spruce-mask)">
283
+ <rect id="b270-spruce-distant-ground" y="300" width="1600" height="164" />
284
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(15 145) scale(.64 .95)" />
285
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(90 171) scale(.55 .82)" />
286
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(164 112) scale(.7 1.1)" />
287
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(244 157) scale(.6 .92)" />
288
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(321 137) scale(.66 1.02)" />
289
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(405 186) scale(.55 .82)" />
290
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(478 163) scale(.67 .96)" />
291
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(560 194) scale(.58 .83)" />
292
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(643 180) scale(.6 .91)" />
293
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(719 212) scale(.5 .77)" />
294
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(790 192) scale(.58 .88)" />
295
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(875 211) scale(.52 .78)" />
296
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(950 174) scale(.62 .96)" />
297
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1030 193) scale(.54 .84)" />
298
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1110 148) scale(.7 1.02)" />
299
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1195 166) scale(.58 .91)" />
300
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1270 108) scale(.7 1.15)" />
301
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1350 153) scale(.6 .94)" />
302
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1435 129) scale(.64 1.05)" />
303
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1515 168) scale(.58 .88)" />
304
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1590 139) scale(.7 1)" />
305
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(53 237) scale(.3 .36)" />
306
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(205 225) scale(.34 .43)" />
307
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(365 247) scale(.28 .31)" />
308
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(520 231) scale(.33 .4)" />
309
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(681 241) scale(.29 .34)" />
310
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(834 218) scale(.36 .47)" />
311
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1068 237) scale(.31 .37)" />
312
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1235 244) scale(.28 .33)" />
313
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#66816b" transform="translate(1394 228) scale(.34 .42)" />
314
+ </g>
315
+ <%# Keep the foreground outside the distant haze mask so no trees show through it. %>
316
+ <g id="b270-spruce-near" fill="#0c2017">
317
+ <rect id="b270-spruce-near-ground" y="350" width="1600" height="114" />
318
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(-15 104) scale(1.1 1.65)" />
319
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(115 64) scale(1.08 1.78)" />
320
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(228 153) scale(.87 1.4)" />
321
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(343 121) scale(1 1.56)" />
322
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(455 189) scale(.83 1.26)" />
323
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(561 161) scale(.94 1.4)" />
324
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(676 227) scale(.75 1.12)" />
325
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(781 202) scale(.88 1.25)" />
326
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(890 231) scale(.76 1.1)" />
327
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(998 182) scale(.89 1.32)" />
328
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1112 153) scale(.98 1.46)" />
329
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1227 106) scale(1.03 1.65)" />
330
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1343 162) scale(.9 1.38)" />
331
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1460 73) scale(1.12 1.78)" />
332
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1580 133) scale(.97 1.5)" />
333
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(55 213) scale(.62 1.08)" />
334
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(394 245) scale(.58 .94)" />
335
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(622 132) scale(.78 1.52)" />
336
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(944 263) scale(.55 .88)" />
337
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1171 230) scale(.65 1.04)" />
338
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1524 201) scale(.7 1.17)" />
339
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(173 282) scale(.34 .4)" />
340
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(283 265) scale(.4 .49)" />
341
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(506 290) scale(.32 .35)" />
342
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(727 271) scale(.38 .46)" />
343
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(835 297) scale(.3 .31)" />
344
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1054 277) scale(.36 .43)" />
345
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1284 288) scale(.33 .36)" />
346
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1398 260) scale(.42 .52)" />
347
+ <path class="b270-spruce-tree" d="<%= spruce_tree_path %>" fill="#0c2017" transform="translate(1552 294) scale(.31 .33)" />
348
+ </g>
349
+ </g>
350
+ </svg>
351
+
352
+ <svg class="b270-theme-scenery" data-scenery="dark_blue" viewBox="0 0 1600 440" preserveAspectRatio="xMaxYMax slice"
353
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
354
+ <defs>
355
+ <radialGradient id="b270-moon-halo">
356
+ <stop offset="0" stop-color="#c7d8ec" stop-opacity=".16" />
357
+ <stop offset=".45" stop-color="#9fb7d3" stop-opacity=".07" />
358
+ <stop offset="1" stop-color="#9fb7d3" stop-opacity="0" />
359
+ </radialGradient>
360
+ <linearGradient id="b270-lake-water" x1="0" y1="0" x2="0" y2="1">
361
+ <stop offset="0" stop-color="#526e91" stop-opacity=".14" />
362
+ <stop offset="1" stop-color="#526e91" stop-opacity="0" />
363
+ </linearGradient>
364
+ <linearGradient id="b270-moon-reflection" x1="0" y1="0" x2="0" y2="1">
365
+ <stop offset="0" stop-color="#c6d6e9" stop-opacity=".3" />
366
+ <stop offset=".5" stop-color="#abc3df" stop-opacity=".17" />
367
+ <stop offset="1" stop-color="#abc3df" stop-opacity="0" />
368
+ </linearGradient>
369
+ <filter id="b270-water-far" x="-20%" y="-50%" width="140%" height="200%" color-interpolation-filters="sRGB">
370
+ <feTurbulence type="fractalNoise" baseFrequency=".008 .14" numOctaves="1" seed="7" result="ripples" />
371
+ <feDisplacementMap in="SourceGraphic" in2="ripples" scale="2" xChannelSelector="R" yChannelSelector="G" />
372
+ <feGaussianBlur stdDeviation=".15" />
373
+ </filter>
374
+ <filter id="b270-water-middle" x="-20%" y="-50%" width="140%" height="200%" color-interpolation-filters="sRGB">
375
+ <feTurbulence type="fractalNoise" baseFrequency=".008 .14" numOctaves="1" seed="7" result="ripples" />
376
+ <feDisplacementMap in="SourceGraphic" in2="ripples" scale="5" xChannelSelector="R" yChannelSelector="G" />
377
+ <feGaussianBlur stdDeviation=".45" />
378
+ </filter>
379
+ <filter id="b270-water-near" x="-20%" y="-50%" width="140%" height="200%" color-interpolation-filters="sRGB">
380
+ <feTurbulence type="fractalNoise" baseFrequency=".008 .14" numOctaves="1" seed="7" result="ripples" />
381
+ <feDisplacementMap in="SourceGraphic" in2="ripples" scale="9" xChannelSelector="R" yChannelSelector="G" />
382
+ <feGaussianBlur stdDeviation="1.1" />
383
+ </filter>
384
+ <mask id="b270-greek-reflection" maskUnits="userSpaceOnUse" x="0" y="240" width="1600" height="200">
385
+ <g fill="white" font-family="Georgia, Times New Roman, serif" font-size="28" text-anchor="middle">
386
+ <g filter="url(#b270-water-far)">
387
+ <text transform="translate(1510 250) scale(.38 -.22)">ΟΛΟΓΟΣ</text>
388
+ <text transform="translate(1510 270) scale(.5 -.28)">ΘΕΟΣΗΝ</text>
389
+ </g>
390
+ <g filter="url(#b270-water-middle)">
391
+ <text transform="translate(1510 293) scale(.66 -.36)">ΘΕΟΝΚΑΙ</text>
392
+ <text transform="translate(1510 319) scale(.8 -.44)">ΗΝΠΡΟΣΤΟΝ</text>
393
+ <text transform="translate(1510 348) scale(.95 -.54)">ΚΑΙΟΛΟΓΟΣ</text>
394
+ </g>
395
+ <g filter="url(#b270-water-near)">
396
+ <text transform="translate(1510 380) scale(1.1 -.68)">ΗΝΟΛΟΓΟΣ</text>
397
+ <text transform="translate(1510 411) scale(1.4 -.85)">ΕΝΑΡΧΗ</text>
398
+ </g>
399
+ </g>
400
+ </mask>
401
+ <radialGradient id="b270-moon-mobile-glow">
402
+ <stop offset="0" stop-color="#c6d6e9" stop-opacity=".18" />
403
+ <stop offset=".5" stop-color="#abc3df" stop-opacity=".07" />
404
+ <stop offset="1" stop-color="#abc3df" stop-opacity="0" />
405
+ </radialGradient>
406
+ <clipPath id="b270-lake-clip">
407
+ <rect y="240" width="1600" height="200" />
408
+ </clipPath>
409
+ </defs>
410
+ <circle cx="1510" cy="162" r="105" fill="url(#b270-moon-halo)" />
411
+ <circle id="b270-midnight-moon" cx="1510" cy="162" r="31" fill="#c7d5e5" opacity=".55" />
412
+ <g fill="#71869d" opacity=".1">
413
+ <ellipse cx="1501" cy="155" rx="9" ry="12" />
414
+ <ellipse cx="1518" cy="173" rx="11" ry="7" />
415
+ </g>
416
+ <path id="b270-lake-shore" fill="#0c1829" opacity=".42"
417
+ d="M0 224Q110 201 230 216T465 220 710 226 950 215 1190 222 1420 224 1600 218V240H0Z" />
418
+ <rect y="240" width="1600" height="200" fill="url(#b270-lake-water)" />
419
+ <path d="M0 240H1600" stroke="#8fa7c3" stroke-width=".7" opacity=".12" />
420
+ <g clip-path="url(#b270-lake-clip)">
421
+ <ellipse cx="1510" cy="305" rx="145" ry="125" fill="url(#b270-moon-halo)" opacity=".6" />
422
+ <rect id="b270-lake-reflection" fill="url(#b270-moon-reflection)"
423
+ x="0" y="240" width="1600" height="200" mask="url(#b270-greek-reflection)" />
424
+ <%# Phones use a quiet pool of moonlight instead of the reflected scripture. %>
425
+ <ellipse class="b270-mobile-scenery" cx="1510" cy="332" rx="65" ry="92" fill="url(#b270-moon-mobile-glow)" />
426
+ <%# The rippled letters supply the desktop water texture without straight lines crossing the reflections. %>
427
+ </g>
428
+ </svg>
429
+
430
+ <svg class="b270-theme-scenery" data-scenery="hebrew_water"
431
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
432
+ <%# A viewport-based layer keeps the faint reflection beneath the Hebrew column on narrow screens. %>
433
+ <g class="b270-hebrew-water" lang="he" direction="rtl" unicode-bidi="isolate"
434
+ fill="#c6d6e9" text-anchor="middle" opacity=".4" font-family="Noto Serif Hebrew, Times New Roman, serif">
435
+ <g filter="url(#b270-water-far)" opacity=".3">
436
+ <text transform="translate(0 10) scale(.38 -.22)">הארץ</text>
437
+ <text transform="translate(0 30) scale(.5 -.28)">ואת</text>
438
+ </g>
439
+ <g filter="url(#b270-water-middle)" opacity=".2">
440
+ <text transform="translate(0 53) scale(.66 -.36)">השּׁמים</text>
441
+ <text transform="translate(0 79) scale(.8 -.44)">את</text>
442
+ <text transform="translate(0 108) scale(.95 -.54)">אלהים</text>
443
+ </g>
444
+ <g filter="url(#b270-water-near)" opacity=".09">
445
+ <text transform="translate(0 140) scale(1.1 -.68)">בּרא</text>
446
+ <text transform="translate(0 171) scale(1.4 -.85)">בּראשׁית</text>
447
+ </g>
448
+ </g>
449
+ </svg>
450
+
451
+ <svg class="b270-theme-scenery b270-theme-scripture" data-scenery="scripture"
452
+ xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
453
+ <%# Shared across themes; explicit lines preserve word breaks and Hebrew vowel marks. %>
454
+ <g class="b270-scripture-letters" fill="currentColor" text-anchor="middle" opacity=".22">
455
+ <g lang="he" direction="rtl" unicode-bidi="isolate" font-family="Noto Serif Hebrew, Times New Roman, serif">
456
+ <text x="10%" y="30%" dy="-3.2em">בּראשׁית</text>
457
+ <text x="10%" y="30%" dy="-2em">בּרא</text>
458
+ <text x="10%" y="30%" dy="-.8em">אלהים</text>
459
+ <text x="10%" y="30%" dy=".4em">את</text>
460
+ <text x="10%" y="30%" dy="1.6em">השּׁמים</text>
461
+ <text x="10%" y="30%" dy="2.8em">ואת</text>
462
+ <text x="10%" y="30%" dy="4em">הארץ</text>
463
+ </g>
464
+ <g lang="el" direction="ltr" text-anchor="middle" font-family="Georgia, Times New Roman, serif">
465
+ <text y="30%" dy="-3.2em"><tspan>ΕΝΑΡΧΗ</tspan></text>
466
+ <text y="30%" dy="-2em"><tspan>ΗΝΟΛΟΓΟΣ</tspan></text>
467
+ <text y="30%" dy="-.8em"><tspan>ΚΑΙΟΛΟΓΟΣ</tspan></text>
468
+ <text y="30%" dy=".4em"><tspan>ΗΝΠΡΟΣΤΟΝ</tspan></text>
469
+ <text y="30%" dy="1.6em"><tspan>ΘΕΟΝΚΑΙ</tspan></text>
470
+ <text y="30%" dy="2.8em"><tspan>ΘΕΟΣΗΝ</tspan></text>
471
+ <text y="30%" dy="4em"><tspan>ΟΛΟΓΟΣ</tspan></text>
472
+ </g>
473
+ </g>
474
+ </svg>
@@ -0,0 +1,108 @@
1
+ <script nonce="<%= content_security_policy_nonce %>" data-refresh-path="<%= refresh_session_path %>">
2
+ (() => {
3
+ if (window.Bible270ThemeUI) {
4
+ window.Bible270ThemeUI.initialize();
5
+ return;
6
+ }
7
+
8
+ const refreshPath = document.currentScript.dataset.refreshPath;
9
+ const selector = "select[data-b270-theme-url]";
10
+
11
+ function status(select, message) {
12
+ const region = document.getElementById(select.dataset.b270ThemeStatus || "b270-theme-status");
13
+ if (region) {
14
+ region.textContent = message;
15
+ region.dataset.error = String(message.startsWith("Couldn’t save"));
16
+ }
17
+ }
18
+
19
+ function syncColorScheme() {
20
+ const mode = document.body.dataset.displayMode || "system";
21
+ document.documentElement.dataset.b270DisplayMode = mode;
22
+ const meta = document.getElementById("b270-color-scheme");
23
+ if (meta) {
24
+ meta.content = ["light", "sage", "mist"].includes(mode) ? "only light" :
25
+ ["dark", "dark_green", "dark_blue"].includes(mode) ? "dark" : "light dark";
26
+ }
27
+ }
28
+
29
+ function initialize() {
30
+ syncColorScheme();
31
+ document.querySelectorAll("[data-b270-theme-fallback]").forEach((button) => { button.hidden = true; });
32
+ }
33
+
34
+ document.addEventListener("change", async (event) => {
35
+ const select = event.target;
36
+ if (!select.matches(selector) || select.disabled) return;
37
+
38
+ const mode = select.value;
39
+ const group = Array.from(document.querySelectorAll(selector))
40
+ .filter((other) => other.isConnected && (
41
+ other.dataset.b270ThemeUrl === select.dataset.b270ThemeUrl ||
42
+ (other.dataset.b270ThemeApply === "true" && select.dataset.b270ThemeApply === "true")
43
+ ))
44
+ .map((other) => ({ select: other, previous: other.dataset.b270SavedTheme }));
45
+ group.forEach(({ select: other }) => {
46
+ other.disabled = true;
47
+ other.setAttribute("aria-busy", "true");
48
+ if (other !== select) {
49
+ const region = document.getElementById(other.dataset.b270ThemeStatus || "b270-theme-status");
50
+ if (region?.dataset.error === "true") status(other, "");
51
+ }
52
+ });
53
+ status(select, "Saving theme…");
54
+
55
+ try {
56
+ // A suspended mobile tab may need a fresh session and CSRF token.
57
+ const session = await fetch(refreshPath, {
58
+ credentials: "same-origin", cache: "no-store", headers: { Accept: "text/plain" }
59
+ });
60
+ const token = session.headers.get("X-CSRF-Token");
61
+ if (!session.ok || !token) throw new Error("Could not refresh the session.");
62
+ const meta = document.querySelector("meta[name='csrf-token']");
63
+ if (meta) meta.content = token;
64
+ document.querySelectorAll("input[name='authenticity_token']").forEach((input) => { input.value = token; });
65
+
66
+ const response = await fetch(select.dataset.b270ThemeUrl, {
67
+ method: "PATCH", credentials: "same-origin", cache: "no-store",
68
+ headers: { Accept: "application/json", "Content-Type": "application/json", "X-CSRF-Token": token },
69
+ body: JSON.stringify({ display_mode: mode })
70
+ });
71
+ const result = await response.json();
72
+ if (!response.ok || result.display_mode !== mode) throw new Error("Theme was not saved.");
73
+
74
+ group.forEach(({ select: other }) => {
75
+ other.value = mode;
76
+ other.dataset.b270SavedTheme = mode;
77
+ });
78
+ if (!select.isConnected) return;
79
+ if (select.dataset.b270ThemeApply === "true") {
80
+ document.body.dataset.displayMode = mode;
81
+ syncColorScheme();
82
+ }
83
+ status(select, "Theme saved.");
84
+ } catch (_error) {
85
+ group.forEach(({ select: other, previous }) => { other.value = previous; });
86
+ if (select.isConnected) status(select, "Couldn’t save the theme. Please select it again to retry.");
87
+ } finally {
88
+ group.forEach(({ select: other }) => {
89
+ other.disabled = false;
90
+ other.removeAttribute("aria-busy");
91
+ });
92
+ }
93
+ });
94
+
95
+ // Do not race a profile submission against an in-flight theme update.
96
+ document.addEventListener("submit", (event) => {
97
+ const select = event.target.querySelector(selector);
98
+ if (select?.disabled) {
99
+ event.preventDefault();
100
+ status(select, "Please wait for the theme to finish saving.");
101
+ }
102
+ });
103
+
104
+ document.addEventListener("turbo:load", initialize);
105
+ window.Bible270ThemeUI = { initialize };
106
+ initialize();
107
+ })();
108
+ </script>
@@ -1,7 +1,16 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <%
3
+ display_mode = current_reader&.effective_display_mode || Bible270::Reader::DEFAULT_DISPLAY_MODE
4
+ color_scheme = case display_mode
5
+ when "light", "sage", "mist" then "only light"
6
+ when "system" then "light dark"
7
+ else "dark"
8
+ end
9
+ %>
10
+ <html lang="en" data-b270-display-mode="<%= display_mode %>">
3
11
  <head>
4
12
  <meta charset="utf-8">
13
+ <meta id="b270-color-scheme" name="color-scheme" content="<%= color_scheme %>">
5
14
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
15
  <title><%= content_for?(:title) ? yield(:title) : Bible270.config.app_name %></title>
7
16
  <%= csrf_meta_tags %>
@@ -13,7 +22,8 @@
13
22
  <%= render "bible270/shared/styles" %>
14
23
  <%= yield :head %>
15
24
  </head>
16
- <body class="b270" data-display-mode="<%= current_reader&.effective_display_mode || Bible270::Reader::DEFAULT_DISPLAY_MODE %>">
25
+ <body class="b270" data-display-mode="<%= display_mode %>">
26
+ <%= render "bible270/shared/theme_scenery" %>
17
27
  <div class="b270-wrap">
18
28
  <%= render "bible270/shared/header" %>
19
29
  <% if flash[:alert] %><div class="b270-flash alert"><%= flash[:alert] %></div><% end %>
@@ -23,6 +33,7 @@
23
33
  <%= render 'bible270/shared/day_index', expanded: content_for?(:expand_day_index) unless content_for?(:hide_day_index) %>
24
34
  <%= b270_footer %>
25
35
  </div>
36
+ <%= render "bible270/shared/theme_ui" %>
26
37
  <%= render "bible270/shared/scripture_popups" %>
27
38
  <%= render "bible270/shared/video_embeds" %>
28
39
  </body>
data/config/routes.rb CHANGED
@@ -44,6 +44,7 @@ Bible270::Engine.routes.draw do
44
44
 
45
45
  get 'profile', to: 'profiles#edit', as: :profile
46
46
  patch 'profile', to: 'profiles#update'
47
+ patch 'profile/theme', to: 'profiles#update_theme', as: :profile_theme
47
48
  delete 'profile/avatar', to: 'profiles#remove_avatar', as: :profile_avatar
48
49
 
49
50
  get 'reflections', to: 'comments#index', as: :reflections
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.10.2'
4
+ VERSION = '1.11.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.2
4
+ version: 1.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -189,6 +189,8 @@ files:
189
189
  - app/views/bible270/shared/_reflections.html.erb
190
190
  - app/views/bible270/shared/_scripture_popups.html.erb
191
191
  - app/views/bible270/shared/_styles.html.erb
192
+ - app/views/bible270/shared/_theme_scenery.html.erb
193
+ - app/views/bible270/shared/_theme_ui.html.erb
192
194
  - app/views/bible270/shared/_track_bars.html.erb
193
195
  - app/views/bible270/shared/_video_embeds.html.erb
194
196
  - app/views/bible270/sign_in_mailer/magic_link.html.erb