@hanology/cham-browser 0.4.64 → 0.4.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanology/cham-browser",
3
- "version": "0.4.64",
3
+ "version": "0.4.66",
4
4
  "description": "CHAM — browser-compatible parser, serializer, and site generator for Classical Han Annotated Markdown",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -2,7 +2,7 @@
2
2
  <html lang="zh-Hant">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>CHAM</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -364,7 +364,7 @@ onBeforeUnmount(() => {
364
364
  }
365
365
 
366
366
  .ann-sheet-scroll {
367
- padding: 4px 16px 24px;
367
+ padding: 4px 16px max(24px, env(safe-area-inset-bottom, 0px));
368
368
  overflow-y: auto;
369
369
  overscroll-behavior: contain;
370
370
  flex: 1;
@@ -384,45 +384,45 @@ onBeforeUnmount(() => {
384
384
 
385
385
  /* ─── Active annotation on page ─── */
386
386
  :global(.ann-target.ann-active) {
387
- background: rgba(194, 58, 43, 0.12) !important;
388
- box-shadow: 0 0 0 2px rgba(194, 58, 43, 0.15);
387
+ background: color-mix(in srgb, var(--vermillion) 12%, transparent) !important;
388
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--vermillion) 15%, transparent);
389
389
  border-radius: 2px;
390
390
  }
391
391
  :global(.ann-target.ann-active.pronunciation) {
392
- background: rgba(58, 107, 94, 0.12) !important;
393
- box-shadow: 0 0 0 2px rgba(58, 107, 94, 0.15);
392
+ background: color-mix(in srgb, var(--jade) 12%, transparent) !important;
393
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--jade) 15%, transparent);
394
394
  }
395
395
  :global(.ann-target.ann-active.person) {
396
- background: rgba(58, 90, 140, 0.12) !important;
397
- box-shadow: 0 0 0 2px rgba(58, 90, 140, 0.15);
396
+ background: color-mix(in srgb, var(--ann-person) 12%, transparent) !important;
397
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-person) 15%, transparent);
398
398
  }
399
399
  :global(.ann-target.ann-active.place) {
400
- background: rgba(139, 105, 20, 0.12) !important;
401
- box-shadow: 0 0 0 2px rgba(139, 105, 20, 0.15);
400
+ background: color-mix(in srgb, var(--ann-place) 12%, transparent) !important;
401
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-place) 15%, transparent);
402
402
  }
403
403
  :global(.ann-target.ann-active.event) {
404
- background: rgba(107, 76, 138, 0.12) !important;
405
- box-shadow: 0 0 0 2px rgba(107, 76, 138, 0.15);
404
+ background: color-mix(in srgb, var(--ann-event) 12%, transparent) !important;
405
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-event) 15%, transparent);
406
406
  }
407
407
  :global(.ann-target.ann-active.date) {
408
- background: rgba(42, 122, 122, 0.12) !important;
409
- box-shadow: 0 0 0 2px rgba(42, 122, 122, 0.15);
408
+ background: color-mix(in srgb, var(--ann-date) 12%, transparent) !important;
409
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-date) 15%, transparent);
410
410
  }
411
411
  :global(.ann-target.ann-active.allusion) {
412
- background: rgba(181, 101, 29, 0.12) !important;
413
- box-shadow: 0 0 0 2px rgba(181, 101, 29, 0.15);
412
+ background: color-mix(in srgb, var(--ann-allusion) 12%, transparent) !important;
413
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-allusion) 15%, transparent);
414
414
  }
415
415
  :global(.ann-target.ann-active.etymology) {
416
- background: rgba(107, 91, 149, 0.12) !important;
417
- box-shadow: 0 0 0 2px rgba(107, 91, 149, 0.15);
416
+ background: color-mix(in srgb, var(--ann-etymology) 12%, transparent) !important;
417
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-etymology) 15%, transparent);
418
418
  }
419
419
  :global(.ann-target.ann-active.commentary) {
420
- background: rgba(192, 57, 43, 0.12) !important;
421
- box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.15);
420
+ background: color-mix(in srgb, var(--ann-commentary) 12%, transparent) !important;
421
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-commentary) 15%, transparent);
422
422
  }
