@jjlmoya/utils-alcohol 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 (62) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +19 -0
  3. package/src/category/i18n/es.ts +28 -0
  4. package/src/category/i18n/fr.ts +19 -0
  5. package/src/category/index.ts +12 -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 +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +19 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +155 -0
  14. package/src/pages/[locale].astro +271 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/content_mandatory.test.ts +32 -0
  17. package/src/tests/faq_count.test.ts +17 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/seo_length.test.ts +39 -0
  20. package/src/tests/tool_validation.test.ts +17 -0
  21. package/src/tool/alcoholClearance/component.astro +219 -0
  22. package/src/tool/alcoholClearance/component.css +369 -0
  23. package/src/tool/alcoholClearance/i18n/en.ts +172 -0
  24. package/src/tool/alcoholClearance/i18n/es.ts +181 -0
  25. package/src/tool/alcoholClearance/i18n/fr.ts +163 -0
  26. package/src/tool/alcoholClearance/index.ts +50 -0
  27. package/src/tool/alcoholClearance/logic.ts +59 -0
  28. package/src/tool/beerCooler/component.astro +236 -0
  29. package/src/tool/beerCooler/component.css +381 -0
  30. package/src/tool/beerCooler/i18n/en.ts +168 -0
  31. package/src/tool/beerCooler/i18n/es.ts +181 -0
  32. package/src/tool/beerCooler/i18n/fr.ts +168 -0
  33. package/src/tool/beerCooler/index.ts +49 -0
  34. package/src/tool/beerCooler/logic.ts +34 -0
  35. package/src/tool/carbonationCalculator/component.astro +225 -0
  36. package/src/tool/carbonationCalculator/component.css +483 -0
  37. package/src/tool/carbonationCalculator/i18n/en.ts +175 -0
  38. package/src/tool/carbonationCalculator/i18n/es.ts +179 -0
  39. package/src/tool/carbonationCalculator/i18n/fr.ts +175 -0
  40. package/src/tool/carbonationCalculator/index.ts +48 -0
  41. package/src/tool/carbonationCalculator/logic.ts +40 -0
  42. package/src/tool/cocktailBalancer/bibliography.astro +14 -0
  43. package/src/tool/cocktailBalancer/component.astro +396 -0
  44. package/src/tool/cocktailBalancer/component.css +1218 -0
  45. package/src/tool/cocktailBalancer/data/IngredientRepository.ts +83 -0
  46. package/src/tool/cocktailBalancer/data/Presets.ts +122 -0
  47. package/src/tool/cocktailBalancer/domain/Ingredient.ts +29 -0
  48. package/src/tool/cocktailBalancer/i18n/en.ts +193 -0
  49. package/src/tool/cocktailBalancer/i18n/es.ts +193 -0
  50. package/src/tool/cocktailBalancer/i18n/fr.ts +193 -0
  51. package/src/tool/cocktailBalancer/index.ts +68 -0
  52. package/src/tool/cocktailBalancer/logic.ts +118 -0
  53. package/src/tool/cocktailBalancer/seo.astro +53 -0
  54. package/src/tool/partyKeg/component.astro +269 -0
  55. package/src/tool/partyKeg/component.css +660 -0
  56. package/src/tool/partyKeg/i18n/en.ts +162 -0
  57. package/src/tool/partyKeg/i18n/es.ts +166 -0
  58. package/src/tool/partyKeg/i18n/fr.ts +162 -0
  59. package/src/tool/partyKeg/index.ts +46 -0
  60. package/src/tool/partyKeg/logic.ts +36 -0
  61. package/src/tools.ts +14 -0
  62. package/src/types.ts +72 -0
