@jjlmoya/utils-health 1.1.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.
Files changed (155) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +60 -0
  3. package/src/category/i18n/es.ts +60 -0
  4. package/src/category/i18n/fr.ts +60 -0
  5. package/src/category/index.ts +22 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +28 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +36 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/binauralTuner/bibliography.astro +14 -0
  25. package/src/tool/binauralTuner/component.astro +687 -0
  26. package/src/tool/binauralTuner/i18n/en.ts +187 -0
  27. package/src/tool/binauralTuner/i18n/es.ts +187 -0
  28. package/src/tool/binauralTuner/i18n/fr.ts +187 -0
  29. package/src/tool/binauralTuner/index.ts +27 -0
  30. package/src/tool/binauralTuner/seo.astro +14 -0
  31. package/src/tool/binauralTuner/ui.ts +18 -0
  32. package/src/tool/bloodUnitConverter/bibliography.astro +14 -0
  33. package/src/tool/bloodUnitConverter/component.astro +915 -0
  34. package/src/tool/bloodUnitConverter/i18n/en.ts +227 -0
  35. package/src/tool/bloodUnitConverter/i18n/es.ts +250 -0
  36. package/src/tool/bloodUnitConverter/i18n/fr.ts +218 -0
  37. package/src/tool/bloodUnitConverter/index.ts +27 -0
  38. package/src/tool/bloodUnitConverter/seo.astro +14 -0
  39. package/src/tool/bloodUnitConverter/ui.ts +38 -0
  40. package/src/tool/bmiCalculator/bibliography.astro +14 -0
  41. package/src/tool/bmiCalculator/component.astro +415 -0
  42. package/src/tool/bmiCalculator/i18n/en.ts +217 -0
  43. package/src/tool/bmiCalculator/i18n/es.ts +221 -0
  44. package/src/tool/bmiCalculator/i18n/fr.ts +217 -0
  45. package/src/tool/bmiCalculator/index.ts +27 -0
  46. package/src/tool/bmiCalculator/seo.astro +14 -0
  47. package/src/tool/bmiCalculator/ui.ts +21 -0
  48. package/src/tool/breathingVisualizer/bibliography.astro +14 -0
  49. package/src/tool/breathingVisualizer/component.astro +636 -0
  50. package/src/tool/breathingVisualizer/i18n/en.ts +206 -0
  51. package/src/tool/breathingVisualizer/i18n/es.ts +206 -0
  52. package/src/tool/breathingVisualizer/i18n/fr.ts +206 -0
  53. package/src/tool/breathingVisualizer/index.ts +27 -0
  54. package/src/tool/breathingVisualizer/seo.astro +14 -0
  55. package/src/tool/breathingVisualizer/ui.ts +31 -0
  56. package/src/tool/caffeineTracker/bibliography.astro +14 -0
  57. package/src/tool/caffeineTracker/component.astro +1210 -0
  58. package/src/tool/caffeineTracker/i18n/en.ts +198 -0
  59. package/src/tool/caffeineTracker/i18n/es.ts +198 -0
  60. package/src/tool/caffeineTracker/i18n/fr.ts +198 -0
  61. package/src/tool/caffeineTracker/index.ts +27 -0
  62. package/src/tool/caffeineTracker/logic.ts +31 -0
  63. package/src/tool/caffeineTracker/seo.astro +14 -0
  64. package/src/tool/caffeineTracker/ui.ts +36 -0
  65. package/src/tool/daltonismSimulator/bibliography.astro +14 -0
  66. package/src/tool/daltonismSimulator/component.astro +383 -0
  67. package/src/tool/daltonismSimulator/i18n/en.ts +188 -0
  68. package/src/tool/daltonismSimulator/i18n/es.ts +218 -0
  69. package/src/tool/daltonismSimulator/i18n/fr.ts +168 -0
  70. package/src/tool/daltonismSimulator/index.ts +27 -0
  71. package/src/tool/daltonismSimulator/seo.astro +14 -0
  72. package/src/tool/daltonismSimulator/ui.ts +20 -0
  73. package/src/tool/digestionStopwatch/bibliography.astro +14 -0
  74. package/src/tool/digestionStopwatch/component.astro +627 -0
  75. package/src/tool/digestionStopwatch/i18n/en.ts +173 -0
  76. package/src/tool/digestionStopwatch/i18n/es.ts +173 -0
  77. package/src/tool/digestionStopwatch/i18n/fr.ts +173 -0
  78. package/src/tool/digestionStopwatch/index.ts +27 -0
  79. package/src/tool/digestionStopwatch/logic.ts +63 -0
  80. package/src/tool/digestionStopwatch/seo.astro +14 -0
  81. package/src/tool/digestionStopwatch/ui.ts +20 -0
  82. package/src/tool/epworthSleepinessScale/bibliography.astro +14 -0
  83. package/src/tool/epworthSleepinessScale/component.astro +528 -0
  84. package/src/tool/epworthSleepinessScale/i18n/en.ts +217 -0
  85. package/src/tool/epworthSleepinessScale/i18n/es.ts +217 -0
  86. package/src/tool/epworthSleepinessScale/i18n/fr.ts +217 -0
  87. package/src/tool/epworthSleepinessScale/index.ts +27 -0
  88. package/src/tool/epworthSleepinessScale/seo.astro +14 -0
  89. package/src/tool/epworthSleepinessScale/ui.ts +27 -0
  90. package/src/tool/hydrationCalculator/bibliography.astro +14 -0
  91. package/src/tool/hydrationCalculator/component.astro +694 -0
  92. package/src/tool/hydrationCalculator/i18n/en.ts +217 -0
  93. package/src/tool/hydrationCalculator/i18n/es.ts +222 -0
  94. package/src/tool/hydrationCalculator/i18n/fr.ts +199 -0
  95. package/src/tool/hydrationCalculator/index.ts +27 -0
  96. package/src/tool/hydrationCalculator/seo.astro +14 -0
  97. package/src/tool/hydrationCalculator/ui.ts +28 -0
  98. package/src/tool/pelliRobsonTest/bibliography.astro +14 -0
  99. package/src/tool/pelliRobsonTest/component.astro +653 -0
  100. package/src/tool/pelliRobsonTest/i18n/en.ts +205 -0
  101. package/src/tool/pelliRobsonTest/i18n/es.ts +205 -0
  102. package/src/tool/pelliRobsonTest/i18n/fr.ts +205 -0
  103. package/src/tool/pelliRobsonTest/index.ts +27 -0
  104. package/src/tool/pelliRobsonTest/seo.astro +14 -0
  105. package/src/tool/pelliRobsonTest/ui.ts +21 -0
  106. package/src/tool/peripheralVisionTrainer/bibliography.astro +14 -0
  107. package/src/tool/peripheralVisionTrainer/component.astro +678 -0
  108. package/src/tool/peripheralVisionTrainer/i18n/en.ts +224 -0
  109. package/src/tool/peripheralVisionTrainer/i18n/es.ts +224 -0
  110. package/src/tool/peripheralVisionTrainer/i18n/fr.ts +211 -0
  111. package/src/tool/peripheralVisionTrainer/index.ts +27 -0
  112. package/src/tool/peripheralVisionTrainer/seo.astro +14 -0
  113. package/src/tool/peripheralVisionTrainer/ui.ts +26 -0
  114. package/src/tool/readingDistanceCalculator/bibliography.astro +14 -0
  115. package/src/tool/readingDistanceCalculator/component.astro +588 -0
  116. package/src/tool/readingDistanceCalculator/i18n/en.ts +202 -0
  117. package/src/tool/readingDistanceCalculator/i18n/es.ts +215 -0
  118. package/src/tool/readingDistanceCalculator/i18n/fr.ts +193 -0
  119. package/src/tool/readingDistanceCalculator/index.ts +31 -0
  120. package/src/tool/readingDistanceCalculator/seo.astro +14 -0
  121. package/src/tool/readingDistanceCalculator/ui.ts +18 -0
  122. package/src/tool/screenDecompressionTime/bibliography.astro +14 -0
  123. package/src/tool/screenDecompressionTime/component.astro +671 -0
  124. package/src/tool/screenDecompressionTime/i18n/en.ts +225 -0
  125. package/src/tool/screenDecompressionTime/i18n/es.ts +247 -0
  126. package/src/tool/screenDecompressionTime/i18n/fr.ts +225 -0
  127. package/src/tool/screenDecompressionTime/index.ts +27 -0
  128. package/src/tool/screenDecompressionTime/seo.astro +14 -0
  129. package/src/tool/screenDecompressionTime/ui.ts +32 -0
  130. package/src/tool/tinnitusReliever/bibliography.astro +14 -0
  131. package/src/tool/tinnitusReliever/component.astro +581 -0
  132. package/src/tool/tinnitusReliever/i18n/en.ts +161 -0
  133. package/src/tool/tinnitusReliever/i18n/es.ts +161 -0
  134. package/src/tool/tinnitusReliever/i18n/fr.ts +161 -0
  135. package/src/tool/tinnitusReliever/index.ts +27 -0
  136. package/src/tool/tinnitusReliever/seo.astro +14 -0
  137. package/src/tool/tinnitusReliever/ui.ts +9 -0
  138. package/src/tool/ubeCalculator/bibliography.astro +14 -0
  139. package/src/tool/ubeCalculator/component.astro +683 -0
  140. package/src/tool/ubeCalculator/i18n/en.ts +200 -0
  141. package/src/tool/ubeCalculator/i18n/es.ts +200 -0
  142. package/src/tool/ubeCalculator/i18n/fr.ts +196 -0
  143. package/src/tool/ubeCalculator/index.ts +27 -0
  144. package/src/tool/ubeCalculator/seo.astro +14 -0
  145. package/src/tool/ubeCalculator/ui.ts +26 -0
  146. package/src/tool/waterPurifier/bibliography.astro +14 -0
  147. package/src/tool/waterPurifier/component.astro +628 -0
  148. package/src/tool/waterPurifier/i18n/en.ts +167 -0
  149. package/src/tool/waterPurifier/i18n/es.ts +167 -0
  150. package/src/tool/waterPurifier/i18n/fr.ts +167 -0
  151. package/src/tool/waterPurifier/index.ts +27 -0
  152. package/src/tool/waterPurifier/seo.astro +14 -0
  153. package/src/tool/waterPurifier/ui.ts +18 -0
  154. package/src/tools.ts +19 -0
  155. package/src/types.ts +72 -0
