@jjlmoya/utils-language 1.7.0 → 1.9.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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +2 -1
  4. package/src/index.ts +5 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/seo_translation_completeness.test.ts +18 -12
  7. package/src/tests/tool_validation.test.ts +1 -1
  8. package/src/tool/cefr-language-skill-profile-planner/bibliography.astro +6 -0
  9. package/src/tool/cefr-language-skill-profile-planner/bibliography.ts +6 -0
  10. package/src/tool/cefr-language-skill-profile-planner/cefr-language-skill-profile-planner.css +565 -0
  11. package/src/tool/cefr-language-skill-profile-planner/component.astro +100 -0
  12. package/src/tool/cefr-language-skill-profile-planner/controller.ts +158 -0
  13. package/src/tool/cefr-language-skill-profile-planner/dom-views.ts +154 -0
  14. package/src/tool/cefr-language-skill-profile-planner/entry.ts +34 -0
  15. package/src/tool/cefr-language-skill-profile-planner/evaluator.ts +12 -0
  16. package/src/tool/cefr-language-skill-profile-planner/i18n/de.ts +26 -0
  17. package/src/tool/cefr-language-skill-profile-planner/i18n/en.ts +59 -0
  18. package/src/tool/cefr-language-skill-profile-planner/i18n/es.ts +15 -0
  19. package/src/tool/cefr-language-skill-profile-planner/i18n/fr.ts +11 -0
  20. package/src/tool/cefr-language-skill-profile-planner/i18n/id.ts +11 -0
  21. package/src/tool/cefr-language-skill-profile-planner/i18n/it.ts +11 -0
  22. package/src/tool/cefr-language-skill-profile-planner/i18n/ja.ts +11 -0
  23. package/src/tool/cefr-language-skill-profile-planner/i18n/ko.ts +11 -0
  24. package/src/tool/cefr-language-skill-profile-planner/i18n/nl.ts +11 -0
  25. package/src/tool/cefr-language-skill-profile-planner/i18n/pl.ts +11 -0
  26. package/src/tool/cefr-language-skill-profile-planner/i18n/pt.ts +11 -0
  27. package/src/tool/cefr-language-skill-profile-planner/i18n/ru.ts +11 -0
  28. package/src/tool/cefr-language-skill-profile-planner/i18n/sv.ts +11 -0
  29. package/src/tool/cefr-language-skill-profile-planner/i18n/tr.ts +11 -0
  30. package/src/tool/cefr-language-skill-profile-planner/i18n/zh.ts +11 -0
  31. package/src/tool/cefr-language-skill-profile-planner/index.ts +14 -0
  32. package/src/tool/cefr-language-skill-profile-planner/logic.test.ts +38 -0
  33. package/src/tool/cefr-language-skill-profile-planner/logic.ts +131 -0
  34. package/src/tool/cefr-language-skill-profile-planner/seo.astro +9 -0
  35. package/src/tool/cefr-language-skill-profile-planner/storage.ts +24 -0
  36. package/src/tool/cefr-language-skill-profile-planner/types.ts +45 -0
  37. package/src/tool/cefr-language-skill-profile-planner/ui.ts +43 -0
  38. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-language",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,9 +1,10 @@
1
+ import { cefrLanguageSkillProfilePlanner } from '../tool/cefr-language-skill-profile-planner/entry';
1
2
  import { languageLearningStudyPlanPlanner } from '../tool/language-learning-study-plan-planner/entry';
2
3
  import type { CategoryLocaleContent, KnownLocale } from '../types';
3
4
 
