@jjlmoya/utils-books 1.8.0 → 1.10.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 (71) hide show
  1. package/.github/workflows/ci.yml +79 -0
  2. package/.gitignore +1 -0
  3. package/astro.config.mjs +6 -1
  4. package/package.json +18 -5
  5. package/scripts/postbuild.mjs +17 -0
  6. package/scripts/postinstall.mjs +2 -4
  7. package/src/category/index.ts +2 -1
  8. package/src/components/ProductionBreadcrumb.astro +134 -0
  9. package/src/components/ProductionStructuredData.astro +46 -0
  10. package/src/components/ProductionWidget.astro +182 -0
  11. package/src/entries.ts +4 -1
  12. package/src/i18n/header-ui.ts +15 -0
  13. package/src/i18n/language-ui.ts +9 -0
  14. package/src/i18n/languages.ts +24 -0
  15. package/src/identity/brands.ts +5 -0
  16. package/src/index.ts +1 -0
  17. package/src/layouts/ProductionCategoryPage.astro +184 -0
  18. package/src/layouts/ProductionPage.astro +218 -0
  19. package/src/layouts/ProductionUtilityPage.astro +283 -0
  20. package/src/mfe/assets.ts +39 -0
  21. package/src/mfe/category-ui.ts +34 -0
  22. package/src/mfe/manifest.ts +45 -0
  23. package/src/mfe/routes.ts +50 -0
  24. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  25. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  26. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  27. package/src/pages/index.astro +1 -2
  28. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  29. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  30. package/src/pages/utilidades/[slug].astro +90 -0
  31. package/src/pages/utilidades/categorias/[category].astro +38 -0
  32. package/src/tests/locale_completeness.test.ts +1 -1
  33. package/src/tests/mfe_cache_contract.test.ts +14 -0
  34. package/src/tests/mfe_manifest.test.ts +28 -0
  35. package/src/tests/registry_contract.test.ts +67 -0
  36. package/src/tests/tool_validation.test.ts +1 -1
  37. package/src/tool/book-index-page-budget-calculator/bibliography.astro +6 -0
  38. package/src/tool/book-index-page-budget-calculator/bibliography.ts +17 -0
  39. package/src/tool/book-index-page-budget-calculator/book-index-page-budget-calculator.css +478 -0
  40. package/src/tool/book-index-page-budget-calculator/component.astro +134 -0
  41. package/src/tool/book-index-page-budget-calculator/controller.ts +114 -0
  42. package/src/tool/book-index-page-budget-calculator/dom-views.ts +89 -0
  43. package/src/tool/book-index-page-budget-calculator/entry.ts +39 -0
  44. package/src/tool/book-index-page-budget-calculator/evaluator.ts +26 -0
  45. package/src/tool/book-index-page-budget-calculator/i18n/de.ts +35 -0
  46. package/src/tool/book-index-page-budget-calculator/i18n/en.ts +113 -0
  47. package/src/tool/book-index-page-budget-calculator/i18n/es.ts +35 -0
  48. package/src/tool/book-index-page-budget-calculator/i18n/fr.ts +8 -0
  49. package/src/tool/book-index-page-budget-calculator/i18n/id.ts +35 -0
  50. package/src/tool/book-index-page-budget-calculator/i18n/it.ts +8 -0
  51. package/src/tool/book-index-page-budget-calculator/i18n/ja.ts +8 -0
  52. package/src/tool/book-index-page-budget-calculator/i18n/ko.ts +8 -0
  53. package/src/tool/book-index-page-budget-calculator/i18n/nl.ts +8 -0
  54. package/src/tool/book-index-page-budget-calculator/i18n/pl.ts +8 -0
  55. package/src/tool/book-index-page-budget-calculator/i18n/pt.ts +8 -0
  56. package/src/tool/book-index-page-budget-calculator/i18n/ru.ts +8 -0
  57. package/src/tool/book-index-page-budget-calculator/i18n/sv.ts +8 -0
  58. package/src/tool/book-index-page-budget-calculator/i18n/tr.ts +8 -0
  59. package/src/tool/book-index-page-budget-calculator/i18n/zh.ts +8 -0
  60. package/src/tool/book-index-page-budget-calculator/index.ts +11 -0
  61. package/src/tool/book-index-page-budget-calculator/logic.test.ts +52 -0
  62. package/src/tool/book-index-page-budget-calculator/logic.ts +86 -0
  63. package/src/tool/book-index-page-budget-calculator/seo.astro +15 -0
  64. package/src/tool/book-index-page-budget-calculator/storage.ts +17 -0
  65. package/src/tool/book-index-page-budget-calculator/ui.ts +41 -0
  66. package/src/tools.ts +2 -1
  67. package/src/types.ts +4 -6
  68. package/src/worker.ts +27 -0
  69. package/tsconfig.json +14 -5
  70. package/src/pages/[locale]/[slug].astro +0 -164
  71. package/src/pages/[locale].astro +0 -251
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createUtilityManifestResponse } from "../mfe/manifest";
3
+
4
+ describe("MFE utility manifest", () => {
5
+ it("returns a versioned installable manifest for the current utility", async () => {
6
+ const response = createUtilityManifestResponse({
7
+ title: "Card draw odds calculator",
8
+ description: "Calculate card draw probabilities.",
9
+ startUrl: "/en/utilities/categories/books/card-draw-odds-calculator/",
10
+ englishSlug: "card-draw-odds-calculator",
11
+ });
12
+ const manifest = await response.json() as {
13
+ name: string;
14
+ start_url: string;
15
+ scope: string;
16
+ icons: { src: string; sizes: string; type: string; purpose: string }[];
17
+ };
18
+
19
+ expect(response.headers.get("Content-Type")).toContain("application/manifest+json");
20
+ expect(response.headers.get("Cache-Control")).toContain("immutable");
21
+ expect(manifest.name).toBe("Card draw odds calculator");
22
+ expect(manifest.start_url).toBe(manifest.scope);
23
+ expect(manifest.icons[0]?.src).toContain("/_utilities/books/images/card-draw-odds-calculator.webp?version=");
24
+ expect(manifest.icons[0]?.sizes).toBe("512x512");
25
+ expect(manifest.icons[0]?.type).toBe("image/webp");
26
+ expect(manifest.icons[0]?.purpose).toBe("any");
27
+ });
28
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { existsSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { pathToFileURL } from 'url';
5
+ import { ALL_ENTRIES } from '../entries';
6
+ import { ALL_TOOLS } from '../tools';
7
+ import type { ToolDefinition } from '../types';
8
+
9
+ function isToolDefinition(value: unknown): value is ToolDefinition {
10
+ if (!value || typeof value !== 'object') return false;
11
+
12
+ const candidate = value as Record<string, unknown>;
13
+ return Boolean(
14
+ candidate.entry &&
15
+ typeof candidate.entry === 'object' &&
16
+ typeof candidate.Component === 'function' &&
17
+ typeof candidate.SEOComponent === 'function' &&
18
+ typeof candidate.BibliographyComponent === 'function',
19
+ );
20
+ }
21
+
22
+ const toolRoot = join(process.cwd(), 'src', 'tool');
23
+ const toolDirectories = readdirSync(toolRoot, { withFileTypes: true })
24
+ .filter((entry) => entry.isDirectory() && existsSync(join(toolRoot, entry.name, 'index.ts')))
25
+ .map((entry) => entry.name)
26
+ .sort();
27
+
28
+ describe('Library registry contract', () => {
29
+ it('keeps ALL_ENTRIES and ALL_TOOLS synchronized by id', () => {
30
+ const entryIds = ALL_ENTRIES.map((entry) => entry.id);
31
+ const toolIds = ALL_TOOLS.map((tool) => tool.entry.id);
32
+
33
+ expect(new Set(entryIds).size).toBe(entryIds.length);
34
+ expect(new Set(toolIds).size).toBe(toolIds.length);
35
+ expect([...toolIds].sort()).toEqual([...entryIds].sort());
36
+ });
37
+
38
+ it('registers every tool runtime index in both public registries', async () => {
39
+ const failures: string[] = [];
40
+
41
+ for (const directory of toolDirectories) {
42
+ const runtimeModule = await import(
43
+ pathToFileURL(join(toolRoot, directory, 'index.ts')).href,
44
+ );
45
+ const definitions = [...new Set(Object.values(runtimeModule).filter(isToolDefinition))];
46
+
47
+ if (definitions.length !== 1) {
48
+ failures.push(`${directory}: expected exactly one ToolDefinition export, found ${definitions.length}`);
49
+ continue;
50
+ }
51
+
52
+ const [definition] = definitions;
53
+ if (!definition) {
54
+ failures.push(`${directory}: runtime definition is undefined`);
55
+ continue;
56
+ }
57
+ if (!ALL_TOOLS.some((tool) => tool.entry.id === definition.entry.id)) {
58
+ failures.push(`${directory}: runtime definition is missing from ALL_TOOLS`);
59
+ }
60
+ if (!ALL_ENTRIES.some((entry) => entry.id === definition.entry.id)) {
61
+ failures.push(`${directory}: runtime entry is missing from ALL_ENTRIES`);
62
+ }
63
+ }
64
+
65
+ expect(failures).toEqual([]);
66
+ });
67
+ });
@@ -5,7 +5,7 @@ import { booksCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(3);
8
+ expect(ALL_TOOLS.length).toBe(4);
9
9
  });
10
10
 
11
11
  it('booksCategory should be defined', () => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { BOOK_INDEX_BIBLIOGRAPHY } from './bibliography';
4
+ ---
5
+
6
+ <Bibliography links={BOOK_INDEX_BIBLIOGRAPHY} />
@@ -0,0 +1,17 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const BOOK_INDEX_BIBLIOGRAPHY: BibliographyEntry[] = [
4
+ {
5
+ name: 'Oxford University Press, Index',
6
+ url: 'https://academic.oup.com/pages/for-authors/books/the-book-publishing-process/writing-and-content-preparation/index',
7
+ },
8
+ {
9
+ name: 'CIMSUR UNAM, Criterios Editoriales',
10
+ url: 'https://libros.cimsur.unam.mx/index.php/cimsur/CriteriosEditoriales',
11
+ },
12
+ ];
13
+
14
+ export const BOOK_INDEX_BIBLIOGRAPHY_TRACEABILITY = [
15
+ { source: 'Oxford University Press', region: 'United Kingdom', language: 'English', supports: 'An index is back matter, should reflect reader use, and is finalized against typeset material.' },
16
+ { source: 'CIMSUR UNAM', region: 'Mexico', language: 'Spanish', supports: 'Analytical indexes are alphabetic editorial matter and should be included in the work plan.' },
17
+ ] as const;
@@ -0,0 +1,478 @@
1
+ .book-index-tool {
2
+ --n-page: #f4ecdf;
3
+ --n-surface: #fffaf3;
4
+ --n-paper: #fffdf8;
5
+ --n-ink: #201c18;
6
+ --n-muted: #74685c;
7
+ --n-line: #d9cbbb;
8
+ --n-accent: #c65334;
9
+ --n-accent-soft: #f3d9c8;
10
+ --n-body: #d58a49;
11
+ --n-index: #27766d;
12
+ --n-good: #27724d;
13
+ --n-warn: #b64b32;
14
+
15
+ background: var(--n-surface);
16
+ border: 1px solid var(--n-line);
17
+ color: var(--n-ink);
18
+ margin: 0 auto;
19
+ max-width: 1180px;
20
+ min-width: 0;
21
+ overflow: hidden;
22
+ width: 100%;
23
+ }
24
+
25
+ .book-index-tool,
26
+ .book-index-tool *,
27
+ .book-index-tool *::before,
28
+ .book-index-tool *::after {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ .theme-dark .book-index-tool {
33
+ --n-page: #111820;
34
+ --n-surface: #1a222d;
35
+ --n-paper: #242f3b;
36
+ --n-ink: #edf2f4;
37
+ --n-muted: #adbac6;
38
+ --n-line: #3a4756;
39
+ --n-accent: #e3ab82;
40
+ --n-accent-soft: #473a35;
41
+ --n-body: #b98a64;
42
+ --n-index: #79aaa1;
43
+ --n-good: #8fbc9c;
44
+ --n-warn: #d58b78;
45
+ }
46
+
47
+ .index-workbench {
48
+ background: var(--n-page);
49
+ min-width: 0;
50
+ }
51
+
52
+ .budget-summary {
53
+ background: var(--n-surface);
54
+ border-bottom: 1px solid var(--n-line);
55
+ padding: clamp(1rem, 2.4vw, 1.65rem);
56
+ }
57
+
58
+ .summary-topline {
59
+ align-items: center;
60
+ display: flex;
61
+ gap: 1rem;
62
+ justify-content: space-between;
63
+ margin-bottom: 1rem;
64
+ }
65
+
66
+ .summary-topline > span {
67
+ color: var(--n-muted);
68
+ font-size: 0.74rem;
69
+ font-weight: 800;
70
+ letter-spacing: 0.1em;
71
+ text-transform: uppercase;
72
+ }
73
+
74
+ .summary-topline strong {
75
+ color: var(--n-accent);
76
+ font-size: 0.9rem;
77
+ }
78
+
79
+ .summary-grid {
80
+ align-items: stretch;
81
+ display: grid;
82
+ gap: 1.25rem;
83
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
84
+ }
85
+
86
+ .summary-total,
87
+ .budget-strip,
88
+ .summary-brief {
89
+ min-width: 0;
90
+ }
91
+
92
+ .summary-total {
93
+ border-right: 1px solid var(--n-line);
94
+ display: flex;
95
+ flex-direction: column;
96
+ justify-content: center;
97
+ padding-right: 1.25rem;
98
+ }
99
+
100
+ .summary-total > span,
101
+ .summary-total small,
102
+ .strip-labels,
103
+ .strip-target {
104
+ color: var(--n-muted);
105
+ font-size: 0.73rem;
106
+ font-weight: 800;
107
+ letter-spacing: 0.04em;
108
+ text-transform: uppercase;
109
+ }
110
+
111
+ .summary-total > strong {
112
+ color: var(--n-accent);
113
+ font-size: clamp(3rem, 7vw, 5.2rem);
114
+ letter-spacing: -0.07em;
115
+ line-height: 0.92;
116
+ margin: 0.35rem 0 0.55rem;
117
+ }
118
+
119
+ .summary-total small {
120
+ font-size: 0.72rem;
121
+ letter-spacing: 0;
122
+ text-transform: none;
123
+ }
124
+
125
+ .summary-total small b,
126
+ .strip-labels b,
127
+ .strip-target strong {
128
+ color: var(--n-ink);
129
+ }
130
+
131
+ .budget-strip {
132
+ display: flex;
133
+ flex-direction: column;
134
+ justify-content: center;
135
+ min-width: 0;
136
+ }
137
+
138
+ .strip-labels,
139
+ .strip-target {
140
+ align-items: center;
141
+ display: flex;
142
+ justify-content: space-between;
143
+ gap: 0.75rem;
144
+ }
145
+
146
+ .strip-labels span:last-child {
147
+ color: var(--n-index);
148
+ }
149
+
150
+ .strip-track {
151
+ background: var(--n-line);
152
+ display: flex;
153
+ height: clamp(2.6rem, 6vw, 4rem);
154
+ margin: 0.6rem 0 0.55rem;
155
+ min-width: 0;
156
+ overflow: hidden;
157
+ }
158
+
159
+ .strip-body,
160
+ .strip-index {
161
+ display: block;
162
+ min-width: 1rem;
163
+ transition: width 0.25s ease, background-color 0.25s ease;
164
+ }
165
+
166
+ .strip-body {
167
+ background: var(--n-body);
168
+ width: var(--paper-share, 90%);
169
+ }
170
+
171
+ .strip-index {
172
+ background: var(--n-index);
173
+ width: var(--paper-share, 10%);
174
+ }
175
+
176
+ .strip-target {
177
+ border-top: 1px dashed var(--n-muted);
178
+ padding-top: 0.55rem;
179
+ }
180
+
181
+ .strip-target strong {
182
+ font-size: 0.9rem;
183
+ }
184
+
185
+ .summary-brief {
186
+ align-self: center;
187
+ border-left: 3px solid var(--n-good);
188
+ min-width: 0;
189
+ overflow-wrap: anywhere;
190
+ padding-left: 0.9rem;
191
+ }
192
+
193
+ [data-status='tight'] .summary-brief { border-left-color: var(--n-warn); }
194
+ [data-status='roomy'] .summary-brief { border-left-color: var(--n-index); }
195
+
196
+ .status-dot {
197
+ background: var(--n-good);
198
+ border-radius: 50%;
199
+ display: inline-block;
200
+ height: 0.55rem;
201
+ margin-right: 0.4rem;
202
+ vertical-align: 0.05em;
203
+ width: 0.55rem;
204
+ }
205
+
206
+ [data-status='tight'] .status-dot { background: var(--n-warn); }
207
+ [data-status='roomy'] .status-dot { background: var(--n-index); }
208
+
209
+ .summary-brief strong {
210
+ color: var(--n-ink);
211
+ font-size: 0.78rem;
212
+ }
213
+
214
+ .summary-brief p {
215
+ color: var(--n-muted);
216
+ font-size: 0.82rem;
217
+ line-height: 1.45;
218
+ margin: 0.45rem 0 0;
219
+ }
220
+
221
+ .workbench-controls {
222
+ display: grid;
223
+ gap: 1px;
224
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
225
+ padding: 1px;
226
+ }
227
+
228
+ .control-block {
229
+ background: var(--n-surface);
230
+ display: grid;
231
+ gap: 1.15rem;
232
+ min-width: 0;
233
+ padding: clamp(1rem, 2.4vw, 1.45rem);
234
+ }
235
+
236
+ .field,
237
+ .dimension-field,
238
+ .unit-field,
239
+ .density-field {
240
+ min-width: 0;
241
+ }
242
+
243
+ .field label,
244
+ .field-row label,
245
+ .unit-field legend,
246
+ .density-field legend {
247
+ color: var(--n-ink);
248
+ display: block;
249
+ font-size: 0.75rem;
250
+ font-weight: 800;
251
+ letter-spacing: 0.04em;
252
+ margin-bottom: 0.45rem;
253
+ text-transform: uppercase;
254
+ }
255
+
256
+ .field-row {
257
+ display: grid;
258
+ gap: 0.55rem;
259
+ grid-template-columns: 1fr 1fr;
260
+ }
261
+
262
+ .field-row label:last-child {
263
+ grid-column: 2;
264
+ grid-row: 1;
265
+ }
266
+
267
+ input {
268
+ background: var(--n-paper);
269
+ border: 1px solid var(--n-line);
270
+ border-radius: 0;
271
+ color: var(--n-ink);
272
+ font: inherit;
273
+ font-size: 1.05rem;
274
+ min-height: 2.8rem;
275
+ min-width: 0;
276
+ padding: 0.65rem 0.75rem;
277
+ width: 100%;
278
+ }
279
+
280
+ input:focus-visible,
281
+ button:focus-visible {
282
+ outline: 3px solid var(--n-accent);
283
+ outline-offset: 2px;
284
+ }
285
+
286
+ .input-row {
287
+ align-items: center;
288
+ display: grid;
289
+ gap: 0.45rem;
290
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
291
+ }
292
+
293
+ .times {
294
+ color: var(--n-muted);
295
+ font-size: 1.1rem;
296
+ font-weight: 800;
297
+ }
298
+
299
+ .field-hint {
300
+ color: var(--n-muted);
301
+ display: block;
302
+ font-size: 0.74rem;
303
+ line-height: 1.35;
304
+ margin-top: 0.4rem;
305
+ }
306
+
307
+ fieldset {
308
+ border: 0;
309
+ margin: 0;
310
+ padding: 0;
311
+ }
312
+
313
+ .unit-switch,
314
+ .density-options {
315
+ display: grid;
316
+ gap: 0.45rem;
317
+ grid-template-columns: repeat(2, minmax(0, 1fr));
318
+ }
319
+
320
+ .density-options {
321
+ grid-template-columns: repeat(3, minmax(0, 1fr));
322
+ }
323
+
324
+ button {
325
+ background: var(--n-paper);
326
+ border: 1px solid var(--n-line);
327
+ border-radius: 0;
328
+ color: var(--n-ink);
329
+ cursor: pointer;
330
+ font: inherit;
331
+ min-height: 2.8rem;
332
+ min-width: 0;
333
+ padding: 0.55rem 0.6rem;
334
+ }
335
+
336
+ button[aria-pressed='true'] {
337
+ background: var(--n-accent-soft);
338
+ border-color: var(--n-accent);
339
+ color: var(--n-ink);
340
+ font-weight: 800;
341
+ }
342
+
343
+ .density-field .field-hint {
344
+ line-height: 1.2;
345
+ margin: 0 0 0.45rem;
346
+ }
347
+
348
+ .density-options button {
349
+ align-items: center;
350
+ display: flex;
351
+ gap: 0.35rem;
352
+ justify-content: center;
353
+ overflow: hidden;
354
+ text-overflow: ellipsis;
355
+ white-space: nowrap;
356
+ }
357
+
358
+ .density-mark {
359
+ border: 1px solid var(--n-accent);
360
+ display: inline-block;
361
+ flex: 0 0 auto;
362
+ height: 0.75rem;
363
+ width: 0.75rem;
364
+ }
365
+
366
+ .airy-mark { background: var(--n-paper); }
367
+ .standard-mark { background: var(--n-accent-soft); }
368
+ .compact-mark { background: var(--n-accent); }
369
+
370
+ .reading-details {
371
+ background: var(--n-surface);
372
+ border-top: 1px solid var(--n-line);
373
+ display: grid;
374
+ gap: 0;
375
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
376
+ padding: 1rem clamp(1rem, 2.4vw, 1.45rem);
377
+ }
378
+
379
+ .reading-item {
380
+ border-right: 1px solid var(--n-line);
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 0.25rem;
384
+ min-width: 0;
385
+ padding: 0 0.9rem;
386
+ }
387
+
388
+ .reading-item:first-child {
389
+ padding-left: 0;
390
+ }
391
+
392
+ .reading-item:last-child {
393
+ border-right: 0;
394
+ padding-right: 0;
395
+ }
396
+
397
+ .reading-item span {
398
+ color: var(--n-muted);
399
+ font-size: 0.68rem;
400
+ font-weight: 800;
401
+ letter-spacing: 0.04em;
402
+ line-height: 1.25;
403
+ text-transform: uppercase;
404
+ }
405
+
406
+ .reading-item strong {
407
+ color: var(--n-ink);
408
+ font-size: 1.15rem;
409
+ overflow-wrap: anywhere;
410
+ }
411
+
412
+ .source-note {
413
+ color: var(--n-muted);
414
+ font-size: 0.77rem;
415
+ line-height: 1.5;
416
+ margin: 0;
417
+ padding: 0 clamp(1rem, 2.4vw, 1.45rem) 1.25rem;
418
+ }
419
+
420
+ @media (max-width: 900px) {
421
+ .summary-grid {
422
+ grid-template-columns: minmax(130px, 0.65fr) minmax(240px, 1.35fr);
423
+ }
424
+
425
+ .summary-brief {
426
+ grid-column: 1 / -1;
427
+ }
428
+
429
+ .workbench-controls {
430
+ grid-template-columns: 1fr 1fr;
431
+ }
432
+
433
+ .control-block:last-child {
434
+ grid-column: 1 / -1;
435
+ }
436
+
437
+ .reading-details {
438
+ grid-template-columns: repeat(3, minmax(0, 1fr));
439
+ row-gap: 1rem;
440
+ }
441
+
442
+ .reading-item:nth-child(3) {
443
+ border-right: 0;
444
+ }
445
+
446
+ .reading-item:nth-child(4) {
447
+ padding-left: 0;
448
+ }
449
+ }
450
+
451
+ @media (max-width: 600px) {
452
+ .summary-grid,
453
+ .workbench-controls,
454
+ .reading-details { grid-template-columns: 1fr; }
455
+ .summary-total {
456
+ border-bottom: 1px solid var(--n-line);
457
+ border-right: 0;
458
+ padding: 0 0 1rem;
459
+ }
460
+
461
+ .summary-total > strong {
462
+ font-size: 3.7rem;
463
+ }
464
+ .summary-brief,
465
+ .control-block:last-child { grid-column: auto; }
466
+ .control-block { gap: 1rem; }
467
+ .reading-item,
468
+ .reading-item:nth-child(3),
469
+ .reading-item:nth-child(4) {
470
+ border-bottom: 1px solid var(--n-line);
471
+ border-right: 0;
472
+ padding: 0 0 0.7rem;
473
+ }
474
+ .reading-item:last-child {
475
+ border-bottom: 0;
476
+ padding-bottom: 0;
477
+ }
478
+ }