dsh-theme-gallery 0.1.4 → 0.2.0

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.
package/lib/client.js CHANGED
@@ -105,6 +105,21 @@ window.__ModuleLoader__.load({
105
105
  */
106
106
  const READING = { bg: '#FFFFFF', alpha: 0.62, blur: 3, maxWidth: 640 }
107
107
 
108
+ /**
109
+ * The package version this bundle was built from.
110
+ *
111
+ * Written by `scripts/embed-themes.mjs` from package.json, because the browser
112
+ * half cannot read its own manifest: the client module system resolves `require`
113
+ * against the platform seed table and other plugins' boot-graph rows, not
114
+ * against this package's files. The panel shows it so a reader can compare what
115
+ * they have with what npm publishes.
116
+ *
117
+ * Kept honest by two checks: `scripts/publish-check.mjs` compares it with
118
+ * package.json, and the release workflow fails when re-running the embed step
119
+ * changes a tracked file.
120
+ */
121
+ const BUNDLED_VERSION = '0.2.0'
122
+
108
123
  /**
109
124
  * Themes this package contributes, inlined from lib/themes/*.json.
110
125
  *
@@ -406,6 +421,294 @@ window.__ModuleLoader__.load({
406
421
  "fish": 3
407
422
  }
408
423
  },
424
+ {
425
+ "id": "pei-an-jie-xin",
426
+ "label": "佩安杰心",
427
+ "description": "檀香禅影,自在安顿 —— 复刻自电商新零售系统管理后台同名主题",
428
+ "colorScheme": "light",
429
+ "tokens": {
430
+ "--dsw-alias-bg-base": {
431
+ "light": "linear-gradient(to bottom,#FCFAF5 0%,#F8F3E9 55%,#F3ECDD 100%)",
432
+ "dark": "linear-gradient(to bottom,#FCFAF5 0%,#F8F3E9 55%,#F3ECDD 100%)"
433
+ },
434
+ "--dsw-alias-bg-layer-1": {
435
+ "light": "#FDFBF7",
436
+ "dark": "#FDFBF7"
437
+ },
438
+ "--dsw-alias-bg-layer-2": {
439
+ "light": "#FFFFFF",
440
+ "dark": "#FFFFFF"
441
+ },
442
+ "--dsw-alias-bg-layer-3": {
443
+ "light": "#FFFFFF",
444
+ "dark": "#FFFFFF"
445
+ },
446
+ "--dsw-alias-bg-overlay": {
447
+ "light": "#EFE7D7",
448
+ "dark": "#EFE7D7"
449
+ },
450
+ "--dsw-alias-bg-skeleton": {
451
+ "light": "#7A5C3E14",
452
+ "dark": "#7A5C3E14"
453
+ },
454
+ "--dsw-alias-bg-module-platform": {
455
+ "light": "#F3EBDD",
456
+ "dark": "#F3EBDD"
457
+ },
458
+ "--dsw-alias-bg-multi-select": {
459
+ "light": "#F3EBDD",
460
+ "dark": "#F3EBDD"
461
+ },
462
+ "--dsw-alias-border-l1": {
463
+ "light": "#7A5C3E0F",
464
+ "dark": "#7A5C3E0F"
465
+ },
466
+ "--dsw-alias-border-l2": {
467
+ "light": "#7A5C3E21",
468
+ "dark": "#7A5C3E21"
469
+ },
470
+ "--dsw-alias-border-l3": {
471
+ "light": "#7A5C3E2E",
472
+ "dark": "#7A5C3E2E"
473
+ },
474
+ "--dsw-alias-border-l4": {
475
+ "light": "#7A5C3E3D",
476
+ "dark": "#7A5C3E3D"
477
+ },
478
+ "--dsw-alias-brand-primary": {
479
+ "light": "#7A5C3E",
480
+ "dark": "#7A5C3E"
481
+ },
482
+ "--dsw-alias-brand-primary-invert": {
483
+ "light": "#FFFFFF",
484
+ "dark": "#FFFFFF"
485
+ },
486
+ "--dsw-alias-brand-text": {
487
+ "light": "#7A5C3E",
488
+ "dark": "#7A5C3E"
489
+ },
490
+ "--dsw-alias-label-primary": {
491
+ "light": "#3F3222",
492
+ "dark": "#3F3222"
493
+ },
494
+ "--dsw-alias-label-primary-bluish": {
495
+ "light": "#3F3222",
496
+ "dark": "#3F3222"
497
+ },
498
+ "--dsw-alias-label-secondary": {
499
+ "light": "#5E4A38",
500
+ "dark": "#5E4A38"
501
+ },
502
+ "--dsw-alias-label-tertiary": {
503
+ "light": "#85705A",
504
+ "dark": "#85705A"
505
+ },
506
+ "--dsw-alias-label-caption": {
507
+ "light": "#85705A",
508
+ "dark": "#85705A"
509
+ },
510
+ "--dsw-alias-label-dimmed": {
511
+ "light": "#AC9C86",
512
+ "dark": "#AC9C86"
513
+ },
514
+ "--dsw-alias-label-primary-dimmed": {
515
+ "light": "#5E4A38",
516
+ "dark": "#5E4A38"
517
+ },
518
+ "--dsw-alias-label-primary-foreground": {
519
+ "light": "#FFFFFF",
520
+ "dark": "#FFFFFF"
521
+ },
522
+ "--dsw-alias-label-primary-inverted": {
523
+ "light": "#FFFFFF",
524
+ "dark": "#FFFFFF"
525
+ },
526
+ "--dsw-alias-link": {
527
+ "light": "#B4653A",
528
+ "dark": "#B4653A"
529
+ },
530
+ "--dsw-alias-interactive-bg-hover": {
531
+ "light": "#9C7B5814",
532
+ "dark": "#9C7B5814"
533
+ },
534
+ "--dsw-alias-interactive-bg-active": {
535
+ "light": "#B4653A2E",
536
+ "dark": "#B4653A2E"
537
+ },
538
+ "--dsw-alias-interactive-bg-hover-solid": {
539
+ "light": "#F3EBDD",
540
+ "dark": "#F3EBDD"
541
+ },
542
+ "--dsw-alias-interactive-bg-hover-accent": {
543
+ "light": "#B4653A3D",
544
+ "dark": "#B4653A3D"
545
+ },
546
+ "--dsw-alias-button-primary-fill": {
547
+ "light": "#7A5C3E",
548
+ "dark": "#7A5C3E"
549
+ },
550
+ "--dsw-alias-button-primary-hover": {
551
+ "light": "#9C7B58",
552
+ "dark": "#9C7B58"
553
+ },
554
+ "--dsw-alias-button-primary-dimmed": {
555
+ "light": "#7A5C3E47",
556
+ "dark": "#7A5C3E47"
557
+ },
558
+ "--dsw-alias-button-ghost-active-fill": {
559
+ "light": "#B4653A29",
560
+ "dark": "#B4653A29"
561
+ },
562
+ "--dsw-alias-button-ghost-active-border": {
563
+ "light": "#B4653A",
564
+ "dark": "#B4653A"
565
+ },
566
+ "--dsw-alias-button-ghost-active-hover": {
567
+ "light": "#B4653A47",
568
+ "dark": "#B4653A47"
569
+ },
570
+ "--dsw-alias-button-info-fill": {
571
+ "light": "#9C7B58",
572
+ "dark": "#9C7B58"
573
+ },
574
+ "--dsw-alias-button-info-hover": {
575
+ "light": "#8A6E52",
576
+ "dark": "#8A6E52"
577
+ },
578
+ "--dsw-alias-button-elevated-fill": {
579
+ "light": "#FFFFFF",
580
+ "dark": "#FFFFFF"
581
+ },
582
+ "--dsw-alias-button-floating-fill": {
583
+ "light": "#FFFFFF",
584
+ "dark": "#FFFFFF"
585
+ },
586
+ "--dsw-alias-button-floating-hover": {
587
+ "light": "#FAF6EE",
588
+ "dark": "#FAF6EE"
589
+ },
590
+ "--dsw-alias-button-contrast-fill": {
591
+ "light": "#3F3222",
592
+ "dark": "#3F3222"
593
+ },
594
+ "--dsw-alias-markdown-code-block": {
595
+ "light": "#F5EFE3",
596
+ "dark": "#F5EFE3"
597
+ },
598
+ "--dsw-alias-markdown-code-block-banner": {
599
+ "light": "#EDE4D2",
600
+ "dark": "#EDE4D2"
601
+ },
602
+ "--dsw-alias-markdown-inline-code": {
603
+ "light": "#F3EBDD",
604
+ "dark": "#F3EBDD"
605
+ },
606
+ "--dsw-alias-markdown-citation": {
607
+ "light": "#F3EBDD",
608
+ "dark": "#F3EBDD"
609
+ },
610
+ "--dsw-alias-markdown-tag": {
611
+ "light": "#F3EBDD",
612
+ "dark": "#F3EBDD"
613
+ },
614
+ "--dsw-alias-markdown-placeholder": {
615
+ "light": "#EDE4D2",
616
+ "dark": "#EDE4D2"
617
+ },
618
+ "--dsw-alias-markdown-code-segment-selected": {
619
+ "light": "#B4653A29",
620
+ "dark": "#B4653A29"
621
+ },
622
+ "--dsw-alias-markdown-code-segment-unselected": {
623
+ "light": "#F5EFE3",
624
+ "dark": "#F5EFE3"
625
+ },
626
+ "--dsw-alias-scrollbar-bg-l1": {
627
+ "light": "#7A5C3E29",
628
+ "dark": "#7A5C3E29"
629
+ },
630
+ "--dsw-alias-scrollbar-bg-l2": {
631
+ "light": "#7A5C3E38",
632
+ "dark": "#7A5C3E38"
633
+ },
634
+ "--dsw-alias-scrollbar-hover-l1": {
635
+ "light": "#7A5C3E52",
636
+ "dark": "#7A5C3E52"
637
+ },
638
+ "--dsw-alias-scrollbar-hover-l2": {
639
+ "light": "#7A5C3E66",
640
+ "dark": "#7A5C3E66"
641
+ },
642
+ "--dsw-alias-tooltip-bg": {
643
+ "light": "#3F3222",
644
+ "dark": "#3F3222"
645
+ },
646
+ "--dsw-alias-toast-bg": {
647
+ "light": "#FFFFFF",
648
+ "dark": "#FFFFFF"
649
+ },
650
+ "--dsw-alias-state-error-primary": {
651
+ "light": "#C65B6A",
652
+ "dark": "#C65B6A"
653
+ },
654
+ "--dsw-alias-state-error-secondary": {
655
+ "light": "#DB5A6B",
656
+ "dark": "#DB5A6B"
657
+ },
658
+ "--dsw-alias-state-success-primary": {
659
+ "light": "#21A675",
660
+ "dark": "#21A675"
661
+ },
662
+ "--dsw-alias-state-success-secondary": {
663
+ "light": "#7A5C3E",
664
+ "dark": "#7A5C3E"
665
+ },
666
+ "--dsw-alias-state-success-tertiary": {
667
+ "light": "#C4B39A",
668
+ "dark": "#C4B39A"
669
+ },
670
+ "--dsw-alias-state-warn-primary": {
671
+ "light": "#B97F3A",
672
+ "dark": "#B97F3A"
673
+ },
674
+ "--dsw-alias-state-warn-secondary": {
675
+ "light": "#D9A45E",
676
+ "dark": "#D9A45E"
677
+ },
678
+ "--dsw-alias-state-warn-tertiary": {
679
+ "light": "#EFDCC0",
680
+ "dark": "#EFDCC0"
681
+ },
682
+ "--dsw-alias-state-warn-label": {
683
+ "light": "#8A5A22",
684
+ "dark": "#8A5A22"
685
+ },
686
+ "--dsw-alias-state-business-primary": {
687
+ "light": "#B4653A",
688
+ "dark": "#B4653A"
689
+ },
690
+ "--dsw-alias-state-business-tertiary": {
691
+ "light": "#D08A5C",
692
+ "dark": "#D08A5C"
693
+ },
694
+ "--dsw-specific-sidebar-fill": {
695
+ "light": "linear-gradient(to bottom,#F2E8D8 0%,#EBDCC4 22%,#E0CDAD 42%,#D4BC98 60%,#C3A87F 78%,#A98D6B 100%)",
696
+ "dark": "linear-gradient(to bottom,#F2E8D8 0%,#EBDCC4 22%,#E0CDAD 42%,#D4BC98 60%,#C3A87F 78%,#A98D6B 100%)"
697
+ }
698
+ },
699
+ "reading": {
700
+ "colorScheme": "light",
701
+ "bg": "#FFFFFF",
702
+ "alpha": 0.62,
703
+ "blur": 3,
704
+ "maxWidth": 640
705
+ },
706
+ "accent": "#B4653A",
707
+ "ambient": {
708
+ "kind": "jiexin",
709
+ "dust": 4
710
+ }
711
+ },
409
712
  {
410
713
  "id": "shan-qing-ting-cai",
411
714
  "label": "山青婷彩",
@@ -693,79 +996,367 @@ window.__ModuleLoader__.load({
693
996
  "kind": "shan",
694
997
  "petals": 7
695
998
  }
696
- }
697
- ]
698
-
699
- /**
700
- * Marks the ambient nodes this run created.
701
- *
702
- * The layer lives on the document body, so parentage can no longer identify
703
- * ownership. Anything found without this stamp is debris from an earlier build —
704
- * and because the shell keeps its DOM across a plugin reload, such nodes outlive
705
- * the code that made them.
706
- */
707
- const AMBIENT_OWNER = 'theme-gallery'
708
-
709
- /**
710
- * Ambient scenery drawn inside the sidebar column.
711
- *
712
- * Ported from the source admin system, where each theme carried its own
713
- * animation component mounted into a `left-sidebar-theme-container`. The
714
- * artwork is reproduced here (mountains, mist, water, dragonflies, falling
715
- * petals for 山青婷彩; corner glow, rising bubbles, swaying seaweed for
716
- * 梦海游鱼) with two deliberate changes:
717
- *
718
- * - sizes are expressed in **percentages and em**, not the source system's
719
- * fixed 223 px sidebar width, so the scene scales to whatever width the
720
- * user drags the sidebar to;
721
- * - nothing here carries colour of its own beyond the ported artwork, and the
722
- * layer is `pointer-events:none` / `z-index:0`, so it can never intercept a
723
- * click or cover a menu item. The source system has the same rule, and a
724
- * bug note there records opaque mountains hiding the bottom menu rows.
725
- *
726
- * `#dsh-theme-ambient` is a seat this plugin injects into the sidebar column;
727
- * `syncAmbient()` fills it, and each theme chooses its scene by `ambient.kind`.
728
- */
729
- const AMBIENT_CSS = [
730
- /* The layer, arranged exactly like the working `dsh-theme-firefly` ambient layer:
731
- a full-viewport fixed element styled by a CLASS, appended to `document.body`, at
732
- `z-index: 60`.
733
- Every other arrangement of this layer failed to paint on this machine — precise
734
- inline geometry, maximum z-index, `documentElement` as the parent, inline styles
735
- per element. The firefly plugin uses this one and renders, so this is now the
736
- layer's arrangement. The scenery is placed INSIDE it (`.dsh-ambient-scene`). */
737
- '#dsh-theme-ambient{position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:60}',
738
- '.dsh-ambient-scene{overflow:hidden}',
739
- /* The scene roots carry their own positioning inline too; the selectors below are
740
- kept only where a rule cannot be inlined (`@keyframes`) or where they must reach
741
- a shell element. */
742
- '.ZTP-Xa_sidebarCol{position:relative}',
743
-
744
- /* BRING-UP PROBE — remove with the rest of the diagnostics.
745
- The control layer, copied rule-for-rule from the working `dsh-theme-firefly`
746
- plugin: geometry and stacking from a class, motion from `@keyframes`, and only
747
- the per-element random values inline. */
748
- '.dsh-amb-control{position:fixed;inset:0;pointer-events:none;z-index:60;overflow:hidden}',
749
- /* BRING-UP PROBE — the scene box inside the proven layer. No `pointer-events` here on
750
- purpose: the layer above already disables it, and this box is the thing being
751
- tested rather than a decoration that must stay click-through. */
752
- '.dsh-amb-control-scene{overflow:hidden}',
753
-
754
- /* The scenery's own geometry. These live here rather than inline because the scene
755
- markup is built as a string, and they must reach it wherever it is mounted — which
756
- is now inside the proven layer rather than a layer of its own. */
757
- '.dsh-amb-control-scene .sta-mountains{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:3}',
758
- '.dsh-amb-control-scene .sta-mountains svg{display:block;width:100%;height:100%}',
759
- '.dsh-amb-control-scene .sta-mist{position:absolute;height:1.6em;border-radius:1000px;z-index:4;',
760
- 'background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent);',
761
- 'filter:blur(5px);opacity:.85;animation:dsh-amb-mist 26s ease-in-out infinite alternate}',
762
- '.dsh-amb-control-scene .sta-mist-1{width:62%;top:56%;left:12%}',
763
- '.dsh-amb-control-scene .sta-mist-2{width:44%;top:61%;left:38%;opacity:.6;',
764
- 'animation-duration:32s;animation-delay:-9s}',
765
- '.dsh-amb-control-scene .sta-pond{position:absolute;left:0;right:0;bottom:0;height:13%;z-index:5;',
766
- 'background:linear-gradient(to bottom,rgba(104,178,150,.62),rgba(66,141,113,.78))}',
767
- '.dsh-amb-control-scene .sta-pond-line{position:absolute;top:0;left:0;right:0;height:1.2px;opacity:.6;',
768
- 'background:linear-gradient(90deg,transparent,rgba(255,255,255,.9),transparent)}',
999
+ },
1000
+ {
1001
+ "id": "ying-mu-cai-yun",
1002
+ "label": "营慕彩云",
1003
+ "description": "营幕之下,彩云为伴 —— 复刻自电商新零售系统管理后台同名主题",
1004
+ "colorScheme": "light",
1005
+ "tokens": {
1006
+ "--dsw-alias-bg-base": {
1007
+ "light": "linear-gradient(to bottom,#FBFCF8 0%,#F5F8EF 55%,#EEF3E4 100%)",
1008
+ "dark": "linear-gradient(to bottom,#FBFCF8 0%,#F5F8EF 55%,#EEF3E4 100%)"
1009
+ },
1010
+ "--dsw-alias-bg-layer-1": {
1011
+ "light": "#FBFDF7",
1012
+ "dark": "#FBFDF7"
1013
+ },
1014
+ "--dsw-alias-bg-layer-2": {
1015
+ "light": "#FFFFFF",
1016
+ "dark": "#FFFFFF"
1017
+ },
1018
+ "--dsw-alias-bg-layer-3": {
1019
+ "light": "#FFFFFF",
1020
+ "dark": "#FFFFFF"
1021
+ },
1022
+ "--dsw-alias-bg-overlay": {
1023
+ "light": "#E7EFE0",
1024
+ "dark": "#E7EFE0"
1025
+ },
1026
+ "--dsw-alias-bg-skeleton": {
1027
+ "light": "#2D5A3D14",
1028
+ "dark": "#2D5A3D14"
1029
+ },
1030
+ "--dsw-alias-bg-module-platform": {
1031
+ "light": "#ECF3E6",
1032
+ "dark": "#ECF3E6"
1033
+ },
1034
+ "--dsw-alias-bg-multi-select": {
1035
+ "light": "#ECF3E6",
1036
+ "dark": "#ECF3E6"
1037
+ },
1038
+ "--dsw-alias-border-l1": {
1039
+ "light": "#2D5A3D0F",
1040
+ "dark": "#2D5A3D0F"
1041
+ },
1042
+ "--dsw-alias-border-l2": {
1043
+ "light": "#2D5A3D21",
1044
+ "dark": "#2D5A3D21"
1045
+ },
1046
+ "--dsw-alias-border-l3": {
1047
+ "light": "#2D5A3D2E",
1048
+ "dark": "#2D5A3D2E"
1049
+ },
1050
+ "--dsw-alias-border-l4": {
1051
+ "light": "#2D5A3D3D",
1052
+ "dark": "#2D5A3D3D"
1053
+ },
1054
+ "--dsw-alias-brand-primary": {
1055
+ "light": "#2D5A3D",
1056
+ "dark": "#2D5A3D"
1057
+ },
1058
+ "--dsw-alias-brand-primary-invert": {
1059
+ "light": "#FFFFFF",
1060
+ "dark": "#FFFFFF"
1061
+ },
1062
+ "--dsw-alias-brand-text": {
1063
+ "light": "#2D5A3D",
1064
+ "dark": "#2D5A3D"
1065
+ },
1066
+ "--dsw-alias-label-primary": {
1067
+ "light": "#234030",
1068
+ "dark": "#234030"
1069
+ },
1070
+ "--dsw-alias-label-primary-bluish": {
1071
+ "light": "#234030",
1072
+ "dark": "#234030"
1073
+ },
1074
+ "--dsw-alias-label-secondary": {
1075
+ "light": "#3E6B4A",
1076
+ "dark": "#3E6B4A"
1077
+ },
1078
+ "--dsw-alias-label-tertiary": {
1079
+ "light": "#62816C",
1080
+ "dark": "#62816C"
1081
+ },
1082
+ "--dsw-alias-label-caption": {
1083
+ "light": "#62816C",
1084
+ "dark": "#62816C"
1085
+ },
1086
+ "--dsw-alias-label-dimmed": {
1087
+ "light": "#93A996",
1088
+ "dark": "#93A996"
1089
+ },
1090
+ "--dsw-alias-label-primary-dimmed": {
1091
+ "light": "#3E6B4A",
1092
+ "dark": "#3E6B4A"
1093
+ },
1094
+ "--dsw-alias-label-primary-foreground": {
1095
+ "light": "#FFFFFF",
1096
+ "dark": "#FFFFFF"
1097
+ },
1098
+ "--dsw-alias-label-primary-inverted": {
1099
+ "light": "#FFFFFF",
1100
+ "dark": "#FFFFFF"
1101
+ },
1102
+ "--dsw-alias-link": {
1103
+ "light": "#C4899F",
1104
+ "dark": "#C4899F"
1105
+ },
1106
+ "--dsw-alias-interactive-bg-hover": {
1107
+ "light": "#3E6B4A14",
1108
+ "dark": "#3E6B4A14"
1109
+ },
1110
+ "--dsw-alias-interactive-bg-active": {
1111
+ "light": "#FFB3472E",
1112
+ "dark": "#FFB3472E"
1113
+ },
1114
+ "--dsw-alias-interactive-bg-hover-solid": {
1115
+ "light": "#ECF3E6",
1116
+ "dark": "#ECF3E6"
1117
+ },
1118
+ "--dsw-alias-interactive-bg-hover-accent": {
1119
+ "light": "#FFB3473D",
1120
+ "dark": "#FFB3473D"
1121
+ },
1122
+ "--dsw-alias-button-primary-fill": {
1123
+ "light": "#2D5A3D",
1124
+ "dark": "#2D5A3D"
1125
+ },
1126
+ "--dsw-alias-button-primary-hover": {
1127
+ "light": "#3E6B4A",
1128
+ "dark": "#3E6B4A"
1129
+ },
1130
+ "--dsw-alias-button-primary-dimmed": {
1131
+ "light": "#2D5A3D47",
1132
+ "dark": "#2D5A3D47"
1133
+ },
1134
+ "--dsw-alias-button-ghost-active-fill": {
1135
+ "light": "#FFB34729",
1136
+ "dark": "#FFB34729"
1137
+ },
1138
+ "--dsw-alias-button-ghost-active-border": {
1139
+ "light": "#FFB347",
1140
+ "dark": "#FFB347"
1141
+ },
1142
+ "--dsw-alias-button-ghost-active-hover": {
1143
+ "light": "#FFB34747",
1144
+ "dark": "#FFB34747"
1145
+ },
1146
+ "--dsw-alias-button-info-fill": {
1147
+ "light": "#3E6B4A",
1148
+ "dark": "#3E6B4A"
1149
+ },
1150
+ "--dsw-alias-button-info-hover": {
1151
+ "light": "#4A7D58",
1152
+ "dark": "#4A7D58"
1153
+ },
1154
+ "--dsw-alias-button-elevated-fill": {
1155
+ "light": "#FFFFFF",
1156
+ "dark": "#FFFFFF"
1157
+ },
1158
+ "--dsw-alias-button-floating-fill": {
1159
+ "light": "#FFFFFF",
1160
+ "dark": "#FFFFFF"
1161
+ },
1162
+ "--dsw-alias-button-floating-hover": {
1163
+ "light": "#F6FAF2",
1164
+ "dark": "#F6FAF2"
1165
+ },
1166
+ "--dsw-alias-button-contrast-fill": {
1167
+ "light": "#234030",
1168
+ "dark": "#234030"
1169
+ },
1170
+ "--dsw-alias-markdown-code-block": {
1171
+ "light": "#EFF6EA",
1172
+ "dark": "#EFF6EA"
1173
+ },
1174
+ "--dsw-alias-markdown-code-block-banner": {
1175
+ "light": "#E3EEDC",
1176
+ "dark": "#E3EEDC"
1177
+ },
1178
+ "--dsw-alias-markdown-inline-code": {
1179
+ "light": "#ECF3E6",
1180
+ "dark": "#ECF3E6"
1181
+ },
1182
+ "--dsw-alias-markdown-citation": {
1183
+ "light": "#ECF3E6",
1184
+ "dark": "#ECF3E6"
1185
+ },
1186
+ "--dsw-alias-markdown-tag": {
1187
+ "light": "#ECF3E6",
1188
+ "dark": "#ECF3E6"
1189
+ },
1190
+ "--dsw-alias-markdown-placeholder": {
1191
+ "light": "#E3EEDC",
1192
+ "dark": "#E3EEDC"
1193
+ },
1194
+ "--dsw-alias-markdown-code-segment-selected": {
1195
+ "light": "#FFB34729",
1196
+ "dark": "#FFB34729"
1197
+ },
1198
+ "--dsw-alias-markdown-code-segment-unselected": {
1199
+ "light": "#EFF6EA",
1200
+ "dark": "#EFF6EA"
1201
+ },
1202
+ "--dsw-alias-scrollbar-bg-l1": {
1203
+ "light": "#2D5A3D29",
1204
+ "dark": "#2D5A3D29"
1205
+ },
1206
+ "--dsw-alias-scrollbar-bg-l2": {
1207
+ "light": "#2D5A3D38",
1208
+ "dark": "#2D5A3D38"
1209
+ },
1210
+ "--dsw-alias-scrollbar-hover-l1": {
1211
+ "light": "#2D5A3D52",
1212
+ "dark": "#2D5A3D52"
1213
+ },
1214
+ "--dsw-alias-scrollbar-hover-l2": {
1215
+ "light": "#2D5A3D66",
1216
+ "dark": "#2D5A3D66"
1217
+ },
1218
+ "--dsw-alias-tooltip-bg": {
1219
+ "light": "#234030",
1220
+ "dark": "#234030"
1221
+ },
1222
+ "--dsw-alias-toast-bg": {
1223
+ "light": "#FFFFFF",
1224
+ "dark": "#FFFFFF"
1225
+ },
1226
+ "--dsw-alias-state-error-primary": {
1227
+ "light": "#C65B6A",
1228
+ "dark": "#C65B6A"
1229
+ },
1230
+ "--dsw-alias-state-error-secondary": {
1231
+ "light": "#DB5A6B",
1232
+ "dark": "#DB5A6B"
1233
+ },
1234
+ "--dsw-alias-state-success-primary": {
1235
+ "light": "#21A675",
1236
+ "dark": "#21A675"
1237
+ },
1238
+ "--dsw-alias-state-success-secondary": {
1239
+ "light": "#2D5A3D",
1240
+ "dark": "#2D5A3D"
1241
+ },
1242
+ "--dsw-alias-state-success-tertiary": {
1243
+ "light": "#A8C8B2",
1244
+ "dark": "#A8C8B2"
1245
+ },
1246
+ "--dsw-alias-state-warn-primary": {
1247
+ "light": "#B97F3A",
1248
+ "dark": "#B97F3A"
1249
+ },
1250
+ "--dsw-alias-state-warn-secondary": {
1251
+ "light": "#D9A45E",
1252
+ "dark": "#D9A45E"
1253
+ },
1254
+ "--dsw-alias-state-warn-tertiary": {
1255
+ "light": "#EFDCC0",
1256
+ "dark": "#EFDCC0"
1257
+ },
1258
+ "--dsw-alias-state-warn-label": {
1259
+ "light": "#8A5A22",
1260
+ "dark": "#8A5A22"
1261
+ },
1262
+ "--dsw-alias-state-business-primary": {
1263
+ "light": "#FFB347",
1264
+ "dark": "#FFB347"
1265
+ },
1266
+ "--dsw-alias-state-business-tertiary": {
1267
+ "light": "#FFD08A",
1268
+ "dark": "#FFD08A"
1269
+ },
1270
+ "--dsw-specific-sidebar-fill": {
1271
+ "light": "linear-gradient(160deg,#D9E8DC 0%,#E6D9C6 28%,#F2E0BE 58%,#F6E1EE 88%,#DCD5F0 100%)",
1272
+ "dark": "linear-gradient(160deg,#D9E8DC 0%,#E6D9C6 28%,#F2E0BE 58%,#F6E1EE 88%,#DCD5F0 100%)"
1273
+ }
1274
+ },
1275
+ "reading": {
1276
+ "colorScheme": "light",
1277
+ "bg": "#FFFFFF",
1278
+ "alpha": 0.62,
1279
+ "blur": 3,
1280
+ "maxWidth": 640
1281
+ },
1282
+ "accent": "#FFB347",
1283
+ "ambient": {
1284
+ "kind": "caiyun",
1285
+ "stars": 12
1286
+ }
1287
+ }
1288
+ ]
1289
+
1290
+ /**
1291
+ * Marks the ambient nodes this run created.
1292
+ *
1293
+ * The layer lives on the document body, so parentage can no longer identify
1294
+ * ownership. Anything found without this stamp is debris from an earlier build —
1295
+ * and because the shell keeps its DOM across a plugin reload, such nodes outlive
1296
+ * the code that made them.
1297
+ */
1298
+ const AMBIENT_OWNER = 'theme-gallery'
1299
+
1300
+ /**
1301
+ * Ambient scenery drawn inside the sidebar column.
1302
+ *
1303
+ * Ported from the source admin system, where each theme carried its own
1304
+ * animation component mounted into a `left-sidebar-theme-container`. The
1305
+ * artwork is reproduced here (mountains, mist, water, dragonflies, falling
1306
+ * petals for 山青婷彩; corner glow, rising bubbles, swaying seaweed for
1307
+ * 梦海游鱼) with two deliberate changes:
1308
+ *
1309
+ * - sizes are expressed in **percentages and em**, not the source system's
1310
+ * fixed 223 px sidebar width, so the scene scales to whatever width the
1311
+ * user drags the sidebar to;
1312
+ * - nothing here carries colour of its own beyond the ported artwork, and the
1313
+ * layer is `pointer-events:none` / `z-index:0`, so it can never intercept a
1314
+ * click or cover a menu item. The source system has the same rule, and a
1315
+ * bug note there records opaque mountains hiding the bottom menu rows.
1316
+ *
1317
+ * `#dsh-theme-ambient` is a seat this plugin injects into the sidebar column;
1318
+ * `syncAmbient()` fills it, and each theme chooses its scene by `ambient.kind`.
1319
+ */
1320
+ const AMBIENT_CSS = [
1321
+ /* The layer, arranged exactly like the working `dsh-theme-firefly` ambient layer:
1322
+ a full-viewport fixed element styled by a CLASS, appended to `document.body`, at
1323
+ `z-index: 60`.
1324
+ Every other arrangement of this layer failed to paint on this machine — precise
1325
+ inline geometry, maximum z-index, `documentElement` as the parent, inline styles
1326
+ per element. The firefly plugin uses this one and renders, so this is now the
1327
+ layer's arrangement. The scenery is placed INSIDE it (`.dsh-ambient-scene`). */
1328
+ '#dsh-theme-ambient{position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:60}',
1329
+ '.dsh-ambient-scene{overflow:hidden}',
1330
+ /* The scene roots carry their own positioning inline too; the selectors below are
1331
+ kept only where a rule cannot be inlined (`@keyframes`) or where they must reach
1332
+ a shell element. */
1333
+ '.ZTP-Xa_sidebarCol{position:relative}',
1334
+
1335
+ /* BRING-UP PROBE — remove with the rest of the diagnostics.
1336
+ The control layer, copied rule-for-rule from the working `dsh-theme-firefly`
1337
+ plugin: geometry and stacking from a class, motion from `@keyframes`, and only
1338
+ the per-element random values inline. */
1339
+ '.dsh-amb-control{position:fixed;inset:0;pointer-events:none;z-index:60;overflow:hidden}',
1340
+ /* BRING-UP PROBE — the scene box inside the proven layer. No `pointer-events` here on
1341
+ purpose: the layer above already disables it, and this box is the thing being
1342
+ tested rather than a decoration that must stay click-through. */
1343
+ '.dsh-amb-control-scene{overflow:hidden}',
1344
+
1345
+ /* The scenery's own geometry. These live here rather than inline because the scene
1346
+ markup is built as a string, and they must reach it wherever it is mounted — which
1347
+ is now inside the proven layer rather than a layer of its own. */
1348
+ '.dsh-amb-control-scene .sta-mountains{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:3}',
1349
+ '.dsh-amb-control-scene .sta-mountains svg{display:block;width:100%;height:100%}',
1350
+ '.dsh-amb-control-scene .sta-mist{position:absolute;height:1.6em;border-radius:1000px;z-index:4;',
1351
+ 'background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent);',
1352
+ 'filter:blur(5px);opacity:.85;animation:dsh-amb-mist 26s ease-in-out infinite alternate}',
1353
+ '.dsh-amb-control-scene .sta-mist-1{width:62%;top:56%;left:12%}',
1354
+ '.dsh-amb-control-scene .sta-mist-2{width:44%;top:61%;left:38%;opacity:.6;',
1355
+ 'animation-duration:32s;animation-delay:-9s}',
1356
+ '.dsh-amb-control-scene .sta-pond{position:absolute;left:0;right:0;bottom:0;height:13%;z-index:5;',
1357
+ 'background:linear-gradient(to bottom,rgba(104,178,150,.62),rgba(66,141,113,.78))}',
1358
+ '.dsh-amb-control-scene .sta-pond-line{position:absolute;top:0;left:0;right:0;height:1.2px;opacity:.6;',
1359
+ 'background:linear-gradient(90deg,transparent,rgba(255,255,255,.9),transparent)}',
769
1360
  '.dsh-amb-control-scene .sta-ripple{position:absolute;z-index:6;width:.55em;height:.55em}',
770
1361
  '.dsh-amb-control-scene .sta-ripple span{position:absolute;inset:0;',
771
1362
  'border:1.6px solid rgba(217,123,164,.9);border-radius:50%;',
@@ -958,6 +1549,67 @@ window.__ModuleLoader__.load({
958
1549
  'box-shadow:0 0 10px 3px rgba(122,205,255,.55),0 0 22px 6px rgba(122,205,255,.22);',
959
1550
  'animation:dsh-amb-mote linear infinite}',
960
1551
 
1552
+ /* ── 营慕彩云 / 江畔冬云 / 徐山军月 / 佩安杰心 / 光彩凤晨 ─────────────
1553
+ The five scenes ported alongside shan and dream. Their element geometry,
1554
+ colour and per-element timing are INLINE in the scene builders (same rule
1555
+ as shan: a stylesheet that never applies cannot silently collapse them),
1556
+ so the only things that must live here are the `@keyframes` — the one
1557
+ construct that cannot be expressed inline. Each scene prefixes its own
1558
+ keyframe names (ym / jp / xs / pj / gc) so they cannot collide. */
1559
+ '@keyframes dsh-amb-ym-star{0%{opacity:.35;transform:scale(1)}100%{opacity:1;transform:scale(1.25)}}',
1560
+ '@keyframes dsh-amb-ym-drift{from{transform:translateX(0)}to{transform:translateX(24em)}}',
1561
+ '@keyframes dsh-amb-ym-sea{from{transform:translateX(0)}to{transform:translateX(-50%)}}',
1562
+ '@keyframes dsh-amb-ym-bob{0%,100%{transform:translateY(0) rotate(-2.5deg)}50%{transform:translateY(-9px) rotate(2.5deg)}}',
1563
+ /* The balloons' wide round-trip paths. Deliberately two distinct keyframes, so the
1564
+ two never move as a pair: the lower sidebar rows are covered and exposed in turn
1565
+ rather than blocked by one stationary balloon.
1566
+ The main balloon's loop is (0, +1.5em) → (7em, +0.5em) → back: the START and END
1567
+ sit 1.5em below its resting line and the rightmost apex sits 0.5em below it, so the
1568
+ path rises while travelling right and falls while returning — up, down, left and
1569
+ right — while the apex itself stays exactly where it was accepted.
1570
+ EVERY vertical term is at or below the resting baseline (y >= 0), and that is a
1571
+ requirement, not taste: the scene box clips at its own top edge (`overflow:hidden`),
1572
+ so any stop above y=0 slices the envelope's crown off at the apex, which is the hard
1573
+ crop the user reported. */
1574
+ '@keyframes dsh-amb-ym-wander{0%{transform:translate(0,1.5em)}50%{transform:translate(7em,0.5em)}100%{transform:translate(0,1.5em)}}',
1575
+ '@keyframes dsh-amb-ym-wander-2{0%{transform:translate(0,0)}50%{transform:translate(-5em,1.6em)}100%{transform:translate(0,0)}}',
1576
+ '@keyframes dsh-amb-jp-cloud{from{transform:translateX(0)}to{transform:translateX(22em)}}',
1577
+ '@keyframes dsh-amb-jp-snow{0%{transform:translate(0,-0.5em);opacity:0}8%{opacity:.9}'
1578
+ + '50%{transform:translate(0.9em,9em)}92%{opacity:.9}100%{transform:translate(-0.5em,19em);opacity:0}}',
1579
+ '@keyframes dsh-amb-jp-ripple{from{transform:translateX(0)}to{transform:translateX(25em)}}',
1580
+ '@keyframes dsh-amb-jp-glint{0%{opacity:.25;transform:scale(.8)}100%{opacity:.95;transform:scale(1.3)}}',
1581
+ '@keyframes dsh-amb-jp-boat{from{transform:translateX(0)}to{transform:translateX(3em)}}',
1582
+ '@keyframes dsh-amb-jp-bob{0%,100%{transform:translateY(0) rotate(-1.2deg)}50%{transform:translateY(-3px) rotate(1.4deg)}}',
1583
+ '@keyframes dsh-amb-jp-sway{0%,100%{transform:rotate(0deg)}50%{transform:rotate(3deg)}}',
1584
+ '@keyframes dsh-amb-xs-star{0%{opacity:.3;transform:scale(.85)}100%{opacity:1;transform:scale(1.25)}}',
1585
+ '@keyframes dsh-amb-xs-night{from{transform:translateX(0)}to{transform:translateX(26em)}}',
1586
+ '@keyframes dsh-amb-xs-meteor{0%{transform:rotate(-32deg) translateX(0);opacity:0}3%{opacity:1}'
1587
+ + '9%{transform:rotate(-32deg) translateX(-9em);opacity:0}'
1588
+ + '100%{transform:rotate(-32deg) translateX(-9em);opacity:0}}',
1589
+ '@keyframes dsh-amb-pj-smoke{0%{stroke-dashoffset:90;opacity:0}12%{opacity:.7}70%{opacity:.45}'
1590
+ + '100%{stroke-dashoffset:-20;opacity:0}}',
1591
+ '@keyframes dsh-amb-pj-dust{0%{transform:translate(0,0);opacity:0}15%{opacity:.9}'
1592
+ + '55%{transform:translate(-0.6em,-1.9em);opacity:.5}100%{transform:translate(0.5em,-4em);opacity:0}}',
1593
+ '@keyframes dsh-amb-gc-ray{0%{transform:translateY(-40%);opacity:.3}50%{transform:translateY(30%);opacity:.8}'
1594
+ + '100%{transform:translateY(90%);opacity:.3}}',
1595
+ '@keyframes dsh-amb-gc-feather{0%{transform:translate(0,0) rotate(0deg);opacity:0}10%{opacity:.85}'
1596
+ + '25%{transform:translate(1.9em,6em) rotate(95deg)}50%{transform:translate(-1.9em,13em) rotate(190deg);opacity:.85}'
1597
+ + '75%{transform:translate(1.9em,20em) rotate(285deg)}90%{opacity:.8}'
1598
+ + '100%{transform:translate(0,26em) rotate(380deg);opacity:0}}',
1599
+ '@keyframes dsh-amb-gc-fly{0%{transform:translate(-11em,0.6em) scaleX(1);opacity:0}6%{opacity:.95}'
1600
+ + '40%{transform:translate(-3em,-1.1em) scaleX(1);opacity:.95}'
1601
+ + '46%{transform:translate(2.5em,-0.5em) scaleX(1);opacity:.85}'
1602
+ + '50%{transform:translate(9em,0) scaleX(1);opacity:0}'
1603
+ + '50.1%{transform:translate(9em,0) scaleX(-1);opacity:0}'
1604
+ + '56%{transform:translate(2.5em,-0.6em) scaleX(-1);opacity:.9}'
1605
+ + '90%{transform:translate(-9em,0.4em) scaleX(-1);opacity:.6}'
1606
+ + '96%{transform:translate(-11em,0.7em) scaleX(-1);opacity:0}'
1607
+ + '96.1%{transform:translate(-11em,0.7em) scaleX(1);opacity:0}'
1608
+ + '100%{transform:translate(-11em,0.6em) scaleX(1);opacity:0}}',
1609
+ '@keyframes dsh-amb-gc-bob{0%,100%{transform:translateY(0) rotate(0deg)}50%{transform:translateY(-5px) rotate(-1.5deg)}}',
1610
+ '@keyframes dsh-amb-gc-wing{0%{transform:rotate(0deg)}40%{transform:rotate(-3.5deg)}60%{transform:rotate(2.5deg)}100%{transform:rotate(0deg)}}',
1611
+ '@keyframes dsh-amb-gc-dew{0%{opacity:.3;transform:scale(1)}100%{opacity:1;transform:scale(1.5)}}',
1612
+
961
1613
  /* Respect a user who has asked the system for less motion: the scenery
962
1614
  stays, the movement does not. */
963
1615
  '@media (prefers-reduced-motion:reduce){#dsh-theme-ambient *{animation:none!important}}',
@@ -971,6 +1623,10 @@ window.__ModuleLoader__.load({
971
1623
  current: '当前',
972
1624
  applied: '已应用',
973
1625
  empty: '正在读取官方主题注册表…',
1626
+ // The diagnostics below the cards are a FEATURE — a live reading of the active skin —
1627
+ // and a wall of monospace text reads as a fault unless the panel says otherwise.
1628
+ diagNote: '以下为有意设计的诊断信息,便于直观查看皮肤的实时运行状态,并非系统报错;'
1629
+ + '带 ⚠ 的行才表示装饰未生效',
974
1630
  }
975
1631
  const en = {
976
1632
  title: 'Theme skins',
@@ -979,6 +1635,8 @@ window.__ModuleLoader__.load({
979
1635
  current: 'Current',
980
1636
  applied: 'Applied',
981
1637
  empty: 'Reading the theme registry…',
1638
+ diagNote: 'The lines below are intentional diagnostics of the live skin state, '
1639
+ + 'not an error report; only lines starting with ⚠ mean the scenery is not working',
982
1640
  }
983
1641
 
984
1642
  /**
@@ -1009,6 +1667,12 @@ window.__ModuleLoader__.load({
1009
1667
  'background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);',
1010
1668
  'border-radius:8px;padding:8px 10px;word-break:break-all}',
1011
1669
  '.tg-warn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}',
1670
+ /* The diagnostics live BELOW the cards, set apart by their own margin and a divider,
1671
+ so they read as a separate reference block rather than as part of the picker — and
1672
+ so the picker itself stays the first thing the eye lands on. */
1673
+ '.tg-diag{margin-top:14px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2);',
1674
+ 'display:flex;flex-direction:column;gap:10px}',
1675
+ '.tg-diag-note{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary)}',
1012
1676
  '.tg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}',
1013
1677
  '.tg-card{display:flex;flex-direction:column;gap:8px;padding:12px;text-align:left;cursor:pointer;',
1014
1678
  'background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);',
@@ -1643,155 +2307,976 @@ window.__ModuleLoader__.load({
1643
2307
  }
1644
2308
 
1645
2309
  /**
1646
- * One hovering dragonfly: an outer element on its flight path and an inner one
1647
- * bobbing on the wingbeat, so the two motions compose.
1648
- * @param suffix - gradient id suffix.
1649
- * @param className - the positioning class.
1650
- * @param position - inline position.
1651
- * @param flight - animation shorthand for the flight path.
1652
- * @param delay - animation delay.
1653
- * @returns the dragonfly markup.
2310
+ * One hovering dragonfly: an outer element on its flight path and an inner one
2311
+ * bobbing on the wingbeat, so the two motions compose.
2312
+ * @param suffix - gradient id suffix.
2313
+ * @param className - the positioning class.
2314
+ * @param position - inline position.
2315
+ * @param flight - animation shorthand for the flight path.
2316
+ * @param delay - animation delay.
2317
+ * @returns the dragonfly markup.
2318
+ */
2319
+ function dragonflyBlock(suffix, className, position, flight, delay) {
2320
+ return open('div', {
2321
+ class: `sta-dfly ${className}`,
2322
+ style: `position:absolute;z-index:8;will-change:transform;${position};`
2323
+ + `animation:${flight};animation-delay:${delay}`,
2324
+ }) + open('div', {
2325
+ class: 'sta-bob',
2326
+ style: `animation:dsh-amb-bob ${suffix === '2' ? '1.1s' : '.9s'} ease-in-out infinite;`
2327
+ + `animation-delay:${suffix === '2' ? '-.4s' : '0s'}`,
2328
+ }) + dragonflyMarkup(suffix) + '</div></div>'
2329
+ }
2330
+
2331
+ /**
2332
+ * One seaweed blade. The sway is applied to the group so the blades move from their
2333
+ * base, and each blade animates on its own phase.
2334
+ * @param key - blade index.
2335
+ * @param d - path data.
2336
+ * @param gradient - gradient id.
2337
+ * @param width - stroke width.
2338
+ * @param opacity - blade opacity.
2339
+ * @returns the blade markup.
2340
+ */
2341
+ function bladeMarkup(key, d, gradient, width, opacity) {
2342
+ const durations = { 1: '6s', 2: '7.4s', 3: '5.2s', 4: '8.1s', 5: '6.6s' }
2343
+ const delays = { 1: '0s', 2: '-1.6s', 3: '-2.8s', 4: '-.9s', 5: '-3.4s' }
2344
+ return open('g', {
2345
+ class: `dof-blade dof-blade-${key}`,
2346
+ style: `transform-origin:50% 100%;animation:dsh-amb-sway ${durations[key]} ease-in-out infinite alternate;`
2347
+ + `animation-delay:${delays[key]}`,
2348
+ }) + open('path', {
2349
+ d, fill: `url(#${gradient})`, stroke: `url(#${gradient})`, 'stroke-width': width, opacity,
2350
+ }) + '</path></g>'
2351
+ }
2352
+
2353
+ function seaweedMarkup() {
2354
+ const blade = bladeMarkup
2355
+
2356
+ return open('svg', {
2357
+ viewBox: '0 0 140 120',
2358
+ preserveAspectRatio: 'none',
2359
+ style: 'display:block;width:100%;height:100%',
2360
+ })
2361
+ + open('defs', {})
2362
+ + open('linearGradient', { id: 'dsh-dof-weed-a', x1: '0', y1: '1', x2: '0', y2: '0' })
2363
+ + open('stop', { offset: '0', 'stop-color': '#1E6E93' }) + '</stop>'
2364
+ + open('stop', { offset: '0.55', 'stop-color': '#3E93BC' }) + '</stop>'
2365
+ + open('stop', { offset: '1', 'stop-color': '#A5DEF0', 'stop-opacity': '0.85' }) + '</stop>'
2366
+ + '</linearGradient>'
2367
+ + open('linearGradient', { id: 'dsh-dof-weed-b', x1: '0', y1: '1', x2: '0', y2: '0' })
2368
+ + open('stop', { offset: '0', 'stop-color': '#2B7FA6' }) + '</stop>'
2369
+ + open('stop', { offset: '0.6', 'stop-color': '#4FA3C6' }) + '</stop>'
2370
+ + open('stop', { offset: '1', 'stop-color': '#B8E2F2', 'stop-opacity': '0.85' }) + '</stop>'
2371
+ + '</linearGradient>'
2372
+ + open('linearGradient', { id: 'dsh-dof-weed-c', x1: '0', y1: '1', x2: '0', y2: '0' })
2373
+ + open('stop', { offset: '0', 'stop-color': '#4A78A8' }) + '</stop>'
2374
+ + open('stop', { offset: '0.6', 'stop-color': '#7FA9CE' }) + '</stop>'
2375
+ + open('stop', { offset: '1', 'stop-color': '#CBE2F4', 'stop-opacity': '0.8' }) + '</stop>'
2376
+ + '</linearGradient>'
2377
+ + '</defs>'
2378
+ + blade('1', 'M 22 120 C 12 96, 26 74, 18 48 C 15 38, 18 28, 24 20 C 20 34, 24 44, 30 60 C 36 80, 30 100, 32 120 Z', 'dsh-dof-weed-a', '2.2', '0.95')
2379
+ + blade('2', 'M 48 120 C 40 98, 54 80, 46 56 C 42 44, 48 34, 56 24 C 50 40, 56 52, 60 68 C 64 88, 56 104, 58 120 Z', 'dsh-dof-weed-b', '2', '0.92')
2380
+ + blade('3', 'M 74 120 C 68 102, 78 88, 72 68 C 69 58, 72 48, 78 40 C 74 52, 78 62, 82 76 C 86 94, 80 108, 82 120 Z', 'dsh-dof-weed-c', '1.8', '0.88')
2381
+ + blade('4', 'M 96 120 C 92 104, 102 90, 96 72 C 93 62, 96 54, 102 46 C 98 58, 102 68, 106 82 C 110 98, 102 110, 104 120 Z', 'dsh-dof-weed-a', '1.6', '0.85')
2382
+ + blade('5', 'M 118 120 C 114 108, 122 96, 117 82 C 115 74, 117 68, 121 62 C 118 72, 121 80, 124 92 C 127 104, 121 112, 123 120 Z', 'dsh-dof-weed-b', '1.4', '0.8')
2383
+ + open('ellipse', { cx: '30', cy: '119', rx: '14', ry: '4', fill: '#7FAFC6', opacity: '0.55' }) + '</ellipse>'
2384
+ + open('ellipse', { cx: '72', cy: '120', rx: '10', ry: '3.4', fill: '#8FB9CE', opacity: '0.5' }) + '</ellipse>'
2385
+ + open('ellipse', { cx: '108', cy: '119.5', rx: '12', ry: '3.6', fill: '#7FAFC6', opacity: '0.45' }) + '</ellipse>'
2386
+ + '</svg>'
2387
+ }
2388
+
2389
+ /**
2390
+ * One cartoon fish, swimming across the water.
2391
+ *
2392
+ * Ported from the source system's separate `FishAnimation.vue`. That component drives the
2393
+ * fish from JavaScript through entering / bubbling / leaving phases on a 60-second cycle,
2394
+ * but this skin is a static bundle with no component runtime to host a script — so the
2395
+ * same artwork swims continuously instead, on a CSS `@keyframes` cross, with the tail and
2396
+ * the whole body on separate animations so it reads as swimming rather than sliding.
2397
+ *
2398
+ * The artwork is reproduced shape for shape: the body curve, tail, dorsal fin, pectoral
2399
+ * fin, and the three-part eye. The colours are retuned into the theme's blue family —
2400
+ * the source values (`#38bdf8` body, `#1d4ed8` fins) sat outside the dream palette and
2401
+ * read as a sticker, the same treatment shan's mountains got when their source greens
2402
+ * were darkened for this sidebar.
2403
+ * @param suffix - unique id suffix, so several fish can coexist.
2404
+ * @param size - rendered width in em.
2405
+ * @param top - vertical position within the water, as a percentage.
2406
+ * @param duration - seconds for one crossing.
2407
+ * @param delay - animation delay, so the fish do not move in lockstep.
2408
+ * @param flip - whether this fish swims right-to-left instead.
2409
+ * @returns the fish markup.
2410
+ */
2411
+ function fishMarkup(suffix, size, top, duration, delay, flip) {
2412
+ const anim = flip ? 'dsh-amb-swim-back' : 'dsh-amb-swim'
2413
+ return open('div', {
2414
+ class: `dof-fish${flip ? ' dof-fish-flip' : ''}`,
2415
+ style: 'position:absolute;left:0;opacity:.94;'
2416
+ + `top:${top}%;width:${size}em;`
2417
+ + `animation:${anim} ${duration}s linear infinite;animation-delay:${delay}s`,
2418
+ })
2419
+ + open('div', {
2420
+ class: 'dof-fish-bob',
2421
+ style: `animation-duration:${(duration / 8).toFixed(2)}s`,
2422
+ })
2423
+ + open('svg', {
2424
+ viewBox: '0 0 50 18',
2425
+ preserveAspectRatio: 'xMidYMid meet',
2426
+ style: 'display:block;width:100%;height:auto;overflow:visible',
2427
+ })
2428
+ + open('g', { class: 'dof-fish-body' })
2429
+ // Body — recoloured into the theme's own blue family. The source values
2430
+ // (`#38bdf8` body, `#1d4ed8` fins) sat outside the dream palette and read
2431
+ // as a sticker pasted on the water; the same treatment shan's mountains
2432
+ // got when their source greens were darkened for this sidebar.
2433
+ + open('path', {
2434
+ d: 'M10 10 C20 5 35 5 45 10 C40 15 25 15 10 10 Z',
2435
+ fill: '#5FA5D6', stroke: '#2B6E9E', 'stroke-width': '1',
2436
+ }) + '</path>'
2437
+ // Tail
2438
+ + open('path', {
2439
+ d: 'M10 10 L5 7 L5 13 Z',
2440
+ fill: '#2B6E9E', stroke: '#2B6E9E', 'stroke-width': '1', class: 'dof-fish-tail',
2441
+ }) + '</path>'
2442
+ // Dorsal fin
2443
+ + open('path', {
2444
+ d: 'M20 7 L25 3 L30 7',
2445
+ fill: '#2B6E9E', stroke: '#2B6E9E', 'stroke-width': '1',
2446
+ }) + '</path>'
2447
+ // Pectoral fin
2448
+ + open('path', {
2449
+ d: 'M35 9 L40 12 L45 9',
2450
+ fill: '#5FA5D6', stroke: '#2B6E9E', 'stroke-width': '1',
2451
+ }) + '</path>'
2452
+ // Eye: white, pupil, highlight
2453
+ + open('circle', { cx: '40', cy: '8', r: '2', fill: '#FFFFFF' }) + '</circle>'
2454
+ + open('circle', { cx: '41', cy: '8', r: '1', fill: '#16384F' }) + '</circle>'
2455
+ + open('circle', { cx: '40.5', cy: '7.5', r: '0.5', fill: '#FFFFFF' }) + '</circle>'
2456
+ + '</g>'
2457
+ + '</svg>'
2458
+ + '</div></div>'
2459
+ }
2460
+
2461
+ /**
2462
+ * One drifting cloud-sea copy for 营慕彩云, ported shape-for-shape from
2463
+ * `YingMuCaiYunAnimation.vue`. The source drifts a 200%-wide flex track holding
2464
+ * two copies of the same SVG, so one marquee keyframe loops seamlessly; each
2465
+ * copy carries its own gradient id because two tracks (back and front) and two
2466
+ * preview seats all live in one document.
2467
+ * @param variant - 'back' (deep purple, slow) or 'front' (pink-gold, quicker).
2468
+ * @param copy - unique id suffix for this copy's gradient.
2469
+ * @returns one sea-section SVG.
2470
+ */
2471
+ function ymSeaSvg(variant, copy) {
2472
+ const back = variant === 'back'
2473
+ const gradient = `dsh-ym-sea-${variant}-${copy}`
2474
+ return open('svg', {
2475
+ viewBox: back ? '0 0 240 70' : '0 0 240 80',
2476
+ preserveAspectRatio: 'none',
2477
+ style: 'width:50%;height:100%;flex:none;display:block',
2478
+ })
2479
+ + open('defs', {})
2480
+ + open('linearGradient', { id: gradient, x1: '0', y1: '0', x2: '0', y2: '1' })
2481
+ + open('stop', { offset: '0', 'stop-color': back ? '#C9B6E4' : '#FFF0F7' }) + '</stop>'
2482
+ + open('stop', { offset: '1', 'stop-color': back ? '#A68FCA' : '#E8B4D6' }) + '</stop>'
2483
+ + '</linearGradient>'
2484
+ + '</defs>'
2485
+ + open('path', {
2486
+ d: back
2487
+ ? 'M0 70 L0 44 Q14 40 22 46 Q26 28 44 30 Q54 16 72 22 Q84 8 102 16 Q118 6 134 14 '
2488
+ + 'Q150 8 164 18 Q178 12 190 24 Q206 20 216 34 Q230 32 240 42 L240 70 Z'
2489
+ : 'M0 80 L0 52 Q12 48 22 54 Q28 36 48 38 Q60 24 78 30 Q92 14 110 24 Q126 12 142 22 '
2490
+ + 'Q158 14 172 26 Q188 22 198 36 Q214 34 224 48 Q234 50 240 56 L240 80 Z',
2491
+ fill: `url(#${gradient})`,
2492
+ }) + '</path>'
2493
+ + '</svg>'
2494
+ }
2495
+
2496
+ /**
2497
+ * One hot-air balloon for 营慕彩云, ported from the source component's two
2498
+ * inline SVGs: envelope with highlight, suspension ropes, and a basket.
2499
+ * @param variant - 'main' (orange-gold-pink, near) or 'mini' (pink-purple, far).
2500
+ * @returns the balloon SVG.
2501
+ */
2502
+ function ymBalloonMarkup(variant) {
2503
+ const main = variant === 'main'
2504
+ const gradient = main ? 'dsh-ym-balloon-main' : 'dsh-ym-balloon-mini'
2505
+ const svg = open('svg', {
2506
+ viewBox: main ? '0 0 64 96' : '0 0 40 62',
2507
+ style: 'display:block;width:100%;height:auto;overflow:visible;'
2508
+ + 'filter:drop-shadow(0 4px 6px rgba(90,58,90,.25))',
2509
+ })
2510
+ + open('defs', {})
2511
+ + open('linearGradient', { id: gradient, x1: '0', y1: '0', x2: '1', y2: '1' })
2512
+ + (main
2513
+ ? open('stop', { offset: '0', 'stop-color': '#FFC96B' }) + '</stop>'
2514
+ + open('stop', { offset: '0.55', 'stop-color': '#FFB347' }) + '</stop>'
2515
+ + open('stop', { offset: '1', 'stop-color': '#F08BB4' }) + '</stop>'
2516
+ : open('stop', { offset: '0', 'stop-color': '#F2C9E3' }) + '</stop>'
2517
+ + open('stop', { offset: '1', 'stop-color': '#A68FCA' }) + '</stop>')
2518
+ + '</linearGradient>'
2519
+ + '</defs>'
2520
+ if (main) {
2521
+ return svg
2522
+ + open('path', {
2523
+ d: 'M32 3 C48 3 59 15 59 30 C59 45 47 58 38 66 L26 66 C17 58 5 45 5 30 C5 15 16 3 32 3 Z',
2524
+ fill: `url(#${gradient})`,
2525
+ }) + '</path>'
2526
+ + open('path', {
2527
+ d: 'M32 3 C36 3 39 15 39 30 C39 45 37 58 35.5 66 L28.5 66 C27 58 25 45 25 30 C25 15 28 3 32 3 Z',
2528
+ fill: '#FFE3B8', opacity: '0.85',
2529
+ }) + '</path>'
2530
+ + open('path', { d: 'M13 13 C9 22 10 40 18 55', stroke: '#E88BB0', 'stroke-width': '1.4', fill: 'none', opacity: '0.55' }) + '</path>'
2531
+ + open('path', { d: 'M51 13 C55 22 54 40 46 55', stroke: '#E88BB0', 'stroke-width': '1.4', fill: 'none', opacity: '0.55' }) + '</path>'
2532
+ + open('path', { d: 'M27.5 66 L28.5 75 M36.5 66 L35.5 75', stroke: '#8A6A4A', 'stroke-width': '1.2' }) + '</path>'
2533
+ + open('rect', { x: '25.5', y: '75', width: '13', height: '9', rx: '2.5', fill: '#9C7C5C' }) + '</rect>'
2534
+ + open('path', { d: 'M25.5 78.5 L38.5 78.5', stroke: '#7E6146', 'stroke-width': '1.2' }) + '</path>'
2535
+ + '</svg>'
2536
+ }
2537
+ return svg
2538
+ + open('path', {
2539
+ d: 'M20 2 C30 2 36 10 36 19 C36 29 28 37 22.5 42 L17.5 42 C12 37 4 29 4 19 C4 10 10 2 20 2 Z',
2540
+ fill: `url(#${gradient})`,
2541
+ }) + '</path>'
2542
+ + open('path', {
2543
+ d: 'M20 2 C23 2 25.5 10 25.5 19 C25.5 29 24 37 23 42 L17 42 C16 37 14.5 29 14.5 19 C14.5 10 17 2 20 2 Z',
2544
+ fill: '#F6E7F4', opacity: '0.9',
2545
+ }) + '</path>'
2546
+ + open('path', { d: 'M17.5 42 L18 47 M22.5 42 L22 47', stroke: '#8A6A4A', 'stroke-width': '1' }) + '</path>'
2547
+ + open('rect', { x: '16', y: '47', width: '8', height: '6', rx: '1.5', fill: '#8A6A4A' }) + '</rect>'
2548
+ + '</svg>'
2549
+ }
2550
+
2551
+ /**
2552
+ * The 营慕彩云 scene: a dusk glow, warm stars, blurred drifting clouds, two
2553
+ * marquee cloud-seas (deep purple behind, pink-gold in front) and two bobbing
2554
+ * hot-air balloons.
2555
+ *
2556
+ * Ported from `YingMuCaiYunAnimation.vue`. The source stars are white on a dusk
2557
+ * sky; on this sidebar's pastel gradient white would vanish, so they are retuned
2558
+ * into the theme's warm gold — the same treatment the dream fish got when their
2559
+ * sticker blues moved into the theme family.
2560
+ * @param stars - how many stars to seed.
2561
+ * @returns the scene markup.
2562
+ */
2563
+ function caiyunAmbientScene(stars) {
2564
+ const count = Math.max(0, Math.min(30, stars ?? 12))
2565
+ let starNodes = ''
2566
+ for (let n = 1; n <= count; n += 1) {
2567
+ const size = 2 + ((n * 3) % 2)
2568
+ starNodes += open('div', {
2569
+ class: 'ym-star',
2570
+ style: 'position:absolute;border-radius:50%;'
2571
+ + `left:${((n * 37) % 84) + 6}%;top:${((n * 23) % 44) + 4}%;`
2572
+ + `width:${size}px;height:${size}px;`
2573
+ + 'background:radial-gradient(circle,#FFF7E2 0%,rgba(255,232,190,.55) 100%);'
2574
+ + 'box-shadow:0 0 4px rgba(255,224,160,.8);'
2575
+ + 'animation:dsh-amb-ym-star 2.2s ease-in-out infinite alternate;'
2576
+ + `animation-delay:${-(n * 0.37)}s`,
2577
+ }) + '</div>'
2578
+ }
2579
+
2580
+ const DRIFTS = [
2581
+ ['1', '18em', '5em', '8%', '70s', '-22s'],
2582
+ ['2', '24em', '7em', '28%', '110s', '-44s'],
2583
+ ['3', '16em', '4.5em', '52%', '90s', '-66s'],
2584
+ ]
2585
+ let driftNodes = ''
2586
+ for (const [key, width, height, top, duration, delay] of DRIFTS) {
2587
+ driftNodes += open('div', {
2588
+ class: `ym-drift ym-drift-${key}`,
2589
+ style: `position:absolute;border-radius:1000px;left:-14em;top:${top};width:${width};height:${height};`
2590
+ + 'background:radial-gradient(ellipse,rgba(232,180,214,.55) 0%,rgba(166,143,202,.3) 60%,rgba(166,143,202,0) 100%);'
2591
+ + 'filter:blur(10px);'
2592
+ + `animation:dsh-amb-ym-drift ${duration} linear infinite;animation-delay:${delay}`,
2593
+ }) + '</div>'
2594
+ }
2595
+
2596
+ const sea = (variant, bottom, height, zIndex, opacity, duration) => open('div', {
2597
+ class: `ym-sea ym-sea-${variant}`,
2598
+ style: `position:absolute;left:0;right:0;bottom:${bottom};height:${height};z-index:${zIndex};`
2599
+ + `opacity:${opacity};overflow:hidden`,
2600
+ })
2601
+ + open('div', {
2602
+ class: 'ym-sea-track',
2603
+ style: `position:absolute;left:0;top:0;width:200%;height:100%;display:flex;`
2604
+ + `animation:dsh-amb-ym-sea ${duration} linear infinite`,
2605
+ })
2606
+ + ymSeaSvg(variant, 'a') + ymSeaSvg(variant, 'b')
2607
+ + '</div></div>'
2608
+
2609
+ // The outer element carries a WIDE WANDER and the inner one keeps the source's
2610
+ // small bob + sway. Two separate transforms on two elements, so they compose:
2611
+ // the balloon drifts across the band while still breathing.
2612
+ //
2613
+ // Why the wander: the balloons legitimately float over the sidebar's lower rows,
2614
+ // and a bigger path means they take turns covering and exposing that text instead
2615
+ // of hovering in one spot. The layer/z-index is deliberately NOT lowered — being
2616
+ // in front is what gives them depth, and they are never still for long.
2617
+ const balloon = (variant, position, size, duration, delay, zIndex, wander, wanderDelay) => open('div', {
2618
+ class: `ym-balloon ym-balloon-${variant}`,
2619
+ style: `position:absolute;pointer-events:none;${position}width:${size};z-index:${zIndex};`
2620
+ + `animation:${wander} ease-in-out infinite;animation-delay:${wanderDelay}`,
2621
+ })
2622
+ + open('div', {
2623
+ class: 'ym-bob',
2624
+ style: `transform-origin:50% 25%;animation:dsh-amb-ym-bob ${duration} ease-in-out infinite;`
2625
+ + `animation-delay:${delay}`,
2626
+ })
2627
+ + ymBalloonMarkup(variant)
2628
+ + '</div></div>'
2629
+
2630
+ return open('div', { class: 'ym', style: 'position:absolute;inset:0;display:block' })
2631
+ + open('div', {
2632
+ class: 'ym-glow',
2633
+ style: 'position:absolute;left:0;right:0;bottom:0;height:52%;z-index:1;'
2634
+ + 'background:radial-gradient(ellipse at 35% 100%,rgba(255,214,170,.4) 0%,rgba(255,214,170,0) 70%)',
2635
+ }) + '</div>'
2636
+ + starNodes
2637
+ + driftNodes
2638
+ + sea('back', '16%', '26%', 4, '0.75', '80s')
2639
+ + balloon('mini', 'right:10%;bottom:56%;', '2.6em', '9.5s', '-3.5s', 5,
2640
+ 'dsh-amb-ym-wander-2 38s', '-13s')
2641
+ + sea('front', '0%', '32%', 6, '1', '50s')
2642
+ + balloon('main', 'left:12%;bottom:38%;', '4.6em', '7s', '0s', 7,
2643
+ 'dsh-amb-ym-wander 30s', '0s')
2644
+ + '</div>'
2645
+ }
2646
+
2647
+ /**
2648
+ * One 乌篷船 (black-awning boat) for 江畔冬云, ported from
2649
+ * `JiangPanDongYunAnimation.vue`: hull, awning, poling oar and a warm bow
2650
+ * lantern, plus its water reflection. The source carries the reflection as a
2651
+ * second SVG; here one SVG holds both, the mirror produced by a flipped
2652
+ * `<use>`, so there is no second element to keep in place.
2653
+ * @returns the boat SVG.
2654
+ */
2655
+ function jpBoatMarkup() {
2656
+ return open('svg', { viewBox: '0 0 120 92', style: 'display:block;width:100%;height:auto' })
2657
+ + open('defs', {})
2658
+ + open('g', { id: 'dsh-jp-boat-art' })
2659
+ + open('path', { d: 'M 6 24 Q 60 36 114 22 Q 106 36 62 40 Q 22 38 6 24 Z', fill: '#10222C' }) + '</path>'
2660
+ + open('path', { d: 'M 38 24 C 44 10, 78 10, 88 23 Z', fill: '#0C1B24' }) + '</path>'
2661
+ + open('path', { d: 'M 22 26 L 30 -4', stroke: '#0C1B24', 'stroke-width': '2', 'stroke-linecap': 'round', fill: 'none' }) + '</path>'
2662
+ + open('circle', { cx: '14', cy: '21', r: '2.6', fill: '#F2C879' }) + '</circle>'
2663
+ + open('circle', { cx: '14', cy: '21', r: '5.5', fill: '#F2C879', opacity: '0.25' }) + '</circle>'
2664
+ + '</g>'
2665
+ + '</defs>'
2666
+ + open('use', { href: '#dsh-jp-boat-art' }) + '</use>'
2667
+ + open('use', {
2668
+ href: '#dsh-jp-boat-art',
2669
+ transform: 'translate(0,90) scale(1,-1)',
2670
+ style: 'opacity:.22;filter:blur(2.5px)',
2671
+ }) + '</use>'
2672
+ + '</svg>'
2673
+ }
2674
+
2675
+ /**
2676
+ * The 江畔冬云 scene: a pale winter moon, slow drifting clouds, fine snow, and a
2677
+ * river band carrying a moonlight column, flowing ripples, twinkling wave
2678
+ * glints, a drifting sampan with its reflection, and swaying bank reeds.
2679
+ *
2680
+ * Ported from `JiangPanDongYunAnimation.vue`. The source moon and clouds sit on
2681
+ * a near-black sky; here they sit on the pastel one, so the moon keeps its
2682
+ * source colours but leans on its halo to separate, and the night clouds keep
2683
+ * their shape at a lighter opacity.
2684
+ * @param snow - how many snowflakes to seed.
2685
+ * @returns the scene markup.
2686
+ */
2687
+ function dongyunAmbientScene(snow) {
2688
+ const count = Math.max(0, Math.min(30, snow ?? 8))
2689
+ let snowNodes = ''
2690
+ for (let n = 1; n <= count; n += 1) {
2691
+ const size = 2 + ((n * 3) % 3)
2692
+ snowNodes += open('div', {
2693
+ class: 'jp-snowflake',
2694
+ style: 'position:absolute;top:-0.5em;border-radius:50%;'
2695
+ + `left:${((n * 29) % 88) + 5}%;width:${size}px;height:${size}px;`
2696
+ + 'background:radial-gradient(circle,#FFFFFF 0%,rgba(255,255,255,.75) 100%);'
2697
+ + 'box-shadow:0 0 4px rgba(255,255,255,.85);'
2698
+ + `animation:dsh-amb-jp-snow ${9 + ((n * 7) % 8)}s linear infinite;`
2699
+ + `animation-delay:${-(n * 1.9)}s`,
2700
+ }) + '</div>'
2701
+ }
2702
+
2703
+ const CLOUDS = [
2704
+ ['1', '12em', '3.2em', '10%', '95s', '0s'],
2705
+ ['2', '15em', '4em', '27%', '130s', '-40s'],
2706
+ ['3', '10.5em', '2.8em', '44%', '110s', '-70s'],
2707
+ ]
2708
+ let cloudNodes = ''
2709
+ for (const [key, width, height, top, duration, delay] of CLOUDS) {
2710
+ cloudNodes += open('div', {
2711
+ class: `jp-cloud jp-cloud-${key}`,
2712
+ style: `position:absolute;border-radius:1000px;left:-16em;top:${top};width:${width};height:${height};z-index:3;`
2713
+ + 'background:radial-gradient(ellipse,rgba(226,235,242,.85) 0%,rgba(190,206,220,.45) 60%,rgba(190,206,220,0) 100%);'
2714
+ + 'filter:blur(10px);'
2715
+ + `animation:dsh-amb-jp-cloud ${duration} linear infinite;animation-delay:${delay}`,
2716
+ }) + '</div>'
2717
+ }
2718
+
2719
+ const RIPPLES = [
2720
+ ['1', '9em', '30%', '17s', '0s', '0.4'],
2721
+ ['2', '7em', '55%', '23s', '-9s', '0.3'],
2722
+ ['3', '10.5em', '78%', '14s', '-4s', '0.4'],
2723
+ ]
2724
+ let rippleNodes = ''
2725
+ for (const [key, width, top, duration, delay, opacity] of RIPPLES) {
2726
+ rippleNodes += open('div', {
2727
+ class: `jp-ripple jp-ripple-${key}`,
2728
+ style: `position:absolute;height:2px;border-radius:2px;left:-10em;top:${top};width:${width};opacity:${opacity};`
2729
+ + 'background:linear-gradient(90deg,transparent 0%,rgba(210,232,240,.9) 30%,rgba(210,232,240,.5) 60%,transparent 100%);'
2730
+ + `animation:dsh-amb-jp-ripple ${duration} linear infinite;animation-delay:${delay}`,
2731
+ }) + '</div>'
2732
+ }
2733
+
2734
+ let glintNodes = ''
2735
+ for (let n = 1; n <= 5; n += 1) {
2736
+ glintNodes += open('div', {
2737
+ class: 'jp-glint',
2738
+ style: 'position:absolute;width:3px;height:3px;border-radius:50%;'
2739
+ + `left:${12 + ((n * 19) % 76)}%;top:${28 + ((n * 23) % 58)}%;`
2740
+ + 'background:radial-gradient(circle,#EAF6FA 0%,rgba(234,246,250,.4) 100%);'
2741
+ + 'box-shadow:0 0 6px rgba(220,240,250,.9);'
2742
+ + 'animation:dsh-amb-jp-glint 2.6s ease-in-out infinite alternate;'
2743
+ + `animation-delay:${-(n * 0.6)}s`,
2744
+ }) + '</div>'
2745
+ }
2746
+
2747
+ const REEDS = [
2748
+ ['1', 'M 14 120 C 15 92, 17 62, 24 34', 'M 26 27', '4.6', '13', '9 26 27', '5.2s', '0s'],
2749
+ ['2', 'M 34 120 C 36 96, 42 66, 54 42', 'M 57 35', '4.2', '12', '16 57 35', '6.1s', '-2.1s'],
2750
+ ['3', 'M 56 120 C 56 100, 58 78, 62 56', 'M 63 49', '3.8', '11', '5 63 49', '4.6s', '-1.2s'],
2751
+ ['4', 'M 76 120 C 80 100, 90 78, 102 58', 'M 105 51', '3.6', '10.5', '22 105 51', '5.8s', '-3s'],
2752
+ ]
2753
+ let reedNodes = ''
2754
+ for (const [key, stem, head, rx, ry, rot, duration, delay] of REEDS) {
2755
+ const [angle, cx, cy] = rot.split(' ')
2756
+ reedNodes += open('g', {
2757
+ class: `jp-reed jp-reed-${key}`,
2758
+ style: `transform-box:fill-box;transform-origin:50% 100%;`
2759
+ + `animation:dsh-amb-jp-sway ${duration} ease-in-out infinite;animation-delay:${delay}`,
2760
+ })
2761
+ + open('path', { d: stem, stroke: '#0E202A', 'stroke-width': '2.2', fill: 'none', 'stroke-linecap': 'round' }) + '</path>'
2762
+ + open('ellipse', {
2763
+ cx, cy, rx, ry, fill: '#12242F',
2764
+ transform: `rotate(${angle} ${cx} ${cy})`,
2765
+ }) + '</ellipse>'
2766
+ + '</g>'
2767
+ }
2768
+
2769
+ return open('div', { class: 'jp', style: 'position:absolute;inset:0;display:block' })
2770
+ + open('div', {
2771
+ class: 'jp-moon',
2772
+ style: 'position:absolute;top:5%;right:9%;width:3em;height:3em;border-radius:50%;z-index:2;opacity:.95;'
2773
+ + 'background:radial-gradient(circle at 38% 34%,#FDFAF2 0%,#E8E4D4 55%,#CBD2D8 100%);'
2774
+ + 'box-shadow:0 0 1.4em 0.35em rgba(240,244,240,.5),0 0 3.5em 1em rgba(190,210,225,.4)',
2775
+ }) + '</div>'
2776
+ + cloudNodes
2777
+ + snowNodes
2778
+ + open('div', {
2779
+ class: 'jp-river',
2780
+ style: 'position:absolute;left:0;right:0;bottom:0;height:38%;z-index:7;'
2781
+ + 'background:linear-gradient(to bottom,rgba(150,178,196,.4) 0%,rgba(110,142,164,.5) 100%);'
2782
+ + 'box-shadow:inset 0 0.9em 1.1em -0.6em rgba(16,34,44,.5)',
2783
+ })
2784
+ + open('div', {
2785
+ class: 'jp-waterline',
2786
+ style: 'position:absolute;top:0;left:0;right:0;height:1.5px;opacity:.5;'
2787
+ + 'background:linear-gradient(90deg,transparent 0%,rgba(214,234,242,.9) 35%,rgba(214,234,242,.35) 70%,transparent 100%)',
2788
+ }) + '</div>'
2789
+ + open('div', {
2790
+ class: 'jp-moonlight',
2791
+ style: 'position:absolute;top:4%;left:62%;width:3em;height:82%;border-radius:40%;filter:blur(5px);'
2792
+ + 'background:linear-gradient(to bottom,rgba(226,240,246,.18) 0%,rgba(226,240,246,.06) 55%,transparent 100%)',
2793
+ }) + '</div>'
2794
+ + rippleNodes
2795
+ + glintNodes
2796
+ + '</div>'
2797
+ + open('div', {
2798
+ class: 'jp-boat',
2799
+ style: 'position:absolute;bottom:22%;left:13%;width:7em;z-index:8;'
2800
+ + 'animation:dsh-amb-jp-boat 46s ease-in-out infinite alternate',
2801
+ })
2802
+ + open('div', {
2803
+ class: 'jp-boat-bob',
2804
+ style: 'transform-origin:50% 100%;animation:dsh-amb-jp-bob 5.2s ease-in-out infinite',
2805
+ })
2806
+ + jpBoatMarkup()
2807
+ + '</div></div>'
2808
+ + open('div', {
2809
+ class: 'jp-reeds',
2810
+ style: 'position:absolute;bottom:0;left:0;width:9em;z-index:9',
2811
+ })
2812
+ + open('svg', { viewBox: '0 0 120 120', style: 'display:block;width:100%;height:auto' })
2813
+ + reedNodes
2814
+ + open('path', { d: 'M 2 120 C 20 118, 42 112, 62 103', stroke: '#0E202A', 'stroke-width': '2.4', fill: 'none', 'stroke-linecap': 'round' }) + '</path>'
2815
+ + open('path', { d: 'M 6 120 C 28 118.5, 54 114, 82 108', stroke: '#0E202A', 'stroke-width': '2', fill: 'none', 'stroke-linecap': 'round', opacity: '0.85' }) + '</path>'
2816
+ + open('path', { d: 'M 18 120 C 40 119, 64 116.5, 92 113', stroke: '#0E202A', 'stroke-width': '1.6', fill: 'none', 'stroke-linecap': 'round', opacity: '0.7' }) + '</path>'
2817
+ + '</svg>'
2818
+ + '</div>'
2819
+ + '</div>'
2820
+ }
2821
+
2822
+ /**
2823
+ * One silhouette pine for 徐山军月, ported verbatim from
2824
+ * `XuShanJunYueAnimation.vue`'s `<defs>` art: a tiered pine over a short trunk.
2825
+ * @returns the pine group markup (a `<g>` for reuse inside one SVG).
2826
+ */
2827
+ function xsPineMarkup() {
2828
+ return open('g', { id: 'dsh-xsj-pine-art' })
2829
+ + open('path', {
2830
+ d: 'M 12 120 L 12 112 L 5 112 L 12 100 L 3 100 L 10 88 L 4 88 L 12 74 L 20 88 L 14 88 L 21 100 L 12 100 L 19 112 L 12 112 Z',
2831
+ fill: '#101A11',
2832
+ }) + '</path>'
2833
+ + open('rect', { x: '10.6', y: '112', width: '2.8', height: '8', fill: '#101A11' }) + '</rect>'
2834
+ + '</g>'
2835
+ }
2836
+
2837
+ /**
2838
+ * The 徐山军月 scene: a starfield, a full moon veiled by slow night clouds, a
2839
+ * periodic meteor, two dark mountain ridges, and a row of standing pines.
2840
+ *
2841
+ * Ported from `XuShanJunYueAnimation.vue`. The source stars and moon are pale
2842
+ * cream on a near-black sky; on the pastel sidebar they would vanish, so they
2843
+ * are warmed into the theme's brass family (the same retune the dream fish and
2844
+ * the caiyun stars got), while the ridges and pines keep their verbatim dark
2845
+ * silhouettes — dark on light reads as the classic ink shape.
2846
+ * @param stars - how many stars to seed.
2847
+ * @returns the scene markup.
2848
+ */
2849
+ function junyueAmbientScene(stars) {
2850
+ const count = Math.max(0, Math.min(30, stars ?? 12))
2851
+ let starNodes = ''
2852
+ for (let n = 1; n <= count; n += 1) {
2853
+ const size = 2 + ((n * 3) % 2)
2854
+ starNodes += open('div', {
2855
+ class: 'xs-star',
2856
+ style: 'position:absolute;border-radius:50%;'
2857
+ + `left:${((n * 37) % 84) + 6}%;top:${((n * 23) % 48) + 4}%;`
2858
+ + `width:${size}px;height:${size}px;`
2859
+ + 'background:radial-gradient(circle,#F5EFD2 0%,rgba(212,178,102,.55) 100%);'
2860
+ + 'box-shadow:0 0 5px rgba(212,178,102,.75);'
2861
+ + 'animation:dsh-amb-xs-star 2.4s ease-in-out infinite alternate;'
2862
+ + `animation-delay:${-(n * 0.7)}s`,
2863
+ }) + '</div>'
2864
+ }
2865
+
2866
+ const CLOUDS = [
2867
+ ['1', '10.5em', '1.6em', '12%', '90s', '0s'],
2868
+ ['2', '8.5em', '1.25em', '26%', '120s', '-55s'],
2869
+ ]
2870
+ let cloudNodes = ''
2871
+ for (const [key, width, height, top, duration, delay] of CLOUDS) {
2872
+ cloudNodes += open('div', {
2873
+ class: `xs-cloud xs-cloud-${key}`,
2874
+ style: `position:absolute;border-radius:1000px;left:-12em;top:${top};width:${width};height:${height};z-index:3;`
2875
+ + 'background:linear-gradient(90deg,rgba(24,32,25,0) 0%,rgba(24,32,25,.22) 30%,rgba(24,32,25,.22) 70%,rgba(24,32,25,0) 100%);'
2876
+ + 'filter:blur(5px);'
2877
+ + `animation:dsh-amb-xs-night ${duration} linear infinite;animation-delay:${delay}`,
2878
+ }) + '</div>'
2879
+ }
2880
+
2881
+ return open('div', { class: 'xs', style: 'position:absolute;inset:0;display:block' })
2882
+ + starNodes
2883
+ + open('div', {
2884
+ class: 'xs-moon',
2885
+ style: 'position:absolute;top:7%;right:11%;width:3.6em;height:3.6em;border-radius:50%;z-index:2;'
2886
+ + 'background:radial-gradient(circle at 36% 32%,#FBF6E4 0%,#EFE7CB 58%,#D6CCA8 100%);'
2887
+ + 'box-shadow:0 0 1.6em 0.5em rgba(245,239,217,.5),0 0 4em 1.4em rgba(212,178,102,.22)',
2888
+ }) + '</div>'
2889
+ + cloudNodes
2890
+ + open('div', {
2891
+ class: 'xs-meteor',
2892
+ style: 'position:absolute;top:16%;left:66%;width:4.6em;height:2px;border-radius:2px;z-index:4;'
2893
+ + 'background:linear-gradient(90deg,transparent,rgba(245,242,220,.95));'
2894
+ + 'animation:dsh-amb-xs-meteor 9s ease-in infinite',
2895
+ })
2896
+ + open('span', {
2897
+ style: 'position:absolute;right:-2px;top:-2.2px;width:5px;height:5px;border-radius:50%;'
2898
+ + 'background:#FFFBEA;box-shadow:0 0 8px 2px rgba(255,251,234,.9)',
2899
+ }) + '</span>'
2900
+ + '</div>'
2901
+ + open('div', {
2902
+ class: 'xs-ridges',
2903
+ style: 'position:absolute;left:0;right:0;bottom:0;height:46%;z-index:5',
2904
+ })
2905
+ + open('svg', { viewBox: '0 0 223 160', preserveAspectRatio: 'none', style: 'display:block;width:100%;height:100%' })
2906
+ + open('defs', {})
2907
+ + open('linearGradient', { id: 'dsh-xsj-ridge-back', x1: '0', y1: '0', x2: '0', y2: '1' })
2908
+ + open('stop', { offset: '0', 'stop-color': '#31412F' }) + '</stop>'
2909
+ + open('stop', { offset: '1', 'stop-color': '#273528' }) + '</stop>'
2910
+ + '</linearGradient>'
2911
+ + open('linearGradient', { id: 'dsh-xsj-ridge-front', x1: '0', y1: '0', x2: '0', y2: '1' })
2912
+ + open('stop', { offset: '0', 'stop-color': '#1F2C1F' }) + '</stop>'
2913
+ + open('stop', { offset: '1', 'stop-color': '#161F16' }) + '</stop>'
2914
+ + '</linearGradient>'
2915
+ + '</defs>'
2916
+ + open('path', {
2917
+ d: 'M 0 62 Q 28 30 58 52 Q 92 18 126 46 Q 158 24 188 50 Q 206 38 223 46 L 223 160 L 0 160 Z',
2918
+ fill: 'url(#dsh-xsj-ridge-back)',
2919
+ }) + '</path>'
2920
+ + open('path', {
2921
+ d: 'M 0 104 Q 34 72 68 94 Q 104 64 142 92 Q 176 74 223 100 L 223 160 L 0 160 Z',
2922
+ fill: 'url(#dsh-xsj-ridge-front)',
2923
+ }) + '</path>'
2924
+ + '</svg>'
2925
+ + '</div>'
2926
+ + open('div', {
2927
+ class: 'xs-pines',
2928
+ style: 'position:absolute;bottom:0;left:3%;width:8.5em;z-index:6',
2929
+ })
2930
+ + open('svg', { viewBox: '0 0 140 120', style: 'display:block;width:100%;height:auto' })
2931
+ + open('defs', {}) + xsPineMarkup() + '</defs>'
2932
+ + open('use', { href: '#dsh-xsj-pine-art', transform: 'translate(-4,-8) scale(1.35)' }) + '</use>'
2933
+ + open('use', { href: '#dsh-xsj-pine-art', transform: 'translate(34,2) scale(1.0)' }) + '</use>'
2934
+ + open('use', { href: '#dsh-xsj-pine-art', transform: 'translate(66,-4) scale(1.18)' }) + '</use>'
2935
+ + open('use', { href: '#dsh-xsj-pine-art', transform: 'translate(102,6) scale(0.88)' }) + '</use>'
2936
+ + open('use', { href: '#dsh-xsj-pine-art', transform: 'translate(126,0) scale(1.05)' }) + '</use>'
2937
+ + '</svg>'
2938
+ + '</div>'
2939
+ + '</div>'
2940
+ }
2941
+
2942
+ /**
2943
+ * The 佩安杰心 scene: misty hills behind, a zen enso circle with a seated
2944
+ * meditator, an incense burner with two rising smoke threads, drifting dust
2945
+ * motes, and the theme's own words.
2946
+ *
2947
+ * Ported from `PeiAnJieXinAnimation.vue`. The artwork paths are verbatim; the
2948
+ * scene SVG's viewBox is cropped to the artwork (the source spans a full-height
2949
+ * sidebar and places the group far down inside it), which keeps the same drawing
2950
+ * coordinates while fitting the band.
2951
+ * @param dust - how many dust motes to seed.
2952
+ * @returns the scene markup.
1654
2953
  */
1655
- function dragonflyBlock(suffix, className, position, flight, delay) {
1656
- return open('div', {
1657
- class: `sta-dfly ${className}`,
1658
- style: `position:absolute;z-index:8;will-change:transform;${position};`
1659
- + `animation:${flight};animation-delay:${delay}`,
1660
- }) + open('div', {
1661
- class: 'sta-bob',
1662
- style: `animation:dsh-amb-bob ${suffix === '2' ? '1.1s' : '.9s'} ease-in-out infinite;`
1663
- + `animation-delay:${suffix === '2' ? '-.4s' : '0s'}`,
1664
- }) + dragonflyMarkup(suffix) + '</div></div>'
2954
+ function jiexinAmbientScene(dust) {
2955
+ const count = Math.max(0, Math.min(20, dust ?? 4))
2956
+ let dustNodes = ''
2957
+ for (let n = 1; n <= count; n += 1) {
2958
+ const size = 2.5 + ((n * 5) % 4)
2959
+ dustNodes += open('div', {
2960
+ class: 'pj-dust',
2961
+ style: 'position:absolute;border-radius:50%;'
2962
+ + `left:${((n * 17) % 50) + 26}%;top:${((n * 13) % 30) + 28}%;`
2963
+ + `width:${size}px;height:${size}px;`
2964
+ + 'background:radial-gradient(circle,rgba(255,244,214,.95) 0%,rgba(255,244,214,0) 100%);'
2965
+ + 'box-shadow:0 0 6px rgba(255,236,190,.8);'
2966
+ + `animation:dsh-amb-pj-dust ${8 + ((n * 5) % 5)}s linear infinite;`
2967
+ + `animation-delay:${-(n * 2)}s`,
2968
+ }) + '</div>'
2969
+ }
2970
+
2971
+ return open('div', { class: 'pj', style: 'position:absolute;inset:0;display:block' })
2972
+ + open('div', {
2973
+ class: 'pj-hills',
2974
+ style: 'position:absolute;left:0;right:0;top:8%;height:24%;z-index:2;opacity:.62',
2975
+ })
2976
+ + open('svg', { viewBox: '0 0 223 80', preserveAspectRatio: 'none', style: 'display:block;width:100%;height:100%' })
2977
+ + open('path', {
2978
+ d: 'M 0 60 Q 34 22 70 46 Q 108 14 146 44 Q 186 20 223 48 L 223 80 L 0 80 Z',
2979
+ fill: '#B49B78', opacity: '0.55',
2980
+ }) + '</path>'
2981
+ + open('path', {
2982
+ d: 'M 0 72 Q 40 42 82 60 Q 126 34 168 58 Q 196 44 223 60 L 223 80 L 0 80 Z',
2983
+ fill: '#A08A67', opacity: '0.6',
2984
+ }) + '</path>'
2985
+ + '</svg>'
2986
+ + '</div>'
2987
+ + open('div', {
2988
+ class: 'pj-zenscene',
2989
+ style: 'position:absolute;left:0;right:0;bottom:8%;height:62%;z-index:3',
2990
+ })
2991
+ + open('svg', {
2992
+ viewBox: '0 480 200 175',
2993
+ preserveAspectRatio: 'xMidYMax meet',
2994
+ style: 'display:block;width:100%;height:100%',
2995
+ })
2996
+ + open('defs', {})
2997
+ + open('radialGradient', { id: 'dsh-pjx-fig-grad', cx: '50%', cy: '32%', r: '80%' })
2998
+ + open('stop', { offset: '0%', 'stop-color': '#6B5238', 'stop-opacity': '0.9' }) + '</stop>'
2999
+ + open('stop', { offset: '60%', 'stop-color': '#57432C', 'stop-opacity': '0.85' }) + '</stop>'
3000
+ + open('stop', { offset: '100%', 'stop-color': '#4A3823', 'stop-opacity': '0.8' }) + '</stop>'
3001
+ + '</radialGradient>'
3002
+ + open('filter', { id: 'dsh-pjx-haze', x: '-30%', y: '-30%', width: '160%', height: '160%' })
3003
+ + open('feGaussianBlur', { stdDeviation: '1.8' }) + '</feGaussianBlur>'
3004
+ + '</filter>'
3005
+ + open('filter', { id: 'dsh-pjx-soft-glow', x: '-60%', y: '-60%', width: '220%', height: '220%' })
3006
+ + open('feGaussianBlur', { stdDeviation: '7' }) + '</feGaussianBlur>'
3007
+ + '</filter>'
3008
+ + open('g', { id: 'dsh-pjx-enso-art' })
3009
+ + open('circle', {
3010
+ cx: '80', cy: '64', r: '56', fill: 'none', stroke: '#A87E4C', 'stroke-width': '5',
3011
+ 'stroke-linecap': 'round', 'stroke-dasharray': '316 36',
3012
+ transform: 'rotate(-78 80 64)', opacity: '0.62',
3013
+ }) + '</circle>'
3014
+ + open('circle', {
3015
+ cx: '80', cy: '64', r: '56', fill: 'none', stroke: '#D9B98C', 'stroke-width': '1.8',
3016
+ 'stroke-linecap': 'round', 'stroke-dasharray': '255 92',
3017
+ transform: 'rotate(-60 80 64)', opacity: '0.5',
3018
+ }) + '</circle>'
3019
+ + '</g>'
3020
+ + open('g', { id: 'dsh-pjx-meditator-art' })
3021
+ + open('g', { filter: 'url(#dsh-pjx-soft-glow)', opacity: '0.35' })
3022
+ + open('path', {
3023
+ d: 'M 37 112 C 41 97, 58 90, 80 90 C 102 90, 119 97, 123 112 C 112 117.5, 96 119.5, 80 119.5 C 64 119.5, 48 117.5, 37 112 Z',
3024
+ fill: '#6B5238',
3025
+ }) + '</path>'
3026
+ + open('circle', { cx: '80', cy: '33', r: '13.5', fill: '#6B5238' }) + '</circle>'
3027
+ + open('path', {
3028
+ d: 'M 64 52 C 60 68, 62 80, 67 93 C 75 97.5, 85 97.5, 93 93 C 98 80, 100 68, 96 52 C 91 44.5, 69 44.5, 64 52 Z',
3029
+ fill: '#6B5238',
3030
+ }) + '</path>'
3031
+ + '</g>'
3032
+ + open('ellipse', {
3033
+ cx: '80', cy: '119', rx: '47', ry: '8.5', fill: '#8A6A46', opacity: '0.7',
3034
+ filter: 'url(#dsh-pjx-haze)',
3035
+ }) + '</ellipse>'
3036
+ + open('g', { fill: 'url(#dsh-pjx-fig-grad)', filter: 'url(#dsh-pjx-haze)' })
3037
+ + open('path', {
3038
+ d: 'M 37 112 C 41 97, 58 90, 80 90 C 102 90, 119 97, 123 112 C 112 117.5, 96 119.5, 80 119.5 C 64 119.5, 48 117.5, 37 112 Z',
3039
+ }) + '</path>'
3040
+ + open('path', {
3041
+ d: 'M 64 52 C 60 68, 62 80, 67 93 C 75 97.5, 85 97.5, 93 93 C 98 80, 100 68, 96 52 C 91 44.5, 69 44.5, 64 52 Z',
3042
+ }) + '</path>'
3043
+ + open('path', { d: 'M 64 54 C 55 62, 51 78, 53 93 C 56 99, 62 101, 67 99 C 62 85, 62 68, 67 56 Z' }) + '</path>'
3044
+ + open('path', { d: 'M 96 54 C 105 62, 109 78, 107 93 C 104 99, 98 101, 93 99 C 98 85, 98 68, 93 56 Z' }) + '</path>'
3045
+ + open('ellipse', { cx: '80', cy: '95', rx: '7', ry: '4.5' }) + '</ellipse>'
3046
+ + open('circle', { cx: '80', cy: '33', r: '13.5' }) + '</circle>'
3047
+ + '</g>'
3048
+ + '</g>'
3049
+ + '</defs>'
3050
+ + open('g', { transform: 'translate(18, 492) scale(1.17)' })
3051
+ + open('use', { href: '#dsh-pjx-enso-art' }) + '</use>'
3052
+ + open('use', { href: '#dsh-pjx-meditator-art' }) + '</use>'
3053
+ + '</g>'
3054
+ + open('g', {})
3055
+ + open('path', {
3056
+ d: 'M 20 631 C 20 623, 25 619, 32 619 C 39 619, 44 623, 44 631 Z',
3057
+ fill: '#6B5644', opacity: '0.8',
3058
+ }) + '</path>'
3059
+ + open('ellipse', { cx: '32', cy: '620', rx: '11.7', ry: '3', fill: '#55432F', opacity: '0.9' }) + '</ellipse>'
3060
+ + open('path', { d: 'M 32 620 L 32 604', stroke: '#8A6E52', 'stroke-width': '1.9', 'stroke-linecap': 'round', fill: 'none' }) + '</path>'
3061
+ + open('circle', { cx: '32', cy: '603.5', r: '1.9', fill: '#E8A05A' }) + '</circle>'
3062
+ + open('circle', { cx: '32', cy: '603.5', r: '4', fill: '#E8A05A', opacity: '0.3' }) + '</circle>'
3063
+ + open('path', {
3064
+ class: 'pj-smoke pj-smoke-1',
3065
+ d: 'M 32 601 C 28 593, 36 586, 32 577 C 28 568, 35 561, 31 551',
3066
+ fill: 'none', stroke: '#FFFFFF', 'stroke-width': '2.4', 'stroke-linecap': 'round', opacity: '0.85',
3067
+ style: 'stroke-dasharray:90;stroke-dashoffset:90;filter:blur(1.4px);'
3068
+ + 'animation:dsh-amb-pj-smoke 7s linear infinite',
3069
+ }) + '</path>'
3070
+ + open('path', {
3071
+ class: 'pj-smoke pj-smoke-2',
3072
+ d: 'M 32 601 C 36 592, 28 584, 33 575 C 37 566, 30 558, 34 549',
3073
+ fill: 'none', stroke: '#FFF6E8', 'stroke-width': '1.9', 'stroke-linecap': 'round', opacity: '0.6',
3074
+ style: 'stroke-dasharray:90;stroke-dashoffset:90;filter:blur(1.6px);'
3075
+ + 'animation:dsh-amb-pj-smoke 7s linear infinite;animation-delay:-3.5s',
3076
+ }) + '</path>'
3077
+ + '</g>'
3078
+ + '</svg>'
3079
+ + '</div>'
3080
+ + dustNodes
3081
+ + open('div', {
3082
+ class: 'pj-words',
3083
+ style: 'position:absolute;left:0;right:0;bottom:2%;text-align:center;z-index:7;'
3084
+ + "font-family:'KaiTi','STKaiti','SimSun',serif;font-size:1.05em;line-height:1;"
3085
+ + 'color:#6B5233;letter-spacing:.45em;text-shadow:0 1px 0 rgba(255,250,240,.6)',
3086
+ })
3087
+ + '自在<span style="color:#B08D5F;margin:0 2px"> · </span>安顿'
3088
+ + '</div>'
3089
+ + '</div>'
1665
3090
  }
1666
3091
 
1667
3092
  /**
1668
- * One seaweed blade. The sway is applied to the group so the blades move from their
1669
- * base, and each blade animates on its own phase.
1670
- * @param key - blade index.
1671
- * @param d - path data.
1672
- * @param gradient - gradient id.
1673
- * @param width - stroke width.
1674
- * @param opacity - blade opacity.
1675
- * @returns the blade markup.
3093
+ * One falling phoenix feather for 光彩凤晨, ported from
3094
+ * `GuangCaiFengChenAnimation.vue`: a full gradient vane over a gold rachis.
3095
+ * Each feather carries its own gradient id, the same rule the dragonflies follow.
3096
+ * @param key - unique id suffix for this feather's gradient.
3097
+ * @returns the feather SVG.
1676
3098
  */
1677
- function bladeMarkup(key, d, gradient, width, opacity) {
1678
- const durations = { 1: '6s', 2: '7.4s', 3: '5.2s', 4: '8.1s', 5: '6.6s' }
1679
- const delays = { 1: '0s', 2: '-1.6s', 3: '-2.8s', 4: '-.9s', 5: '-3.4s' }
1680
- return open('g', {
1681
- class: `dof-blade dof-blade-${key}`,
1682
- style: `transform-origin:50% 100%;animation:dsh-amb-sway ${durations[key]} ease-in-out infinite alternate;`
1683
- + `animation-delay:${delays[key]}`,
1684
- }) + open('path', {
1685
- d, fill: `url(#${gradient})`, stroke: `url(#${gradient})`, 'stroke-width': width, opacity,
1686
- }) + '</path></g>'
1687
- }
1688
-
1689
- function seaweedMarkup() {
1690
- const blade = bladeMarkup
1691
-
3099
+ function gcFeatherMarkup(key) {
3100
+ const gradient = `dsh-gc-feather-${key}`
1692
3101
  return open('svg', {
1693
- viewBox: '0 0 140 120',
1694
- preserveAspectRatio: 'none',
1695
- style: 'display:block;width:100%;height:100%',
3102
+ viewBox: '0 0 40 90',
3103
+ style: 'display:block;width:100%;height:auto;overflow:visible;'
3104
+ + 'filter:drop-shadow(0 0 5px rgba(255,214,0,.3))',
1696
3105
  })
1697
3106
  + open('defs', {})
1698
- + open('linearGradient', { id: 'dsh-dof-weed-a', x1: '0', y1: '1', x2: '0', y2: '0' })
1699
- + open('stop', { offset: '0', 'stop-color': '#1E6E93' }) + '</stop>'
1700
- + open('stop', { offset: '0.55', 'stop-color': '#3E93BC' }) + '</stop>'
1701
- + open('stop', { offset: '1', 'stop-color': '#A5DEF0', 'stop-opacity': '0.85' }) + '</stop>'
1702
- + '</linearGradient>'
1703
- + open('linearGradient', { id: 'dsh-dof-weed-b', x1: '0', y1: '1', x2: '0', y2: '0' })
1704
- + open('stop', { offset: '0', 'stop-color': '#2B7FA6' }) + '</stop>'
1705
- + open('stop', { offset: '0.6', 'stop-color': '#4FA3C6' }) + '</stop>'
1706
- + open('stop', { offset: '1', 'stop-color': '#B8E2F2', 'stop-opacity': '0.85' }) + '</stop>'
1707
- + '</linearGradient>'
1708
- + open('linearGradient', { id: 'dsh-dof-weed-c', x1: '0', y1: '1', x2: '0', y2: '0' })
1709
- + open('stop', { offset: '0', 'stop-color': '#4A78A8' }) + '</stop>'
1710
- + open('stop', { offset: '0.6', 'stop-color': '#7FA9CE' }) + '</stop>'
1711
- + open('stop', { offset: '1', 'stop-color': '#CBE2F4', 'stop-opacity': '0.8' }) + '</stop>'
3107
+ + open('linearGradient', { id: gradient, x1: '0', y1: '0', x2: '0', y2: '1' })
3108
+ + open('stop', { offset: '0%', 'stop-color': '#FFEB3B', 'stop-opacity': '0.9' }) + '</stop>'
3109
+ + open('stop', { offset: '60%', 'stop-color': '#FF6F00', 'stop-opacity': '0.8' }) + '</stop>'
3110
+ + open('stop', { offset: '100%', 'stop-color': '#D50000', 'stop-opacity': '0' }) + '</stop>'
1712
3111
  + '</linearGradient>'
1713
3112
  + '</defs>'
1714
- + blade('1', 'M 22 120 C 12 96, 26 74, 18 48 C 15 38, 18 28, 24 20 C 20 34, 24 44, 30 60 C 36 80, 30 100, 32 120 Z', 'dsh-dof-weed-a', '2.2', '0.95')
1715
- + blade('2', 'M 48 120 C 40 98, 54 80, 46 56 C 42 44, 48 34, 56 24 C 50 40, 56 52, 60 68 C 64 88, 56 104, 58 120 Z', 'dsh-dof-weed-b', '2', '0.92')
1716
- + blade('3', 'M 74 120 C 68 102, 78 88, 72 68 C 69 58, 72 48, 78 40 C 74 52, 78 62, 82 76 C 86 94, 80 108, 82 120 Z', 'dsh-dof-weed-c', '1.8', '0.88')
1717
- + blade('4', 'M 96 120 C 92 104, 102 90, 96 72 C 93 62, 96 54, 102 46 C 98 58, 102 68, 106 82 C 110 98, 102 110, 104 120 Z', 'dsh-dof-weed-a', '1.6', '0.85')
1718
- + blade('5', 'M 118 120 C 114 108, 122 96, 117 82 C 115 74, 117 68, 121 62 C 118 72, 121 80, 124 92 C 127 104, 121 112, 123 120 Z', 'dsh-dof-weed-b', '1.4', '0.8')
1719
- + open('ellipse', { cx: '30', cy: '119', rx: '14', ry: '4', fill: '#7FAFC6', opacity: '0.55' }) + '</ellipse>'
1720
- + open('ellipse', { cx: '72', cy: '120', rx: '10', ry: '3.4', fill: '#8FB9CE', opacity: '0.5' }) + '</ellipse>'
1721
- + open('ellipse', { cx: '108', cy: '119.5', rx: '12', ry: '3.6', fill: '#7FAFC6', opacity: '0.45' }) + '</ellipse>'
3113
+ + open('path', { d: 'M 20 4 C 27 22, 27 50, 20 86 C 13 50, 13 22, 20 4 Z', fill: `url(#${gradient})` }) + '</path>'
3114
+ + open('path', { d: 'M 20 8 L 20 82', stroke: '#FFE082', 'stroke-width': '1.2', 'stroke-linecap': 'round', opacity: '0.55', fill: 'none' }) + '</path>'
1722
3115
  + '</svg>'
1723
3116
  }
1724
3117
 
1725
3118
  /**
1726
- * One cartoon fish, swimming across the water.
3119
+ * The 光彩凤晨 scene: sweeping dawn rays, falling phoenix feathers, a
3120
+ * brush-stroke phoenix flying out and back (the flip happens while transparent,
3121
+ * exactly as the source notes), and twinkling dew.
1727
3122
  *
1728
- * Ported from the source system's separate `FishAnimation.vue`. That component drives the
1729
- * fish from JavaScript through entering / bubbling / leaving phases on a 60-second cycle,
1730
- * but this skin is a static bundle with no component runtime to host a script — so the
1731
- * same artwork swims continuously instead, on a CSS `@keyframes` cross, with the tail and
1732
- * the whole body on separate animations so it reads as swimming rather than sliding.
1733
- *
1734
- * The artwork is reproduced shape for shape: the body curve, tail, dorsal fin, pectoral
1735
- * fin, and the three-part eye. The colours are retuned into the theme's blue family —
1736
- * the source values (`#38bdf8` body, `#1d4ed8` fins) sat outside the dream palette and
1737
- * read as a sticker, the same treatment shan's mountains got when their source greens
1738
- * were darkened for this sidebar.
1739
- * @param suffix - unique id suffix, so several fish can coexist.
1740
- * @param size - rendered width in em.
1741
- * @param top - vertical position within the water, as a percentage.
1742
- * @param duration - seconds for one crossing.
1743
- * @param delay - animation delay, so the fish do not move in lockstep.
1744
- * @param flip - whether this fish swims right-to-left instead.
1745
- * @returns the fish markup.
3123
+ * Ported from `GuangCaiFengChenAnimation.vue`. The source's fill path references
3124
+ * a gradient that file never defines, so the rendered phoenix is strokes only —
3125
+ * this port keeps that rendered look with `fill="none"` instead of inventing a
3126
+ * fill the source never showed. Stroke widths are the source's effective values
3127
+ * (its `!important` class overrides baked in).
3128
+ * @param feathers - how many feathers to seed.
3129
+ * @param dew - how many dew drops to seed.
3130
+ * @returns the scene markup.
1746
3131
  */
1747
- function fishMarkup(suffix, size, top, duration, delay, flip) {
1748
- const anim = flip ? 'dsh-amb-swim-back' : 'dsh-amb-swim'
1749
- return open('div', {
1750
- class: `dof-fish${flip ? ' dof-fish-flip' : ''}`,
1751
- style: 'position:absolute;left:0;opacity:.94;'
1752
- + `top:${top}%;width:${size}em;`
1753
- + `animation:${anim} ${duration}s linear infinite;animation-delay:${delay}s`,
3132
+ function fengchenAmbientScene(feathers, dew) {
3133
+ const featherCount = Math.max(0, Math.min(16, feathers ?? 6))
3134
+ let featherNodes = ''
3135
+ for (let n = 1; n <= featherCount; n += 1) {
3136
+ featherNodes += open('div', {
3137
+ class: 'gc-feather',
3138
+ style: `position:absolute;top:-6em;left:${((n * 23) % 80) + 8}%;width:2.6em;opacity:0;`
3139
+ + `animation:dsh-amb-gc-feather ${15 + ((n * 3) % 8)}s ease-in-out infinite;`
3140
+ + `animation-delay:${-(n * 1.5)}s`,
3141
+ })
3142
+ + open('div', {
3143
+ style: `transform:scale(${(0.5 + ((n * 7) % 6) / 10).toFixed(1)})`,
3144
+ })
3145
+ + gcFeatherMarkup(String(n))
3146
+ + '</div></div>'
3147
+ }
3148
+
3149
+ const dewCount = Math.max(0, Math.min(30, dew ?? 10))
3150
+ let dewNodes = ''
3151
+ for (let n = 1; n <= dewCount; n += 1) {
3152
+ dewNodes += open('div', {
3153
+ class: 'gc-dew',
3154
+ style: 'position:absolute;width:3px;height:3px;border-radius:50%;opacity:.5;'
3155
+ + `left:${((n * 37) % 84) + 6}%;top:${((n * 29) % 62) + 6}%;`
3156
+ + 'background:radial-gradient(circle,#FFFFFF 0%,rgba(255,214,0,.8) 100%);'
3157
+ + 'box-shadow:0 0 8px rgba(255,214,0,.8);'
3158
+ + 'animation:dsh-amb-gc-dew 3s ease-in-out infinite alternate;'
3159
+ + `animation-delay:${-(n * 0.2)}s`,
3160
+ }) + '</div>'
3161
+ }
3162
+
3163
+ const ray = (key, rotation, delay) => open('div', {
3164
+ class: `gc-ray gc-ray-${key}`,
3165
+ style: `position:absolute;top:26%;left:-30%;width:160%;height:46%;`
3166
+ + `transform:rotate(${rotation}deg);opacity:.5;pointer-events:none`,
1754
3167
  })
1755
3168
  + open('div', {
1756
- class: 'dof-fish-bob',
1757
- style: `animation-duration:${(duration / 8).toFixed(2)}s`,
3169
+ style: 'position:absolute;inset:0;'
3170
+ + 'background:linear-gradient(45deg,transparent 0%,rgba(255,214,0,.12) 30%,rgba(255,111,0,.06) 60%,transparent 100%);'
3171
+ + `animation:dsh-amb-gc-ray 8s ease-in-out infinite alternate;animation-delay:${delay}`,
3172
+ }) + '</div>'
3173
+ + '</div>'
3174
+
3175
+ return open('div', { class: 'gc', style: 'position:absolute;inset:0;display:block' })
3176
+ + ray('1', '-18', '0s')
3177
+ + ray('2', '-9', '-0.5s')
3178
+ + ray('3', '0', '-1s')
3179
+ + ray('4', '9', '-1.5s')
3180
+ + ray('5', '18', '-2s')
3181
+ + dewNodes
3182
+ + featherNodes
3183
+ + open('div', {
3184
+ class: 'gc-phoenix',
3185
+ style: 'position:absolute;top:14%;left:0;width:9.5em;z-index:8;will-change:transform;'
3186
+ + 'animation:dsh-amb-gc-fly 28s linear infinite',
3187
+ })
3188
+ + open('div', {
3189
+ class: 'gc-bob',
3190
+ style: 'animation:dsh-amb-gc-bob 3.4s ease-in-out infinite',
1758
3191
  })
1759
3192
  + open('svg', {
1760
- viewBox: '0 0 50 18',
1761
- preserveAspectRatio: 'xMidYMid meet',
1762
- style: 'display:block;width:100%;height:auto;overflow:visible',
3193
+ viewBox: '0 0 800 600',
3194
+ style: 'display:block;width:100%;height:auto;overflow:visible;'
3195
+ + 'filter:drop-shadow(0 0 18px rgba(255,214,0,.55))',
1763
3196
  })
1764
- + open('g', { class: 'dof-fish-body' })
1765
- // Body — recoloured into the theme's own blue family. The source values
1766
- // (`#38bdf8` body, `#1d4ed8` fins) sat outside the dream palette and read
1767
- // as a sticker pasted on the water; the same treatment shan's mountains
1768
- // got when their source greens were darkened for this sidebar.
3197
+ + open('defs', {})
3198
+ + open('linearGradient', { id: 'dsh-gc-phoenix-stroke', x1: '0', y1: '0', x2: '1', y2: '0' })
3199
+ + open('stop', { offset: '0%', 'stop-color': '#FFEB3B' }) + '</stop>'
3200
+ + open('stop', { offset: '40%', 'stop-color': '#FF9800' }) + '</stop>'
3201
+ + open('stop', { offset: '100%', 'stop-color': '#D50000' }) + '</stop>'
3202
+ + '</linearGradient>'
3203
+ + open('filter', { id: 'dsh-gc-glow' })
3204
+ + open('feGaussianBlur', { stdDeviation: '2', result: 'coloredBlur' }) + '</feGaussianBlur>'
3205
+ + open('feMerge', {})
3206
+ + open('feMergeNode', { in: 'coloredBlur' }) + '</feMergeNode>'
3207
+ + open('feMergeNode', { in: 'SourceGraphic' }) + '</feMergeNode>'
3208
+ + '</feMerge>'
3209
+ + '</filter>'
3210
+ + '</defs>'
3211
+ + open('g', { filter: 'url(#dsh-gc-glow)', transform: 'translate(100, 100) scale(1.2)' })
1769
3212
  + open('path', {
1770
- d: 'M10 10 C20 5 35 5 45 10 C40 15 25 15 10 10 Z',
1771
- fill: '#5FA5D6', stroke: '#2B6E9E', 'stroke-width': '1',
3213
+ d: 'M 380 50 C 360 40 350 60 360 90 C 350 120 380 140 320 180 C 280 220 350 280 200 350 C 150 380 100 360 50 400 C 140 300 220 220 320 180 C 360 140 380 90 380 50 Z',
3214
+ fill: 'none',
1772
3215
  }) + '</path>'
1773
- // Tail
1774
3216
  + open('path', {
1775
- d: 'M10 10 L5 7 L5 13 Z',
1776
- fill: '#2B6E9E', stroke: '#2B6E9E', 'stroke-width': '1', class: 'dof-fish-tail',
3217
+ class: 'gc-head',
3218
+ d: 'M 380 50 Q 360 40 346 58 T 358 90 L 374 86',
3219
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '3.5', fill: 'none', 'stroke-linecap': 'round',
1777
3220
  }) + '</path>'
1778
- // Dorsal fin
1779
3221
  + open('path', {
1780
- d: 'M20 7 L25 3 L30 7',
1781
- fill: '#2B6E9E', stroke: '#2B6E9E', 'stroke-width': '1',
3222
+ class: 'gc-crest',
3223
+ d: 'M 360 50 Q 340 20 310 30',
3224
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '2.5', fill: 'none', 'stroke-linecap': 'round',
1782
3225
  }) + '</path>'
1783
- // Pectoral fin
1784
3226
  + open('path', {
1785
- d: 'M35 9 L40 12 L45 9',
1786
- fill: '#5FA5D6', stroke: '#2B6E9E', 'stroke-width': '1',
3227
+ class: 'gc-crest',
3228
+ d: 'M 365 55 Q 355 35 330 40',
3229
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '2.5', fill: 'none', 'stroke-linecap': 'round',
1787
3230
  }) + '</path>'
1788
- // Eye: white, pupil, highlight
1789
- + open('circle', { cx: '40', cy: '8', r: '2', fill: '#FFFFFF' }) + '</circle>'
1790
- + open('circle', { cx: '41', cy: '8', r: '1', fill: '#16384F' }) + '</circle>'
1791
- + open('circle', { cx: '40.5', cy: '7.5', r: '0.5', fill: '#FFFFFF' }) + '</circle>'
3231
+ + open('path', {
3232
+ class: 'gc-neck',
3233
+ d: 'M 360 90 C 350 120, 380 140, 320 180',
3234
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '7', fill: 'none', 'stroke-linecap': 'round',
3235
+ }) + '</path>'
3236
+ + open('path', {
3237
+ class: 'gc-wing gc-wing-main',
3238
+ d: 'M 340 110 C 260 70, 200 40, 140 90',
3239
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '6', fill: 'none', 'stroke-linecap': 'round',
3240
+ style: 'transform-box:fill-box;transform-origin:30% 40%;animation:dsh-amb-gc-wing 3.4s ease-in-out infinite',
3241
+ }) + '</path>'
3242
+ + open('path', {
3243
+ d: 'M 300 100 C 250 80, 180 80, 140 120',
3244
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '3', fill: 'none', 'stroke-linecap': 'round',
3245
+ }) + '</path>'
3246
+ + open('path', {
3247
+ d: 'M 280 110 C 240 100, 180 110, 150 140',
3248
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '2', fill: 'none', 'stroke-linecap': 'round',
3249
+ }) + '</path>'
3250
+ + open('path', {
3251
+ class: 'gc-wing gc-wing-back',
3252
+ d: 'M 360 100 C 400 78, 452 66, 490 92',
3253
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '4', fill: 'none', 'stroke-linecap': 'round',
3254
+ style: 'transform-box:fill-box;transform-origin:30% 40%;animation:dsh-amb-gc-wing 3.4s ease-in-out infinite',
3255
+ }) + '</path>'
3256
+ + open('path', {
3257
+ d: 'M 370 110 C 400 100, 430 95, 460 110',
3258
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '2', fill: 'none', 'stroke-linecap': 'round',
3259
+ }) + '</path>'
3260
+ + open('path', {
3261
+ class: 'gc-tail-1',
3262
+ d: 'M 320 180 C 280 220, 350 280, 200 350 C 150 380, 100 360, 50 400',
3263
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '5', fill: 'none', 'stroke-linecap': 'round',
3264
+ }) + '</path>'
3265
+ + open('path', {
3266
+ class: 'gc-tail-2',
3267
+ d: 'M 310 185 C 250 250, 200 280, 100 300',
3268
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '4', fill: 'none', 'stroke-linecap': 'round',
3269
+ }) + '</path>'
3270
+ + open('path', {
3271
+ class: 'gc-tail-3',
3272
+ d: 'M 330 175 C 300 220, 250 350, 150 420',
3273
+ stroke: 'url(#dsh-gc-phoenix-stroke)', 'stroke-width': '3', fill: 'none', 'stroke-linecap': 'round',
3274
+ }) + '</path>'
3275
+ + open('circle', { cx: '365', cy: '70', r: '2.5', fill: '#FFE082' }) + '</circle>'
1792
3276
  + '</g>'
1793
3277
  + '</svg>'
1794
3278
  + '</div></div>'
3279
+ + '</div>'
1795
3280
  }
1796
3281
 
1797
3282
  /**
@@ -1874,7 +3359,17 @@ window.__ModuleLoader__.load({
1874
3359
  ? shanAmbientScene(options?.petals)
1875
3360
  : kind === 'dream'
1876
3361
  ? dreamAmbientScene(options?.bubbles, options?.motes, options?.fish)
1877
- : ''
3362
+ : kind === 'caiyun'
3363
+ ? caiyunAmbientScene(options?.stars)
3364
+ : kind === 'dongyun'
3365
+ ? dongyunAmbientScene(options?.snow)
3366
+ : kind === 'junyue'
3367
+ ? junyueAmbientScene(options?.stars)
3368
+ : kind === 'jiexin'
3369
+ ? jiexinAmbientScene(options?.dust)
3370
+ : kind === 'fengchen'
3371
+ ? fengchenAmbientScene(options?.feathers, options?.dew)
3372
+ : ''
1878
3373
  // The builder is fed only attributes and hex values, but this is the one place
1879
3374
  // markup from outside this file could ever arrive, so it refuses anyway.
1880
3375
  if (/<script|\son[a-z]+\s*=/i.test(markup)) {
@@ -2633,7 +4128,9 @@ window.__ModuleLoader__.load({
2633
4128
  // the two need different fixes.
2634
4129
  const scene = seat.firstElementChild
2635
4130
  const sceneRect = scene === null ? null : scene.getBoundingClientRect()
2636
- const art = seat.querySelector('.sta-mountains, .dof-seaweed')
4131
+ const art = seat.querySelector(
4132
+ '.sta-mountains, .dof-seaweed, .ym-sea-front, .jp-river, .xs-ridges, .pj-zenscene, .gc-phoenix',
4133
+ )
2637
4134
  const artRect = art === null ? null : art.getBoundingClientRect()
2638
4135
  return {
2639
4136
  found: true,
@@ -2767,6 +4264,28 @@ window.__ModuleLoader__.load({
2767
4264
  return BUNDLED_THEMES.find((theme) => theme.id === id)
2768
4265
  }
2769
4266
 
4267
+ /**
4268
+ * Read one token out of a bundled skin, resolving the pair form to the skin's own scheme.
4269
+ *
4270
+ * The bundled skins store most tokens as `{ light, dark }` pairs even though a skin itself is
4271
+ * single-scheme. `flatten` collapses them for `register`; this is the same read, exposed
4272
+ * separately because the accent diagnostic needs ONE token's declared value without building
4273
+ * the whole registrable definition — and because that diagnostic lives at factory scope, where
4274
+ * `flatten` (declared inside `mountGallery`) is not reachable. Reaching for it from here is the
4275
+ * exact scope error `tests/check-scope-reach.mjs` exists to catch.
4276
+ * @param definition - a bundled skin.
4277
+ * @param name - the token name.
4278
+ * @returns the declared value, or undefined when the skin has no such token.
4279
+ */
4280
+ function declaredToken(definition, name) {
4281
+ const raw = definition?.tokens?.[name]
4282
+ if (raw === undefined) return undefined
4283
+ // Deliberately not `raw?.[…]`: a skin whose token is neither a string nor a pair is a data
4284
+ // error, and `flatten` (the other caller) must keep failing loudly on it rather than
4285
+ // registering an unset variable that paints nothing.
4286
+ return typeof raw === 'string' ? raw : raw[definition.colorScheme]
4287
+ }
4288
+
2770
4289
  /**
2771
4290
  * Disposer of the active accent layer, when one is stacked.
2772
4291
  *
@@ -2779,6 +4298,20 @@ window.__ModuleLoader__.load({
2779
4298
  */
2780
4299
  let accentLayerDispose
2781
4300
 
4301
+ /**
4302
+ * The accent colour {@link accentLayerDispose} was built from.
4303
+ *
4304
+ * Declared here for the same reason as the disposer above: `syncAccent` both reads and writes
4305
+ * it, and it is called during mount, so a `let` initialised later in the body would be in its
4306
+ * temporal dead zone at the first call.
4307
+ *
4308
+ * It exists so that re-stacking can be skipped when the colour has not moved. Without it every
4309
+ * `theme/change` — including ones this plugin caused and ones caused by something else —
4310
+ * disposed and re-created the layer, which emits again.
4311
+ * @type {string|undefined}
4312
+ */
4313
+ let stackedAccent
4314
+
2782
4315
  /**
2783
4316
  * The plugin context, published for the factory-level helpers.
2784
4317
  *
@@ -2827,15 +4360,46 @@ window.__ModuleLoader__.load({
2827
4360
  // `try` swallowed it, and the accent marker silently never appeared. The module-level `ctx`
2828
4361
  // below is assigned at mount and read here.
2829
4362
  if (ctx === undefined) return
2830
- if (accentLayerDispose !== undefined) {
2831
- accentLayerDispose()
2832
- accentLayerDispose = undefined
2833
- }
2834
- if (typeof accent !== 'string' || accent === '') return
2835
- accentLayerDispose = ctx.theme.overrideTokens('theme-gallery: accent', {
2836
- '--dsw-alias-button-ghost-active-fill': { light: `${accent}29`, dark: `${accent}29` },
2837
- '--dsw-alias-button-ghost-active-border': { light: accent, dark: accent },
2838
- '--dsw-alias-button-ghost-active-hover': { light: `${accent}47`, dark: `${accent}47` },
4363
+ const wanted = typeof accent === 'string' && accent !== '' ? accent : undefined
4364
+ // ── WHY BOTH A SKIP AND THE SELF-EMIT GUARD ARE NEEDED ───────────────────────────────────
4365
+ //
4366
+ // `overrideTokens` and the disposer it returns BOTH emit `theme/change`, and this function
4367
+ // is reached from `publish()`, which is itself driven by that event:
4368
+ //
4369
+ // publish → syncSkin → syncAccent → overrideTokens → theme/change → publish → …
4370
+ //
4371
+ // Every level nests inside the previous CALL, so nothing ever yields to the event loop. The
4372
+ // stack therefore grows until the engine refuses to grow it further and throws
4373
+ // `RangeError: Maximum call stack size exceeded` — and because every level has its own
4374
+ // `try/catch` (see `syncSkin`), each one logs the SAME message on the way out. That is why
4375
+ // the console showed a flood of "could not stack the accent layer" lines rather than one
4376
+ // error: one flood, hundreds of identical lines, each from a different depth.
4377
+ //
4378
+ // Fixing it takes both halves, and they are not interchangeable:
4379
+ //
4380
+ // • the SKIP stops the churn — an accent that has not moved does not need a new layer, so
4381
+ // an unrelated `theme/change` (the shell's `adopt()`, a streamed render, another plugin)
4382
+ // no longer replaces the layer and causes a repaint;
4383
+ // • the GUARD stops the RE-ENTRY — it is what makes the `theme/change` emitted by our own
4384
+ // write invisible to the subscription, so the cycle cannot close even when the colour
4385
+ // genuinely did change.
4386
+ //
4387
+ // Only the guard is a correctness requirement; the skip is what keeps the layer stable. The
4388
+ // same shape is used by `stackSkinTokens` for the palette and by `syncReadingLayer`.
4389
+ if (wanted === stackedAccent) return
4390
+ emitting(() => {
4391
+ if (accentLayerDispose !== undefined) {
4392
+ accentLayerDispose()
4393
+ accentLayerDispose = undefined
4394
+ }
4395
+ stackedAccent = undefined
4396
+ if (wanted === undefined) return
4397
+ accentLayerDispose = ctx.theme.overrideTokens('theme-gallery: accent', {
4398
+ '--dsw-alias-button-ghost-active-fill': { light: `${wanted}29`, dark: `${wanted}29` },
4399
+ '--dsw-alias-button-ghost-active-border': { light: wanted, dark: wanted },
4400
+ '--dsw-alias-button-ghost-active-hover': { light: `${wanted}47`, dark: `${wanted}47` },
4401
+ })
4402
+ stackedAccent = wanted
2839
4403
  })
2840
4404
  }
2841
4405
 
@@ -2852,8 +4416,27 @@ window.__ModuleLoader__.load({
2852
4416
  *
2853
4417
  * The service makes this checkable without any guesswork: `composeActive` folds every
2854
4418
  * override layer into `snapshot.active.tokens` before publishing, so the composed value is
2855
- * readable straight off `getTheme()`. If the layer is registered, the token IS there; if it
2856
- * is not, the token is absent or still the built-in value.
4419
+ * readable straight off `getTheme()`.
4420
+ *
4421
+ * ── WHAT CAN AND CANNOT PROVE IT ─────────────────────────────────────────
4422
+ *
4423
+ * The composed VALUE is the only channel that carries information, and only when it differs
4424
+ * from what the skin declares for that same token:
4425
+ *
4426
+ * • both bundled skins already ship `button-ghost-active-*` in their own palettes, AND the
4427
+ * palette layer supplies them too — so the token COUNT is 67 either way. An earlier
4428
+ * comment here claimed a count that "does not move" disproves the layer; for these skins
4429
+ * it cannot move, so the claim was wrong and the count is reported for context only;
4430
+ * • 梦海游鱼 declares `#2B74B5` for that token but its accent is `#FFD166`, so reading back
4431
+ * `#FFD166` proves the accent layer composed OVER the palette — the palette alone cannot
4432
+ * produce it;
4433
+ * • 山青婷彩's accent is `#E88BB0` and it declares `#E88BB0` for the same token, so its
4434
+ * reading is identical whether or not the layer is there. That is a property of the SKIN,
4435
+ * not of the layer, and the verdict below says so instead of implying the reading proved
4436
+ * something.
4437
+ *
4438
+ * The verdict is therefore computed, never asserted: it compares the composed value with the
4439
+ * skin's declared value and with the accent the layer was built from.
2857
4440
  * @returns the reading, as one segment of the panel line.
2858
4441
  */
2859
4442
  function describeAccentLayer() {
@@ -2864,16 +4447,31 @@ window.__ModuleLoader__.load({
2864
4447
  const active = snapshot?.active
2865
4448
  const tokens = active?.tokens ?? {}
2866
4449
  const TOKEN = '--dsw-alias-button-ghost-active-border'
2867
- const value = tokens[TOKEN]
4450
+ const composed = tokens[TOKEN]
4451
+ const definition = typeof active?.id === 'string' ? bundledTheme(active.id) : undefined
4452
+ const declared = declaredToken(definition, TOKEN)
4453
+ const accent = definition?.accent
2868
4454
  const layers = snapshot?.overrides === undefined
2869
4455
  ? '(服务未暴露)'
2870
4456
  : String(snapshot.overrides.size ?? snapshot.overrides.length ?? '?')
2871
- // Its OWN key count too: layers folding into `active.tokens` is the mechanism, so a
2872
- // token count that does not move when the layer is stacked also disproves it.
4457
+ // Computed, not assumed — see the note above. `不可分辨` is a real answer: it says this
4458
+ // skin's own value coincides with its accent, so this reading proves nothing either way.
4459
+ const verdict = accent === undefined
4460
+ ? '非本包皮肤(无法判断)'
4461
+ : accent === declared
4462
+ ? '不可分辨(强调色=皮肤自备值)'
4463
+ : composed === accent
4464
+ ? '生效(强调色压过自备值)'
4465
+ : composed === declared
4466
+ ? '未生效(仍是自备值)'
4467
+ : `未知(既非强调色也非自备值)`
2873
4468
  return `激活层[id=${active?.id ?? '?'}`
2874
4469
  + ` 层数=${layers}`
2875
- + ` token数=${Object.keys(tokens).length}`
2876
- + ` ${TOKEN.replace('--dsw-alias-', '')}=${value === undefined ? '(缺失)' : String(value)}]`
4470
+ + ` token数=${Object.keys(tokens).length}(自备同名令牌,不构成判据)`
4471
+ + ` ${TOKEN.replace('--dsw-alias-', '')}=${composed === undefined ? '(缺失)' : String(composed)}`
4472
+ + ` 自备=${declared === undefined ? '(无)' : String(declared)}`
4473
+ + ` 强调色=${accent ?? '(无)'}`
4474
+ + ` 判据=${verdict}]`
2877
4475
  } catch (error) {
2878
4476
  return `激活层[读取失败: ${String(error && error.message ? error.message : error)}]`
2879
4477
  }
@@ -2922,18 +4520,11 @@ window.__ModuleLoader__.load({
2922
4520
  jsx('span', { className: 'tg-title', children: t('title') }),
2923
4521
  jsx('span', { className: 'tg-hint', children: t('hint') }),
2924
4522
  jsx('span', { className: 'tg-hint', children: t('count', { count: ids.length }) }),
4523
+ // What this copy is, so a reader can compare it with the version npm
4524
+ // publishes without digging through the profile.
4525
+ jsx('span', { className: 'tg-hint', children: `v${BUNDLED_VERSION}` }),
2925
4526
  ],
2926
4527
  }),
2927
- debugEnabled() ? jsx('div', { className: 'tg-debug', children: themeDiagnostics(selected) }) : null,
2928
- // Routine reports wait for the debug switch; warnings and errors print
2929
- // regardless — when the active skin's decorations are missing, the reason
2930
- // has to reach the person looking at the panel.
2931
- showScenery
2932
- ? jsx('div', {
2933
- className: `tg-debug${ambientWarning(selected) === null ? '' : ' tg-warn'}`,
2934
- children: scenery,
2935
- })
2936
- : null,
2937
4528
  jsx('div', {
2938
4529
  className: 'tg-grid',
2939
4530
  children: ids.map((id) => jsx(ThemeCard, {
@@ -2947,6 +4538,31 @@ window.__ModuleLoader__.load({
2947
4538
  t,
2948
4539
  }, id)),
2949
4540
  }),
4541
+ // ── the diagnostics go BELOW the cards, set apart, behind a caption ──────
4542
+ //
4543
+ // They used to sit between the header and the picker, where a wall of monospace
4544
+ // text buried the thing the panel exists for. Below the cards they read as a
4545
+ // reference block — and the caption is what keeps them from being mistaken for a
4546
+ // malfunction, which is exactly how a user read them. They stay CONDITIONAL:
4547
+ // detail waits for the debug switch, while a warning or a failure always prints.
4548
+ (debugEnabled() || showScenery)
4549
+ ? jsxs('div', {
4550
+ className: 'tg-diag',
4551
+ children: [
4552
+ jsx('div', { className: 'tg-diag-note', children: t('diagNote') }),
4553
+ debugEnabled() ? jsx('div', { className: 'tg-debug', children: themeDiagnostics(selected) }) : null,
4554
+ // Routine reports wait for the debug switch; warnings and errors print
4555
+ // regardless — when the active skin's decorations are missing, the reason
4556
+ // has to reach the person looking at the panel.
4557
+ showScenery
4558
+ ? jsx('div', {
4559
+ className: `tg-debug${ambientWarning(selected) === null ? '' : ' tg-warn'}`,
4560
+ children: scenery,
4561
+ })
4562
+ : null,
4563
+ ],
4564
+ })
4565
+ : null,
2950
4566
  ],
2951
4567
  })
2952
4568
  }
@@ -3725,8 +5341,10 @@ window.__ModuleLoader__.load({
3725
5341
  */
3726
5342
  function flatten(definition) {
3727
5343
  const tokens = {}
3728
- for (const [name, value] of Object.entries(definition.tokens ?? {})) {
3729
- tokens[name] = typeof value === 'string' ? value : value[definition.colorScheme]
5344
+ for (const name of Object.keys(definition.tokens ?? {})) {
5345
+ // Delegated so the pair rule has exactly ONE implementation — the accent diagnostic reads
5346
+ // a single token through the same helper, and two copies of this rule would drift.
5347
+ tokens[name] = declaredToken(definition, name)
3730
5348
  }
3731
5349
  return { ...definition, tokens }
3732
5350
  }
@@ -4101,6 +5719,17 @@ window.__ModuleLoader__.load({
4101
5719
  let readingTag
4102
5720
  /** Disposer of the active reading token layer, when one is stacked. */
4103
5721
  let readingLayerDispose
5722
+ /**
5723
+ * The lightened ground {@link readingLayerDispose} was built from.
5724
+ *
5725
+ * Same role as `stackedAccent`: `syncReadingLayer` writes to the theme service, and it is
5726
+ * scheduled from a `MutationObserver` on `document.body` — so without this, every batch of
5727
+ * DOM mutations would replace the layer, which emits `theme/change`, which repaints the
5728
+ * shell, which mutates the DOM again. That is a loop through the microtask queue rather than
5729
+ * through the stack, so it would burn CPU steadily instead of throwing.
5730
+ * @type {string|undefined}
5731
+ */
5732
+ let stackedReading
4104
5733
 
4105
5734
  /**
4106
5735
  * Install the reading rule, once per plugin lifetime.
@@ -4199,25 +5828,41 @@ window.__ModuleLoader__.load({
4199
5828
  * `overrideTokens(source, tokens)` folds a `{ light, dark }`-shaped layer
4200
5829
  * over the ACTIVE theme, so the lightening follows whichever skin and
4201
5830
  * palette is in play and disappears cleanly when the transcript empties.
4202
- * The source string is the layer's identity: re-calling it with the same
4203
- * source replaces the layer, which is what makes this idempotent.
5831
+ *
5832
+ * The source string is the layer's identity, so re-stacking the same source
5833
+ * REPLACES the layer instead of adding a second one. Replacement alone is not
5834
+ * idempotence: the call still emits `theme/change`, so it is the skip below
5835
+ * (`wanted === stackedReading`) that keeps an unchanged document from churning
5836
+ * the layer on every mutation batch.
4204
5837
  */
4205
5838
  function syncReadingLayer() {
4206
5839
  if (typeof document === 'undefined') return
4207
5840
  const reading = document.body !== null && document.body.hasAttribute(READING_ATTRIBUTE)
4208
- if (!reading) {
5841
+ // Computed before the skip test, because the skip compares VALUES. `lighten` is pure, so
5842
+ // the same document state always yields the same pair and the comparison is exact.
5843
+ const wanted = reading ? lighten(READING.bg, READING.alpha) : undefined
5844
+ const wantedDark = reading
5845
+ // Lightened ground for both palettes: a light theme wants a softer wash, a dark one a
5846
+ // slightly raised surface. Both keep the theme's hue.
5847
+ ? lighten(READING.bg, Math.max(0, READING.alpha - 0.2))
5848
+ : undefined
5849
+
5850
+ // Both writes below emit `theme/change`, and this function is reached from a
5851
+ // `MutationObserver` on `document.body` — the same tree the shell repaints when the theme
5852
+ // changes. See the comment in `syncAccent` for the full shape; the short version is that
5853
+ // an unguarded write here lets the layer drive its own trigger.
5854
+ if (wanted === stackedReading) return
5855
+ emitting(() => {
4209
5856
  if (readingLayerDispose !== undefined) {
4210
5857
  readingLayerDispose()
4211
5858
  readingLayerDispose = undefined
4212
5859
  }
4213
- return
4214
- }
4215
- // Lightened ground for both palettes: a light theme wants a softer wash,
4216
- // a dark one a slightly raised surface. Both keep the theme's hue.
4217
- const lightened = lighten(READING.bg, READING.alpha)
4218
- const darkLightened = lighten(READING.bg, Math.max(0, READING.alpha - 0.2))
4219
- readingLayerDispose = ctx.theme.overrideTokens('theme-gallery: reading', {
4220
- '--dsw-alias-bg-base': { light: lightened, dark: darkLightened },
5860
+ stackedReading = undefined
5861
+ if (wanted === undefined) return
5862
+ readingLayerDispose = ctx.theme.overrideTokens('theme-gallery: reading', {
5863
+ '--dsw-alias-bg-base': { light: wanted, dark: wantedDark },
5864
+ })
5865
+ stackedReading = wanted
4221
5866
  })
4222
5867
  }
4223
5868
 
@@ -4246,10 +5891,17 @@ window.__ModuleLoader__.load({
4246
5891
  observer.disconnect()
4247
5892
  if (readingTag !== undefined) readingTag.remove()
4248
5893
  readingTag = undefined
4249
- if (readingLayerDispose !== undefined) {
4250
- readingLayerDispose()
4251
- readingLayerDispose = undefined
4252
- }
5894
+ // Guarded for the same reason as the writes in `syncReadingLayer`, and the remembered
5895
+ // value is cleared with the layer: leaving it set would make that function's skip test
5896
+ // believe a layer is still stacked after this one removed it, and it would then refuse
5897
+ // to re-stack.
5898
+ emitting(() => {
5899
+ if (readingLayerDispose !== undefined) {
5900
+ readingLayerDispose()
5901
+ readingLayerDispose = undefined
5902
+ }
5903
+ stackedReading = undefined
5904
+ })
4253
5905
  document.body.removeAttribute(READING_ATTRIBUTE)
4254
5906
  const centre = centreColumn()
4255
5907
  if (centre !== null) centre.style.removeProperty('--dsh-reading-width')