423
423
  :global(.ann-target.ann-active.translation) {
424
- background: rgba(44, 110, 73, 0.12) !important;
425
- box-shadow: 0 0 0 2px rgba(44, 110, 73, 0.15);
424
+ background: color-mix(in srgb, var(--ann-translation) 12%, transparent) !important;
425
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-translation) 15%, transparent);
426
426
  }
427
427
 
428
428
  @media (min-width: 768px) {
@@ -72,7 +72,7 @@ onUnmounted(detach)
72
72
  <style scoped>
73
73
  .btt {
74
74
  position: fixed;
75
- bottom: 80px;
75
+ bottom: max(80px, calc(72px + env(safe-area-inset-bottom, 0px)));
76
76
  right: 24px;
77
77
  width: 40px;
78
78
  height: 40px;
@@ -98,7 +98,7 @@ onUnmounted(detach)
98
98
  }
99
99
 
100
100
  @media (max-width: 768px) {
101
- .btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
101
+ .btt { bottom: max(88px, calc(80px + env(safe-area-inset-bottom, 0px))); right: 16px; width: 36px; height: 36px; }
102
102
  }
103
103
  .btt:hover {
104
104
  background: var(--vermillion);
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  import type { Annotation, VerseLine } from '../types'
3
- import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations, countVerseSpans } from '../composables/useAnnotationRenderer'
4
+ import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations } from '../composables/useAnnotationRenderer'
4
5
 
5
6
  const props = defineProps<{
6
7
  title: string
@@ -15,11 +16,23 @@ const emit = defineEmits<{
15
16
  annotationTap: [event: MouseEvent, annotations: Annotation[]]
16
17
  }>()
17
18
 
19
+ const allVerseSpans = computed(() =>
20
+ props.verses.map((_, i) => buildVerseAnnotations(props.annotations, i))
21
+ )
22
+
23
+ const verseOffsets = computed(() => {
24
+ const offsets: number[] = []
25
+ let acc = 0
26
+ for (const spans of allVerseSpans.value) {
27
+ offsets.push(acc)
28
+ acc += spans.length
29
+ }
30
+ return offsets
31
+ })
32
+
18
33
  function verseHtml(index: number): string {
19
- let offset = 0
20
- for (let i = 0; i < index; i++) offset += countVerseSpans(props.annotations, i)
21
- const spans = buildVerseAnnotations(props.annotations, index)
22
- return renderAnnotatedText(props.verses[index].text, spans, false, offset)
34
+ const spans = allVerseSpans.value[index]
35
+ return renderAnnotatedText(props.verses[index].text, spans, false, verseOffsets.value[index])
23
36
  }
24
37
 
25
38
  function onHover(event: MouseEvent) {
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  import type { Annotation, VerseLine, PieceSource } from '../types'
3
- import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations, countVerseSpans } from '../composables/useAnnotationRenderer'
4
+ import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations } from '../composables/useAnnotationRenderer'
4
5
 
