@jjlmoya/utils-aquarium 1.65.0 → 1.67.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 (72) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +2 -0
  5. package/src/layouts/ProductionCategoryPage.astro +184 -184
  6. package/src/mfe/category-ui.ts +34 -34
  7. package/src/tests/qa-test-helpers.ts +32 -0
  8. package/src/tests/qa_bibliography_links.test.ts +48 -0
  9. package/src/tests/qa_claim_evidence.test.ts +69 -0
  10. package/src/tests/qa_logic_reference_coverage.test.ts +46 -0
  11. package/src/tests/qa_runtime_i18n.test.ts +138 -0
  12. package/src/tests/tool_validation.test.ts +5 -3
  13. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/aquarium-calcium-alkalinity-dosing-calculator.css +611 -0
  14. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/bibliography.astro +7 -0
  15. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/bibliography.ts +5 -0
  16. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/component.astro +93 -0
  17. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/controller.ts +140 -0
  18. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/dom-views.ts +246 -0
  19. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/entry.ts +27 -0
  20. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/de.ts +2 -0
  21. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/en.ts +3 -0
  22. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/es.ts +2 -0
  23. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/fr.ts +2 -0
  24. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/id.ts +2 -0
  25. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/it.ts +2 -0
  26. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/ja.ts +2 -0
  27. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/ko.ts +2 -0
  28. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/nl.ts +2 -0
  29. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/pl.ts +2 -0
  30. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/pt.ts +2 -0
  31. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/ru.ts +2 -0
  32. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/shared.ts +63 -0
  33. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/sv.ts +2 -0
  34. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/tr.ts +2 -0
  35. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/translations.ts +155 -0
  36. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/i18n/zh.ts +2 -0
  37. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/index.ts +11 -0
  38. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/logic.test.ts +33 -0
  39. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/logic.ts +112 -0
  40. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/seo.astro +11 -0
  41. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/ui.ts +44 -0
  42. package/src/tool/aquariumCalciumAlkalinityDosingCalculator/view-helpers.ts +68 -0
  43. package/src/tool/aquariumSaltDosingCalculator/aquarium-salt-dosing-calculator.css +503 -0
  44. package/src/tool/aquariumSaltDosingCalculator/bibliography.astro +6 -0
  45. package/src/tool/aquariumSaltDosingCalculator/bibliography.ts +5 -0
  46. package/src/tool/aquariumSaltDosingCalculator/component.astro +36 -0
  47. package/src/tool/aquariumSaltDosingCalculator/controller.ts +104 -0
  48. package/src/tool/aquariumSaltDosingCalculator/dom-views.ts +74 -0
  49. package/src/tool/aquariumSaltDosingCalculator/entry.ts +13 -0
  50. package/src/tool/aquariumSaltDosingCalculator/i18n/de.ts +2 -0
  51. package/src/tool/aquariumSaltDosingCalculator/i18n/en.ts +2 -0
  52. package/src/tool/aquariumSaltDosingCalculator/i18n/es.ts +2 -0
  53. package/src/tool/aquariumSaltDosingCalculator/i18n/fr.ts +2 -0
  54. package/src/tool/aquariumSaltDosingCalculator/i18n/id.ts +2 -0
  55. package/src/tool/aquariumSaltDosingCalculator/i18n/it.ts +2 -0
  56. package/src/tool/aquariumSaltDosingCalculator/i18n/ja.ts +2 -0
  57. package/src/tool/aquariumSaltDosingCalculator/i18n/ko.ts +2 -0
  58. package/src/tool/aquariumSaltDosingCalculator/i18n/nl.ts +2 -0
  59. package/src/tool/aquariumSaltDosingCalculator/i18n/pl.ts +2 -0
  60. package/src/tool/aquariumSaltDosingCalculator/i18n/pt.ts +2 -0
  61. package/src/tool/aquariumSaltDosingCalculator/i18n/ru.ts +2 -0
  62. package/src/tool/aquariumSaltDosingCalculator/i18n/sv.ts +2 -0
  63. package/src/tool/aquariumSaltDosingCalculator/i18n/tr.ts +2 -0
  64. package/src/tool/aquariumSaltDosingCalculator/i18n/zh.ts +2 -0
  65. package/src/tool/aquariumSaltDosingCalculator/index.ts +11 -0
  66. package/src/tool/aquariumSaltDosingCalculator/localized-content.ts +151 -0
  67. package/src/tool/aquariumSaltDosingCalculator/logic.test.ts +46 -0
  68. package/src/tool/aquariumSaltDosingCalculator/logic.ts +85 -0
  69. package/src/tool/aquariumSaltDosingCalculator/seo.astro +11 -0
  70. package/src/tool/aquariumSaltDosingCalculator/storage.ts +19 -0
  71. package/src/tool/aquariumSaltDosingCalculator/ui.ts +47 -0
  72. package/src/tools.ts +12 -0