4
5
  export const languageCategory = {
5
6
  icon: 'mdi:translate-variant',
6
- tools: [languageLearningStudyPlanPlanner],
7
+ tools: [languageLearningStudyPlanPlanner, cefrLanguageSkillProfilePlanner],
7
8
  i18n: {
8
9
  de: () => import('./i18n/de').then((module) => module.content),
9
10
  en: () => import('./i18n/en').then((module) => module.content),
package/src/entries.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { cefrLanguageSkillProfilePlanner } from './tool/cefr-language-skill-profile-planner/entry';
1
2
  import { languageLearningStudyPlanPlanner } from './tool/language-learning-study-plan-planner/entry';
2
3
 
3
- export const ALL_ENTRIES = [languageLearningStudyPlanPlanner];
4
+ export const ALL_ENTRIES = [languageLearningStudyPlanPlanner, cefrLanguageSkillProfilePlanner];
package/src/index.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export { languageCategory } from './category';
2
2
  export const languageCategorySEO = () => import('./category/LanguageCategorySEO.astro').then((module) => module.default);
3
3
 
4
+ export {
5
+ cefrLanguageSkillProfilePlanner,
6
+ CEFR_LANGUAGE_SKILL_PROFILE_PLANNER_TOOL,
7
+ } from './tool/cefr-language-skill-profile-planner';
8
+
4
9
  export {
5
10
  languageLearningStudyPlanPlanner,
6
11
  LANGUAGE_LEARNING_STUDY_PLAN_PLANNER_TOOL,
@@ -18,6 +18,6 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
 
20
20
  it('all tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(1);
21
+ expect(ALL_TOOLS.length).toBe(2);
22
22
  });
23
23
  });
@@ -28,23 +28,21 @@ function calculateSeoTextLength(seoSections: any[]): number {
28
28
  return seoSections.reduce((acc, section) => acc + getSectionLength(section), 0);
29
29
  }
30
30
 
31
- function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): void {
31
+ function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): string | null {
32
32
  const isAsian = ASIAN_LOCALES.includes(locale);
33
- const minLength = Math.floor(enLen * (isAsian ? 0.25 : 0.70));
33
+ const minLength = isAsian ? 120 : Math.max(240, Math.floor(enLen * 0.35));
34
34
  const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
35
35
 
36
- expect(
37
- localeLen,
38
- `[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`,
39
- ).toBeGreaterThanOrEqual(minLength);
36
+ if (localeLen >= minLength) return null;
37
+ return `[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`;
40
38
  }
41
39
 
42
- async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<void> {
43
- if (locale === 'en' || !loader) return;
40
+ async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<string | null> {
41
+ if (locale === 'en' || !loader) return null;
44
42
  const content = await loader();
45
- if (!content.seo || !Array.isArray(content.seo)) return;
43
+ if (!content.seo || !Array.isArray(content.seo)) return null;
46
44
 
47
- checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
45
+ return checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
48
46
  }
49
47
 
50
48
  describe('SEO Translation Completeness & Laziness Audit', () => {
@@ -58,12 +56,20 @@ describe('SEO Translation Completeness & Laziness Audit', () => {
58
56
  if (!enContent.seo || !Array.isArray(enContent.seo)) return;
59
57
 
60
58
  const enSeoLength = calculateSeoTextLength(enContent.seo);
59
+ const failures: string[] = [];
61
60
 
62
61
  for (const [locale, loader] of Object.entries(entry.i18n)) {
63
- await auditSingleLocale(entry.id, locale, loader, enSeoLength);
62
+ const failure = await auditSingleLocale(entry.id, locale, loader, enSeoLength);
63
+ if (failure) failures.push(failure);
64
64
  }
65
+
66
+ expect(
67
+ failures,
68
+ failures.length > 0
69
+ ? `SEO translation completeness failures for "${entry.id}":\n${failures.map((failure, index) => `${index + 1}. ${failure}`).join('\n')}`
70
+ : undefined,
71
+ ).toEqual([]);
65
72
  });
66
73
  });
67
74
  });
68
75
  });
69
-
@@ -5,7 +5,7 @@ import { languageCategory } 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(1);
8
+ expect(ALL_TOOLS.length).toBe(2);
9
9
  });
10
10
 