5
6
  const props = defineProps<{
6
7
  num: number
@@ -17,12 +18,24 @@ const emit = defineEmits<{
17
18
  annotationTap: [event: MouseEvent, annotations: Annotation[]]
18
19
  }>()
19
20
 
21
+ const allVerseSpans = computed(() =>
22
+ props.verses.map((_, i) => buildVerseAnnotations(props.annotations, i))
23
+ )
24
+
25
+ const verseOffsets = computed(() => {
26
+ const offsets: number[] = []
27
+ let acc = 0
28
+ for (const spans of allVerseSpans.value) {
29
+ offsets.push(acc)
30
+ acc += spans.length
31
+ }
32
+ return offsets
33
+ })
34
+
20
35
  function verseHtml(index: number): string {
21
36
  const useRuby = props.vertical
22
- let offset = 0
23
- for (let i = 0; i < index; i++) offset += countVerseSpans(props.annotations, i)
24
- const spans = buildVerseAnnotations(props.annotations, index)
25
- return renderAnnotatedText(props.verses[index].text, spans, useRuby, offset)
37
+ const spans = allVerseSpans.value[index]
38
+ return renderAnnotatedText(props.verses[index].text, spans, useRuby, verseOffsets.value[index])
26
39
  }
27
40
 
28
41
  function onHover(event: MouseEvent) {
@@ -97,8 +97,8 @@ const preview = computed(() => {
97
97
  box-sizing: border-box;
98
98
  overflow: hidden;
99
99
  height: auto;
100
- -webkit-mask-image: linear-gradient(to left, black 60%, transparent);
101
- mask-image: linear-gradient(to left, black 60%, transparent);
100
+ -webkit-mask-image: linear-gradient(to left, black 80%, transparent);
101
+ mask-image: linear-gradient(to left, black 80%, transparent);
102
102
  }
103
103
  .pc-vertical .pc-num {
104
104
  font-size: 11px;
@@ -1,15 +1,25 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  import type { PronSegment } from '../types'
4
+ import { useI18n } from '../composables/useI18n'
3
5
 
4
- defineProps<{
6
+ const { t } = useI18n()
7
+
8
+ const props = defineProps<{
5
9
  segment: PronSegment
6
10
  }>()
11
+
12
+ const label = computed(() => {
13
+ if (props.segment.lang === 'yue') return t('pron.yue')
14
+ if (props.segment.lang === 'cmn') return t('pron.cmn')
15
+ return props.segment.label
16
+ })
7
17
  </script>
8
18
 
9
19
  <template>
10
20
  <span class="pron-group">
11
21
  <span class="pron-badge" :class="segment.lang === 'yue' ? 'pron-yue' : 'pron-cmn'">
12
- {{ segment.label }}
22
+ {{ label }}
13
23
  </span>
14
24
  <span class="pron-text">{{ segment.parts.join(' ') }}</span>
15
25
  </span>
@@ -68,12 +68,12 @@ function close() { open.value = false }
68
68
  <div class="rt-label">{{ t('settings.theme') }}</div>
69
69
  <div class="rt-options">
70
70
  <button
71
- v-for="t in THEMES"
72
- :key="t"
71
+ v-for="th in THEMES"
72
+ :key="th"
73
73
  class="rt-opt rt-theme"
74
- :class="{ active: theme === t, ['theme-' + t]: true }"
75
- @click="setTheme(t)"
76
- >{{ t('theme.' + t) }}</button>
74
+ :class="{ active: theme === th, ['theme-' + th]: true }"
75
+ @click="setTheme(th)"
76
+ >{{ t('theme.' + th) }}</button>
77
77
  </div>
78
78
  </div>
79
79
  <div class="rt-group">
@@ -117,13 +117,13 @@ function close() { open.value = false }
117
117
  <style scoped>
118
118
  .rt {
119
119
  position: fixed;
120
- bottom: 24px;
120
+ bottom: max(24px, calc(16px + env(safe-area-inset-bottom, 0px)));
121
121
  right: 24px;
122
122
  z-index: 500;
123
123
  }
124
124
 
125
125
  @media (max-width: 768px) {
126
- .rt { bottom: 16px; right: 16px; }
126
+ .rt { bottom: max(16px, calc(12px + env(safe-area-inset-bottom, 0px))); right: 16px; }
127
127
  .rt-panel {
128
128
  position: fixed;
129
129
  bottom: 0;
@@ -134,6 +134,7 @@ function close() { open.value = false }
134
134
  max-height: 60vh;
135
135
  overflow-y: auto;
136
136
  overscroll-behavior: contain;
137
+ padding-bottom: max(16px, env(safe-area-inset-bottom, 0px));
137
138
  animation: slideUpMobile 0.3s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
138
139
  }
139
140
  @keyframes slideUpMobile {
@@ -1,8 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, onMounted, onUnmounted } from 'vue'
3
3
  import { parseAnnotationBlock } from '../utils/annotationParser'
4
+ import { useI18n } from '../composables/useI18n'
4
5
  import PronunciationGroup from './PronunciationGroup.vue'
5
6
 
7
+ const { locale } = useI18n()
8
+
6
9
  const props = defineProps<{
7
10
  num: string
8
11
  label: string
@@ -49,6 +52,12 @@ const displayNum = computed(() => {
49
52
  return props.num
50
53
  })
51
54
 
55
+ const displayLabel = computed(() => {
56
+ if (!props.special) return props.label
57
+ if (locale.value === 'en') return props.label
58
+ return '【' + props.label + '】'
59
+ })
60
+
52
61
  const entries = computed(() =>
53
62
  props.isAnnotations ? parseAnnotationBlock(props.text) : []
54
63
  )
@@ -64,7 +73,7 @@ const paragraphsHtml = computed(() => {
64
73
  <div v-if="text" ref="rootRef" class="sb-root" :class="{ 'sb-vertical': vertical, 'sb-visible': visible }">
65
74
  <div class="sb-header">
66
75
  <span v-if="displayNum" class="sb-num" :class="{ special }">{{ displayNum }}</span>
67
- <h3>{{ special ? '【' + label + '】' : label }}</h3>
76
+ <h3>{{ displayLabel }}</h3>
68
77
  </div>
69
78
  <div v-if="isAnnotations" class="sb-text sb-ann-list">
70
79
  <div v-for="entry in entries" :key="entry.num" class="sb-ann-entry">
@@ -78,12 +78,12 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
78
78
  <div class="ss-label">{{ t('settings.theme') }}</div>
79
79
  <div class="ss-options">
80
80
  <button
81
- v-for="t in THEMES"
82
- :key="t"
81
+ v-for="th in THEMES"
82
+ :key="th"
83
83
  class="ss-opt"
84
- :class="{ active: theme === t }"
85
- @click="setTheme(t)"
86
- >{{ t('theme.' + t) }}</button>
84
+ :class="{ active: theme === th }"
85
+ @click="setTheme(th)"
86
+ >{{ t('theme.' + th) }}</button>
87
87
  </div>
88
88
  </div>
89
89
  <div class="ss-group">
@@ -135,7 +135,7 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
135
135
  border-left: 1px solid var(--border);
136
136
  display: flex; flex-direction: column;
137
137
  align-items: center;
138
- padding: 12px 0;
138
+ padding: max(12px, env(safe-area-inset-top, 0px)) 0 max(12px, env(safe-area-inset-bottom, 0px));
139
139
  z-index: 200;
140
140
  gap: 8px;
141
141
  }
@@ -347,11 +347,11 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
347
347
  }
348
348
 
349
349
  @media (max-width: 768px) {
350
- .sidenav { width: 44px; padding: 8px 0; gap: 5px; }
350
+ .sidenav { width: 44px; padding: max(8px, env(safe-area-inset-top, 0px)) 0 max(8px, env(safe-area-inset-bottom, 0px)); gap: 5px; }
351
351
  .sn-brand { width: 30px; height: 36px; margin-bottom: 2px; }
352
352
  .sn-seal { font-size: 14px; }
353
- .sn-btn { width: 28px; height: 28px; }
354
- .sn-btn svg { width: 15px; height: 15px; }
353
+ .sn-btn { width: 32px; height: 32px; }
354
+ .sn-btn svg { width: 16px; height: 16px; }
355
355
  .sn-context { font-size: 10px; max-height: 70px; }
356
356
  .sn-settings { width: 180px; right: 52px; padding: 12px; }
357
357
  .sn-layout-tag { width: 20px; height: 20px; font-size: 10px; }
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  import type { Annotation, VerseLine } from '../types'
3
- import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations, countVerseSpans } from '../composables/useAnnotationRenderer'
4
+ import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations } from '../composables/useAnnotationRenderer'
4
5
 
5
6
  const props = defineProps<{
6
7
  title: string
@@ -17,11 +18,23 @@ const emit = defineEmits<{
17
18
  openAuthor: [name: string]
18
19
  }>()
19
20
 
21
+ const allVerseSpans = computed(() =>
22
+ props.verses.map((_, i) => buildVerseAnnotations(props.annotations, i))
23
+ )
24
+
25
+ const verseOffsets = computed(() => {
26
+ const offsets: number[] = []
27
+ let acc = 0
28
+ for (const spans of allVerseSpans.value) {
29
+ offsets.push(acc)
30
+ acc += spans.length
31
+ }
32
+ return offsets
33
+ })
34
+
20
35
  function verseHtml(index: number): string {
21
- let offset = 0
22
- for (let i = 0; i < index; i++) offset += countVerseSpans(props.annotations, i)
23
- const spans = buildVerseAnnotations(props.annotations, index)
24
- return renderAnnotatedText(props.verses[index].text, spans, true, offset)
36
+ const spans = allVerseSpans.value[index]
37
+ return renderAnnotatedText(props.verses[index].text, spans, true, verseOffsets.value[index])
25
38
  }
26
39
 
27
40
  function onHover(event: MouseEvent) {
@@ -60,7 +73,7 @@ function onTap(event: MouseEvent) {
60
73
  --ann-shadow-y: -2px;
61
74
  writing-mode: vertical-rl;
62
75
  text-orientation: mixed;
63
- height: calc(100vh - 100px);
76
+ height: calc(100vh - var(--nav-width, 56px));
64
77
  padding: 32px 24px;
65
78
  background: var(--surface);
66
79
  border: 1px solid var(--border);
@@ -94,8 +94,9 @@ const messages: Record<Locale, Record<string, string>> = {
94
94
  'theme.sepia': '暖',
95
95
  'theme.dark': '暗',
96
96
  'theme.oled': '黑',
97
- 'annotation.kind.pronunciation': '讀音',
98
- 'annotation.kind.semantic': '釋義',
97
+ 'pron.yue': '',
98
+ 'pron.cmn': '',
99
+ 'annotation.kind.pronunciation': '讀音', 'annotation.kind.semantic': '釋義',
99
100
  'annotation.kind.etymology': '詞源',
100
101
  'annotation.kind.note': '備注',
101
102
  'annotation.kind.definition': '釋義',
@@ -192,6 +193,8 @@ const messages: Record<Locale, Record<string, string>> = {
192
193
  'theme.sepia': '暖',
193
194
  'theme.dark': '暗',
194
195
  'theme.oled': '黑',
196
+ 'pron.yue': '粤',
197
+ 'pron.cmn': '普',
195
198
  'annotation.kind.pronunciation': '读音',
196
199
  'annotation.kind.semantic': '释义',
197
200
  'annotation.kind.etymology': '词源',
@@ -290,6 +293,8 @@ const messages: Record<Locale, Record<string, string>> = {
290
293
  'theme.sepia': 'Warm',
291
294
  'theme.dark': 'Dark',
292
295
  'theme.oled': 'Black',
296
+ 'pron.yue': 'Yue',
297
+ 'pron.cmn': 'Man',
293
298
  'annotation.kind.pronunciation': 'Pronunciation',
294
299
  'annotation.kind.semantic': 'Definition',
295
300
  'annotation.kind.etymology': 'Etymology',
@@ -18,44 +18,44 @@
18
18
 
19
19
  /* ─── Annotation kind hover states ─── */
20
20
  .ann-target:hover {
21
- background: rgba(194, 58, 43, 0.1);
22
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(194, 58, 43, 0.08);
21
+ background: color-mix(in srgb, var(--vermillion) 10%, transparent);
22
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--vermillion) 8%, transparent);
23
23
  }
24
24
  .ann-target.pronunciation:hover {
25
- background: rgba(58, 107, 94, 0.1);
26
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(58, 107, 94, 0.08);
25
+ background: color-mix(in srgb, var(--jade) 10%, transparent);
26
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--jade) 8%, transparent);
27
27
  }
28
28
  .ann-target.person:hover {
29
- background: rgba(58, 90, 140, 0.1);
30
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(58, 90, 140, 0.08);
29
+ background: color-mix(in srgb, var(--ann-person) 10%, transparent);
30
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-person) 8%, transparent);
31
31
  }
32
32
  .ann-target.place:hover {
33
- background: rgba(139, 105, 20, 0.1);
34
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(139, 105, 20, 0.08);
33
+ background: color-mix(in srgb, var(--ann-place) 10%, transparent);
34
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-place) 8%, transparent);
35
35
  }
36
36
  .ann-target.event:hover {
37
- background: rgba(107, 76, 138, 0.1);
38
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(107, 76, 138, 0.08);
37
+ background: color-mix(in srgb, var(--ann-event) 10%, transparent);
38
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-event) 8%, transparent);
39
39
  }
40
40
  .ann-target.date:hover {
41
- background: rgba(42, 122, 122, 0.1);
42
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(42, 122, 122, 0.08);
41
+ background: color-mix(in srgb, var(--ann-date) 10%, transparent);
42
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-date) 8%, transparent);
43
43
  }
