@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,225 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { CarbonationUI } from "./index";
4
+ import './component.css';
5
+
6
+ interface Props {
7
+ ui: CarbonationUI;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <carbonation-calculator class="carb-app" data-ui={JSON.stringify(ui)}>
14
+ <div class="carb-card">
15
+ <div class="carb-layout">
16
+ <div class="carb-left">
17
+ <div class="carb-sec">
18
+ <div class="carb-params-deco">
19
+ <Icon name="mdi:beer" class="carb-deco-icon" />
20
+ </div>
21
+
22
+ <h2 class="carb-params-title">
23
+ <span class="carb-params-title-icon">
24
+ <Icon name="mdi:calculator" class="carb-title-svg" />
25
+ </span>
26
+ {ui.parametersTitle}
27
+ </h2>
28
+
29
+ <div class="units-toggle">
30
+ <button id="btnMetric" class="unit-btn unit-btn-active">{ui.metricBtn}</button>
31
+ <button id="btnImperial" class="unit-btn">{ui.imperialBtn}</button>
32
+ </div>
33
+
34
+ <div class="carb-inputs">
35
+ <div class="input-group">
36
+ <label class="input-label">
37
+ {ui.volumeLabel}
38
+ <span id="volUnit" class="input-badge input-badge-amber">{ui.litersUnit}</span>
39
+ </label>
40
+ <input type="number" id="beerVolume" value="20" step="0.5" min="0" class="carb-input" />
41
+ </div>
42
+
43
+ <div class="input-group">
44
+ <label class="input-label">
45
+ {ui.maxTempLabel}
46
+ <span id="tempUnit" class="input-badge input-badge-blue">{ui.celsiusUnit}</span>
47
+ </label>
48
+ <input type="number" id="beerTemp" value="20" step="1" class="carb-input" />
49
+ </div>
50
+
51
+ <div class="co2-section">
52
+ <div class="co2-header">
53
+ <label class="co2-label">{ui.desiredCo2Label}</label>
54
+ <div class="co2-value-wrap">
55
+ <span id="co2ValDisplay" class="co2-value">2.4</span>
56
+ <span class="co2-unit">{ui.volUnit}</span>
57
+ </div>
58
+ </div>
59
+ <input type="range" id="co2Slider" min="1.0" max="4.5" step="0.1" value="2.4" class="co2-slider" />
60
+ <div class="preset-row">
61
+ {['Stout', 'Ale', 'Lager', 'Wheat'].map((s, idx) => (
62
+ <button class="preset-btn" data-val={[1.8, 2.4, 2.6, 3.5][idx]}>
63
+ <div class="preset-tick"></div>
64
+ <span class="preset-label">{s}</span>
65
+ </button>
66
+ ))}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="carb-sec">
73
+ <h3 class="results-title">
74
+ <Icon name="mdi:sack" class="results-title-icon" />
75
+ {ui.resultsTitle}
76
+ </h3>
77
+ <div class="results-list">
78
+ <div class="result-row result-row-primary">
79
+ <span class="result-name">{ui.tableSugarLabel}</span>
80
+ <span id="resTable" class="result-value result-value-primary">0 g</span>
81
+ </div>
82
+ <div class="result-row">
83
+ <span class="result-name result-name-muted">{ui.cornSugarLabel}</span>
84
+ <span id="resCorn" class="result-value">0 g</span>
85
+ </div>
86
+ <div class="result-row">
87
+ <span class="result-name result-name-muted">{ui.dmeLabel}</span>
88
+ <span id="resDME" class="result-value">0 g</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="carb-right">
95
+ <div id="safetyBox" class="safety-box" style="display:none">
96
+ <Icon name="mdi:alert-decagram" class="safety-icon" /> {ui.safetyTitle}
97
+ </div>
98
+
99
+ <div class="beer-visual-wrap">
100
+ <div class="beer-glass">
101
+ <div id="bubbles" class="glass-bubbles"></div>
102
+ <div id="foam" class="glass-foam"></div>
103
+ </div>
104
+ <div class="glass-shine"></div>
105
+ </div>
106
+
107
+ <div class="foam-info">
108
+ <p id="foamText" class="foam-text"></p>
109
+ <p class="foam-hint">{ui.bubblingVisualizationLabel}</p>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </carbonation-calculator>
115
+
116
+ <script>
117
+ import { calculatePrimingSugar } from "./logic";
118
+ import type { CarbonationUI } from "./index";
119
+
120
+ class CarbonationCalculator extends HTMLElement {
121
+ state = { units: 'metric', co2: 2.4, vol: 20, temp: 20 };
122
+ ui = {} as CarbonationUI;
123
+
124
+ constructor() {
125
+ super();
126
+ this.ui = JSON.parse(this.dataset.ui || "{}") as CarbonationUI;
127
+ }
128
+
129
+ connectedCallback() { this.init(); }
130
+
131
+ getEls() {
132
+ const q = <T extends Element>(s: string) => this.querySelector<T>(s);
133
+ return {
134
+ vol: q<HTMLInputElement>("#beerVolume"), temp: q<HTMLInputElement>("#beerTemp"),
135
+ co2: q<HTMLInputElement>("#co2Slider"), mBtn: q<HTMLButtonElement>("#btnMetric"),
136
+ iBtn: q<HTMLButtonElement>("#btnImperial"), vUnit: q<HTMLElement>("#volUnit"),
137
+ tUnit: q<HTMLElement>("#tempUnit"), cDisp: q<HTMLElement>("#co2ValDisplay"),
138
+ rT: q<HTMLElement>("#resTable"), rC: q<HTMLElement>("#resCorn"),
139
+ rD: q<HTMLElement>("#resDME"), safe: q<HTMLElement>("#safetyBox"),
140
+ foam: q<HTMLElement>("#foam"), fText: q<HTMLElement>("#foamText"),
141
+ bubs: q<HTMLElement>("#bubbles"),
142
+ };
143
+ }
144
+
145
+ convertVolTemp(v: number, t: number): { v: number; t: number } {
146
+ if (this.state.units !== 'imperial') return { v, t };
147
+ return { v: v / 0.264172, t: (t - 32) * 5 / 9 };
148
+ }
149
+
150
+ applyResults(els: ReturnType<typeof this.getEls>) {
151
+ if (!els.vol || !els.temp || !els.co2) return;
152
+ const conv = this.convertVolTemp(parseFloat(els.vol.value), parseFloat(els.temp.value));
153
+ const res = calculatePrimingSugar(conv.v, parseFloat(els.co2.value), conv.t);
154
+ if (els.cDisp) els.cDisp.innerText = els.co2.value;
155
+ if (els.rT) els.rT.innerText = res.tableSugar + " g";
156
+ if (els.rC) els.rC.innerText = res.cornSugar + " g";
157
+ if (els.rD) els.rD.innerText = res.dme + " g";
158
+ }
159
+
160
+ getFoamState(co2: number): { text: string; cls: string } {
161
+ if (co2 > 3.5) return { text: this.ui.safetyTitle, cls: "foam-text foam-text-red" };
162
+ if (co2 < 1.8) return { text: this.ui.lowCarbonation, cls: "foam-text foam-text-muted" };
163
+ if (co2 < 2.8) return { text: this.ui.optimalCarbonation, cls: "foam-text foam-text-amber" };
164
+ return { text: this.ui.highEffervescence, cls: "foam-text foam-text-orange" };
165
+ }
166
+
167
+ applyFoam(els: ReturnType<typeof this.getEls>) {
168
+ if (!els.co2 || !els.foam || !els.fText || !els.safe) return;
169
+ const co2 = parseFloat(els.co2.value);
170
+ els.foam.style.height = (20 + (co2 - 1) / 3.5 * 100) + "px";
171
+ els.safe.style.display = co2 > 3.5 ? "flex" : "none";
172
+ const foam = this.getFoamState(co2);
173
+ els.fText.innerText = foam.text; els.fText.className = foam.cls;
174
+ if (els.bubs) this.setBubs(co2, els.bubs);
175
+ }
176
+
177
+ init() {
178
+ const els = this.getEls();
179
+ const update = () => { this.applyResults(els); this.applyFoam(els); };
180
+ if (els.vol) els.vol.oninput = update;
181
+ if (els.temp) els.temp.oninput = update;
182
+ if (els.co2) els.co2.oninput = update;
183
+ if (els.mBtn) els.mBtn.onclick = () => this.switchMetric(els);
184
+ if (els.iBtn) els.iBtn.onclick = () => this.switchImperial(els);
185
+ this.querySelectorAll<HTMLButtonElement>(".preset-btn").forEach(b => {
186
+ b.onclick = () => { if (els.co2) { els.co2.value = b.dataset.val ?? '2.4'; update(); } };
187
+ });
188
+ update();
189
+ }
190
+
191
+ switchMetric(els: ReturnType<typeof this.getEls>) {
192
+ if (this.state.units !== 'imperial' || !els.vol || !els.temp || !els.mBtn || !els.iBtn || !els.vUnit || !els.tUnit) return;
193
+ els.vol.value = (parseFloat(els.vol.value) / 0.264172).toFixed(1);
194
+ els.temp.value = String(Math.round((parseFloat(els.temp.value) - 32) * 5 / 9));
195
+ this.state.units = 'metric';
196
+ els.vUnit.innerText = this.ui.litersUnit; els.tUnit.innerText = this.ui.celsiusUnit;
197
+ els.mBtn.className = "unit-btn unit-btn-active"; els.iBtn.className = "unit-btn";
198
+ this.applyResults(els); this.applyFoam(els);
199
+ }
200
+
201
+ switchImperial(els: ReturnType<typeof this.getEls>) {
202
+ if (this.state.units !== 'metric' || !els.vol || !els.temp || !els.mBtn || !els.iBtn || !els.vUnit || !els.tUnit) return;
203
+ els.vol.value = (parseFloat(els.vol.value) * 0.264172).toFixed(1);
204
+ els.temp.value = String(Math.round(parseFloat(els.temp.value) * 9 / 5 + 32));
205
+ this.state.units = 'imperial';
206
+ els.vUnit.innerText = this.ui.gallonsUnit; els.tUnit.innerText = this.ui.fahrenheitUnit;
207
+ els.iBtn.className = "unit-btn unit-btn-active"; els.mBtn.className = "unit-btn";
208
+ this.applyResults(els); this.applyFoam(els);
209
+ }
210
+
211
+ setBubs(co2: number, container: HTMLElement) {
212
+ if (container.children.length === 0) {
213
+ for (let i = 0; i < 30; i++) {
214
+ const b = document.createElement("div"); b.className = "calc-b";
215
+ const s = 2 + Math.random() * 5; b.style.width = s + "px"; b.style.height = s + "px";
216
+ b.style.left = Math.random() * 100 + "%"; b.style.animationDelay = Math.random() * 5 + "s";
217
+ b.style.setProperty("--s", String(0.7 + Math.random())); container.appendChild(b);
218
+ }
219
+ }
220
+ const dur = 4 * (2 / Math.max(1, co2));
221
+ Array.from(container.children).forEach(b => (b as HTMLElement).style.animationDuration = (dur + Math.random()) + "s");
222
+ }
223
+ }
224
+ customElements.define("carbonation-calculator", CarbonationCalculator);
225
+ </script>
@@ -0,0 +1,483 @@
1
+ .carb-app {
2
+ display: block;
3
+ width: 100%;
4
+ max-width: 72rem;
5
+ margin: 0 auto;
6
+ padding: 0.5rem;
7
+ }
8
+
9
+ .carb-card {
10
+ background: #fff;
11
+ border: 1px solid #e4e4e7;
12
+ border-radius: 1.25rem;
13
+ overflow: clip;
14
+ box-shadow: 0 4px 20px rgba(0,0,0,0.06);
15
+ color: #18181b;
16
+ }
17
+
18
+ .theme-dark .carb-card {
19
+ background: #18181b;
20
+ border-color: #27272a;
21
+ color: #f4f4f5;
22
+ }
23
+
24
+ .carb-layout {
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+
29
+ @media (min-width: 1024px) {
30
+ .carb-layout {
31
+ flex-direction: row;
32
+ align-items: stretch;
33
+ }
34
+ }
35
+
36
+ .carb-left {
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ @media (min-width: 1024px) {
42
+ .carb-left {
43
+ width: 50%;
44
+ border-right: 1px solid #e4e4e7;
45
+ }
46
+ }
47
+
48
+ .theme-dark .carb-left { border-color: #27272a; }
49
+
50
+ .carb-sec {
51
+ padding: 1.5rem;
52
+ border-bottom: 1px solid #e4e4e7;
53
+ position: relative;
54
+ overflow: hidden;
55
+ }
56
+
57
+ .theme-dark .carb-sec { border-color: #27272a; }
58
+ .carb-sec:last-child { border-bottom: none; }
59
+
60
+ .carb-params-deco {
61
+ position: absolute;
62
+ top: 0;
63
+ right: 0;
64
+ padding: 1rem;
65
+ opacity: 0.07;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .carb-deco-icon {
70
+ width: 5rem;
71
+ height: 5rem;
72
+ color: #f59e0b;
73
+ transform: rotate(12deg);
74
+ }
75
+
76
+ .carb-params-title {
77
+ font-size: 1.5rem;
78
+ font-weight: 900;
79
+ margin: 0 0 1.25rem;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 0.75rem;
83
+ }
84
+
85
+ .carb-params-title-icon {
86
+ background: #f59e0b;
87
+ border-radius: 0.75rem;
88
+ padding: 0.5rem;
89
+ color: #fff;
90
+ box-shadow: 0 4px 12px rgba(245,158,11,0.3);
91
+ display: flex;
92
+ }
93
+
94
+ .carb-title-svg {
95
+ width: 1.25rem;
96
+ height: 1.25rem;
97
+ }
98
+
99
+ .units-toggle {
100
+ display: flex;
101
+ background: #f4f4f5;
102
+ padding: 0.25rem;
103
+ border-radius: 0.75rem;
104
+ width: fit-content;
105
+ margin-bottom: 1.25rem;
106
+ }
107
+
108
+ .theme-dark .units-toggle { background: #27272a; }
109
+
110
+ .unit-btn {
111
+ padding: 0.4rem 1.25rem;
112
+ border-radius: 0.5rem;
113
+ font-size: 0.875rem;
114
+ font-weight: 700;
115
+ border: none;
116
+ cursor: pointer;
117
+ transition: all 0.2s;
118
+ color: #71717a;
119
+ background: transparent;
120
+ }
121
+
122
+ .theme-dark .unit-btn { color: #a1a1aa; }
123
+ .unit-btn:hover { color: #18181b; }
124
+ .theme-dark .unit-btn:hover { color: #e4e4e7; }
125
+
126
+ .unit-btn-active {
127
+ background: #fff;
128
+ color: #18181b;
129
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
130
+ }
131
+
132
+ .theme-dark .unit-btn-active {
133
+ background: #3f3f46;
134
+ color: #fff;
135
+ }
136
+
137
+ .carb-inputs {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 1.25rem;
141
+ position: relative;
142
+ z-index: 1;
143
+ }
144
+
145
+ .input-group {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 0.5rem;
149
+ }
150
+
151
+ .input-label {
152
+ display: flex;
153
+ justify-content: space-between;
154
+ align-items: center;
155
+ font-size: 0.875rem;
156
+ font-weight: 700;
157
+ color: #71717a;
158
+ text-transform: uppercase;
159
+ letter-spacing: 0.05em;
160
+ }
161
+
162
+ .theme-dark .input-label { color: #a1a1aa; }
163
+
164
+ .input-badge {
165
+ padding: 0.125rem 0.5rem;
166
+ border-radius: 0.25rem;
167
+ font-size: 0.75rem;
168
+ }
169
+
170
+ .input-badge-amber {
171
+ background: #fef3c7;
172
+ color: #b45309;
173
+ }
174
+
175
+ .theme-dark .input-badge-amber {
176
+ background: rgba(245,158,11,0.2);
177
+ color: #fbbf24;
178
+ }
179
+
180
+ .input-badge-blue {
181
+ background: #dbeafe;
182
+ color: #1d4ed8;
183
+ }
184
+
185
+ .theme-dark .input-badge-blue {
186
+ background: rgba(59,130,246,0.2);
187
+ color: #60a5fa;
188
+ }
189
+
190
+ .carb-input {
191
+ width: 100%;
192
+ background: #fafafa;
193
+ border: 2px solid #e4e4e7;
194
+ border-radius: 0.75rem;
195
+ padding: 0.875rem 1rem;
196
+ font-size: 1.125rem;
197
+ font-weight: 700;
198
+ outline: none;
199
+ transition: border-color 0.2s;
200
+ box-sizing: border-box;
201
+ }
202
+
203
+ .theme-dark .carb-input {
204
+ background: rgba(0,0,0,0.2);
205
+ border-color: #3f3f46;
206
+ color: #f4f4f5;
207
+ }
208
+
209
+ .carb-input:focus { border-color: #f59e0b; }
210
+
211
+ .co2-section {
212
+ padding-top: 1.25rem;
213
+ border-top: 1px dashed #e4e4e7;
214
+ }
215
+
216
+ .theme-dark .co2-section { border-color: #3f3f46; }
217
+
218
+ .co2-header {
219
+ display: flex;
220
+ justify-content: space-between;
221
+ align-items: flex-end;
222
+ margin-bottom: 1rem;
223
+ }
224
+
225
+ .co2-label {
226
+ font-weight: 700;
227
+ font-size: 0.875rem;
228
+ color: #71717a;
229
+ text-transform: uppercase;
230
+ letter-spacing: 0.05em;
231
+ }
232
+
233
+ .theme-dark .co2-label { color: #a1a1aa; }
234
+ .co2-value-wrap { text-align: right; }
235
+
236
+ .co2-value {
237
+ font-size: 2rem;
238
+ font-weight: 900;
239
+ color: #f59e0b;
240
+ line-height: 1;
241
+ }
242
+
243
+ .co2-unit {
244
+ font-size: 0.75rem;
245
+ font-weight: 700;
246
+ color: #a1a1aa;
247
+ text-transform: uppercase;
248
+ margin-left: 0.25rem;
249
+ }
250
+
251
+ .co2-slider {
252
+ width: 100%;
253
+ height: 1rem;
254
+ background: #e4e4e7;
255
+ border-radius: 9999px;
256
+ appearance: none;
257
+ cursor: pointer;
258
+ accent-color: #f59e0b;
259
+ }
260
+
261
+ .theme-dark .co2-slider { background: #3f3f46; }
262
+
263
+ .preset-row {
264
+ display: flex;
265
+ justify-content: space-between;
266
+ margin-top: 0.75rem;
267
+ padding: 0 0.25rem;
268
+ }
269
+
270
+ .preset-btn {
271
+ display: flex;
272
+ flex-direction: column;
273
+ align-items: center;
274
+ background: none;
275
+ border: none;
276
+ cursor: pointer;
277
+ opacity: 0.5;
278
+ transition: opacity 0.2s;
279
+ padding: 0;
280
+ }
281
+
282
+ .preset-btn:hover { opacity: 1; }
283
+
284
+ .preset-tick {
285
+ width: 4px;
286
+ height: 10px;
287
+ background: #d4d4d8;
288
+ margin-bottom: 0.25rem;
289
+ }
290
+
291
+ .preset-label {
292
+ font-size: 0.75rem;
293
+ font-weight: 700;
294
+ color: #71717a;
295
+ }
296
+
297
+ .results-title {
298
+ font-size: 1.125rem;
299
+ font-weight: 700;
300
+ margin: 0 0 1.25rem;
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 0.75rem;
304
+ }
305
+
306
+ .results-title-icon {
307
+ width: 1.25rem;
308
+ height: 1.25rem;
309
+ color: #a1a1aa;
310
+ }
311
+
312
+ .results-list {
313
+ display: flex;
314
+ flex-direction: column;
315
+ gap: 0.75rem;
316
+ }
317
+
318
+ .result-row {
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: space-between;
322
+ padding: 0.875rem;
323
+ border-radius: 0.75rem;
324
+ border: 1px solid #f4f4f5;
325
+ }
326
+
327
+ .theme-dark .result-row { border-color: #27272a; }
328
+
329
+ .result-row-primary {
330
+ background: #fafafa;
331
+ border-color: #e4e4e7;
332
+ }
333
+
334
+ .theme-dark .result-row-primary {
335
+ background: rgba(0,0,0,0.2);
336
+ border-color: #3f3f46;
337
+ }
338
+
339
+ .result-name { font-weight: 700; }
340
+
341
+ .result-name-muted {
342
+ font-weight: 500;
343
+ color: #71717a;
344
+ }
345
+
346
+ .theme-dark .result-name-muted { color: #a1a1aa; }
347
+ .result-value {
348
+ font-size: 1.125rem;
349
+ font-weight: 700;
350
+ }
351
+
352
+ .result-value-primary {
353
+ font-size: 1.375rem;
354
+ font-weight: 900;
355
+ color: #4f46e5;
356
+ }
357
+
358
+ .theme-dark .result-value-primary { color: #818cf8; }
359
+
360
+ .carb-right {
361
+ display: flex;
362
+ flex-direction: column;
363
+ align-items: center;
364
+ justify-content: center;
365
+ padding: 1.5rem;
366
+ background: #fafafa;
367
+ }
368
+
369
+ @media (min-width: 1024px) {
370
+ .carb-right { width: 50%; }
371
+ }
372
+
373
+ .theme-dark .carb-right { background: rgba(0,0,0,0.15); }
374
+
375
+ .safety-box {
376
+ width: 100%;
377
+ margin-bottom: 1.25rem;
378
+ background: #ef4444;
379
+ color: #fff;
380
+ padding: 0.75rem 1.25rem;
381
+ border-radius: 0.75rem;
382
+ box-shadow: 0 4px 12px rgba(239,68,68,0.3);
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
386
+ gap: 0.75rem;
387
+ font-weight: 700;
388
+ text-transform: uppercase;
389
+ animation: bounce 1s infinite;
390
+ }
391
+
392
+ .safety-icon {
393
+ width: 1.25rem;
394
+ height: 1.25rem;
395
+ }
396
+
397
+ @keyframes bounce {
398
+ 0%, 100% { transform: translateY(0); }
399
+ 50% { transform: translateY(-4px); }
400
+ }
401
+
402
+ @keyframes calc-rise {
403
+ 0% {
404
+ transform: translateY(100%) scale(0.5);
405
+ opacity: 0;
406
+ }
407
+
408
+ 20% { opacity: 0.8; }
409
+
410
+ 100% {
411
+ transform: translateY(-120%) scale(var(--s, 1));
412
+ opacity: 0;
413
+ }
414
+ }
415
+
416
+ .calc-b {
417
+ position: absolute;
418
+ background: radial-gradient(circle at 30% 30%, #fff, rgba(255,255,255,0.2));
419
+ border-radius: 50%;
420
+ animation: calc-rise linear infinite;
421
+ bottom: -20px;
422
+ }
423
+
424
+ .beer-visual-wrap {
425
+ position: relative;
426
+ width: 240px;
427
+ height: 400px;
428
+ }
429
+
430
+ .beer-glass {
431
+ position: absolute;
432
+ inset: 0;
433
+ background: linear-gradient(to bottom, #fbbf24, #d97706, #78350f);
434
+ border-radius: 0 0 2rem 2rem;
435
+ z-index: 1;
436
+ clip-path: polygon(5% 0, 95% 0, 85% 100%, 15% 100%);
437
+ }
438
+
439
+ .glass-bubbles {
440
+ position: absolute;
441
+ inset: 0;
442
+ opacity: 0.5;
443
+ overflow: hidden;
444
+ }
445
+
446
+ .glass-foam {
447
+ position: absolute;
448
+ top: 0;
449
+ width: 100%;
450
+ background: #fcf5e5;
451
+ transition: height 0.5s;
452
+ height: 20px;
453
+ }
454
+
455
+ .glass-shine {
456
+ position: absolute;
457
+ inset: 0;
458
+ border: 4px solid rgba(255,255,255,0.2);
459
+ border-bottom-width: 4px;
460
+ border-radius: 0 0 2rem 2rem;
461
+ z-index: 2;
462
+ clip-path: polygon(0 0, 100% 0, 90% 100%, 10% 100%);
463
+ }
464
+
465
+ .foam-info {
466
+ margin-top: 1.5rem;
467
+ text-align: center;
468
+ }
469
+
470
+ .foam-text {
471
+ font-size: 1.25rem;
472
+ font-weight: 900;
473
+ text-transform: uppercase;
474
+ letter-spacing: 0.1em;
475
+ opacity: 0.8;
476
+ margin: 0 0 0.5rem;
477
+ }
478
+
479
+ .foam-hint {
480
+ color: #a1a1aa;
481
+ font-size: 0.875rem;
482
+ margin: 0;
483
+ }