kavyabhand commited on
Commit
7e6b030
Β·
verified Β·
1 Parent(s): 8ec00bd

Deploy Aether Garden application

Browse files
Files changed (5) hide show
  1. app.py +186 -46
  2. ui/book_display.py +36 -8
  3. ui/place_ribbon.py +5 -3
  4. ui/relationship_graph.py +16 -6
  5. ui/styles.css +269 -1
app.py CHANGED
@@ -96,9 +96,10 @@ def opening_gate_html() -> str:
96
  </div>
97
  <div class="realm-opening-inner">
98
  <div class="realm-book-stage">
99
- <div class="realm-book-closed" id="realm-book-closed">
 
100
  <div class="realm-book-spine" aria-hidden="true"></div>
101
- <div class="realm-cover-card">
102
  <p class="realm-cover-kicker">Book of Ages, Vol. I</p>
103
  <div class="realm-cover-calligraphy" aria-hidden="true"></div>
104
  <h1 class="realm-cover-title"><span>AETHER</span><span>GARDEN</span></h1>
@@ -106,6 +107,15 @@ def opening_gate_html() -> str:
106
  <button id="realm-opening-enter" type="button">Open The Tome</button>
107
  </div>
108
  </div>
 
 
 
 
 
 
 
 
 
109
  <div class="realm-book-open" id="realm-book-open" aria-hidden="true">
110
  <div class="realm-book-page realm-book-page-left"></div>
111
  <div class="realm-book-page realm-book-page-right"></div>
@@ -213,11 +223,15 @@ def _prewarm_oracle() -> None:
213
 
214
  def render_location_panel(location_id: int | None) -> str:
215
  if not location_id:
216
- return '<div class="location-panel"><p class="feed-empty">Click a location on the map to explore.</p></div>'
 
 
 
 
217
 
218
  location = get_location_by_id(location_id)
219
  if not location:
220
- return ""
221
 
222
  entities = get_entities_by_location(location_id)
223
  chips = "".join(
@@ -231,18 +245,23 @@ def render_location_panel(location_id: int | None) -> str:
231
  special = location.get("special_property", "")
232
 
233
  return f"""
234
- <div class="location-panel location-panel-animate open">
235
- <div class="location-panel-banner" style="{img_style}">
236
- <div class="location-panel-scrim"></div>
237
- <h3 class="location-panel-name">{location['name']}</h3>
238
- </div>
239
- <div class="location-panel-content">
240
- <p class="location-panel-desc">{location['short_description']}</p>
241
- <p class="location-panel-special">✦ {special}</p>
242
- <p style="color: var(--realm-text-tertiary); font-size: 0.8rem; margin-bottom: 0.75rem;">
243
- {len(entities)} souls currently here
244
- </p>
245
- <div class="location-entity-list">{chips}{more}</div>
 
 
 
 
 
246
  </div>
247
  </div>
248
  """
@@ -341,8 +360,18 @@ def filter_entities(location_filter, type_filter, status_filter, search):
341
 
342
 
343
  def filter_book(day, entry_type, search):
 
 
 
 
 
 
 
 
 
 
344
  return render_book_for_day(
345
- day=int(day) if day else None,
346
  entry_type=entry_type,
347
  search=search,
348
  limit=12,
@@ -616,10 +645,11 @@ def build_app() -> gr.Blocks:
616
  with gr.Tab("The Realm", id="realm"):
617
  with gr.Column(elem_classes=["tome-spread-shell"]):
618
  with gr.Row(elem_classes=["tome-spread-row"]):
619
- with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]):
620
  gr.HTML(
 
621
  '<p class="tome-printed-kicker">The Living Realm</p>'
622
- '<p class="tome-printed-hint">Tap a place on the map β€” its tale unfolds below.</p>'
623
  )
624
  world_map = gr.HTML(
625
  value=render_world_map(),
@@ -627,9 +657,12 @@ def build_app() -> gr.Blocks:
627
  )
628
  location_panel = gr.HTML(
629
  value=render_location_panel(None),
 
630
  )
 
631
 
632
- with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
 
633
  realm_stats = gr.HTML(value=render_realm_stats())
634
  current_event = gr.HTML(value=render_current_event())
635
  realm_pulse = gr.HTML(value=render_realm_pulse(limit=4))
@@ -668,6 +701,7 @@ def build_app() -> gr.Blocks:
668
  lines=2,
669
  interactive=True,
670
  )
 
671
 
672
  with gr.Column(elem_classes=["tome-offpage-store"]):
673
  world_vignette = gr.HTML(value=render_world_vignette())
@@ -708,7 +742,11 @@ def build_app() -> gr.Blocks:
708
  with gr.Column(elem_classes=["tome-spread-shell"]):
709
  with gr.Row(elem_classes=["tome-spread-row"]):
710
  with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-explore-index"]):
711
- place_ribbon = gr.HTML(value=render_place_ribbon(_default_loc_id))
 
 
 
 
712
  explore_place_pick = gr.Textbox(visible=False, elem_id="explore_place_pick")
713
  wander_btn = gr.Button("✦ Wander at random", variant="secondary", size="sm")
714
 
@@ -717,7 +755,12 @@ def build_app() -> gr.Blocks:
717
  '<p class="tome-printed-kicker">You step inside</p>'
718
  '<p class="explore-controls-hint">WASD walk Β· drag look Β· E talk Β· golden wisps are live visitors</p>'
719
  )
720
- diorama_view = gr.HTML(value=render_diorama(_default_loc_id))
 
 
 
 
 
721
  portal_pick = gr.Textbox(visible=False, elem_id="portal_location_pick")
722
 
723
  with gr.Column(elem_classes=["tome-offpage-store"]):
@@ -752,6 +795,12 @@ def build_app() -> gr.Blocks:
752
  with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-book-index"]):
753
  day_tabs = gr.HTML(value=render_day_tabs(_init_book_day))
754
  book_day_pick = gr.Textbox(visible=False, elem_id="book_day_pick")
 
 
 
 
 
 