@@ -0,0 +1,611 @@
1
+ .acad-root {
2
+ --acad-ink: #152c2d;
3
+ --acad-ink-soft: #254647;
4
+ --acad-paper: #f7f0e6;
5
+ --acad-paper-deep: #e9dfd1;
6
+ --acad-teal: #159da3;
7
+ --acad-teal-dark: #08727a;
8
+ --acad-coral: #e87e62;
9
+ --acad-amber: #e7a64c;
10
+ --acad-muted: #5b7070;
11
+ --acad-line: rgba(21, 44, 45, 0.18);
12
+
13
+ display: block;
14
+ color: var(--acad-ink);
15
+ }
16
+
17
+ .acad-card {
18
+ min-width: 0;
19
+ border: 1px solid var(--acad-line);
20
+ border-radius: 1.4rem;
21
+ overflow: hidden;
22
+ box-shadow: 0 1.2rem 2.8rem rgba(15, 35, 36, 0.09);
23
+ }
24
+
25
+ .acad-workspace {
26
+ display: grid;
27
+ grid-template-columns: minmax(0, 1.16fr) minmax(310px, 0.84fr);
28
+ background: var(--acad-paper);
29
+ }
30
+
31
+ .acad-result {
32
+ background: var(--acad-ink);
33
+ color: var(--acad-paper);
34
+ }
35
+
36
+ .acad-controls {
37
+ background: var(--acad-paper);
38
+ border-left: 1px solid var(--acad-line);
39
+ padding-bottom: clamp(1.25rem, 3vw, 1.8rem);
40
+ }
41
+
42
+ .acad-card-head {
43
+ display: flex;
44
+ align-items: flex-start;
45
+ justify-content: space-between;
46
+ gap: 1rem;
47
+ padding: clamp(1.25rem, 3vw, 1.8rem) clamp(1.25rem, 3vw, 1.8rem) 0;
48
+ }
49
+
50
+ .acad-heading-group {
51
+ min-width: 0;
52
+ }
53
+
54
+ .acad-kicker {
55
+ margin: 0 0 0.45rem;
56
+ color: var(--acad-amber);
57
+ font-size: 0.69rem;
58
+ font-weight: 800;
59
+ letter-spacing: 0.14em;
60
+ line-height: 1.2;
61
+ text-transform: uppercase;
62
+ }
63
+
64
+ .acad-controls .acad-kicker {
65
+ color: var(--acad-teal-dark);
66
+ }
67
+
68
+ .acad-card h2 {
69
+ margin: 0;
70
+ font-size: clamp(1.55rem, 3vw, 2.2rem);
71
+ letter-spacing: -0.04em;
72
+ line-height: 0.98;
73
+ }
74
+
75
+ .acad-action {
76
+ flex: 0 0 auto;
77
+ max-width: 9.5rem;
78
+ border: 1px solid rgba(247, 240, 230, 0.28);
79
+ border-radius: 999px;
80
+ padding: 0.52rem 0.72rem;
81
+ color: #f8d59b;
82
+ font-size: 0.72rem;
83
+ font-weight: 700;
84
+ line-height: 1.15;
85
+ text-align: center;
86
+ }
87
+
88
+ .acad-dose-visual {
89
+ position: relative;
90
+ margin: 1.45rem clamp(1.25rem, 3vw, 1.8rem) 0;
91
+ padding: 1.15rem;
92
+ border: 1px solid rgba(247, 240, 230, 0.16);
93
+ border-radius: 1rem;
94
+ background: linear-gradient(145deg, #1f4849, #173637);
95
+ }
96
+
97
+ .acad-dose-visual-head {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.6rem;
101
+ color: #a8cbc7;
102
+ font-size: 0.67rem;
103
+ font-weight: 800;
104
+ letter-spacing: 0.09em;
105
+ text-transform: uppercase;
106
+ }
107
+
108
+ .acad-visual-rule {
109
+ height: 1px;
110
+ flex: 1;
111
+ background: rgba(247, 240, 230, 0.2);
112
+ }
113
+
114
+ .acad-dose-path {
115
+ display: grid;
116
+ grid-template-columns: auto minmax(3rem, 1fr) auto;
117
+ align-items: center;
118
+ gap: 0.65rem;
119
+ margin: 1.45rem 0 1.1rem;
120
+ }
121
+
122
+ .acad-dose-stop {
123
+ display: grid;
124
+ gap: 0.18rem;
125
+ }
126
+
127
+ .acad-stop-dot {
128
+ display: block;
129
+ width: 0.65rem;
130
+ height: 0.65rem;
131
+ margin-bottom: 0.12rem;
132
+ border: 2px solid var(--acad-amber);
133
+ border-radius: 50%;
134
+ background: var(--acad-ink);
135
+ box-shadow: 0 0 0 4px rgba(231, 166, 76, 0.14);
136
+ }
137
+
138
+ .acad-dose-stop-target .acad-stop-dot {
139
+ border-color: var(--acad-teal);
140
+ background: var(--acad-teal);
141
+ box-shadow: 0 0 0 4px rgba(21, 157, 163, 0.15);
142
+ }
143
+
144
+ .acad-dose-stop strong {
145
+ font-size: clamp(1.25rem, 3vw, 1.8rem);
146
+ letter-spacing: -0.06em;
147
+ line-height: 1;
148
+ }
149
+
150
+ .acad-dose-stop small {
151
+ color: #a8cbc7;
152
+ font-size: 0.65rem;
153
+ font-weight: 700;
154
+ white-space: nowrap;
155
+ }
156
+
157
+ .acad-dose-line {
158
+ position: relative;
159
+ height: 2px;
160
+ background: rgba(247, 240, 230, 0.24);
161
+ }
162
+
163
+ .acad-dose-line-fill {
164
+ display: block;
165
+ width: 62%;
166
+ height: 100%;
167
+ background: linear-gradient(90deg, var(--acad-amber), var(--acad-coral));
168
+ }
169
+
170
+ .acad-dose-arrow {
171
+ position: absolute;
172
+ top: 50%;
173
+ right: -0.1rem;
174
+ color: var(--acad-coral);
175
+ font-size: 1.2rem;
176
+ line-height: 1;
177
+ transform: translateY(-58%);
178
+ }
179
+
180
+ .acad-dose-chip {
181
+ display: flex;
182
+ align-items: baseline;
183
+ justify-content: space-between;
184
+ gap: 1rem;
185
+ margin-top: 0.4rem;
186
+ padding: 0.8rem 0.9rem;
187
+ border: 1px solid rgba(247, 240, 230, 0.2);
188
+ border-radius: 0.75rem;
189
+ background: rgba(8, 114, 122, 0.32);
190
+ }
191
+
192
+ .acad-dose-chip span {
193
+ color: #a8cbc7;
194
+ font-size: 0.68rem;
195
+ font-weight: 800;
196
+ letter-spacing: 0.08em;
197
+ text-transform: uppercase;
198
+ }
199
+
200
+ .acad-dose-chip strong {
201
+ color: #fff0ce;
202
+ font-size: 1.7rem;
203
+ letter-spacing: -0.06em;
204
+ line-height: 1;
205
+ }
206
+
207
+ .acad-visual-steps {
208
+ display: flex;
209
+ justify-content: space-between;
210
+ gap: 0.5rem;
211
+ margin-top: 1rem;
212
+ color: #a8cbc7;
213
+ font-size: 0.62rem;
214
+ line-height: 1.2;
215
+ }
216
+
217
+ .acad-visual-steps span {
218
+ display: grid;
219
+ gap: 0.2rem;
220
+ }
221
+ .acad-visual-steps span:nth-child(2) {
222
+ text-align: center;
223
+ }
224
+ .acad-visual-steps span:last-child {
225
+ text-align: right;
226
+ }
227
+ .acad-visual-steps b {
228
+ color: var(--acad-coral);
229
+ font-size: 0.65rem;
230
+ }
231
+
232
+ .acad-dose-visual.is-reduce .acad-dose-line-fill {
233
+ width: 62%;
234
+ background: var(--acad-teal);
235
+ }
236
+ .acad-dose-visual.is-reduce .acad-dose-arrow {
237
+ color: var(--acad-teal);
238
+ transform: translateY(-58%) rotate(180deg);
239
+ }
240
+ .acad-dose-visual.is-target .acad-dose-line-fill {
241
+ width: 100%;
242
+ background: var(--acad-teal);
243
+ }
244
+ .acad-dose-visual.is-target .acad-dose-arrow {
245
+ color: var(--acad-teal);
246
+ }
247
+ .acad-dose-visual.is-target .acad-dose-chip {
248
+ background: rgba(21, 157, 163, 0.22);
249
+ }
250
+
251
+ .acad-answer {
252
+ margin: 1.25rem clamp(1.25rem, 3vw, 1.8rem) 0;
253
+ padding: 1rem 0 1.15rem;
254
+ border-top: 1px solid rgba(247, 240, 230, 0.16);
255
+ border-bottom: 1px solid rgba(247, 240, 230, 0.16);
256
+ }
257
+
258
+ .acad-answer-label {
259
+ display: block;
260
+ color: #a8cbc7;
261
+ font-size: 0.7rem;
262
+ font-weight: 800;
263
+ letter-spacing: 0.1em;
264
+ text-transform: uppercase;
265
+ }
266
+
267
+ .acad-answer strong {
268
+ display: block;
269
+ margin: 0.18rem 0;
270
+ color: #fff0ce;
271
+ font-size: clamp(2.25rem, 6vw, 4rem);
272
+ letter-spacing: -0.08em;
273
+ line-height: 0.95;
274
+ }
275
+
276
+ .acad-answer p {
277
+ max-width: 38rem;
278
+ margin: 0.55rem 0 0;
279
+ color: #d4e2da;
280
+ font-size: 0.87rem;
281
+ line-height: 1.48;
282
+ }
283
+
284
+ .acad-stat-grid {
285
+ display: grid;
286
+ grid-template-columns: repeat(2, 1fr);
287
+ gap: 0.55rem;
288
+ margin: 1rem clamp(1.25rem, 3vw, 1.8rem) 0;
289
+ }
290
+
291
+ .acad-stat {
292
+ min-height: 3.9rem;
293
+ padding: 0.72rem;
294
+ border-radius: 0.75rem;
295
+ background: rgba(247, 240, 230, 0.08);
296
+ }
297
+
298
+ .acad-stat span {
299
+ display: block;
300
+ color: #a8cbc7;
301
+ font-size: 0.68rem;
302
+ }
303
+ .acad-stat strong {
304
+ display: block;
305
+ margin-top: 0.34rem;
306
+ font-size: 0.93rem;
307
+ }
308
+ .acad-stat-feature {
309
+ grid-column: 1 / -1;
310
+ background: rgba(21, 157, 163, 0.25);
311
+ }
312
+
313
+ .acad-note {
314
+ margin: 1rem clamp(1.25rem, 3vw, 1.8rem) clamp(1.25rem, 3vw, 1.8rem);
315
+ color: #b8cfca;
316
+ font-size: 0.74rem;
317
+ line-height: 1.5;
318
+ }
319
+
320
+ .acad-parameter-mark {
321
+ display: grid;
322
+ flex: 0 0 auto;
323
+ place-items: center;
324
+ width: 4.3rem;
325
+ height: 4.3rem;
326
+ border: 1px solid rgba(21, 157, 163, 0.4);
327
+ border-radius: 50%;
328
+ background: rgba(21, 157, 163, 0.12);
329
+ color: var(--acad-teal-dark);
330
+ text-align: center;
331
+ }
332
+
333
+ .acad-parameter-mark span {
334
+ font-size: 1.4rem;
335
+ font-weight: 900;
336
+ letter-spacing: -0.06em;
337
+ line-height: 1;
338
+ }
339
+ .acad-parameter-mark small {
340
+ max-width: 3.7rem;
341
+ font-size: 0.53rem;
342
+ font-weight: 800;
343
+ line-height: 1.1;
344
+ }
345
+
346
+ .acad-parameter-picker {
347
+ display: grid;
348
+ grid-template-columns: 1fr 1fr;
349
+ gap: 0.5rem;
350
+ margin: 1.45rem clamp(1.25rem, 3vw, 1.8rem) 0;
351
+ padding: 0.3rem;
352
+ border: 1px solid var(--acad-line);
353
+ border-radius: 0.9rem;
354
+ background: var(--acad-paper-deep);
355
+ }
356
+
357
+ .acad-parameter-picker button {
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: center;
361
+ gap: 0.45rem;
362
+ min-height: 2.8rem;
363
+ border: 0;
364
+ border-radius: 0.65rem;
365
+ background: transparent;
366
+ color: var(--acad-muted);
367
+ cursor: pointer;
368
+ font: inherit;
369
+ font-size: 0.78rem;
370
+ font-weight: 800;
371
+ }
372
+
373
+ .acad-parameter-picker button span {
374
+ color: var(--acad-teal-dark);
375
+ font-size: 0.7rem;
376
+ font-weight: 900;
377
+ letter-spacing: 0.03em;
378
+ }
379
+
380
+ .acad-parameter-picker button:hover,
381
+ .acad-parameter-picker button:focus-visible {
382
+ outline: 2px solid var(--acad-coral);
383
+ outline-offset: 1px;
384
+ }
385
+ .acad-parameter-picker button.is-active {
386
+ background: var(--acad-ink);
387
+ color: var(--acad-paper);
388
+ }
389
+ .acad-parameter-picker button.is-active span {
390
+ color: var(--acad-amber);
391
+ }
392
+
393
+ .acad-fields {
394
+ display: grid;
395
+ grid-template-columns: repeat(2, minmax(0, 1fr));
396
+ gap: 0.8rem;
397
+ margin: 1.35rem clamp(1.25rem, 3vw, 1.8rem) 0;
398
+ }
399
+
400
+ .acad-field {
401
+ display: grid;
402
+ gap: 0.38rem;
403
+ }
404
+ .acad-field-strength {
405
+ grid-column: 1 / -1;
406
+ }
407
+
408
+ .acad-field span,
409
+ .acad-option-row > div > span {
410
+ font-size: 0.74rem;
411
+ font-weight: 800;
412
+ line-height: 1.25;
413
+ }
414
+ .acad-field small {
415
+ color: var(--acad-teal-dark);
416
+ font-size: 0.67rem;
417
+ font-weight: 800;
418
+ }
419
+
420
+ .acad-field input {
421
+ width: 100%;
422
+ box-sizing: border-box;
423
+ border: 1px solid var(--acad-line);
424
+ border-radius: 0.68rem;
425
+ padding: 0.78rem 0.82rem;
426
+ background: rgba(255, 255, 255, 0.58);
427
+ color: var(--acad-ink);
428
+ font: inherit;
429
+ font-size: 1rem;
430
+ }
431
+
432
+ .acad-field input:focus-visible {
433
+ outline: 2px solid var(--acad-coral);
434
+ outline-offset: 2px;
435
+ }
436
+
437
+ .acad-help {
438
+ margin: 0.65rem clamp(1.25rem, 3vw, 1.8rem) 0;
439
+ color: var(--acad-muted);
440
+ font-size: 0.73rem;
441
+ line-height: 1.45;
442
+ }
443
+
444
+ .acad-option-row {
445
+ display: grid;
446
+ grid-template-columns: 1fr 1fr;
447
+ gap: 0.8rem;
448
+ margin: 1.35rem clamp(1.25rem, 3vw, 1.8rem) 0;
449
+ }
450
+
451
+ .acad-option-row > div > span {
452
+ display: block;
453
+ margin-bottom: 0.4rem;
454
+ color: var(--acad-muted);
455
+ }
456
+
457
+ .acad-toggle {
458
+ display: flex;
459
+ flex-wrap: wrap;
460
+ gap: 0.2rem;
461
+ width: max-content;
462
+ max-width: 100%;
463
+ padding: 0.2rem;
464
+ border: 1px solid var(--acad-line);
465
+ border-radius: 99px;
466
+ background: rgba(255, 255, 255, 0.42);
467
+ }
468
+
469
+ .acad-toggle button {
470
+ border: 0;
471
+ border-radius: 99px;
472
+ padding: 0.46rem 0.62rem;
473
+ background: transparent;
474
+ color: var(--acad-muted);
475
+ cursor: pointer;
476
+ font: inherit;
477
+ font-size: 0.7rem;
478
+ font-weight: 800;
479
+ }
480
+
481
+ .acad-toggle button:hover,
482
+ .acad-toggle button:focus-visible {
483
+ color: var(--acad-ink);
484
+ outline: 2px solid var(--acad-coral);
485
+ outline-offset: 1px;
486
+ }
487
+ .acad-toggle button.is-active {
488
+ background: var(--acad-teal);
489
+ color: #fff;
490
+ }
491
+
492
+ .acad-controls-foot {
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: space-between;
496
+ gap: 0.8rem;
497
+ margin: 1.4rem clamp(1.25rem, 3vw, 1.8rem) 0;
498
+ }
499
+
500
+ .acad-reset {
501
+ border: 1px solid var(--acad-line);
502
+ border-radius: 99px;
503
+ padding: 0.62rem 0.82rem;
504
+ background: transparent;
505
+ color: var(--acad-ink);
506
+ cursor: pointer;
507
+ font: inherit;
508
+ font-size: 0.76rem;
509
+ font-weight: 800;
510
+ }
511
+
512
+ .acad-reset:hover,
513
+ .acad-reset:focus-visible {
514
+ border-color: var(--acad-coral);
515
+ outline: 2px solid var(--acad-coral);
516
+ outline-offset: 2px;
517
+ }
518
+
519
+ .acad-local-badge {
520
+ color: var(--acad-teal-dark);
521
+ font-size: 0.68rem;
522
+ font-weight: 800;
523
+ text-align: right;
524
+ }
525
+
526
+ .acad-error {
527
+ margin: 0.8rem clamp(1.25rem, 3vw, 1.8rem) 0;
528
+ color: #a53e35;
529
+ font-size: 0.76rem;
530
+ font-weight: 700;
531
+ }
532
+
533
+ @media (max-width: 820px) {
534
+ .acad-workspace {
535
+ grid-template-columns: 1fr;
536
+ }
537
+ .acad-controls {
538
+ border-top: 1px solid var(--acad-line);
539
+ border-left: 0;
540
+ }
541
+ }
542
+
543
+ @media (max-width: 480px) {
544
+ .acad-card-head {
545
+ flex-direction: column;
546
+ }
547
+ .acad-action {
548
+ max-width: none;
549
+ text-align: left;
550
+ }
551
+ .acad-fields {
552
+ grid-template-columns: 1fr;
553
+ }
554
+ .acad-field-strength {
555
+ grid-column: auto;
556
+ }
557
+ .acad-option-row {
558
+ grid-template-columns: 1fr;
559
+ }
560
+ .acad-dose-visual-head {
561
+ align-items: flex-start;
562
+ flex-direction: column;
563
+ gap: 0.35rem;
564
+ }
565
+ .acad-visual-rule {
566
+ width: 100%;
567
+ }
568
+ }
569
+
570
+ @media (prefers-color-scheme: dark) {
571
+ .acad-root {
572
+ --acad-ink: #edf2e9;
573
+ --acad-paper: #1d3434;
574
+ --acad-paper-deep: #294343;
575
+ --acad-muted: #c1d3cd;
576
+ --acad-line: rgba(237, 242, 233, 0.22);
577
+ }
578
+ .acad-result {
579
+ background: #102426;
580
+ color: #f7f0e6;
581
+ }
582
+ .acad-controls {
583
+ background: #203838;
584
+ }
585
+ .acad-controls .acad-kicker,
586
+ .acad-field small,
587
+ .acad-parameter-mark,
588
+ .acad-local-badge {
589
+ color: #66cbd0;
590
+ }
591
+ .acad-field input {
592
+ background: rgba(0, 0, 0, 0.2);
593
+ color: var(--acad-ink);
594
+ }
595
+ .acad-parameter-picker {
596
+ background: rgba(0, 0, 0, 0.16);
597
+ }
598
+ .acad-parameter-picker button.is-active {
599
+ background: #edf2e9;
600
+ color: #152c2d;
601
+ }
602
+ .acad-parameter-picker button.is-active span {
603
+ color: var(--acad-teal-dark);
604
+ }
605
+ .acad-toggle {
606
+ background: rgba(0, 0, 0, 0.18);
607
+ }
608
+ .acad-reset {
609
+ color: var(--acad-ink);
610
+ }
611
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ import { bibliography } from './bibliography';
3
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
4
+
5
+ ---
6
+
7
+ <SharedBibliography links={bibliography} />
@@ -0,0 +1,5 @@
1
+ export const bibliography = [
2
+ { name: 'NOAA Ocean Service, Ocean acidification and seawater chemistry', url: 'https://oceanservice.noaa.gov/education/tutorial_chemistry/02oceanchemistry.html' },
3
+ { name: 'USGS Water Science School, Hardness of water', url: 'https://www.usgs.gov/special-topics/water-science-school/science/hardness-water' },
4
+ { name: 'FAO, Manual de acuicultura en español', url: 'https://www.fao.org/4/x5685s/x5685s00.htm' },
5
+ ];
@@ -0,0 +1,93 @@
1
+ ---
2
+ import type { AquariumCalciumAlkalinityDosingCalculatorUI } from './ui';
3
+ import './aquarium-calcium-alkalinity-dosing-calculator.css';
4
+
5
+ const { ui } = Astro.props as { ui: AquariumCalciumAlkalinityDosingCalculatorUI };
6
+ ---
7
+
8
+ <div id="aquarium-calcium-alkalinity-dosing-calculator" class="acad-root" data-ui={JSON.stringify(ui)}>
9
+ <section class="acad-card acad-workspace" aria-label={ui.resultTitle}>
10
+ <section class="acad-result" aria-labelledby="acad-result-title">
11
+ <header class="acad-card-head">
12
+ <div class="acad-heading-group">
13
+ <p class="acad-kicker">{ui.toolKicker}</p>
14
+ <h2 id="acad-result-title">{ui.resultTitle}</h2>
15
+ </div>
16
+ <span class="acad-action" data-output="action">{ui.emptyState}</span>
17
+ </header>
18
+
19
+ <div class="acad-dose-visual" data-output="scene" role="img" aria-label={ui.sceneAdd}>
20
+ <div class="acad-dose-visual-head">
21
+ <span>{ui.currentTargetLabel}</span>
22
+ <span class="acad-visual-rule"></span>
23
+ <span data-output="dose-label">{ui.doseLabel}</span>
24
+ </div>
25
+ <div class="acad-dose-path" aria-hidden="true">
26
+ <div class="acad-dose-stop">
27
+ <span class="acad-stop-dot"></span>
28
+ <strong data-output="current-value">—</strong>
29
+ <small data-output="value-unit">{ui.unitCalcium}</small>
30
+ </div>
31
+ <div class="acad-dose-line"><span class="acad-dose-line-fill"></span><span class="acad-dose-arrow">→</span></div>
32
+ <div class="acad-dose-stop acad-dose-stop-target">
33
+ <span class="acad-stop-dot"></span>
34
+ <strong data-output="target-value">—</strong>
35
+ <small data-output="value-unit-target">{ui.unitCalcium}</small>
36
+ </div>
37
+ </div>
38
+ <div class="acad-dose-chip"><span data-output="dose-label-chip">{ui.doseLabel}</span><strong data-output="dose-chip">—</strong></div>
39
+ <div class="acad-visual-steps"><span><b>01</b>{ui.currentLabel}</span><span><b>02</b>{ui.doseLabel}</span><span><b>03</b>{ui.targetLabel}</span></div>
40
+ </div>
41
+
42
+ <div class="acad-answer">
43
+ <span class="acad-answer-label" data-output="dose-label-answer">{ui.doseLabel}</span>
44
+ <strong data-output="dose">—</strong>
45
+ <p data-output="summary">{ui.emptyState}</p>
46
+ </div>
47
+
48
+ <div class="acad-stat-grid" aria-live="polite">
49
+ <div class="acad-stat acad-stat-feature"><span>{ui.resultActionLabel}</span><strong data-output="action-detail">—</strong></div>
50
+ <div class="acad-stat"><span>{ui.currentTargetLabel}</span><strong data-output="current-target">—</strong></div>
51
+ <div class="acad-stat"><span>{ui.differenceLabel}</span><strong data-output="difference">—</strong></div>
52
+ <div class="acad-stat"><span>{ui.waterChangeLabel}</span><strong data-output="water-change">—</strong></div>
53
+ </div>
54
+ <p class="acad-note">{ui.verificationNote}</p>
55
+ </section>
56
+
57
+ <section class="acad-controls" aria-labelledby="acad-configure-title">
58
+ <header class="acad-card-head">
59
+ <div class="acad-heading-group">
60
+ <p class="acad-kicker">{ui.configureTitle}</p>
61
+ <h2 id="acad-configure-title">{ui.parameterLabel}</h2>
62
+ </div>
63
+ <div class="acad-parameter-mark" aria-hidden="true"><span data-output="parameter-short">{ui.calciumShortLabel}</span><small data-output="parameter-unit">{ui.unitCalcium}</small></div>
64
+ </header>
65
+
66
+ <div class="acad-parameter-picker" role="group" aria-label={ui.parameterLabel}>
67
+ <button type="button" data-parameter-choice="calcium" class="is-active" aria-pressed="true"><span>{ui.calciumShortLabel}</span>{ui.calciumLabel}</button>
68
+ <button type="button" data-parameter-choice="alkalinity" aria-pressed="false"><span>{ui.alkalinityShortLabel}</span>{ui.alkalinityLabel}</button>
69
+ </div>
70
+
71
+ <div class="acad-fields">
72
+ <label class="acad-field"><span>{ui.volumeLabel} <small data-output="volume-unit">{ui.litresLabel}</small></span><input data-field="volume" type="number" min="0.1" max="100000" step="0.1" inputmode="decimal" /></label>
73
+ <label class="acad-field"><span>{ui.currentLabel} <small data-output="value-unit">{ui.unitCalcium}</small></span><input data-field="current" type="number" min="0" max="1000" step="0.1" inputmode="decimal" /></label>
74
+ <label class="acad-field"><span>{ui.targetLabel} <small data-output="value-unit-target">{ui.unitCalcium}</small></span><input data-field="target" type="number" min="0" max="1000" step="0.1" inputmode="decimal" /></label>
75
+ <label class="acad-field acad-field-strength"><span>{ui.productStrengthLabel} <small data-output="strength-unit">{ui.unitCalcium} / {ui.millilitresLabel} / 100 L</small></span><input data-field="strength" type="number" min="0.001" max="10000" step="0.1" inputmode="decimal" aria-describedby="acad-strength-help" /></label>
76
+ </div>
77
+ <p id="acad-strength-help" class="acad-help">{ui.productStrengthHint}</p>
78
+
79
+ <div class="acad-option-row">
80
+ <div><span>{ui.doseUnitLabel}</span><div class="acad-toggle acad-toggle-small" role="group" aria-label={ui.doseUnitLabel}><button type="button" data-dose-unit-choice="ml" class="is-active" aria-pressed="true">{ui.millilitresLabel}</button><button type="button" data-dose-unit-choice="g" aria-pressed="false">{ui.gramsLabel}</button></div></div>
81
+ <div><span>{ui.volumeUnitLabel}</span><div class="acad-toggle acad-toggle-small" role="group" aria-label={ui.volumeUnitLabel}><button type="button" data-volume-unit-choice="litres" class="is-active" aria-pressed="true">{ui.litresLabel}</button><button type="button" data-volume-unit-choice="gallons" aria-pressed="false">{ui.gallonsLabel}</button></div></div>
82
+ </div>
83
+ <div class="acad-controls-foot"><button class="acad-reset" type="button" data-action="reset">{ui.resetLabel}</button><span class="acad-local-badge">{ui.localOnlyLabel}</span></div>
84
+ <p class="acad-error" role="alert" data-error hidden>{ui.invalidState}</p>
85
+ </section>
86
+ </section>
87
+ </div>
88
+
89
+ <script>
90
+ import { startCalciumAlkalinityDosingController } from './controller';
91
+ const root = document.querySelector<HTMLElement>('#aquarium-calcium-alkalinity-dosing-calculator');
92
+ if (root?.dataset.ui) startCalciumAlkalinityDosingController({ root, ui: JSON.parse(root.dataset.ui) });
93
+ </script>