@@ -0,0 +1,1210 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import { DRINK_PRESETS, METABOLISM_PRESETS } from './logic';
4
+ import type { CaffeineTrackerUI } from './ui';
5
+
6
+ interface Props {
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const ui = (Astro.props.ui ?? {}) as CaffeineTrackerUI;
11
+
12
+ interface DrinkUI {
13
+ name: string;
14
+ desc: string;
15
+ }
16
+ const drinkUIMap: Record<string, DrinkUI> = {
17
+ espresso: { name: ui.drinkEspressoName, desc: ui.drinkEspressoDesc },
18
+ double_espresso: { name: ui.drinkDoubleEspressoName, desc: ui.drinkDoubleEspressoDesc },
19
+ brewed: { name: ui.drinkBrewedName, desc: ui.drinkBrewedDesc },
20
+ energy_small: { name: ui.drinkEnergySmallName, desc: ui.drinkEnergySmallDesc },
21
+ monster: { name: ui.drinkMonsterName, desc: ui.drinkMonsterDesc },
22
+ soda: { name: ui.drinkSodaName, desc: ui.drinkSodaDesc },
23
+ tea: { name: ui.drinkTeaName, desc: ui.drinkTeaDesc },
24
+ };
25
+
26
+ interface MetaUI {
27
+ label: string;
28
+ desc: string;
29
+ }
30
+ const metaUIMap: Record<string, MetaUI> = {
31
+ fast: { label: ui.metabolismFastLabel, desc: ui.metabolismFastDesc },
32
+ normal: { label: ui.metabolismNormalLabel, desc: ui.metabolismNormalDesc },
33
+ slow: { label: ui.metabolismSlowLabel, desc: ui.metabolismSlowDesc },
34
+ pregnancy: { label: ui.metabolismPregnancyLabel, desc: ui.metabolismPregnancyDesc },
35
+ };
36
+ ---
37
+
38
+ <section class="ct" id="ct-container" data-ui={JSON.stringify(ui)}>
39
+ <div class="ct__card">
40
+ <div class="ct__grid">
41
+
42
+
43
+ <div class="ct__panel ct__scrollable">
44
+
45
+ <div class="ct__section">
46
+ <h2 class="ct__section-title">
47
+ <Icon name="mdi:coffee" class="ct__section-icon" />
48
+ {ui.sectionAddDrink}
49
+ </h2>
50
+ <div class="ct__drink-grid">
51
+ {DRINK_PRESETS.map((drink) => {
52
+ const dui = drinkUIMap[drink.id];
53
+ return (
54
+ <button
55
+ data-drink-id={drink.id}
56
+ data-mg={drink.mg}
57
+ data-name={dui?.name ?? drink.id}
58
+ class="ct__drink-btn"
59
+ >
60
+ <div class="ct__drink-btn-icon">
61
+ <span class="ct__icon-to-fly">
62
+ <Icon name={drink.icon} class="ct__icon-5" />
63
+ </span>
64
+ </div>
65
+ <div class="ct__drink-info">
66
+ <h3 class="ct__drink-name">{dui?.name ?? drink.id}</h3>
67
+ <p class="ct__drink-desc">{dui?.desc ?? `${drink.mg}mg`}</p>
68
+ </div>
69
+ <Icon name="mdi:plus-circle" class="ct__drink-add" />
70
+ </button>
71
+ );
72
+ })}
73
+ </div>
74
+ </div>
75
+
76
+ <div class="ct__section ct__section--bordered">
77
+ <h3 class="ct__meta-title">{ui.sectionMetabolism}</h3>
78
+ <div class="ct__meta-grid">
79
+ {METABOLISM_PRESETS.map((profile) => {
80
+ const mui = metaUIMap[profile.id];
81
+ return (
82
+ <label class="ct__meta-option">
83
+ <input
84
+ type="radio"
85
+ name="ct-metabolism"
86
+ value={profile.halfLife}
87
+ checked={profile.id === 'normal'}
88
+ class="ct__meta-radio"
89
+ />
90
+ <div class="ct__meta-text">
91
+ <div class="ct__meta-label">{mui?.label ?? profile.id}</div>
92
+ <p class="ct__meta-desc">{mui?.desc ?? ''}</p>
93
+ </div>
94
+ </label>
95
+ );
96
+ })}
97
+ </div>
98
+ </div>
99
+
100
+ <div class="ct__section ct__section--bordered">
101
+ <h3 class="ct__journal-header">
102
+ <span>{ui.sectionJournal}</span>
103
+ <span id="ct-journal-count" class="ct__journal-count">0</span>
104
+ </h3>
105
+ <div id="ct-drink-journal" class="ct__journal">
106
+ <div class="ct__journal-empty">
107
+ <p>{ui.journalEmpty}</p>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ </div>
113
+
114
+
115
+ <div class="ct__main">
116
+
117
+ <div class="ct__stats">
118
+ <div class="ct__stats-left">
119
+ <div class="ct__stat">
120
+ <span class="ct__stat-label">{ui.labelCurrentMg}</span>
121
+ <div class="ct__stat-value" id="ct-current-container">
122
+ <span id="ct-current-mg">0</span><span class="ct__stat-unit">mg</span>
123
+ </div>
124
+ </div>
125
+ <div class="ct__stat">
126
+ <span class="ct__stat-label">{ui.labelEliminationTime}</span>
127
+ <div class="ct__stat-value">
128
+ <span id="ct-elimination-time">--</span><span class="ct__stat-unit">h</span>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <button id="ct-reset-btn" class="ct__reset-btn">
133
+ <Icon name="mdi:refresh" class="ct__icon-4" />
134
+ {ui.btnReset}
135
+ </button>
136
+ </div>
137
+
138
+ <div class="ct__main-body ct__scrollable">
139
+
140
+ <div class="ct__chart-wrap">
141
+ <div class="ct__chart-grid" aria-hidden="true">
142
+ {[150, 100, 50, 0].map((val) => (
143
+ <div class="ct__chart-gridline">
144
+ <span class="ct__chart-gridlabel">{val}mg</span>
145
+ </div>
146
+ ))}
147
+ </div>
148
+ <svg id="ct-decaysvg" class="ct__chart-svg" preserveAspectRatio="none">
149
+ <defs>
150
+ <linearGradient id="ct-chartGradient" x1="0" y1="0" x2="0" y2="1">
151
+ <stop offset="0%" stop-color="#f59e0b" stop-opacity="0.3"></stop>
152
+ <stop offset="100%" stop-color="#6366f1" stop-opacity="0"></stop>
153
+ </linearGradient>
154
+ </defs>
155
+ <path id="ct-chart-area" fill="url(#ct-chartGradient)"></path>
156
+ <path
157
+ id="ct-chart-line"
158
+ fill="none"
159
+ class="ct__chart-line"
160
+ stroke-width="3"
161
+ stroke-linecap="round"
162
+ stroke-linejoin="round"
163
+ ></path>
164
+ </svg>
165
+ </div>
166
+
167
+ <div class="ct__sleep">
168
+ <div class="ct__sleep-inner">
169
+ <div class="ct__sleep-controls">
170
+ <div class="ct__sleep-top">
171
+ <label class="ct__sleep-question">{ui.labelSleepQuestion}</label>
172
+ <span id="ct-sleep-time" class="ct__sleep-time">--</span>
173
+ </div>
174
+ <input
175
+ type="range"
176
+ id="ct-sleep-slider"
177
+ min="0"
178
+ max="24"
179
+ value="12"
180
+ step="0.5"
181
+ class="ct__sleep-input"
182
+ />
183
+ </div>
184
+ <div class="ct__sleep-result">
185
+ <div id="ct-sleep-status" class="ct__sleep-status">--</div>
186
+ <div class="ct__sleep-mg">
187
+ <span id="ct-final-mg">0</span
188
+ ><span class="ct__sleep-mg-unit">mg</span>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="ct__icon-cache" aria-hidden="true">
200
+ {DRINK_PRESETS.map((drink) => (
201
+ <span id={`ct-icon-${drink.id}`}><Icon name={drink.icon} /></span>
202
+ ))}
203
+ <span id="ct-icon-remove"><Icon name="mdi:close-circle-outline" /></span>
204
+ </div>
205
+ </section>
206
+
207
+ <script>
208
+ import { calculateDecay } from './logic';
209
+ import type { CaffeineTrackerUI } from './ui';
210
+
211
+ const container = document.getElementById('ct-container');
212
+ const t = JSON.parse(container?.dataset.ui ?? '{}') as CaffeineTrackerUI;
213
+
214
+ const currentMgEl = document.getElementById('ct-current-mg');
215
+ const currentMgContainer = document.getElementById('ct-current-container');
216
+ const eliminationTimeEl = document.getElementById('ct-elimination-time');
217
+ const finalMgEl = document.getElementById('ct-final-mg');
218
+ const sleepSlider = document.getElementById('ct-sleep-slider') as HTMLInputElement | null;
219
+ const sleepTimeDisplay = document.getElementById('ct-sleep-time');
220
+ const sleepStatus = document.getElementById('ct-sleep-status');
221
+ const chartLine = document.getElementById('ct-chart-line') as SVGPathElement | null;
222
+ const chartArea = document.getElementById('ct-chart-area') as SVGPathElement | null;
223
+ const svg = document.getElementById('ct-decaysvg') as SVGSVGElement | null;
224
+ const resetBtn = document.getElementById('ct-reset-btn');
225
+ const journalCountEl = document.getElementById('ct-journal-count');
226
+ const drinkJournalEl = document.getElementById('ct-drink-journal');
227
+
228
+ interface JournalItem {
229
+ id: number;
230
+ name: string;
231
+ mg: number;
232
+ drinkId: string;
233
+ }
234
+
235
+ let journal: JournalItem[] = [];
236
+ let totalMg = 0;
237
+ let halfLife = 5.5;
238
+
239
+ function triggerBump(el: HTMLElement | null, cls: string) {
240
+ if (!el) return;
241
+ el.classList.add(cls);
242
+ setTimeout(() => el.classList.remove(cls), 200);
243
+ }
244
+
245
+ function updateCurrentDisplay(mg: number, animate: boolean) {
246
+ if (!currentMgEl) return;
247
+ currentMgEl.textContent = Math.round(mg).toString();
248
+ if (animate) triggerBump(currentMgContainer, 'ct__current-container--bump');
249
+ }
250
+
251
+ function updateSleepTimeDisplay(hours: number) {
252
+ if (!sleepTimeDisplay) return;
253
+ const d = new Date();
254
+ d.setMinutes(d.getMinutes() + hours * 60);
255
+ const str = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
256
+ sleepTimeDisplay.textContent = `${t.sleepAtPrefix} ${str} (+${hours.toFixed(1)}h)`;
257
+ }
258
+
259
+ function updateSleepStatus(mg: number) {
260
+ if (!sleepStatus) return;
261
+ sleepStatus.classList.remove(
262
+ 'ct__sleep-status--optimal',
263
+ 'ct__sleep-status--moderate',
264
+ 'ct__sleep-status--critical',
265
+ );
266
+ if (mg < 5) {
267
+ sleepStatus.textContent = t.statusOptimal;
268
+ sleepStatus.classList.add('ct__sleep-status--optimal');
269
+ } else if (mg < 20) {
270
+ sleepStatus.textContent = t.statusModerate;
271
+ sleepStatus.classList.add('ct__sleep-status--moderate');
272
+ } else {
273
+ sleepStatus.textContent = t.statusCritical;
274
+ sleepStatus.classList.add('ct__sleep-status--critical');
275
+ }
276
+ }
277
+
278
+ function updateSleepSection() {
279
+ if (!finalMgEl || !sleepSlider) return;
280
+ const sleepHours = parseFloat(sleepSlider.value);
281
+ updateSleepTimeDisplay(sleepHours);
282
+ const finalMg = calculateDecay(totalMg, sleepHours, halfLife);
283
+ finalMgEl.textContent = Math.round(finalMg).toString();
284
+ updateSleepStatus(finalMg);
285
+ }
286
+
287
+ function updateEliminationTime() {
288
+ if (!eliminationTimeEl) return;
289
+ if (totalMg > 0) {
290
+ const ttc = halfLife * Math.log2(totalMg / 5);
291
+ eliminationTimeEl.textContent = ttc > 0 ? Math.round(ttc).toString() : '0';
292
+ } else {
293
+ eliminationTimeEl.textContent = '--';
294
+ }
295
+ }
296
+
297
+ function update(animate = false) {
298
+ totalMg = journal.reduce((acc, item) => acc + item.mg, 0);
299
+ updateCurrentDisplay(totalMg, animate);
300
+ updateSleepSection();
301
+ updateEliminationTime();
302
+ renderJournal();
303
+ drawChart();
304
+ }
305
+
306
+ function drawChart() {
307
+ if (!svg || !chartLine || !chartArea) return;
308
+ const w = svg.clientWidth;
309
+ const h = svg.clientHeight;
310
+ const maxMg = Math.max(150, totalMg + 20);
311
+ const dur = 24;
312
+ let line = '';
313
+ let area = '';
314
+ for (let i = 0; i <= dur; i += 0.2) {
315
+ const mg = calculateDecay(totalMg, i, halfLife);
316
+ const x = (i / dur) * w;
317
+ const y = h - (mg / maxMg) * h;
318
+ if (i === 0) {
319
+ line = `M ${x} ${y}`;
320
+ area = `M ${x} ${h} L ${x} ${y}`;
321
+ } else {
322
+ line += ` L ${x} ${y}`;
323
+ area += ` L ${x} ${y}`;
324
+ }
325
+ }
326
+ area += ` L ${w} ${h} Z`;
327
+ chartLine.setAttribute('d', line);
328
+ chartArea.setAttribute('d', area);
329
+ }
330
+
331
+ function buildItemHTML(item: JournalItem): string {
332
+ const iconEl = document.getElementById(`ct-icon-${item.drinkId}`);
333
+ const removeEl = document.getElementById('ct-icon-remove');
334
+ const iconHtml = iconEl?.innerHTML ?? '';
335
+ const removeHtml = removeEl?.innerHTML ?? '';
336
+ return `<div id="item-${item.id}" class="ct__item">
337
+ <span class="ct__item-icon">${iconHtml}</span>
338
+ <div class="ct__item-info">
339
+ <div class="ct__item-name">${item.name}</div>
340
+ <div class="ct__item-mg">${item.mg}mg</div>
341
+ </div>
342
+ <button data-id="${item.id}" class="ct__remove-btn" aria-label="Remove">
343
+ <span class="ct__remove-icon">${removeHtml}</span>
344
+ </button>
345
+ </div>`;
346
+ }
347
+
348
+ function handleRemove(btn: Element) {
349
+ const id = parseInt(btn.getAttribute('data-id') ?? '0');
350
+ const element = document.getElementById(`item-${id}`);
351
+ if (element) {
352
+ element.classList.add('ct__item--removing');
353
+ setTimeout(() => {
354
+ journal = journal.filter((it) => it.id !== id);
355
+ update();
356
+ }, 300);
357
+ } else {
358
+ journal = journal.filter((it) => it.id !== id);
359
+ update();
360
+ }
361
+ }
362
+
363
+ function attachRemoveListeners() {
364
+ drinkJournalEl?.querySelectorAll('.ct__remove-btn').forEach((btn) => {
365
+ btn.addEventListener('click', () => handleRemove(btn));
366
+ });
367
+ }
368
+
369
+ function renderJournal() {
370
+ if (!drinkJournalEl || !journalCountEl) return;
371
+ journalCountEl.textContent = journal.length.toString();
372
+ if (journal.length === 0) {
373
+ drinkJournalEl.innerHTML = `<div class="ct__journal-empty"><p>${t.journalEmpty}</p></div>`;
374
+ return;
375
+ }
376
+ drinkJournalEl.innerHTML = journal.slice().reverse().map(buildItemHTML).join('');
377
+ attachRemoveListeners();
378
+ }
379
+
380
+ function animateFlyer(
381
+ flyer: HTMLElement,
382
+ src: DOMRect,
383
+ tgt: DOMRect,
384
+ start: number,
385
+ ) {
386
+ const dur = 600;
387
+ const elapsed = performance.now() - start;
388
+ const prog = Math.min(elapsed / dur, 1);
389
+ const ease = prog * (2 - prog);
390
+ const x = src.left + (tgt.left - src.left) * ease;
391
+ const y = src.top + (tgt.top - src.top) * ease;
392
+ flyer.style.transform = `translate(${x - src.left}px,${y - src.top}px) scale(${1 - prog * 0.5})`;
393
+ flyer.style.opacity = `${1 - prog / 2}`;
394
+ if (prog < 1) {
395
+ requestAnimationFrame(() => animateFlyer(flyer, src, tgt, start));
396
+ } else {
397
+ flyer.remove();
398
+ triggerBump(journalCountEl, 'ct__journal-count--bump');
399
+ }
400
+ }
401
+
402
+ function flyToJournal(btn: HTMLElement) {
403
+ const iconSource = btn.querySelector('.ct__icon-to-fly');
404
+ if (!iconSource || !journalCountEl) return;
405
+ const src = iconSource.getBoundingClientRect();
406
+ const tgt = journalCountEl.getBoundingClientRect();
407
+ const flyer = document.createElement('div');
408
+ flyer.className = 'ct__flyer';
409
+ flyer.style.cssText = `left:${src.left}px;top:${src.top}px;width:${src.width}px;height:${src.height}px;`;
410
+ flyer.innerHTML = iconSource.innerHTML;
411
+ document.body.appendChild(flyer);
412
+ animateFlyer(flyer, src, tgt, performance.now());
413
+ }
414
+
415
+ container?.querySelectorAll<HTMLElement>('.ct__drink-btn').forEach((btn) => {
416
+ btn.addEventListener('click', () => {
417
+ const mg = parseInt(btn.getAttribute('data-mg') ?? '0');
418
+ const name = btn.getAttribute('data-name') ?? '';
419
+ const drinkId = btn.getAttribute('data-drink-id') ?? '';
420
+ flyToJournal(btn);
421
+ journal.push({ id: Date.now(), name, mg, drinkId });
422
+ update(true);
423
+ });
424
+ });
425
+
426
+ container?.querySelectorAll<HTMLInputElement>('input[name="ct-metabolism"]').forEach((radio) => {
427
+ radio.addEventListener('change', (e) => {
428
+ halfLife = parseFloat((e.target as HTMLInputElement).value);
429
+ update();
430
+ });
431
+ });
432
+
433
+ sleepSlider?.addEventListener('input', () => update());
434
+ resetBtn?.addEventListener('click', () => {
435
+ journal = [];
436
+ update();
437
+ });
438
+ window.addEventListener('resize', drawChart);
439
+ update();
440
+ </script>
441
+
442
+ <style>
443
+ .ct {
444
+ --ct-bg: #fff;
445
+ --ct-bg-panel: #f8fafc;
446
+ --ct-bg-item: #fff;
447
+ --ct-bg-icon: #f1f5f9;
448
+ --ct-border: #e2e8f0;
449
+ --ct-text: #0f172a;
450
+ --ct-text-sub: #475569;
451
+ --ct-text-muted: #94a3b8;
452
+ --ct-primary: #f59e0b;
453
+ --ct-secondary: #6366f1;
454
+
455
+ max-width: 72rem;
456
+ margin: 0 auto;
457
+ padding: 2rem 1rem;
458
+ }
459
+
460
+ :global(.theme-dark) .ct {
461
+ --ct-bg: #09090b;
462
+ --ct-bg-panel: rgba(24, 24, 27, 0.5);
463
+ --ct-bg-item: #18181b;
464
+ --ct-bg-icon: #27272a;
465
+ --ct-border: #27272a;
466
+ --ct-text: #fafafa;
467
+ --ct-text-sub: #a1a1aa;
468
+ --ct-text-muted: #71717a;
469
+ }
470
+
471
+ .ct__card {
472
+ width: 100%;
473
+ background-color: var(--ct-bg);
474
+ border: 1px solid var(--ct-border);
475
+ border-radius: 3rem;
476
+ box-shadow:
477
+ 0 25px 50px -12px rgba(0, 0, 0, 0.25),
478
+ 0 0 0 1px rgba(99, 102, 241, 0.05);
479
+ overflow: hidden;
480
+ display: flex;
481
+ flex-direction: column;
482
+ transition: all 0.5s;
483
+ }
484
+
485
+ .ct__grid {
486
+ display: grid;
487
+ }
488
+
489
+ .ct__panel {
490
+ background-color: var(--ct-bg-panel);
491
+ padding: 1.5rem;
492
+ display: flex;
493
+ flex-direction: column;
494
+ gap: 1.5rem;
495
+ }
496
+
497
+ .ct__main {
498
+ display: flex;
499
+ flex-direction: column;
500
+ background-color: var(--ct-bg);
501
+ }
502
+
503
+
504
+ @media (min-width: 1024px) {
505
+ .ct__card {
506
+ max-height: 85vh;
507
+ overflow: hidden;
508
+ }
509
+
510
+ .ct__grid {
511
+ grid-template-columns: 33.333% 66.667%;
512
+ overflow: hidden;
513
+ height: 100%;
514
+ min-height: 0;
515
+ }
516
+
517
+ .ct__panel {
518
+ border-right: 1px solid var(--ct-border);
519
+ overflow-y: auto;
520
+ }
521
+
522
+ .ct__main {
523
+ height: 100%;
524
+ overflow: hidden;
525
+ }
526
+ }
527
+
528
+ .ct__scrollable::-webkit-scrollbar {
529
+ width: 3px;
530
+ }
531
+
532
+ .ct__scrollable::-webkit-scrollbar-track {
533
+ background: transparent;
534
+ }
535
+
536
+ .ct__scrollable::-webkit-scrollbar-thumb {
537
+ background: rgba(155, 155, 155, 0.1);
538
+ border-radius: 10px;
539
+ }
540
+
541
+ .ct__scrollable::-webkit-scrollbar-thumb:hover {
542
+ background: rgba(155, 155, 155, 0.3);
543
+ }
544
+
545
+
546
+ .ct__section {
547
+ display: flex;
548
+ flex-direction: column;
549
+ gap: 1rem;
550
+ }
551
+
552
+ .ct__section--bordered {
553
+ padding-top: 1.5rem;
554
+ border-top: 1px solid var(--ct-border);
555
+ }
556
+
557
+ .ct__section-title {
558
+ font-size: 1.125rem;
559
+ font-weight: 700;
560
+ color: var(--ct-text);
561
+ display: flex;
562
+ align-items: center;
563
+ gap: 0.5rem;
564
+ margin: 0;
565
+ }
566
+
567
+ .ct__section-icon {
568
+ color: #92400e;
569
+ width: 1.25rem;
570
+ height: 1.25rem;
571
+ }
572
+
573
+ :global(.theme-dark) .ct__section-icon {
574
+ color: #f59e0b;
575
+ }
576
+
577
+
578
+ .ct__drink-grid {
579
+ display: grid;
580
+ grid-template-columns: 1fr;
581
+ gap: 0.5rem;
582
+ }
583
+
584
+ .ct__drink-btn {
585
+ position: relative;
586
+ width: 100%;
587
+ display: flex;
588
+ align-items: center;
589
+ gap: 0.75rem;
590
+ padding: 0.75rem;
591
+ border-radius: 0.75rem;
592
+ background-color: var(--ct-bg-item);
593
+ border: 1px solid var(--ct-border);
594
+ cursor: pointer;
595
+ text-align: left;
596
+ overflow: hidden;
597
+ transition:
598
+ border-color 0.2s,
599
+ box-shadow 0.2s;
600
+ }
601
+
602
+ .ct__drink-btn:hover {
603
+ border-color: rgba(245, 158, 11, 0.5);
604
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
605
+ }
606
+
607
+ .ct__drink-btn-icon {
608
+ padding: 0.5rem;
609
+ border-radius: 0.5rem;
610
+ background-color: var(--ct-bg-icon);
611
+ color: var(--ct-text-muted);
612
+ transition:
613
+ background-color 0.2s,
614
+ color 0.2s;
615
+ pointer-events: none;
616
+ flex-shrink: 0;
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: center;
620
+ }
621
+
622
+ .ct__drink-btn:hover .ct__drink-btn-icon {
623
+ background-color: rgba(245, 158, 11, 0.1);
624
+ color: var(--ct-primary);
625
+ }
626
+
627
+ .ct__icon-to-fly {
628
+ display: contents;
629
+ }
630
+
631
+ .ct__icon-5 {
632
+ width: 1.25rem;
633
+ height: 1.25rem;
634
+ }
635
+
636
+ .ct__drink-info {
637
+ flex: 1;
638
+ min-width: 0;
639
+ }
640
+
641
+ .ct__drink-name {
642
+ font-weight: 700;
643
+ color: var(--ct-text);
644
+ font-size: 0.75rem;
645
+ white-space: nowrap;
646
+ overflow: hidden;
647
+ text-overflow: ellipsis;
648
+ margin: 0;
649
+ }
650
+
651
+ .ct__drink-desc {
652
+ font-size: 0.5625rem;
653
+ color: var(--ct-text-muted);
654
+ text-transform: uppercase;
655
+ letter-spacing: 0.05em;
656
+ white-space: nowrap;
657
+ overflow: hidden;
658
+ text-overflow: ellipsis;
659
+ margin: 0;
660
+ }
661
+
662
+ .ct__drink-add {
663
+ width: 1rem;
664
+ height: 1rem;
665
+ color: var(--ct-border);
666
+ transition: color 0.2s;
667
+ flex-shrink: 0;
668
+ }
669
+
670
+ .ct__drink-btn:hover .ct__drink-add {
671
+ color: var(--ct-primary);
672
+ }
673
+
674
+
675
+ .ct__meta-title {
676
+ font-size: 0.625rem;
677
+ font-weight: 700;
678
+ text-transform: uppercase;
679
+ letter-spacing: 0.15em;
680
+ color: var(--ct-text-muted);
681
+ margin: 0;
682
+ }
683
+
684
+ .ct__meta-grid {
685
+ display: grid;
686
+ grid-template-columns: 1fr;
687
+ gap: 0.375rem;
688
+ }
689
+
690
+ .ct__meta-option {
691
+ display: flex;
692
+ align-items: center;
693
+ gap: 0.75rem;
694
+ padding: 0.625rem;
695
+ border-radius: 0.75rem;
696
+ cursor: pointer;
697
+ transition:
698
+ background-color 0.2s,
699
+ border-color 0.2s;
700
+ border: 1px solid transparent;
701
+ }
702
+
703
+ .ct__meta-option:hover {
704
+ background-color: var(--ct-bg-item);
705
+ border-color: var(--ct-border);
706
+ }
707
+
708
+ .ct__meta-radio {
709
+ accent-color: #d97706;
710
+ width: 0.875rem;
711
+ height: 0.875rem;
712
+ flex-shrink: 0;
713
+ }
714
+
715
+ .ct__meta-text {
716
+ flex: 1;
717
+ min-width: 0;
718
+ }
719
+
720
+ .ct__meta-label {
721
+ font-size: 0.75rem;
722
+ font-weight: 700;
723
+ color: var(--ct-text);
724
+ transition: color 0.2s;
725
+ }
726
+
727
+ .ct__meta-option:hover .ct__meta-label {
728
+ color: var(--ct-primary);
729
+ }
730
+
731
+ .ct__meta-desc {
732
+ font-size: 0.5625rem;
733
+ color: var(--ct-text-muted);
734
+ line-height: 1.4;
735
+ white-space: nowrap;
736
+ overflow: hidden;
737
+ text-overflow: ellipsis;
738
+ margin: 0;
739
+ }
740
+
741
+
742
+ .ct__journal-header {
743
+ font-size: 0.625rem;
744
+ font-weight: 700;
745
+ text-transform: uppercase;
746
+ letter-spacing: 0.15em;
747
+ color: var(--ct-text-muted);
748
+ display: flex;
749
+ justify-content: space-between;
750
+ align-items: center;
751
+ margin: 0;
752
+ }
753
+
754
+ .ct__journal-count {
755
+ font-size: 0.5625rem;
756
+ background-color: var(--ct-bg-icon);
757
+ padding: 0.125rem 0.375rem;
758
+ border-radius: 9999px;
759
+ font-weight: 700;
760
+ transition: all 0.3s;
761
+ }
762
+
763
+ .ct__journal-count--bump {
764
+ transform: scale(1.25);
765
+ background-color: var(--ct-primary);
766
+ color: #fff;
767
+ }
768
+
769
+ .ct__journal {
770
+ display: flex;
771
+ flex-direction: column;
772
+ gap: 0.375rem;
773
+ }
774
+
775
+
776
+ :global(.ct__journal-empty) {
777
+ text-align: center;
778
+ padding: 1.5rem;
779
+ opacity: 0.3;
780
+ border: 2px dashed var(--ct-border);
781
+ border-radius: 0.75rem;
782
+ }
783
+
784
+ :global(.ct__journal-empty p) {
785
+ font-size: 0.5625rem;
786
+ font-weight: 700;
787
+ text-transform: uppercase;
788
+ letter-spacing: 0.1em;
789
+ margin: 0;
790
+ }
791
+
792
+
793
+ :global(.ct__item) {
794
+ display: flex;
795
+ align-items: center;
796
+ gap: 0.625rem;
797
+ padding: 0.5rem;
798
+ background-color: var(--ct-bg-item);
799
+ border-radius: 0.5rem;
800
+ border: 1px solid var(--ct-border);
801
+ transition:
802
+ opacity 0.3s,
803
+ transform 0.3s;
804
+ }
805
+
806
+ :global(.ct__item--removing) {
807
+ opacity: 0;
808
+ transform: translateX(-1rem);
809
+ }
810
+
811
+ :global(.ct__item-icon) {
812
+ padding: 0.375rem;
813
+ background-color: var(--ct-bg-icon);
814
+ border-radius: 0.25rem;
815
+ color: var(--ct-text-muted);
816
+ flex-shrink: 0;
817
+ display: flex;
818
+ align-items: center;
819
+ justify-content: center;
820
+ width: 1.5rem;
821
+ height: 1.5rem;
822
+ }
823
+
824
+ :global(.ct__item-icon svg) {
825
+ width: 0.75rem;
826
+ height: 0.75rem;
827
+ }
828
+
829
+ :global(.ct__item:hover .ct__item-icon) {
830
+ color: var(--ct-primary);
831
+ }
832
+
833
+ :global(.ct__item-info) {
834
+ flex: 1;
835
+ min-width: 0;
836
+ text-align: left;
837
+ }
838
+
839
+ :global(.ct__item-name) {
840
+ font-size: 0.625rem;
841
+ font-weight: 700;
842
+ color: var(--ct-text);
843
+ white-space: nowrap;
844
+ overflow: hidden;
845
+ text-overflow: ellipsis;
846
+ line-height: 1;
847
+ margin-bottom: 0.125rem;
848
+ }
849
+
850
+ :global(.ct__item-mg) {
851
+ font-size: 0.5rem;
852
+ color: var(--ct-primary);
853
+ font-weight: 700;
854
+ line-height: 1;
855
+ }
856
+
857
+ :global(.ct__remove-btn) {
858
+ padding: 0.375rem;
859
+ color: var(--ct-border);
860
+ border: none;
861
+ background: none;
862
+ cursor: pointer;
863
+ border-radius: 0.25rem;
864
+ transition:
865
+ color 0.2s,
866
+ background-color 0.2s;
867
+ flex-shrink: 0;
868
+ display: flex;
869
+ align-items: center;
870
+ justify-content: center;
871
+ }
872
+
873
+ :global(.ct__remove-btn:hover) {
874
+ color: #f43f5e;
875
+ background-color: rgba(244, 63, 94, 0.1);
876
+ }
877
+
878
+ :global(.ct__remove-icon svg) {
879
+ width: 0.75rem;
880
+ height: 0.75rem;
881
+ }
882
+
883
+
884
+ .ct__stats {
885
+ padding: 2rem;
886
+ border-bottom: 1px solid var(--ct-border);
887
+ background-color: rgba(248, 250, 252, 0.3);
888
+ display: flex;
889
+ flex-wrap: wrap;
890
+ align-items: center;
891
+ justify-content: space-between;
892
+ gap: 2rem;
893
+ flex-shrink: 0;
894
+ }
895
+
896
+ :global(.theme-dark) .ct__stats {
897
+ background-color: rgba(24, 24, 27, 0.1);
898
+ border-bottom-color: #18181b;
899
+ }
900
+
901
+ .ct__stats-left {
902
+ display: flex;
903
+ gap: 3rem;
904
+ text-align: left;
905
+ }
906
+
907
+ .ct__stat {
908
+ display: flex;
909
+ flex-direction: column;
910
+ gap: 0.125rem;
911
+ }
912
+
913
+ .ct__stat-label {
914
+ font-size: 0.5625rem;
915
+ font-weight: 700;
916
+ color: var(--ct-text-muted);
917
+ text-transform: uppercase;
918
+ letter-spacing: 0.1em;
919
+ }
920
+
921
+ .ct__stat-value {
922
+ font-size: 2.25rem;
923
+ font-weight: 900;
924
+ color: var(--ct-text);
925
+ font-variant-numeric: tabular-nums;
926
+ display: flex;
927
+ align-items: baseline;
928
+ gap: 0.25rem;
929
+ transition: all 0.3s;
930
+ transform-origin: left;
931
+ }
932
+
933
+ .ct__current-container--bump {
934
+ transform: scale(1.1);
935
+ color: var(--ct-primary);
936
+ transition:
937
+ transform 0.1s ease-out,
938
+ color 0.1s ease-out;
939
+ }
940
+
941
+ .ct__stat-unit {
942
+ font-size: 0.875rem;
943
+ font-weight: 400;
944
+ color: var(--ct-text-muted);
945
+ }
946
+
947
+ .ct__reset-btn {
948
+ padding: 0.5rem 1rem;
949
+ border-radius: 0.75rem;
950
+ background-color: var(--ct-bg-icon);
951
+ color: var(--ct-text-muted);
952
+ border: none;
953
+ cursor: pointer;
954
+ font-size: 0.625rem;
955
+ font-weight: 700;
956
+ text-transform: uppercase;
957
+ letter-spacing: 0.1em;
958
+ display: flex;
959
+ align-items: center;
960
+ gap: 0.5rem;
961
+ transition: color 0.2s;
962
+ }
963
+
964
+ .ct__reset-btn:hover {
965
+ color: #f43f5e;
966
+ }
967
+
968
+ .ct__icon-4 {
969
+ width: 1rem;
970
+ height: 1rem;
971
+ }
972
+
973
+
974
+ .ct__main-body {
975
+ padding: 2rem;
976
+ display: flex;
977
+ flex-direction: column;
978
+ gap: 2rem;
979
+ }
980
+
981
+ @media (min-width: 1024px) {
982
+ .ct__main-body {
983
+ flex: 1;
984
+ overflow-y: auto;
985
+ }
986
+ }
987
+
988
+
989
+ .ct__chart-wrap {
990
+ min-height: 250px;
991
+ position: relative;
992
+ width: 100%;
993
+ }
994
+
995
+ .ct__chart-grid {
996
+ position: absolute;
997
+ inset: 0;
998
+ display: flex;
999
+ flex-direction: column;
1000
+ justify-content: space-between;
1001
+ pointer-events: none;
1002
+ opacity: 0.3;
1003
+ }
1004
+
1005
+ .ct__chart-gridline {
1006
+ width: 100%;
1007
+ border-top: 1px solid var(--ct-border);
1008
+ display: flex;
1009
+ justify-content: flex-end;
1010
+ }
1011
+
1012
+ .ct__chart-gridlabel {
1013
+ font-size: 0.5rem;
1014
+ margin-top: -0.5rem;
1015
+ padding-right: 0.5rem;
1016
+ color: var(--ct-text-muted);
1017
+ }
1018
+
1019
+ .ct__chart-svg {
1020
+ width: 100%;
1021
+ height: 100%;
1022
+ overflow: visible;
1023
+ }
1024
+
1025
+ .ct__chart-line {
1026
+ stroke: #f59e0b;
1027
+ }
1028
+
1029
+ :global(.theme-dark) .ct__chart-line {
1030
+ stroke: #fbbf24;
1031
+ }
1032
+
1033
+
1034
+ .ct__sleep {
1035
+ background-color: #0f172a;
1036
+ border-radius: 2.5rem;
1037
+ padding: 2rem;
1038
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1039
+ position: relative;
1040
+ overflow: hidden;
1041
+ flex-shrink: 0;
1042
+ background-image: linear-gradient(
1043
+ to bottom right,
1044
+ rgba(245, 158, 11, 0.1),
1045
+ transparent,
1046
+ rgba(99, 102, 241, 0.1)
1047
+ );
1048
+ }
1049
+
1050
+ :global(.theme-dark) .ct__sleep {
1051
+ background-color: #18181b;
1052
+ }
1053
+
1054
+ .ct__sleep-inner {
1055
+ display: flex;
1056
+ flex-direction: column;
1057
+ align-items: center;
1058
+ justify-content: space-between;
1059
+ gap: 2.5rem;
1060
+ position: relative;
1061
+ z-index: 1;
1062
+ }
1063
+
1064
+ @media (min-width: 768px) {
1065
+ .ct__sleep-inner {
1066
+ flex-direction: row;
1067
+ }
1068
+ }
1069
+
1070
+ .ct__sleep-controls {
1071
+ display: flex;
1072
+ flex-direction: column;
1073
+ gap: 1.25rem;
1074
+ width: 100%;
1075
+ }
1076
+
1077
+ @media (min-width: 768px) {
1078
+ .ct__sleep-controls {
1079
+ width: 60%;
1080
+ }
1081
+ }
1082
+
1083
+ .ct__sleep-top {
1084
+ display: flex;
1085
+ justify-content: space-between;
1086
+ align-items: center;
1087
+ text-align: left;
1088
+ }
1089
+
1090
+ .ct__sleep-question {
1091
+ font-size: 0.625rem;
1092
+ font-weight: 700;
1093
+ text-transform: uppercase;
1094
+ letter-spacing: 0.2em;
1095
+ color: #94a3b8;
1096
+ }
1097
+
1098
+ .ct__sleep-time {
1099
+ font-size: 0.75rem;
1100
+ color: var(--ct-primary);
1101
+ font-weight: 700;
1102
+ }
1103
+
1104
+ .ct__sleep-input {
1105
+ width: 100%;
1106
+ height: 0.375rem;
1107
+ background: rgba(255, 255, 255, 0.1);
1108
+ border-radius: 9999px;
1109
+ appearance: none;
1110
+ cursor: pointer;
1111
+ accent-color: var(--ct-primary);
1112
+ }
1113
+
1114
+ .ct__sleep-input::-webkit-slider-thumb {
1115
+ appearance: none;
1116
+ width: 18px;
1117
+ height: 18px;
1118
+ background: var(--ct-primary);
1119
+ border-radius: 50%;
1120
+ cursor: pointer;
1121
+ border: 3px solid #0f172a;
1122
+ box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
1123
+ }
1124
+
1125
+ .ct__sleep-result {
1126
+ text-align: center;
1127
+ flex-shrink: 0;
1128
+ }
1129
+
1130
+ @media (min-width: 768px) {
1131
+ .ct__sleep-result {
1132
+ text-align: right;
1133
+ }
1134
+ }
1135
+
1136
+ .ct__sleep-status {
1137
+ padding: 0.25rem 0.75rem;
1138
+ border-radius: 9999px;
1139
+ font-size: 0.5625rem;
1140
+ font-weight: 900;
1141
+ text-transform: uppercase;
1142
+ letter-spacing: 0.1em;
1143
+ margin-bottom: 0.75rem;
1144
+ display: inline-block;
1145
+ border: 1px solid transparent;
1146
+ }
1147
+
1148
+ .ct__sleep-status--optimal {
1149
+ background-color: rgba(16, 185, 129, 0.2);
1150
+ color: #10b981;
1151
+ border-color: rgba(16, 185, 129, 0.3);
1152
+ box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.1);
1153
+ }
1154
+
1155
+ .ct__sleep-status--moderate {
1156
+ background-color: rgba(245, 158, 11, 0.2);
1157
+ color: #f59e0b;
1158
+ border-color: rgba(245, 158, 11, 0.3);
1159
+ box-shadow: 0 10px 15px -3px rgba(245, 158, 11, 0.1);
1160
+ }
1161
+
1162
+ .ct__sleep-status--critical {
1163
+ background-color: rgba(244, 63, 94, 0.2);
1164
+ color: #f43f5e;
1165
+ border-color: rgba(244, 63, 94, 0.3);
1166
+ box-shadow: 0 10px 15px -3px rgba(244, 63, 94, 0.2);
1167
+ transform: scale(1.05);
1168
+ }
1169
+
1170
+ .ct__sleep-mg {
1171
+ font-size: 3rem;
1172
+ font-weight: 900;
1173
+ color: #fff;
1174
+ line-height: 1;
1175
+ letter-spacing: -0.05em;
1176
+ display: flex;
1177
+ align-items: baseline;
1178
+ justify-content: center;
1179
+ gap: 0.25rem;
1180
+ }
1181
+
1182
+ @media (min-width: 768px) {
1183
+ .ct__sleep-mg {
1184
+ justify-content: flex-end;
1185
+ }
1186
+ }
1187
+
1188
+ .ct__sleep-mg-unit {
1189
+ font-size: 0.875rem;
1190
+ font-weight: 400;
1191
+ opacity: 0.4;
1192
+ margin-left: 0.5rem;
1193
+ }
1194
+
1195
+
1196
+ :global(.ct__flyer) {
1197
+ position: fixed;
1198
+ z-index: 100;
1199
+ pointer-events: none;
1200
+ color: var(--ct-primary);
1201
+ display: flex;
1202
+ align-items: center;
1203
+ justify-content: center;
1204
+ }
1205
+
1206
+
1207
+ .ct__icon-cache {
1208
+ display: none;
1209
+ }
1210
+ </style>