755
  book_filter = gr.Dropdown(
756
  choices=[
757
  ("All", "all"),
@@ -765,7 +814,7 @@ def build_app() -> gr.Blocks:
765
  label="Filter",
766
  )
767
  book_search = gr.Textbox(
768
- placeholder="Search by name...",
769
  label="Search",
770
  )
771
  with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
@@ -777,12 +826,29 @@ def build_app() -> gr.Blocks:
777
  with gr.Column(elem_classes=["tome-spread-shell"]):
778
  with gr.Row(elem_classes=["tome-spread-row"]):
779
  with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]):
780
- bonds_sidebar = gr.HTML(value=render_bonds_sidebar())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
782
  rel_graph = gr.HTML(value=render_relationship_graph())
783
 
784
  with gr.Tab("Souls of the Garden", id="entities"):
785
- featured_chars = gr.HTML(value=render_featured_characters())
786
  entity_pick = gr.Textbox(visible=False, elem_id="entity_pick")
787
 
788
  with gr.Column(elem_classes=["tome-spread-shell"]):
@@ -810,9 +876,21 @@ def build_app() -> gr.Blocks:
810
  scale=1,
811
  )
812
  entity_search = gr.Textbox(placeholder="Search…", label="Find", scale=1)
813
- entity_grid = gr.HTML(value=render_entity_grid(get_all_entities()))
814
- with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
815
- entity_detail = gr.HTML(value=render_entity_hologram(None))
 
 
 
 
 
 
 
 
 
 
 
 
816
 