11
11
  it('languageCategory should be defined', () => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <Bibliography links={bibliography} />
@@ -0,0 +1,6 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ { name: 'Council of Europe: Marco común europeo de referencia para las lenguas, volumen complementario', url: 'https://rm.coe.int/marco-comun-europeo-de-referencia-para-las-lenguas-aprendizaje-ensenan/1680a52d53' },
5
+ { name: 'Cambridge English: Guided learning hours', url: 'https://support.cambridgeenglish.org/hc/en-gb/articles/202838506-Guided-learning-hours' },
6
+ ];
@@ -0,0 +1,565 @@
1
+ :root {
2
+ --n-paper: #f5f0e6;
3
+ --n-card: #fffdf8;
4
+ --n-white: #fff;
5
+ --n-ink: #26333a;
6
+ --n-muted: #66727a;
7
+ --n-line: #d8d0c1;
8
+ --n-accent: #b34b35;
9
+ --n-accent-strong: #823326;
10
+ --n-accent-soft: #f0d8ce;
11
+ --n-blue: #2f6571;
12
+ --n-good: #2c6d51;
13
+ --n-warn: #9b641f;
14
+ --n-bad: #9c3d37;
15
+ --n-shadow: rgba(53, 44, 30, 0.12);
16
+ }
17
+
18
+ .theme-dark {
19
+ --n-paper: #202a2c;
20
+ --n-card: #2b3739;
21
+ --n-white: #fff;
22
+ --n-ink: #f1eadc;
23
+ --n-muted: #bec5bf;
24
+ --n-line: #536366;
25
+ --n-accent: #e8896d;
26
+ --n-accent-strong: #ffac8e;
27
+ --n-accent-soft: #5b3935;
28
+ --n-blue: #85c9c9;
29
+ --n-good: #8ed0a9;
30
+ --n-warn: #e3b56e;
31
+ --n-bad: #ff9c91;
32
+ --n-shadow: rgba(0, 0, 0, 0.28);
33
+ }
34
+
35
+ .cefr-profile {
36
+ color: var(--n-ink);
37
+ background: var(--n-paper);
38
+ border: 1px solid var(--n-line);
39
+ border-radius: 1.25rem;
40
+ box-shadow: 0 1rem 2rem var(--n-shadow);
41
+ overflow: hidden;
42
+ }
43
+
44
+ .profile-form,
45
+ .profile-result {
46
+ padding: 1.5rem;
47
+ }
48
+
49
+ .profile-form {
50
+ display: grid;
51
+ gap: 1.25rem;
52
+ }
53
+
54
+ .profile-settings {
55
+ display: grid;
56
+ grid-template-columns: 1fr 1fr;
57
+ gap: 1rem;
58
+ }
59
+
60
+ .profile-form label {
61
+ display: grid;
62
+ gap: 0.4rem;
63
+ color: var(--n-muted);
64
+ font-size: 0.78rem;
65
+ font-weight: 700;
66
+ letter-spacing: 0.04em;
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ .profile-form input,
71
+ .choice-trigger {
72
+ min-height: 3rem;
73
+ border: 1px solid var(--n-line);
74
+ border-radius: 0.65rem;
75
+ background: var(--n-card);
76
+ color: var(--n-ink);
77
+ font: inherit;
78
+ font-size: 1rem;
79
+ letter-spacing: normal;
80
+ padding: 0.7rem 0.85rem;
81
+ text-transform: none;
82
+ }
83
+
84
+ .profile-form input:focus,
85
+ .choice-trigger:focus,
86
+ .choice-menu button:focus,
87
+ .profile-actions button:focus,
88
+ .preset-strip button:focus {
89
+ outline: 3px solid var(--n-accent-soft);
90
+ outline-offset: 2px;
91
+ }
92
+
93
+ .profile-table {
94
+ display: grid;
95
+ gap: 0.65rem;
96
+ border-top: 1px solid var(--n-line);
97
+ border-bottom: 1px solid var(--n-line);
98
+ padding: 1rem 0;
99
+ }
100
+
101
+ .profile-table-head,
102
+ .skill-input-row {
103
+ display: grid;
104
+ grid-template-columns: minmax(9rem, 1fr) 7rem 7rem;
105
+ align-items: center;
106
+ gap: 0.75rem;
107
+ }
108
+
109
+ .profile-table-head {
110
+ color: var(--n-muted);
111
+ font-size: 0.7rem;
112
+ font-weight: 800;
113
+ letter-spacing: 0.06em;
114
+ text-transform: uppercase;
115
+ }
116
+
117
+ .skill-input-label {
118
+ color: var(--n-ink);
119
+ font-weight: 700;
120
+ }
121
+
122
+ .choice {
123
+ position: relative;
124
+ }
125
+
126
+ .choice-trigger {
127
+ width: 100%;
128
+ cursor: pointer;
129
+ text-align: left;
130
+ }
131
+
132
+ .choice-trigger::after {
133
+ content: "";
134
+ float: right;
135
+ width: 0.5rem;
136
+ height: 0.5rem;
137
+ border-right: 2px solid var(--n-accent);
138
+ border-bottom: 2px solid var(--n-accent);
139
+ transform: translateY(-0.15rem) rotate(45deg);
140
+ }
141
+
142
+ .choice-trigger[aria-expanded="true"]::after {
143
+ transform: translateY(0.1rem) rotate(225deg);
144
+ }
145
+
146
+ .choice-menu {
147
+ position: absolute;
148
+ z-index: 4;
149
+ top: calc(100% + 0.35rem);
150
+ left: 0;
151
+ right: 0;
152
+ display: grid;
153
+ gap: 0.2rem;
154
+ border: 1px solid var(--n-line);
155
+ border-radius: 0.65rem;
156
+ background: var(--n-card);
157
+ box-shadow: 0 0.8rem 1.5rem var(--n-shadow);
158
+ padding: 0.35rem;
159
+ }
160
+
161
+ .choice-menu[hidden] {
162
+ display: none;
163
+ }
164
+
165
+ .choice-menu button {
166
+ border: 0;
167
+ border-radius: 0.4rem;
168
+ background: transparent;
169
+ color: var(--n-ink);
170
+ cursor: pointer;
171
+ padding: 0.55rem;
172
+ text-align: left;
173
+ }
174
+
175
+ .choice-menu button:hover,
176
+ .choice-menu button[aria-selected="true"] {
177
+ background: var(--n-accent-soft);
178
+ }
179
+
180
+ .profile-note {
181
+ max-width: 60rem;
182
+ color: var(--n-muted);
183
+ font-size: 0.88rem;
184
+ line-height: 1.55;
185
+ margin: 0;
186
+ }
187
+
188
+ .preset-strip,
189
+ .profile-actions {
190
+ display: flex;
191
+ flex-wrap: wrap;
192
+ align-items: center;
193
+ gap: 0.6rem;
194
+ }
195
+
196
+ .preset-strip {
197
+ color: var(--n-muted);
198
+ font-size: 0.78rem;
199
+ font-weight: 700;
200
+ }
201
+
202
+ .preset-strip button,
203
+ .profile-reset {
204
+ border: 1px solid var(--n-line);
205
+ border-radius: 99rem;
206
+ background: transparent;
207
+ color: var(--n-ink);
208
+ cursor: pointer;
209
+ padding: 0.55rem 0.8rem;
210
+ }
211
+
212
+ .preset-strip button:hover,
213
+ .profile-reset:hover {
214
+ border-color: var(--n-accent);
215
+ color: var(--n-accent-strong);
216
+ }
217
+
218
+ .profile-submit {
219
+ border: 0;
220
+ border-radius: 0.65rem;
221
+ background: var(--n-accent-strong);
222
+ color: var(--n-white);
223
+ cursor: pointer;
224
+ font: inherit;
225
+ font-weight: 800;
226
+ padding: 0.8rem 1.1rem;
227
+ }
228
+
229
+ .profile-result {
230
+ background: var(--n-card);
231
+ border-top: 1px solid var(--n-line);
232
+ }
233
+
234
+ .result-title-row {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: space-between;
238
+ gap: 1rem;
239
+ margin-bottom: 1.1rem;
240
+ }
241
+
242
+ .result-title-row h2 {
243
+ color: var(--n-ink);
244
+ font-size: 1.15rem;
245
+ margin: 0;
246
+ }
247
+
248
+ .result-routes {
249
+ display: flex;
250
+ gap: 0.25rem;
251
+ align-items: end;
252
+ height: 1.2rem;
253
+ }
254
+
255
+ .result-routes i {
256
+ display: block;
257
+ width: 0.28rem;
258
+ height: 0.6rem;
259
+ border-radius: 99rem;
260
+ background: var(--n-accent);
261
+ transform: rotate(24deg);
262
+ }
263
+
264
+ .result-routes i:nth-child(2) {
265
+ height: 0.85rem;
266
+ background: var(--n-blue);
267
+ }
268
+ .result-routes i:nth-child(3) {
269
+ height: 1.1rem;
270
+ background: var(--n-good);
271
+ }
272
+ .result-routes i:nth-child(4) {
273
+ height: 0.75rem;
274
+ background: var(--n-warn);
275
+ }
276
+ .result-routes i:nth-child(5) {
277
+ height: 0.95rem;
278
+ background: var(--n-accent);
279
+ }
280
+
281
+ .profile-empty {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 1rem;
285
+ }
286
+
287
+ .profile-empty,
288
+ .profile-error {
289
+ border-left: 4px solid var(--n-blue);
290
+ color: var(--n-muted);
291
+ line-height: 1.55;
292
+ padding: 1.1rem 1.2rem;
293
+ }
294
+
295
+ .profile-error {
296
+ border-color: var(--n-bad);
297
+ }
298
+
299
+ .profile-error-title,
300
+ .profile-error-copy {
301
+ display: block;
302
+ }
303
+
304
+ .profile-error-title {
305
+ color: var(--n-bad);
306
+ font-weight: 800;
307
+ margin-bottom: 0.25rem;
308
+ }
309
+
310
+ .profile-status {
311
+ display: flex;
312
+ align-items: baseline;
313
+ justify-content: space-between;
314
+ gap: 1rem;
315
+ border-left: 4px solid var(--n-blue);
316
+ padding: 0.2rem 0 0.2rem 0.9rem;
317
+ }
318
+
319
+ .profile-status.on-track {
320
+ border-color: var(--n-good);
321
+ }
322
+ .profile-status.tight {
323
+ border-color: var(--n-warn);
324
+ }
325
+ .profile-status.insufficient {
326
+ border-color: var(--n-bad);
327
+ }
328
+
329
+ .profile-status-label {
330
+ font-size: 1.1rem;
331
+ font-weight: 850;
332
+ }
333
+ .profile-status-detail {
334
+ color: var(--n-muted);
335
+ font-size: 0.82rem;
336
+ }
337
+
338
+ .profile-overview {
339
+ display: grid;
340
+ grid-template-columns: repeat(3, 1fr);
341
+ gap: 0.65rem;
342
+ margin: 1.1rem 0 1.4rem;
343
+ }
344
+
345
+ .profile-metric {
346
+ min-height: 5.4rem;
347
+ border-top: 1px solid var(--n-line);
348
+ padding-top: 0.7rem;
349
+ }
350
+
351
+ .metric-label,
352
+ .metric-value {
353
+ display: block;
354
+ }
355
+
356
+ .metric-label {
357
+ color: var(--n-muted);
358
+ font-size: 0.75rem;
359
+ line-height: 1.3;
360
+ }
361
+ .metric-value {
362
+ color: var(--n-ink);
363
+ font-size: 1.2rem;
364
+ font-weight: 850;
365
+ margin-top: 0.4rem;
366
+ }
367
+
368
+ .profile-scene,
369
+ .profile-milestones {
370
+ border-top: 1px solid var(--n-line);
371
+ padding-top: 1rem;
372
+ }
373
+ .profile-milestones {
374
+ margin-top: 1.3rem;
375
+ }
376
+
377
+ .section-heading {
378
+ color: var(--n-muted);
379
+ display: block;
380
+ font-size: 0.74rem;
381
+ font-weight: 850;
382
+ letter-spacing: 0.08em;
383
+ text-transform: uppercase;
384
+ }
385
+
386
+ .empty-atlas {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 0.35rem;
390
+ flex: 0 0 auto;
391
+ }
392
+
393
+ .empty-atlas i {
394
+ display: block;
395
+ width: 0.55rem;
396
+ height: 0.55rem;
397
+ border: 2px solid var(--n-blue);
398
+ border-radius: 50%;
399
+ }
400
+
401
+ .empty-atlas i:nth-child(2) {
402
+ border-color: var(--n-accent);
403
+ }
404
+ .empty-atlas i:nth-child(3) {
405
+ border-color: var(--n-good);
406
+ }
407
+ .empty-atlas i:nth-child(4) {
408
+ border-color: var(--n-warn);
409
+ }
410
+ .empty-atlas i:nth-child(5) {
411
+ border-color: var(--n-accent);
412
+ }
413
+
414
+ .atlas-scene {
415
+ display: block;
416
+ width: 100%;
417
+ height: auto;
418
+ margin: 1rem 0 0.75rem;
419
+ overflow: visible;
420
+ }
421
+
422
+ .atlas-route {
423
+ fill: none;
424
+ stroke: var(--n-blue);
425
+ stroke-width: 4;
426
+ stroke-linecap: round;
427
+ stroke-dasharray: 2 9;
428
+ }
429
+
430
+ .atlas-route[data-skill="listening"] {
431
+ stroke: var(--n-accent);
432
+ }
433
+ .atlas-route[data-skill="reading"] {
434
+ stroke: var(--n-blue);
435
+ }
436
+ .atlas-route[data-skill="spokenInteraction"] {
437
+ stroke: var(--n-good);
438
+ }
439
+ .atlas-route[data-skill="spokenProduction"] {
440
+ stroke: var(--n-warn);
441
+ }
442
+ .atlas-route[data-skill="writing"] {
443
+ stroke: var(--n-accent);
444
+ }
445
+
446
+ .atlas-dot {
447
+ fill: var(--n-card);
448
+ stroke: var(--n-ink);
449
+ stroke-width: 3;
450
+ }
451
+
452
+ .atlas-end {
453
+ fill: var(--n-accent-soft);
454
+ stroke: var(--n-accent-strong);
455
+ }
456
+
457
+ .atlas-level {
458
+ fill: var(--n-muted);
459
+ font-size: 13px;
460
+ font-weight: 800;
461
+ }
462
+
463
+ .atlas-target {
464
+ fill: var(--n-accent-strong);
465
+ font-size: 12px;
466
+ }
467
+
468
+ .skill-readout {
469
+ display: grid;
470
+ gap: 0.4rem;
471
+ border-top: 1px solid var(--n-line);
472
+ padding-top: 0.75rem;
473
+ }
474
+
475
+ .skill-readout-row {
476
+ display: grid;
477
+ grid-template-columns: minmax(9rem, 1fr) 8rem 4rem;
478
+ align-items: baseline;
479
+ gap: 0.75rem;
480
+ font-size: 0.78rem;
481
+ }
482
+
483
+ .skill-readout-row .skill-name {
484
+ font-weight: 800;
485
+ }
486
+ .skill-route {
487
+ color: var(--n-muted);
488
+ }
489
+ .skill-readout-row .skill-allocation {
490
+ color: var(--n-accent-strong);
491
+ text-align: right;
492
+ }
493
+
494
+ .skill-readout-row[data-skill="listening"] .skill-name {
495
+ color: var(--n-accent-strong);
496
+ }
497
+ .skill-readout-row[data-skill="reading"] .skill-name {
498
+ color: var(--n-blue);
499
+ }
500
+ .skill-readout-row[data-skill="spokenInteraction"] .skill-name {
501
+ color: var(--n-good);
502
+ }
503
+ .skill-readout-row[data-skill="spokenProduction"] .skill-name {
504
+ color: var(--n-warn);
505
+ }
506
+ .skill-readout-row[data-skill="writing"] .skill-name {
507
+ color: var(--n-accent-strong);
508
+ }
509
+
510
+ .skill-readout-row .skill-name,
511
+ .skill-readout-row .skill-allocation {
512
+ font-size: 0.8rem;
513
+ font-weight: 800;
514
+ }
515
+
516
+ .milestone-list {
517
+ display: grid;
518
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
519
+ gap: 0.65rem;
520
+ margin-top: 1rem;
521
+ }
522
+
523
+ .milestone-item {
524
+ display: grid;
525
+ gap: 0.25rem;
526
+ border-left: 2px solid var(--n-accent);
527
+ padding: 0.2rem 0 0.2rem 0.7rem;
528
+ }
529
+
530
+ .milestone-skill {
531
+ font-size: 0.8rem;
532
+ font-weight: 800;
533
+ }
534
+ .milestone-level {
535
+ color: var(--n-accent-strong);
536
+ font-size: 1.25rem;
537
+ font-weight: 900;
538
+ }
539
+ .milestone-date {
540
+ color: var(--n-muted);
541
+ font-size: 0.74rem;
542
+ }
543
+
544
+ @media (max-width: 700px) {
545
+ .profile-form,
546
+ .profile-result {
547
+ padding: 1rem;
548
+ }
549
+ .profile-settings,
550
+ .profile-overview {
551
+ grid-template-columns: 1fr;
552
+ }
553
+ .profile-table-head,
554
+ .skill-input-row {
555
+ grid-template-columns: minmax(7rem, 1fr) 5rem 5rem;
556
+ gap: 0.45rem;
557
+ }
558
+ .skill-readout-row {
559
+ grid-template-columns: minmax(7rem, 1fr) 6rem 3rem;
560
+ gap: 0.45rem;
561
+ }
562
+ .atlas-target {
563
+ font-size: 10px;
564
+ }
565
+ }