44
44
  .ann-target.allusion:hover {
45
- background: rgba(181, 101, 29, 0.1);
46
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(181, 101, 29, 0.08);
45
+ background: color-mix(in srgb, var(--ann-allusion) 10%, transparent);
46
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-allusion) 8%, transparent);
47
47
  }
48
48
  .ann-target.etymology:hover {
49
- background: rgba(107, 91, 149, 0.1);
50
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(107, 91, 149, 0.08);
49
+ background: color-mix(in srgb, var(--ann-etymology) 10%, transparent);
50
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-etymology) 8%, transparent);
51
51
  }
52
52
  .ann-target.commentary:hover {
53
- background: rgba(192, 57, 43, 0.1);
54
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(192, 57, 43, 0.08);
53
+ background: color-mix(in srgb, var(--ann-commentary) 10%, transparent);
54
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-commentary) 8%, transparent);
55
55
  }
56
56
  .ann-target.translation:hover {
57
- background: rgba(44, 110, 73, 0.1);
58
- box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(44, 110, 73, 0.08);
57
+ background: color-mix(in srgb, var(--ann-translation) 10%, transparent);
58
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-translation) 8%, transparent);
59
59
  }
60
60
 
61
61
  /* ─── Annotation number ─── */
@@ -167,7 +167,7 @@ button:focus-visible {
167
167
 
168
168
  /* ===== ACTIVE ANNOTATION FLASH ===== */
169
169
  @keyframes ann-flash-anim {
170
- 0% { background: rgba(194, 58, 43, 0.25); box-shadow: 0 0 12px rgba(194, 58, 43, 0.15); }
170
+ 0% { background: color-mix(in srgb, var(--vermillion) 25%, transparent); box-shadow: 0 0 12px color-mix(in srgb, var(--vermillion) 15%, transparent); }
171
171
  100% { background: transparent; box-shadow: none; }
172
172
  }
173
173
 
@@ -251,7 +251,7 @@ button:focus-visible {
251
251
  flex-direction: row-reverse;
252
252
  overflow-x: auto;
253
253
  overflow-y: hidden;
254
- margin-right: var(--nav-width, 56px);
254
+ margin-right: calc(var(--nav-width, 56px) + env(safe-area-inset-right, 0px));
255
255
  scrollbar-width: thin;
256
256
  scrollbar-color: var(--gold) transparent;
257
257
  scroll-snap-type: x proximity;
@@ -321,7 +321,7 @@ button:focus-visible {
321
321
  /* ===== DARK MODE VERTICAL WARMTH ===== */
322
322
  [data-theme="dark"] .v-scroll,
323
323
  [data-theme="oled"] .v-scroll {
324
- box-shadow: 0 0 40px rgba(194, 58, 43, 0.03), 0 4px 16px rgba(0, 0, 0, 0.2);
324
+ box-shadow: 0 0 40px color-mix(in srgb, var(--vermillion) 3%, transparent), 0 4px 16px rgba(var(--shadow-rgb), 0.2);
325
325
  }
326
326
 
327
327
  [data-theme="dark"] .sb-vertical,
@@ -151,7 +151,7 @@ function goHome() { router.push('/') }
151
151
  flex-direction: row-reverse;
152
152
  overflow-x: auto;
153
153
  overflow-y: hidden;
154
- margin-right: var(--nav-width, 56px);
154
+ margin-right: calc(var(--nav-width, 56px) + env(safe-area-inset-right, 0px));
155
155
  padding: 0 32px;
156
156
  background: var(--paper);
157
157
  scrollbar-width: thin;
@@ -349,6 +349,6 @@ function goHome() { router.push('/') }
349
349
  .h-hero { flex-direction: column; text-align: center; padding: 24px; }
350
350
  .h-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
351
351
  .h-content { padding: 30px 20px; }
352
- .v-page { padding: 0 16px; }
352
+ .v-page { padding: 0 16px; margin-right: calc(var(--nav-width, 44px) + env(safe-area-inset-right, 0px)); }
353
353
  }
354
354
  </style>
@@ -1332,7 +1332,7 @@ function tcy(n: number): string {
1332
1332
 
1333
1333
  @media (max-width: 768px) {
1334
1334
  /* ─── 直排模式 ─── */
1335
- .v-page { margin-right: var(--nav-width, 44px); }
1335
+ .v-page { margin-right: calc(var(--nav-width, 44px) + env(safe-area-inset-right, 0px)); }
1336
1336
  .v-title-col { padding: 20px 12px; }
1337
1337
  .v-poem-title { font-size: 28px; letter-spacing: 6px; padding-left: 12px; }
1338
1338
  .v-poem-author { font-size: 18px; letter-spacing: 4px; }