@@ -0,0 +1,236 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { BeerCoolerUI } from "./index";
4
+ import './component.css';
5
+
6
+ interface Props {
7
+ ui: BeerCoolerUI;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="beer-app" id="beer-calculator">
14
+ <div class="beer-card">
15
+ <div class="beer-grid">
16
+ <div class="beer-steps-panel">
17
+ <div class="beer-sec">
18
+ <h3 class="step-header">
19
+ <span class="step-number">1</span>
20
+ {ui.step1Title}
21
+ </h3>
22
+ <div class="selector-grid">
23
+ <button class="selector-card" data-group="container" data-value="can">
24
+ <div class="selector-content">
25
+ <Icon name="mdi:beer-outline" class="icon-el" />
26
+ <div class="selector-label">{ui.canLabel}</div>
27
+ <div class="selector-sub">{ui.aluminumLabel}</div>
28
+ </div>
29
+ <div class="bg-el"></div>
30
+ </button>
31
+ <button class="selector-card" data-group="container" data-value="bottle">
32
+ <div class="selector-content">
33
+ <Icon name="mdi:bottle-soda-classic-outline" class="icon-el" />
34
+ <div class="selector-label">{ui.bottleLabel}</div>
35
+ <div class="selector-sub">{ui.glassLabel}</div>
36
+ </div>
37
+ <div class="bg-el"></div>
38
+ </button>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="beer-sec">
43
+ <h3 class="step-header">
44
+ <span class="step-number">2</span>
45
+ {ui.step2Title}
46
+ </h3>
47
+ <div class="selector-grid">
48
+ <button class="selector-card" data-group="location" data-value="fridge">
49
+ <div class="selector-content">
50
+ <Icon name="mdi:fridge-outline" class="icon-el" />
51
+ <div class="selector-label">{ui.fridgeLabel}</div>
52
+ <div class="selector-sub">4°C</div>
53
+ </div>
54
+ <div class="bg-el"></div>
55
+ </button>
56
+ <button class="selector-card" data-group="location" data-value="freezer">
57
+ <div class="selector-content">
58
+ <Icon name="mdi:snowflake" class="icon-el" />
59
+ <div class="selector-label">{ui.freezerLabel}</div>
60
+ <div class="selector-sub">-18°C</div>
61
+ </div>
62
+ <div class="bg-el"></div>
63
+ </button>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="beer-sec">
68
+ <h3 class="step-header">
69
+ <span class="step-number">3</span>
70
+ {ui.step3Title}
71
+ </h3>
72
+ <div class="slider-row">
73
+ <div class="slider-label-row">
74
+ <label class="slider-label">{ui.initialTempLabel}</label>
75
+ <span id="display-temp-start" class="temp-display">20°C</span>
76
+ </div>
77
+ <input type="range" id="temp-start" min="15" max="35" value="20" class="slider-custom slider-dark" />
78
+ </div>
79
+ <div class="slider-row slider-row-last">
80
+ <div class="slider-label-row">
81
+ <label class="slider-label">{ui.targetTempLabel}</label>
82
+ <span id="display-temp-target" class="temp-display">4°C</span>
83
+ </div>
84
+ <input type="range" id="temp-target" min="1" max="8" value="4" step="0.5" class="slider-custom slider-blue" />
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="beer-result-panel">
90
+ <div id="final-card" class="result-inner">
91
+ <div id="empty-state-vis" class="empty-state">
92
+ <div class="empty-icon">
93
+ <Icon name="mdi:timer-sand" class="empty-icon-svg" />
94
+ </div>
95
+ <div class="empty-text">
96
+ <h3 class="empty-title">{ui.readyTitle}</h3>
97
+ <p class="empty-desc">{ui.readyDescription}</p>
98
+ </div>
99
+ </div>
100
+ <div id="result-state" class="result-content" style="display:none">
101
+ <div class="result-time-wrap">
102
+ <div class="result-glow"></div>
103
+ <span id="result-time" class="result-time-num">--</span>
104
+ <span class="result-time-unit">min</span>
105
+ </div>
106
+ <div class="result-labels">
107
+ <p class="result-est-label">{ui.estimatedTimeLabel}</p>
108
+ <p id="finish-time" class="result-finish-time">--:--</p>
109
+ </div>
110
+ <div class="result-meta-grid">
111
+ <div class="result-meta-item">
112
+ <div class="result-meta-label">{ui.methodLabel}</div>
113
+ <div class="result-meta-value" id="res-method">-</div>
114
+ </div>
115
+ <div class="result-meta-item">
116
+ <div class="result-meta-label">{ui.containerLabel}</div>
117
+ <div class="result-meta-value" id="res-container">-</div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <div class="bubbles-layer">
122
+ <div id="bubbles-container"></div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <div style="display:none" id="ui-labels" data-ui={JSON.stringify(ui)}></div>
130
+ </div>
131
+
132
+
133
+ <script>
134
+ import { calculateCoolingTime, getFinishTime } from "./logic";
135
+ import type { ContainerType } from "./logic";
136
+ import type { BeerCoolerUI } from "./index";
137
+
138
+ type LocationType = 'fridge' | 'freezer';
139
+
140
+ class BeerApp {
141
+ state = { container: null as ContainerType | null, location: null as LocationType | null, tempStart: 20, tempTarget: 4 };
142
+ ui = {} as BeerCoolerUI;
143
+
144
+ constructor() {
145
+ const uiEl = document.getElementById("ui-labels");
146
+ if (uiEl) this.ui = JSON.parse(uiEl.dataset.ui || "{}") as BeerCoolerUI;
147
+ this.init();
148
+ }
149
+
150
+ init() {
151
+ document.querySelectorAll<HTMLButtonElement>(".selector-card").forEach(btn => {
152
+ btn.onclick = () => this.onSelectCard(btn);
153
+ });
154
+ const startEl = document.getElementById("temp-start") as HTMLInputElement | null;
155
+ const targetEl = document.getElementById("temp-target") as HTMLInputElement | null;
156
+ if (startEl) startEl.oninput = () => this.onTempStart(startEl);
157
+ if (targetEl) targetEl.oninput = () => this.onTempTarget(targetEl);
158
+ this.createBubbles();
159
+ }
160
+
161
+ onSelectCard(btn: HTMLButtonElement) {
162
+ const group = btn.dataset.group ?? '';
163
+ const val = btn.dataset.value ?? null;
164
+ if (group === 'container') this.state.container = val as ContainerType | null;
165
+ if (group === 'location') this.state.location = val as LocationType | null;
166
+ document.querySelectorAll(`button[data-group="${group}"]`).forEach(b => b.classList.toggle("selected", b === btn));
167
+ this.update();
168
+ }
169
+
170
+ onTempStart(el: HTMLInputElement) {
171
+ this.state.tempStart = parseFloat(el.value);
172
+ const disp = document.getElementById("display-temp-start");
173
+ if (disp) disp.innerText = `${this.state.tempStart}°C`;
174
+ this.update();
175
+ }
176
+
177
+ onTempTarget(el: HTMLInputElement) {
178
+ this.state.tempTarget = parseFloat(el.value);
179
+ const disp = document.getElementById("display-temp-target");
180
+ if (disp) disp.innerText = `${this.state.tempTarget}°C`;
181
+ this.update();
182
+ }
183
+
184
+ update() {
185
+ const empty = document.getElementById("empty-state-vis");
186
+ const result = document.getElementById("result-state");
187
+ if (!empty || !result) return;
188
+ if (!this.state.container || !this.state.location) {
189
+ empty.style.display = "flex";
190
+ result.style.display = "none";
191
+ return;
192
+ }
193
+ this.showResult(empty, result);
194
+ }
195
+
196
+ showResult(empty: HTMLElement, result: HTMLElement) {
197
+ const mins = calculateCoolingTime(this.state);
198
+ empty.style.display = "none";
199
+ result.style.display = "flex";
200
+ const containerEl = document.getElementById("res-container");
201
+ const methodEl = document.getElementById("res-method");
202
+ if (containerEl) containerEl.innerText = this.ui[`${this.state.container}Label`] || (this.state.container ?? '');
203
+ if (methodEl) methodEl.innerText = this.ui[`${this.state.location}Label`] || (this.state.location ?? '');
204
+ this.setTimeDisplay(mins);
205
+ }
206
+
207
+ setTimeDisplay(mins: number) {
208
+ const timeEl = document.getElementById("result-time");
209
+ const finishEl = document.getElementById("finish-time");
210
+ if (!timeEl || !finishEl) return;
211
+ if (mins === 0) { timeEl.innerText = "0"; finishEl.innerText = this.ui.alreadyColdLabel; }
212
+ else if (mins > 400) { timeEl.innerText = "∞"; finishEl.innerText = this.ui.neverColdLabel; }
213
+ else { timeEl.innerText = String(mins); finishEl.innerText = getFinishTime(mins); }
214
+ }
215
+
216
+ createBubbles() {
217
+ const container = document.getElementById("bubbles-container");
218
+ if (!container) return;
219
+ for (let i = 0; i < 20; i++) {
220
+ const b = document.createElement("div");
221
+ b.style.cssText = `position:absolute;background:rgba(255,255,255,0.1);border-radius:50%;width:${Math.random()*6+2}px;height:${Math.random()*6+2}px;left:${Math.random()*100}%;animation:rise ${Math.random()*5+3}s infinite ease-in ${Math.random()*5}s`;
222
+ container.appendChild(b);
223
+ }
224
+ this.injectBubbleStyle();
225
+ }
226
+
227
+ injectBubbleStyle() {
228
+ if (document.getElementById("bubble-style")) return;
229
+ const s = document.createElement("style");
230
+ s.id = "bubble-style";
231
+ s.innerHTML = "@keyframes rise { 0% { transform: translateY(100vh) scale(0); opacity: 0; } 50% { opacity: 0.5; } 100% { transform: translateY(-100px) scale(1); opacity: 0; } }";
232
+ document.head.appendChild(s);
233
+ }
234
+ }
235
+ new BeerApp();
236
+ </script>
@@ -0,0 +1,381 @@
1
+ .beer-app {
2
+ width: 100%;
3
+ max-width: 72rem;
4
+ margin: 0 auto;
5
+ padding: 0.5rem;
6
+ }
7
+
8
+ .beer-card {
9
+ background: #fff;
10
+ border: 1px solid #e2e8f0;
11
+ border-radius: 1.25rem;
12
+ overflow: clip;
13
+ box-shadow: 0 4px 20px rgba(0,0,0,0.06);
14
+ color: #1e293b;
15
+ }
16
+ .theme-dark .beer-card {
17
+ background: #0f172a;
18
+ border-color: #1e293b;
19
+ color: #f1f5f9;
20
+ }
21
+
22
+ .beer-grid { display: grid; }
23
+
24
+ @media (min-width: 1024px) {
25
+ .beer-grid {
26
+ grid-template-columns: 1fr 1fr;
27
+ align-items: stretch;
28
+ }
29
+ }
30
+
31
+ .beer-steps-panel {
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+
36
+ .beer-sec {
37
+ padding: 1.25rem 1.5rem;
38
+ border-bottom: 1px solid #e2e8f0;
39
+ }
40
+ .theme-dark .beer-sec { border-color: #1e293b; }
41
+ .beer-sec:last-child { border-bottom: none; }
42
+
43
+ .step-header {
44
+ font-size: 1rem;
45
+ font-weight: 700;
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 0.75rem;
49
+ margin: 0 0 1rem;
50
+ color: #1e293b;
51
+ }
52
+
53
+ .theme-dark .step-header { color: #f1f5f9; }
54
+ .step-number {
55
+ width: 1.75rem;
56
+ height: 1.75rem;
57
+ border-radius: 50%;
58
+ background: #0f172a;
59
+ color: #fff;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ font-size: 0.75rem;
64
+ flex-shrink: 0;
65
+ }
66
+ .theme-dark .step-number {
67
+ background: #fff;
68
+ color: #0f172a;
69
+ }
70
+
71
+ .selector-grid {
72
+ display: grid;
73
+ grid-template-columns: 1fr 1fr;
74
+ gap: 0.75rem;
75
+ }
76
+
77
+ .selector-card {
78
+ width: 100%;
79
+ text-align: left;
80
+ padding: 1rem;
81
+ border-radius: 0.75rem;
82
+ border: 2px solid #e2e8f0;
83
+ transition: border-color 0.2s;
84
+ position: relative;
85
+ overflow: hidden;
86
+ background: #f8fafc;
87
+ cursor: pointer;
88
+ }
89
+ .theme-dark .selector-card {
90
+ border-color: #1e293b;
91
+ background: #0c1527;
92
+ }
93
+ .selector-card:hover { border-color: #cbd5e1; }
94
+ .theme-dark .selector-card:hover { border-color: #334155; }
95
+
96
+ .selector-content {
97
+ position: relative;
98
+ z-index: 1;
99
+ display: flex;
100
+ flex-direction: column;
101
+ align-items: center;
102
+ gap: 0.5rem;
103
+ }
104
+ .icon-el {
105
+ width: 2rem;
106
+ height: 2rem;
107
+ color: #94a3b8;
108
+ transition: color 0.2s;
109
+ }
110
+ .selector-card:hover .icon-el { color: #f59e0b; }
111
+ .selector-card[data-value="bottle"]:hover .icon-el { color: #10b981; }
112
+ .selector-card[data-value="fridge"]:hover .icon-el { color: #06b6d4; }
113
+ .selector-card[data-value="freezer"]:hover .icon-el { color: #3b82f6; }
114
+
115
+ .selector-label {
116
+ font-weight: 700;
117
+ font-size: 1rem;
118
+ color: #1e293b;
119
+ }
120
+
121
+ .theme-dark .selector-label { color: #f1f5f9; }
122
+
123
+ .selector-sub {
124
+ font-size: 0.7rem;
125
+ color: #64748b;
126
+ font-weight: 500;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.05em;
129
+ }
130
+
131
+ .theme-dark .selector-sub { color: #94a3b8; }
132
+
133
+ .bg-el {
134
+ position: absolute;
135
+ inset: 0;
136
+ opacity: 0;
137
+ transition: opacity 0.2s;
138
+ background: rgba(245,158,11,0.08);
139
+ }
140
+ .selector-card[data-value="bottle"] .bg-el { background: rgba(16,185,129,0.08); }
141
+ .selector-card[data-value="fridge"] .bg-el { background: rgba(6,182,212,0.08); }
142
+ .selector-card[data-value="freezer"] .bg-el { background: rgba(59,130,246,0.08); }
143
+
144
+ .selector-card.selected[data-value="can"] { border-color: #f59e0b; }
145
+ .selector-card.selected[data-value="bottle"] { border-color: #10b981; }
146
+ .selector-card.selected[data-value="fridge"] { border-color: #06b6d4; }
147
+ .selector-card.selected[data-value="freezer"] { border-color: #3b82f6; }
148
+ .selector-card.selected .bg-el { opacity: 1; }
149
+
150
+ .slider-row { margin-bottom: 1.25rem; }
151
+ .slider-row-last { margin-bottom: 0; }
152
+ .slider-label-row {
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: center;
156
+ margin-bottom: 0.75rem;
157
+ }
158
+ .slider-label {
159
+ font-size: 0.75rem;
160
+ font-weight: 700;
161
+ color: #64748b;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.1em;
164
+ }
165
+
166
+ .theme-dark .slider-label { color: #94a3b8; }
167
+
168
+ .temp-display {
169
+ font-size: 1rem;
170
+ font-weight: 700;
171
+ background: #f1f5f9;
172
+ color: #1e293b;
173
+ padding: 0.2rem 0.6rem;
174
+ border-radius: 0.4rem;
175
+ }
176
+
177
+ .theme-dark .temp-display {
178
+ background: #1e293b;
179
+ color: #f1f5f9;
180
+ }
181
+
182
+ .slider-custom {
183
+ width: 100%;
184
+ height: 0.5rem;
185
+ border-radius: 0.5rem;
186
+ appearance: none;
187
+ cursor: pointer;
188
+ background: #e2e8f0;
189
+ }
190
+ .theme-dark .slider-custom { background: #334155; }
191
+ .slider-dark { accent-color: #0f172a; }
192
+ .theme-dark .slider-dark { accent-color: #fff; }
193
+ .slider-blue { accent-color: #3b82f6; }
194
+ .slider-custom::-webkit-slider-thumb {
195
+ -webkit-appearance: none;
196
+ height: 20px;
197
+ width: 20px;
198
+ border-radius: 50%;
199
+ background: currentcolor;
200
+ cursor: pointer;
201
+ margin-top: -6px;
202
+ }
203
+ .slider-custom::-webkit-slider-runnable-track {
204
+ width: 100%;
205
+ height: 8px;
206
+ background: transparent;
207
+ border-radius: 4px;
208
+ }
209
+
210
+ .beer-result-panel {
211
+ background: linear-gradient(to bottom right, #eff6ff, #eef2ff);
212
+ color: #1e293b;
213
+ display: flex;
214
+ align-items: stretch;
215
+ min-height: 380px;
216
+ }
217
+
218
+ @media (min-width: 1024px) { .beer-result-panel { border-left: 1px solid #e2e8f0; } }
219
+
220
+ .theme-dark .beer-result-panel {
221
+ background: linear-gradient(to bottom right, #1e293b, #020617);
222
+ color: #fff;
223
+ }
224
+
225
+ @media (min-width: 1024px) { .theme-dark .beer-result-panel { border-left-color: #334155; } }
226
+
227
+ .result-inner {
228
+ width: 100%;
229
+ display: flex;
230
+ flex-direction: column;
231
+ align-items: center;
232
+ justify-content: center;
233
+ text-align: center;
234
+ padding: 2rem;
235
+ position: relative;
236
+ z-index: 1;
237
+ }
238
+
239
+ .empty-state {
240
+ display: flex;
241
+ flex-direction: column;
242
+ align-items: center;
243
+ gap: 1.5rem;
244
+ }
245
+ .empty-icon {
246
+ width: 4rem;
247
+ height: 4rem;
248
+ border-radius: 50%;
249
+ background: #e0e7ff;
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ color: #6366f1;
254
+ }
255
+
256
+ .theme-dark .empty-icon {
257
+ background: #1e293b;
258
+ color: #475569;
259
+ }
260
+ .empty-icon-svg {
261
+ width: 2rem;
262
+ height: 2rem;
263
+ }
264
+ .empty-title {
265
+ font-size: 1.25rem;
266
+ font-weight: 700;
267
+ color: #475569;
268
+ margin: 0;
269
+ }
270
+
271
+ .theme-dark .empty-title { color: #64748b; }
272
+
273
+ .empty-desc {
274
+ color: #94a3b8;
275
+ margin: 0;
276
+ font-size: 0.875rem;
277
+ }
278
+
279
+ .theme-dark .empty-desc { color: #475569; }
280
+
281
+ .result-content {
282
+ flex-direction: column;
283
+ align-items: center;
284
+ width: 100%;
285
+ position: relative;
286
+ z-index: 1;
287
+ }
288
+ .result-time-wrap {
289
+ position: relative;
290
+ margin-bottom: 1.5rem;
291
+ }
292
+ .result-glow {
293
+ position: absolute;
294
+ inset: -2rem;
295
+ background: rgba(59,130,246,0.2);
296
+ border-radius: 50%;
297
+ filter: blur(2rem);
298
+ }
299
+ .result-time-num {
300
+ position: relative;
301
+ font-size: 5rem;
302
+ font-weight: 900;
303
+ color: #1e293b;
304
+ letter-spacing: -0.05em;
305
+ line-height: 1;
306
+ }
307
+
308
+ .theme-dark .result-time-num { color: #fff; }
309
+
310
+ .result-time-unit {
311
+ position: absolute;
312
+ right: -1.5rem;
313
+ bottom: 0.75rem;
314
+ font-size: 1.25rem;
315
+ font-weight: 700;
316
+ color: #94a3b8;
317
+ }
318
+
319
+ .result-labels { margin-bottom: 2rem; }
320
+ .result-est-label {
321
+ font-size: 0.75rem;
322
+ font-weight: 700;
323
+ text-transform: uppercase;
324
+ letter-spacing: 0.2em;
325
+ color: #6366f1;
326
+ margin: 0 0 0.5rem;
327
+ }
328
+
329
+ .theme-dark .result-est-label { color: #60a5fa; }
330
+
331
+ .result-finish-time {
332
+ font-size: 1.5rem;
333
+ font-weight: 700;
334
+ color: #1e293b;
335
+ margin: 0;
336
+ }
337
+
338
+ .theme-dark .result-finish-time { color: #fff; }
339
+
340
+ .result-meta-grid {
341
+ display: grid;
342
+ grid-template-columns: 1fr 1fr;
343
+ gap: 0.75rem;
344
+ width: 100%;
345
+ text-align: left;
346
+ }
347
+ .result-meta-item {
348
+ background: rgba(255,255,255,0.7);
349
+ padding: 0.75rem;
350
+ border-radius: 0.5rem;
351
+ border: 1px solid #e2e8f0;
352
+ }
353
+
354
+ .theme-dark .result-meta-item {
355
+ background: rgba(30,41,59,0.5);
356
+ border-color: #334155;
357
+ }
358
+
359
+ .result-meta-label {
360
+ font-size: 0.7rem;
361
+ color: #94a3b8;
362
+ text-transform: uppercase;
363
+ font-weight: 700;
364
+ margin-bottom: 0.25rem;
365
+ }
366
+
367
+ .result-meta-value {
368
+ color: #1e293b;
369
+ font-weight: 700;
370
+ font-size: 0.875rem;
371
+ }
372
+
373
+ .theme-dark .result-meta-value { color: #fff; }
374
+
375
+ .bubbles-layer {
376
+ position: absolute;
377
+ inset: 0;
378
+ overflow: hidden;
379
+ pointer-events: none;
380
+ z-index: 0;
381
+ }