817
  _summon_outputs = [
818
  world_map, header, activity_feed, entity_result, location_panel,
@@ -994,14 +1072,37 @@ def build_app() -> gr.Blocks:
994
  day = int(day_state or 1)
995
  return day, render_day_tabs(day), filter_book(day, entry_type, search)
996
 
 
 
 
 
 
 
 
 
997
  book_day_pick.change(
998
  _book_day_change,
999
  [book_day_pick, book_day, book_filter, book_search],
1000
  [book_day, day_tabs, book_display],
 
 
 
 
 
 
 
 
 
1001
  )
1002
  book_filter.change(filter_book, [book_day, book_filter, book_search], [book_display])
1003
  book_search.change(filter_book, [book_day, book_filter, book_search], [book_display])
1004
 
 
 
 
 
 
 
1005
  for f in [loc_filter, type_filter, status_filter, entity_search]:
1006
  f.change(
1007
  filter_entities,
@@ -1144,29 +1245,34 @@ def build_app() -> gr.Blocks:
1144
 
1145
  const closeBook = () => {
1146
  const gate = document.getElementById('realm-opening');
 
1147
  if (!gate) return;
1148
  gate.classList.remove('is-closed', 'is-opened', 'is-prologue', 'is-unfolded', 'is-unfolding');
1149
- gate.classList.add('is-reclosed');
1150
- const closedBook = document.getElementById('realm-book-closed');
1151
  const openBook = document.getElementById('realm-book-open');
1152
  const prologue = document.getElementById('realm-prologue');
1153
- if (closedBook) {
1154
- closedBook.classList.remove('is-opening');
1155
- closedBook.style.opacity = '';
1156
- closedBook.style.pointerEvents = '';
1157
- }
1158
  if (openBook) openBook.setAttribute('aria-hidden', 'true');
1159
  if (prologue) {
1160
  prologue.classList.remove('visible');
1161
  prologue.setAttribute('aria-hidden', 'true');
1162
  }
 
 
 
 
 
 
 
 
1163
  applyTurnFx('prev');
1164
  };
1165
 
1166
  const reopenBook = () => {
1167
  const gate = document.getElementById('realm-opening');
 
1168
  if (!gate) return;
1169
- gate.classList.remove('is-reclosed');
 
1170
  gate.classList.add('is-opened', 'is-closed');
1171
  playOpeningChime();
1172
  };
@@ -1323,6 +1429,7 @@ def build_app() -> gr.Blocks:
1323
  if (!gate || gate.dataset.bound) return;
1324
  gate.dataset.bound = '1';
1325
 
 
1326
  const closedBook = document.getElementById('realm-book-closed');
1327
  const openBook = document.getElementById('realm-book-open');
1328
  const prologue = document.getElementById('realm-prologue');
@@ -1388,7 +1495,7 @@ def build_app() -> gr.Blocks:
1388
  const beginUnfold = () => {
1389
  playOpeningChime();
1390
  gate.classList.add('is-unfolding');
1391
- if (closedBook) closedBook.classList.add('is-opening');
1392
  if (openBook) openBook.setAttribute('aria-hidden', 'false');
1393
  setTimeout(() => {
1394
  gate.classList.add('is-unfolded', 'is-prologue');
@@ -1403,7 +1510,8 @@ def build_app() -> gr.Blocks:
1403
  gate.addEventListener('click', (e) => {
1404
  const target = e.target;
1405
  if (!(target instanceof Element)) return;
1406
- if (gate.classList.contains('is-reclosed') && target.id === 'realm-opening-enter') {
 
1407
  e.preventDefault();
1408
  e.stopPropagation();
1409
  reopenBook();
@@ -1461,19 +1569,31 @@ def build_app() -> gr.Blocks:
1461
  }
1462
  };
1463
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1464
  const bindSpreadClicks = () => {
1465
  document.querySelectorAll('.place-ribbon-item').forEach((btn) => {
1466
- if (btn.dataset.ribbonBound) return;
1467
- btn.dataset.ribbonBound = '1';
1468
- btn.addEventListener('click', () => {
1469
  const id = btn.getAttribute('data-location-id');
1470
  if (id) fireHiddenInput('#explore_place_pick', id);
1471
  });
1472
  });
1473
  document.querySelectorAll('.day-tab').forEach((btn) => {
1474
- if (btn.dataset.dayBound) return;
1475
- btn.dataset.dayBound = '1';
1476
- btn.addEventListener('click', () => {
1477
  const day = btn.getAttribute('data-day');
1478
  if (day) fireHiddenInput('#book_day_pick', day);
1479
  });
@@ -1482,16 +1602,36 @@ def build_app() -> gr.Blocks:
1482
  if (card.dataset.entityBound) return;
1483
  card.dataset.entityBound = '1';
1484
  card.style.cursor = 'pointer';
1485
- card.addEventListener('click', () => {
1486
  const id = card.getAttribute('data-entity-id');
1487
  if (id) fireHiddenInput('#entity_pick', id);
1488
  });
1489
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1490
  };
1491
 
1492
  bind();
1493
  bindSpreadClicks();
1494
- new MutationObserver(() => { bind(); bindSpreadClicks(); }).observe(document.body, { childList: true, subtree: true });
 
 
 
 
1495
 
1496
  window._aetherHoverDiorama = false;
1497
  window._aetherDioramaActive = false;
 
96
  </div>
97
  <div class="realm-opening-inner">
98
  <div class="realm-book-stage">
99
+ <div class="realm-book-3d" id="realm-book-3d">
100
+ <div class="realm-book-face realm-book-front" id="realm-book-closed">
101
  <div class="realm-book-spine" aria-hidden="true"></div>
102
+ <div class="realm-cover-card realm-cover-front">
103
  <p class="realm-cover-kicker">Book of Ages, Vol. I</p>
104
  <div class="realm-cover-calligraphy" aria-hidden="true"></div>
105
  <h1 class="realm-cover-title"><span>AETHER</span><span>GARDEN</span></h1>
 
107
  <button id="realm-opening-enter" type="button">Open The Tome</button>
108
  </div>
109
  </div>
110
+ <div class="realm-book-face realm-book-back" id="realm-book-back" aria-hidden="true">
111
+ <div class="realm-cover-card realm-cover-back">
112
+ <div class="realm-back-ornament" aria-hidden="true">❧</div>
113
+ <p class="realm-cover-kicker">End of Vol. I</p>
114
+ <p class="realm-back-text">The Realm remembereth all things.<br/>Close the cover β€” until thou returnest.</p>
115
+ <button id="realm-reopen-enter" type="button">Open The Tome</button>
116
+ </div>
117
+ </div>
118
+ </div>
119
  <div class="realm-book-open" id="realm-book-open" aria-hidden="true">
120
  <div class="realm-book-page realm-book-page-left"></div>
121
  <div class="realm-book-page realm-book-page-right"></div>
 
223
 
224
  def render_location_panel(location_id: int | None) -> str:
225
  if not location_id:
226
+ return (
227
+ '<div id="location-panel-host" class="location-panel-host">'
228
+ '<p class="feed-empty map-hint">Click a glowing place on the map to unveil its secrets.</p>'
229
+ '</div>'
230
+ )
231
 
232
  location = get_location_by_id(location_id)
233
  if not location:
234
+ return '<div id="location-panel-host" class="location-panel-host"></div>'
235
 
236
  entities = get_entities_by_location(location_id)
237
  chips = "".join(
 
245
  special = location.get("special_property", "")
246
 
247
  return f"""
248
+ <div id="location-panel-host" class="location-panel-host" data-open="1">
249
+ <div class="location-modal-overlay is-open" id="location-modal">
250
+ <div class="location-modal-card">
251
+ <div class="location-modal-close" role="button" tabindex="0" aria-label="Close">βœ•</div>
252
+ <div class="location-panel location-panel-animate open">
253
+ <div class="location-panel-banner" style="{img_style}">
254
+ <div class="location-panel-scrim"></div>
255
+ <h3 class="location-panel-name">{location['name']}</h3>
256
+ </div>
257
+ <div class="location-panel-content">
258
+ <p class="location-panel-desc">{location['short_description']}</p>
259
+ <p class="location-panel-special">✦ {special}</p>
260
+ <p class="location-soul-count">{len(entities)} souls currently here</p>
261
+ <div class="location-entity-list">{chips}{more}</div>
262
+ </div>
263
+ </div>
264
+ </div>
265
  </div>
266
  </div>
267
  """
 
360
 
361
 
362
  def filter_book(day, entry_type, search):
363
+ from ui.book_display import parse_day_query
364
+
365
+ parsed = parse_day_query(search)
366
+ if parsed is not None:
367
+ day = parsed
368
+ search = ""
369
+ try:
370
+ day_int = int(day) if day is not None else None
371
+ except (TypeError, ValueError):
372
+ day_int = None
373
  return render_book_for_day(
374
+ day=day_int,
375
  entry_type=entry_type,
376
  search=search,
377
  limit=12,
 
645
  with gr.Tab("The Realm", id="realm"):
646
  with gr.Column(elem_classes=["tome-spread-shell"]):
647
  with gr.Row(elem_classes=["tome-spread-row"]):
648
+ with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-page-centered"]):
649
  gr.HTML(
650
+ '<div class="tome-ornament top" aria-hidden="true">✦ ─── ❧ ─── ✦</div>'
651
  '<p class="tome-printed-kicker">The Living Realm</p>'
652
+ '<p class="tome-printed-hint">Tap a place on the map β€” a vision shall rise.</p>'
653
  )
654
  world_map = gr.HTML(
655
  value=render_world_map(),
 
657
  )
658
  location_panel = gr.HTML(
659
  value=render_location_panel(None),
660
+ elem_id="location-panel-host-wrap",
661
  )
662
+ gr.HTML('<div class="tome-ornament bottom" aria-hidden="true">❧</div>')
663
 
664
+ with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-page-centered"]):
665
+ gr.HTML('<div class="tome-ornament top" aria-hidden="true">✦ ─── ❧ ─── ✦</div>')
666
  realm_stats = gr.HTML(value=render_realm_stats())
667
  current_event = gr.HTML(value=render_current_event())
668
  realm_pulse = gr.HTML(value=render_realm_pulse(limit=4))
 
701
  lines=2,
702
  interactive=True,
703
  )
704
+ gr.HTML('<div class="tome-ornament bottom" aria-hidden="true">❧</div>')
705
 
706
  with gr.Column(elem_classes=["tome-offpage-store"]):
707
  world_vignette = gr.HTML(value=render_world_vignette())
 
742
  with gr.Column(elem_classes=["tome-spread-shell"]):
743
  with gr.Row(elem_classes=["tome-spread-row"]):
744
  with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-explore-index"]):
745
+ place_ribbon = gr.HTML(
746
+ value=render_place_ribbon(_default_loc_id),
747
+ container=False,
748
+ padding=False,
749
+ )
750
  explore_place_pick = gr.Textbox(visible=False, elem_id="explore_place_pick")
751
  wander_btn = gr.Button("✦ Wander at random", variant="secondary", size="sm")
752
 
 
755
  '<p class="tome-printed-kicker">You step inside</p>'
756
  '<p class="explore-controls-hint">WASD walk Β· drag look Β· E talk Β· golden wisps are live visitors</p>'
757
  )
758
+ diorama_view = gr.HTML(
759
+ value=render_diorama(_default_loc_id),
760
+ container=False,
761
+ padding=False,
762
+ elem_id="explore-diorama-host",
763
+ )
764
  portal_pick = gr.Textbox(visible=False, elem_id="portal_location_pick")
765
 
766
  with gr.Column(elem_classes=["tome-offpage-store"]):
 
795
  with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-book-index"]):
796
  day_tabs = gr.HTML(value=render_day_tabs(_init_book_day))
797
  book_day_pick = gr.Textbox(visible=False, elem_id="book_day_pick")
798
+ _day_choices = list(range(max(1, _init_book_day - 14), _init_book_day + 1))
799
+ book_day_select = gr.Dropdown(
800
+ choices=[(f"Day {d}", d) for d in reversed(_day_choices)],
801
+ value=_init_book_day,
802
+ label="Jump to day",
803
+ )
804
  book_filter = gr.Dropdown(
805
  choices=[
806
  ("All", "all"),
 
814
  label="Filter",
815
  )
816
  book_search = gr.Textbox(
817
+ placeholder='Search name… or type "Day 3"',
818
  label="Search",
819
  )
820
  with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
 
826
  with gr.Column(elem_classes=["tome-spread-shell"]):
827
  with gr.Row(elem_classes=["tome-spread-row"]):
828
  with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]):
829
+ bonds_type_filter = gr.Dropdown(
830
+ choices=[
831
+ ("All bonds", "all"),
832
+ ("Allied", "allied"),
833
+ ("Friends", "friends"),
834
+ ("Rivals", "rivals"),
835
+ ("Enemies", "enemies"),
836
+ ("Mentor", "mentor"),
837
+ ("Owes debt", "owes_debt"),
838
+ ("Curious", "curious"),
839
+ ],
840
+ value="all",
841
+ label="Show connections",
842
+ )
843
+ bonds_sidebar = gr.HTML(
844
+ value=render_bonds_sidebar(),
845
+ container=False,
846
+ padding=False,
847
+ )
848
  with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
849
  rel_graph = gr.HTML(value=render_relationship_graph())
850
 
851
  with gr.Tab("Souls of the Garden", id="entities"):
 
852
  entity_pick = gr.Textbox(visible=False, elem_id="entity_pick")
853
 
854
  with gr.Column(elem_classes=["tome-spread-shell"]):
 
876
  scale=1,
877
  )
878
  entity_search = gr.Textbox(placeholder="Search…", label="Find", scale=1)
879
+ entity_grid = gr.HTML(
880
+ value=render_entity_grid(get_all_entities()),
881
+ container=False,
882
+ padding=False,
883
+ )
884
+ with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-souls-stage"]):
885
+ _first_ent = get_all_entities(limit=1)
886
+ entity_detail = gr.HTML(
887
+ value=render_entity_hologram(_first_ent[0] if _first_ent else None),
888
+ container=False,
889
+ padding=False,
890
+ )
891
+
892
+ with gr.Column(elem_classes=["tome-offpage-store"]):
893
+ featured_chars = gr.HTML(value=render_featured_characters())
894
 
895
  _summon_outputs = [
896
  world_map, header, activity_feed, entity_result, location_panel,
 
1072
  day = int(day_state or 1)
1073
  return day, render_day_tabs(day), filter_book(day, entry_type, search)
1074
 
1075
+ def _book_day_select_change(day_val, entry_type, search):
1076
+ try:
1077
+ day = int(day_val)
1078
+ except (TypeError, ValueError):
1079
+ state = get_world_state()
1080
+ day = int(state.get("current_day", 1) or 1)
1081
+ return day, render_day_tabs(day), filter_book(day, entry_type, search)
1082
+
1083
  book_day_pick.change(
1084
  _book_day_change,
1085
  [book_day_pick, book_day, book_filter, book_search],
1086
  [book_day, day_tabs, book_display],
1087
+ ).then(
1088
+ lambda d: d,
1089
+ inputs=[book_day],
1090
+ outputs=[book_day_select],
1091
+ )
1092
+ book_day_select.change(
1093
+ _book_day_select_change,
1094
+ [book_day_select, book_filter, book_search],
1095
+ [book_day, day_tabs, book_display],
1096
  )
1097
  book_filter.change(filter_book, [book_day, book_filter, book_search], [book_display])
1098
  book_search.change(filter_book, [book_day, book_filter, book_search], [book_display])
1099
 
1100
+ bonds_type_filter.change(
1101
+ render_bonds_sidebar,
1102
+ [bonds_type_filter],
1103
+ [bonds_sidebar],
1104
+ )
1105
+
1106
  for f in [loc_filter, type_filter, status_filter, entity_search]:
1107
  f.change(
1108
  filter_entities,
 
1245
 
1246
  const closeBook = () => {
1247
  const gate = document.getElementById('realm-opening');
1248
+ const book3d = document.getElementById('realm-book-3d');
1249
  if (!gate) return;
1250
  gate.classList.remove('is-closed', 'is-opened', 'is-prologue', 'is-unfolded', 'is-unfolding');
1251
+ gate.classList.add('is-closing', 'is-reclosed');
 
1252
  const openBook = document.getElementById('realm-book-open');
1253
  const prologue = document.getElementById('realm-prologue');
 
 
 
 
 
1254
  if (openBook) openBook.setAttribute('aria-hidden', 'true');
1255
  if (prologue) {
1256
  prologue.classList.remove('visible');
1257
  prologue.setAttribute('aria-hidden', 'true');
1258
  }
1259
+ if (book3d) {
1260
+ book3d.classList.remove('is-opening');
1261
+ setTimeout(() => book3d.classList.add('show-back'), prefersReduced ? 0 : 80);
1262
+ }
1263
+ setTimeout(() => {
1264
+ gate.classList.remove('is-closing');
1265
+ gate.classList.add('is-back-visible');
1266
+ }, prefersReduced ? 60 : 1150);
1267
  applyTurnFx('prev');
1268
  };
1269
 
1270
  const reopenBook = () => {
1271
  const gate = document.getElementById('realm-opening');
1272
+ const book3d = document.getElementById('realm-book-3d');
1273
  if (!gate) return;
1274
+ if (book3d) book3d.classList.remove('show-back', 'is-opening');
1275
+ gate.classList.remove('is-reclosed', 'is-back-visible', 'is-closing');
1276
  gate.classList.add('is-opened', 'is-closed');
1277
  playOpeningChime();
1278
  };
 
1429
  if (!gate || gate.dataset.bound) return;
1430
  gate.dataset.bound = '1';
1431
 
1432
+ const book3d = document.getElementById('realm-book-3d');
1433
  const closedBook = document.getElementById('realm-book-closed');
1434
  const openBook = document.getElementById('realm-book-open');
1435
  const prologue = document.getElementById('realm-prologue');
 
1495
  const beginUnfold = () => {
1496
  playOpeningChime();
1497
  gate.classList.add('is-unfolding');
1498
+ if (book3d) book3d.classList.add('is-opening');
1499
  if (openBook) openBook.setAttribute('aria-hidden', 'false');
1500
  setTimeout(() => {
1501
  gate.classList.add('is-unfolded', 'is-prologue');
 
1510
  gate.addEventListener('click', (e) => {
1511
  const target = e.target;
1512
  if (!(target instanceof Element)) return;
1513
+ if (gate.classList.contains('is-reclosed') &&
1514
+ (target.id === 'realm-opening-enter' || target.id === 'realm-reopen-enter')) {
1515
  e.preventDefault();
1516
  e.stopPropagation();
1517
  reopenBook();
 
1569
  }
1570
  };
1571
 
1572
+ const wireClickable = (el, handler) => {
1573
+ if (!el || el.dataset.boundClick) return;
1574
+ el.dataset.boundClick = '1';
1575
+ el.addEventListener('click', handler);
1576
+ el.addEventListener('keydown', (e) => {
1577
+ if (e.key === 'Enter' || e.key === ' ') {
1578
+ e.preventDefault();
1579
+ handler(e);
1580
+ }
1581
+ });
1582
+ };
1583
+
1584
+ const closeLocationModal = () => {
1585
+ document.querySelectorAll('.location-modal-overlay').forEach((m) => m.classList.remove('is-open'));
1586
+ };
1587
+
1588
  const bindSpreadClicks = () => {
1589
  document.querySelectorAll('.place-ribbon-item').forEach((btn) => {
1590
+ wireClickable(btn, () => {
 
 
1591
  const id = btn.getAttribute('data-location-id');
1592
  if (id) fireHiddenInput('#explore_place_pick', id);
1593
  });
1594
  });
1595
  document.querySelectorAll('.day-tab').forEach((btn) => {
1596
+ wireClickable(btn, () => {
 
 
1597
  const day = btn.getAttribute('data-day');
1598
  if (day) fireHiddenInput('#book_day_pick', day);
1599
  });
 
1602
  if (card.dataset.entityBound) return;
1603
  card.dataset.entityBound = '1';
1604
  card.style.cursor = 'pointer';
1605
+ wireClickable(card, () => {
1606
  const id = card.getAttribute('data-entity-id');
1607
  if (id) fireHiddenInput('#entity_pick', id);
1608
  });
1609
  });
1610
+ document.querySelectorAll('.location-modal-close').forEach((btn) => {
1611
+ wireClickable(btn, closeLocationModal);
1612
+ });
1613
+ document.querySelectorAll('.location-modal-overlay').forEach((overlay) => {
1614
+ if (overlay.dataset.overlayBound) return;
1615
+ overlay.dataset.overlayBound = '1';
1616
+ overlay.addEventListener('click', (e) => {
1617
+ if (e.target === overlay) closeLocationModal();
1618
+ });
1619
+ });
1620
+ };
1621
+
1622
+ const showLocationModal = () => {
1623
+ const host = document.querySelector('#location-panel-host, [data-open="1"] .location-modal-overlay, .location-modal-overlay');
1624
+ const modal = document.querySelector('.location-modal-overlay');
1625
+ if (modal) modal.classList.add('is-open');
1626
  };
1627
 
1628
  bind();
1629
  bindSpreadClicks();
1630
+ new MutationObserver(() => {
1631
+ bind();
1632
+ bindSpreadClicks();
1633
+ if (document.querySelector('.location-modal-overlay .location-panel-name')) showLocationModal();
1634
+ }).observe(document.body, { childList: true, subtree: true });
1635
 
1636
  window._aetherHoverDiorama = false;
1637
  window._aetherDioramaActive = false;
ui/book_display.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
5
- from world.book_of_ages import get_entries
6
  from world.database import get_world_state
7
 
8
  ENTRY_TYPE_LABELS = {
@@ -95,7 +95,7 @@ def render_day_tabs(selected_day: int | None = None) -> str:
95
  for day in reversed(days):
96
  sel = " is-active" if day == selected_day else ""
97
  buttons.append(
98
- f'<button type="button" class="day-tab{sel}" data-day="{day}">Day {day}</button>'
99
  )
100
 
101
  return f"""
@@ -108,6 +108,21 @@ def render_day_tabs(selected_day: int | None = None) -> str:
108
  """
109
 
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  def render_book_for_day(
112
  day: int | None = None,
113
  entry_type: str = "all",
@@ -119,12 +134,25 @@ def render_book_for_day(
119
  state = get_world_state()
120
  day = int(state.get("current_day", 1) or 1)
121
 
122
- entries = get_entries(
123
- limit=limit * 3,
124
- entry_type=entry_type if entry_type != "all" else None,
125
- search=search or None,
126
- )
127
- entries = [e for e in entries if e["world_day"] == day][:limit]
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  if not entries:
130
  return f"""
 
2
 
3
  from __future__ import annotations
4
 
5
+ from world.book_of_ages import get_entries, get_entries_by_day
6
  from world.database import get_world_state
7
 
8
  ENTRY_TYPE_LABELS = {
 
95
  for day in reversed(days):
96
  sel = " is-active" if day == selected_day else ""
97
  buttons.append(
98
+ f'<div class="day-tab{sel}" role="button" tabindex="0" data-day="{day}">Day {day}</div>'
99
  )
100
 
101
  return f"""
 
108
  """
109
 
110
 
111
+ def parse_day_query(text: str | None) -> int | None:
112
+ """Parse 'Day 3', 'day3', or '3' from user input."""
113
+ if not text or not str(text).strip():
114
+ return None
115
+ import re
116
+
117
+ raw = str(text).strip()
118
+ m = re.match(r"^day\s*(\d+)$", raw, re.I)
119
+ if m:
120
+ return int(m.group(1))
121
+ if raw.isdigit():
122
+ return int(raw)
123
+ return None
124
+
125
+
126
  def render_book_for_day(
127
  day: int | None = None,
128
  entry_type: str = "all",
 
134
  state = get_world_state()
135
  day = int(state.get("current_day", 1) or 1)
136
 
137
+ try:
138
+ day = int(day)
139
+ except (TypeError, ValueError):
140
+ state = get_world_state()
141
+ day = int(state.get("current_day", 1) or 1)
142
+
143
+ parsed = parse_day_query(search)
144
+ if parsed is not None:
145
+ day = parsed
146
+ search = ""
147
+
148
+ entries = get_entries_by_day(day)
149
+ if entry_type and entry_type != "all":
150
+ entries = [e for e in entries if e["entry_type"] == entry_type]
151
+ if search:
152
+ q = search.lower()
153
+ entries = [e for e in entries if q in (e.get("content") or "").lower()
154
+ or q in (e.get("title") or "").lower()]
155
+ entries = entries[:limit]
156
 
157
  if not entries:
158
  return f"""
ui/place_ribbon.py CHANGED
@@ -20,18 +20,20 @@ def render_place_ribbon(selected_id: int | None = None) -> str:
20
  else '<span class="ribbon-thumb ribbon-thumb-empty"></span>'
21
  )
22
  items.append(f"""
23
- <button type="button" class="place-ribbon-item{sel}" data-location-id="{loc['id']}"
24
- title="{loc['name']}">
25
  {thumb}
26
  <span class="ribbon-label">{short}</span>
27
  <span class="ribbon-count">{loc.get('entity_count', 0)} souls</span>
28
- </button>
29
  """)
30
 
31
  return f"""
32
  <div class="place-ribbon">
 
33
  <p class="place-ribbon-kicker">Sacred Places</p>
34
  <p class="place-ribbon-hint">Choose a realm β€” the world opens on the right page.</p>
35
  <div class="place-ribbon-list">{"".join(items)}</div>
 
36
  </div>
37
  """
 
20
  else '<span class="ribbon-thumb ribbon-thumb-empty"></span>'
21
  )
22
  items.append(f"""
23
+ <div class="place-ribbon-item{sel}" role="button" tabindex="0"
24
+ data-location-id="{loc['id']}" title="{loc['name']}">
25
  {thumb}
26
  <span class="ribbon-label">{short}</span>
27
  <span class="ribbon-count">{loc.get('entity_count', 0)} souls</span>
28
+ </div>
29
  """)
30
 
31
  return f"""
32
  <div class="place-ribbon">
33
+ <div class="tome-ornament top" aria-hidden="true">✦ ─── ❧ ─── ✦</div>
34
  <p class="place-ribbon-kicker">Sacred Places</p>
35
  <p class="place-ribbon-hint">Choose a realm β€” the world opens on the right page.</p>
36
  <div class="place-ribbon-list">{"".join(items)}</div>
37
+ <div class="tome-ornament bottom" aria-hidden="true">❧</div>
38
  </div>
39
  """
ui/relationship_graph.py CHANGED
@@ -242,13 +242,21 @@ def render_relationship_graph(selected_entity_id: str | None = None) -> str:
242
  return render_bonds_graph_frame(src)
243
 
244
 
245
- def render_bonds_sidebar() -> str:
246
  relationships = get_all_relationships()
 
 
 
247
  if not relationships:
248
- return """
 
 
 
 
 
249
  <div class="bonds-sidebar">
250
  <p class="tome-printed-kicker">The Web of Bonds</p>
251
- <p class="bonds-sidebar-empty">No alliances forged yet β€” wait for the next realm tick.</p>
252
  </div>
253
  """
254
 
@@ -257,17 +265,19 @@ def render_bonds_sidebar() -> str:
257
  for k, c in REL_COLORS.items()
258
  )
259
  rel_list = []
260
- for rel in relationships[:12]:
261
  t = TYPE_SHORT.get(rel["relationship_type"], rel["relationship_type"])
 
262
  rel_list.append(
263
  f'<li><b>{rel["entity_a_name"]}</b> ↔ <b>{rel["entity_b_name"]}</b> '
264
- f'Β· {t} β€” <em>{rel["description"][:72]}</em></li>'
265
  )
266
 
 
267
  return f"""
268
  <div class="bonds-sidebar">
269
  <p class="tome-printed-kicker">The Web of Bonds</p>
270
- <p class="bonds-sidebar-desc">{len(relationships)} threads between living souls.</p>
271
  <div class="rel-legend">{legend}</div>
272
  <ul class="rel-list">{"".join(rel_list)}</ul>
273
  </div>
 
242
  return render_bonds_graph_frame(src)
243
 
244
 
245
+ def render_bonds_sidebar(rel_type: str = "all") -> str:
246
  relationships = get_all_relationships()
247
+ if rel_type and rel_type != "all":
248
+ relationships = [r for r in relationships if r["relationship_type"] == rel_type]
249
+
250
  if not relationships:
251
+ empty = (
252
+ "No bonds of this kind yet β€” try another thread type."
253
+ if rel_type and rel_type != "all"
254
+ else "No alliances forged yet β€” wait for the next realm tick."
255
+ )
256
+ return f"""
257
  <div class="bonds-sidebar">
258
  <p class="tome-printed-kicker">The Web of Bonds</p>
259
+ <p class="bonds-sidebar-empty">{empty}</p>
260
  </div>
261
  """
262
 
 
265
  for k, c in REL_COLORS.items()
266
  )
267
  rel_list = []
268
+ for rel in relationships[:10]:
269
  t = TYPE_SHORT.get(rel["relationship_type"], rel["relationship_type"])
270
+ desc = (rel.get("description") or "A bond still forming…")[:72]
271
  rel_list.append(
272
  f'<li><b>{rel["entity_a_name"]}</b> ↔ <b>{rel["entity_b_name"]}</b> '
273
+ f'Β· {t} β€” <em>{desc}</em></li>'
274
  )
275
 
276
+ total = len(get_all_relationships())
277
  return f"""
278
  <div class="bonds-sidebar">
279
  <p class="tome-printed-kicker">The Web of Bonds</p>
280
+ <p class="bonds-sidebar-desc">{len(relationships)} shown Β· {total} threads total</p>
281
  <div class="rel-legend">{legend}</div>
282
  <ul class="rel-list">{"".join(rel_list)}</ul>
283
  </div>
ui/styles.css CHANGED
@@ -4347,10 +4347,278 @@ body,
4347
  .tome-printed-page .current-event,
4348
  .tome-printed-page .realm-pulse,
4349
  .tome-printed-page .activity-feed,
4350
- .tome-printed-page .location-panel,
4351
  .tome-printed-page .book-container,
4352
  .tome-printed-page .entity-hologram-empty .hologram-stage {
4353
  background: rgba(255, 249, 232, 0.62) !important;
4354
  box-shadow: none !important;
4355
  border: 1px solid rgba(138, 106, 58, 0.22) !important;
4356
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4347
  .tome-printed-page .current-event,
4348
  .tome-printed-page .realm-pulse,
4349
  .tome-printed-page .activity-feed,
 
4350
  .tome-printed-page .book-container,
4351
  .tome-printed-page .entity-hologram-empty .hologram-stage {
4352
  background: rgba(255, 249, 232, 0.62) !important;
4353
  box-shadow: none !important;
4354
  border: 1px solid rgba(138, 106, 58, 0.22) !important;
4355
  }
4356
+
4357
+ /* ── Centered spread pages with ornaments ── */
4358
+ .tome-page-centered {
4359
+ justify-content: center !important;
4360
+ gap: 0.45rem !important;
4361
+ }
4362
+
4363
+ .tome-ornament {
4364
+ text-align: center;
4365
+ font-family: 'Cinzel', serif;
4366
+ font-size: 0.72rem;
4367
+ letter-spacing: 0.22em;
4368
+ color: rgba(122, 90, 40, 0.55);
4369
+ margin: 0.15rem 0;
4370
+ flex-shrink: 0;
4371
+ }
4372
+
4373
+ .tome-ornament.top { margin-bottom: 0.35rem; }
4374
+ .tome-ornament.bottom { margin-top: auto; padding-top: 0.35rem; }
4375
+
4376
+ /* ── Location popup modal ── */
4377
+ .location-panel-host {
4378
+ position: relative;
4379
+ min-height: 0;
4380
+ }
4381
+
4382
+ .location-panel-host .map-hint {
4383
+ text-align: center;
4384
+ font-family: 'IM Fell English', serif;
4385
+ font-style: italic;
4386
+ color: #5c4228;
4387
+ padding: 0.5rem;
4388
+ }
4389
+
4390
+ .location-modal-overlay {
4391
+ display: none;
4392
+ position: fixed;
4393
+ inset: 0;
4394
+ z-index: 8800;
4395
+ background: rgba(12, 8, 5, 0.72);
4396
+ backdrop-filter: blur(4px);
4397
+ align-items: center;
4398
+ justify-content: center;
4399
+ padding: 1rem;
4400
+ }
4401
+
4402
+ .location-modal-overlay.is-open {
4403
+ display: flex;
4404
+ animation: modalFadeIn 0.35s ease;
4405
+ }
4406
+
4407
+ @keyframes modalFadeIn {
4408
+ from { opacity: 0; }
4409
+ to { opacity: 1; }
4410
+ }
4411
+
4412
+ .location-modal-card {
4413
+ position: relative;
4414
+ width: min(420px, 92vw);
4415
+ max-height: min(78vh, 560px);
4416
+ overflow: auto;
4417
+ border-radius: 14px;
4418
+ transform: perspective(900px) rotateX(6deg) scale(0.98);
4419
+ animation: modalPop 0.42s cubic-bezier(0.34, 1.2, 0.64, 1);
4420
+ box-shadow:
4421
+ 0 28px 64px rgba(0, 0, 0, 0.55),
4422
+ 0 0 40px rgba(201, 161, 59, 0.15);
4423
+ }
4424
+
4425
+ @keyframes modalPop {
4426
+ from { opacity: 0; transform: perspective(900px) rotateX(14deg) translateY(24px) scale(0.92); }
4427
+ to { opacity: 1; transform: perspective(900px) rotateX(6deg) scale(0.98); }
4428
+ }
4429
+
4430
+ .location-modal-close {
4431
+ position: absolute;
4432
+ top: 0.55rem;
4433
+ right: 0.65rem;
4434
+ z-index: 3;
4435
+ width: 28px;
4436
+ height: 28px;
4437
+ border-radius: 50%;
4438
+ background: rgba(20, 12, 8, 0.65);
4439
+ color: #f4e8c8;
4440
+ display: grid;
4441
+ place-items: center;
4442
+ cursor: pointer;
4443
+ font-size: 0.9rem;
4444
+ border: 1px solid rgba(201, 161, 59, 0.35);
4445
+ }
4446
+
4447
+ .location-modal-card .location-panel {
4448
+ margin: 0 !important;
4449
+ border: none !important;
4450
+ box-shadow: none !important;
4451
+ }
4452
+
4453
+ .location-soul-count {
4454
+ color: #5c4228;
4455
+ font-size: 0.8rem;
4456
+ margin-bottom: 0.75rem;
4457
+ font-family: 'IM Fell English', serif;
4458
+ }
4459
+
4460
+ /* ── Explore spread β€” force 3D diorama visible ── */
4461
+ .tome-explore-index,
4462
+ .tome-explore-stage {
4463
+ display: flex !important;
4464
+ flex-direction: column !important;
4465
+ min-height: 0 !important;
4466
+ flex: 1 1 auto !important;
4467
+ }
4468
+
4469
+ .tome-explore-index .place-ribbon {
4470
+ flex: 1 1 auto;
4471
+ min-height: 180px;
4472
+ }
4473
+
4474
+ .tome-explore-index .place-ribbon-list {
4475
+ flex: 1 1 auto;
4476
+ min-height: 140px;
4477
+ max-height: 380px;
4478
+ }
4479
+
4480
+ .tome-explore-stage > .block,
4481
+ .tome-explore-stage > .form,
4482
+ .tome-explore-stage #explore-diorama-host,
4483
+ .tome-explore-stage #explore-diorama-host > div {
4484
+ flex: 1 1 auto !important;
4485
+ min-height: 260px !important;
4486
+ display: flex !important;
4487
+ flex-direction: column !important;
4488
+ }
4489
+
4490
+ .tome-explore-stage .diorama-wrap {
4491
+ flex: 1 1 auto !important;
4492
+ min-height: 260px !important;
4493
+ max-height: 100% !important;
4494
+ margin: 0 !important;
4495
+ display: flex !important;
4496
+ flex-direction: column !important;
4497
+ }
4498
+
4499
+ .tome-explore-stage .diorama-header {
4500
+ display: none !important;
4501
+ }
4502
+
4503
+ .tome-explore-stage .diorama-stage {
4504
+ flex: 1 1 auto !important;
4505
+ min-height: 240px !important;
4506
+ }
4507
+
4508
+ .tome-explore-stage .diorama-frame {
4509
+ min-height: 240px !important;
4510
+ height: 100% !important;
4511
+ max-height: 400px !important;
4512
+ }
4513
+
4514
+ .tome-explore-stage .diorama-empty {
4515
+ flex: 1 1 auto;
4516
+ min-height: 240px;
4517
+ display: grid;
4518
+ place-items: center;
4519
+ padding: 1rem;
4520
+ text-align: center;
4521
+ border: 1px dashed rgba(138, 106, 58, 0.35);
4522
+ border-radius: 12px;
4523
+ font-family: 'IM Fell English', serif;
4524
+ }
4525
+
4526
+ /* ── Souls spread fix ── */
4527
+ .tome-souls-index,
4528
+ .tome-souls-stage {
4529
+ display: flex !important;
4530
+ flex-direction: column !important;
4531
+ min-height: 0 !important;
4532
+ }
4533
+
4534
+ .tome-souls-index .entity-grid {
4535
+ flex: 1 1 auto;
4536
+ max-height: none;
4537
+ min-height: 200px;
4538
+ }
4539
+
4540
+ .tome-souls-stage .entity-hologram {
4541
+ flex: 1 1 auto;
4542
+ min-height: 280px;
4543
+ }
4544
+
4545
+ #tome-spreads [role="tabpanel"]:has(#entities) .featured-section {
4546
+ display: none !important;
4547
+ }
4548
+
4549
+ /* ── Book 3D cover flip ── */
4550
+ .realm-book-3d {
4551
+ position: relative;
4552
+ width: min(440px, 90vw);
4553
+ min-height: min(520px, 72vh);
4554
+ transform-style: preserve-3d;
4555
+ transition: transform 1.15s cubic-bezier(0.45, 0, 0.2, 1);
4556
+ }
4557
+
4558
+ .realm-book-3d.show-back {
4559
+ transform: rotateY(180deg);
4560
+ }
4561
+
4562
+ .realm-book-3d.is-opening {
4563
+ opacity: 0;
4564
+ transform: rotateY(-12deg) translateZ(-30px);
4565
+ pointer-events: none;
4566
+ }
4567
+
4568
+ .realm-book-face {
4569
+ position: absolute;
4570
+ inset: 0;
4571
+ backface-visibility: hidden;
4572
+ -webkit-backface-visibility: hidden;
4573
+ }
4574
+
4575
+ .realm-book-back {
4576
+ transform: rotateY(180deg);
4577
+ }
4578
+
4579
+ .realm-cover-back {
4580
+ display: flex;
4581
+ flex-direction: column;
4582
+ align-items: center;
4583
+ justify-content: center;
4584
+ min-height: min(480px, 70vh);
4585
+ background:
4586
+ radial-gradient(circle at 30% 20%, rgba(255, 240, 198, 0.08), transparent 32%),
4587
+ radial-gradient(circle at 70% 80%, rgba(0, 0, 0, 0.28), transparent 40%),
4588
+ linear-gradient(155deg, #3d2616 0%, #24150c 42%, #120a06 100%) !important;
4589
+ }
4590
+
4591
+ .realm-back-ornament {
4592
+ font-size: 2.4rem;
4593
+ color: rgba(201, 161, 59, 0.45);
4594
+ margin-bottom: 0.75rem;
4595
+ }
4596
+
4597
+ .realm-back-text {
4598
+ font-family: 'IM Fell English', serif;
4599
+ font-style: italic;
4600
+ color: #d8c49a;
4601
+ line-height: 1.55;
4602
+ margin: 0 0 1.25rem;
4603
+ max-width: 22rem;
4604
+ }
4605
+
4606
+ .realm-opening.is-reclosed .realm-book-stage {
4607
+ display: flex !important;
4608
+ align-items: center;
4609
+ justify-content: center;
4610
+ }
4611
+
4612
+ .realm-opening.is-reclosed .realm-book-3d {
4613
+ opacity: 1 !important;
4614
+ pointer-events: auto !important;
4615
+ }
4616
+
4617
+ .realm-opening.is-closing .realm-book-stage {
4618
+ perspective: 1400px;
4619
+ }
4620
+
4621
+ .realm-opening.is-back-visible .realm-book-open,
4622
+ .realm-opening.is-back-visible .realm-prologue-card {
4623
+ display: none !important;
4624
+ }