@jjlmoya/utils-games-development 1.54.0 → 1.56.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 (65) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +7 -1
  3. package/src/index.ts +2 -0
  4. package/src/tests/contrast_legibility.test.ts +291 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
  8. package/src/tool/localizationSanitizer/localization-sanitizer.css +17 -15
  9. package/src/tool/pixelArtPaletteSwapper/app-client.ts +229 -0
  10. package/src/tool/pixelArtPaletteSwapper/bibliography.astro +6 -0
  11. package/src/tool/pixelArtPaletteSwapper/bibliography.ts +16 -0
  12. package/src/tool/pixelArtPaletteSwapper/component.astro +153 -0
  13. package/src/tool/pixelArtPaletteSwapper/entry.ts +28 -0
  14. package/src/tool/pixelArtPaletteSwapper/i18n/de.ts +233 -0
  15. package/src/tool/pixelArtPaletteSwapper/i18n/en.ts +233 -0
  16. package/src/tool/pixelArtPaletteSwapper/i18n/es.ts +233 -0
  17. package/src/tool/pixelArtPaletteSwapper/i18n/fr.ts +233 -0
  18. package/src/tool/pixelArtPaletteSwapper/i18n/id.ts +233 -0
  19. package/src/tool/pixelArtPaletteSwapper/i18n/it.ts +233 -0
  20. package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +225 -0
  21. package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +225 -0
  22. package/src/tool/pixelArtPaletteSwapper/i18n/nl.ts +233 -0
  23. package/src/tool/pixelArtPaletteSwapper/i18n/pl.ts +233 -0
  24. package/src/tool/pixelArtPaletteSwapper/i18n/pt.ts +233 -0
  25. package/src/tool/pixelArtPaletteSwapper/i18n/ru.ts +233 -0
  26. package/src/tool/pixelArtPaletteSwapper/i18n/sv.ts +233 -0
  27. package/src/tool/pixelArtPaletteSwapper/i18n/tr.ts +233 -0
  28. package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +225 -0
  29. package/src/tool/pixelArtPaletteSwapper/index.ts +12 -0
  30. package/src/tool/pixelArtPaletteSwapper/logic.test.ts +43 -0
  31. package/src/tool/pixelArtPaletteSwapper/logic.ts +171 -0
  32. package/src/tool/pixelArtPaletteSwapper/pixel-art-palette-swapper.css +728 -0
  33. package/src/tool/pixelArtPaletteSwapper/seo.astro +15 -0
  34. package/src/tool/pixelArtPaletteSwapper/ui.ts +35 -0
  35. package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
  36. package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
  37. package/src/tool/retroSfxGenerator/client.ts +263 -0
  38. package/src/tool/retroSfxGenerator/component.astro +146 -0
  39. package/src/tool/retroSfxGenerator/entry.ts +27 -0
  40. package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
  41. package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
  42. package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
  43. package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
  44. package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
  45. package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
  46. package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
  47. package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
  48. package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
  49. package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
  50. package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
  51. package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
  52. package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
  53. package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
  54. package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
  55. package/src/tool/retroSfxGenerator/index.ts +11 -0
  56. package/src/tool/retroSfxGenerator/logic.test.ts +61 -0
  57. package/src/tool/retroSfxGenerator/logic.ts +214 -0
  58. package/src/tool/retroSfxGenerator/retro-sfx-generator.css +435 -0
  59. package/src/tool/retroSfxGenerator/seo.astro +15 -0
  60. package/src/tool/retroSfxGenerator/ui.ts +41 -0
  61. package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
  62. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
  63. package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +8 -8
  64. package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
  65. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-games-development",
3
- "version": "1.54.0",
3
+ "version": "1.56.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -12,6 +12,10 @@ export { localizationSanitizer } from './tool/localizationSanitizer/entry';
12
12
  export type { LocalizationSanitizerUI, LocalizationSanitizerLocaleContent } from './tool/localizationSanitizer/entry';
13
13
  export { steamBbcodeTranslator } from './tool/steamBbcodeTranslator/entry';
14
14
  export type { SteamBbcodeTranslatorUI, SteamBbcodeTranslatorLocaleContent } from './tool/steamBbcodeTranslator/entry';
15
+ export { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
16
+ export type { RetroSfxGeneratorUI, RetroSfxGeneratorLocaleContent } from './tool/retroSfxGenerator/entry';
17
+ export { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
18
+ export type { PixelArtPaletteSwapperUI, PixelArtPaletteSwapperLocaleContent } from './tool/pixelArtPaletteSwapper/entry';
15
19
  export { gamesCategory } from './category';
16
20
  import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
17
21
  import { itchioGameTester } from './tool/itchioGameTester/entry';
@@ -19,5 +23,7 @@ import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
19
23
  import { audioLoopPointFinder } from './tool/audioLoopPointFinder/entry';
20
24
  import { saveFileEditor } from './tool/saveFileEditor/entry';
21
25
  import { localizationSanitizer } from './tool/localizationSanitizer/entry';
26
+ import { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
27
+ import { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
22
28
 
23
- export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer];
29
+ export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, retroSfxGenerator, pixelArtPaletteSwapper];
package/src/index.ts CHANGED
@@ -18,5 +18,7 @@ export type {
18
18
  export { ALL_ENTRIES, ALL_TOOLS } from './tools';
19
19
  export { STEAM_CAPSULE_GENERATOR_TOOL, steamCapsuleGenerator } from './tool/steamCapsuleGenerator';
20
20
  export { STEAM_BBCODE_TRANSLATOR_TOOL, steamBbcodeTranslator } from './tool/steamBbcodeTranslator';
21
+ export { RETRO_SFX_GENERATOR_TOOL, retroSfxGenerator } from './tool/retroSfxGenerator';
22
+ export { PIXEL_ART_PALETTE_SWAPPER_TOOL, pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper';
21
23
 
22
24
  export type { ToolLocaleContent as GamesToolLocaleContent } from './types';
@@ -0,0 +1,291 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+
5
+ type Color = { r: number; g: number; b: number; a: number };
6
+ type Declaration = { name: string; value: string };
7
+ type CssRule = { selector: string; declarations: Declaration[]; line: number };
8
+ type Theme = 'light' | 'dark';
9
+ type Variables = Record<string, string>;
10
+
11
+ const toolRoot = join(process.cwd(), 'src', 'tool');
12
+ const minimumTextContrast = 4.5;
13
+ const minimumReviewFontSizeRem = 0.75;
14
+ const themes: Theme[] = ['light', 'dark'];
15
+
16
+ function findFiles(directory: string, extensions: string[]): string[] {
17
+ const files: string[] = [];
18
+
19
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
20
+ const path = join(directory, entry.name);
21
+ if (entry.isDirectory()) files.push(...findFiles(path, extensions));
22
+ else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(path);
23
+ }
24
+
25
+ return files;
26
+ }
27
+
28
+ function relativePath(path: string): string {
29
+ return relative(process.cwd(), path).replace(/\\/g, '/');
30
+ }
31
+
32
+ function lineAt(source: string, index: number): number {
33
+ return source.slice(0, index).split('\n').length;
34
+ }
35
+
36
+ function removeComments(source: string): string {
37
+ return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ' '));
38
+ }
39
+
40
+ function cssSources(path: string): string[] {
41
+ const source = readFileSync(path, 'utf8');
42
+ if (path.endsWith('.css')) return [source];
43
+
44
+ return Array.from(source.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi), (match) => match[1])
45
+ .filter((style): style is string => style !== undefined);
46
+ }
47
+
48
+ function parseDeclarations(body: string): Declaration[] {
49
+ return body
50
+ .split(';')
51
+ .map((declaration) => {
52
+ const separator = declaration.indexOf(':');
53
+ if (separator === -1) return null;
54
+ return {
55
+ name: declaration.slice(0, separator).trim().toLowerCase(),
56
+ value: declaration.slice(separator + 1).trim(),
57
+ };
58
+ })
59
+ .filter((declaration): declaration is Declaration => declaration !== null && declaration.name.length > 0);
60
+ }
61
+
62
+ function parseRules(source: string): CssRule[] {
63
+ const css = removeComments(source);
64
+ const rules: CssRule[] = [];
65
+ const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
66
+
67
+ for (const match of css.matchAll(rulePattern)) {
68
+ const selector = match[1]?.trim();
69
+ const body = match[2];
70
+ if (selector && body !== undefined) {
71
+ const declarations = parseDeclarations(body);
72
+ if (declarations.length > 0) {
73
+ rules.push({ selector, declarations, line: lineAt(css, match.index ?? 0) });
74
+ }
75
+ }
76
+ }
77
+
78
+ return rules;
79
+ }
80
+
81
+ function channel(value: string): number | null {
82
+ const trimmed = value.trim();
83
+ if (trimmed.endsWith('%')) {
84
+ const percentage = Number.parseFloat(trimmed);
85
+ return Number.isFinite(percentage) ? (percentage / 100) * 255 : null;
86
+ }
87
+
88
+ const number = Number.parseFloat(trimmed);
89
+ return Number.isFinite(number) ? number : null;
90
+ }
91
+
92
+ function expandHex(hex: string): string {
93
+ return hex.length <= 4 ? hex.split('').map((part) => part + part).join('') : hex;
94
+ }
95
+
96
+ function hexValues(expanded: string): number[] | null {
97
+ const values = expanded.match(/[\da-f]{2}/gi)?.map((part) => Number.parseInt(part, 16));
98
+ if (!values || values.length < 3) return null;
99
+ return values;
100
+ }
101
+
102
+ function parseHexColor(value: string): Color | null {
103
+ const hex = value.match(/^#([\da-f]{3,8})$/i)?.[1];
104
+ if (!hex) return null;
105
+ const expanded = expandHex(hex);
106
+ if (![6, 8].includes(expanded.length)) return null;
107
+ const values = hexValues(expanded);
108
+ if (!values) return null;
109
+ const [r, g, b, alpha] = values;
110
+ return { r: r!, g: g!, b: b!, a: alpha === undefined ? 1 : alpha / 255 };
111
+ }
112
+
113
+ function rgbChannels(parts: string[]): [number, number, number] | null {
114
+ const [r, g, b] = parts.slice(0, 3).map(channel);
115
+ if ([r, g, b].some((value) => value === null || value === undefined)) return null;
116
+ return [r!, g!, b!];
117
+ }
118
+
119
+ function alphaValue(value: string | undefined): number | null {
120
+ if (value === undefined) return 1;
121
+ const alpha = Number.parseFloat(value);
122
+ return Number.isFinite(alpha) ? alpha : null;
123
+ }
124
+
125
+ function parseRgbParts(rgb: string): Color | null {
126
+ const parts = rgb.replace('/', ',').split(',').map((part) => part.trim());
127
+ if (parts.length < 3) return null;
128
+ const channels = rgbChannels(parts);
129
+ const alpha = alphaValue(parts[3]);
130
+ if (!channels || alpha === null) return null;
131
+ const [r, g, b] = channels;
132
+ return { r, g, b, a: alpha };
133
+ }
134
+
135
+ function parseRgbColor(value: string): Color | null {
136
+ const rgb = value.match(/^rgba?\(([^)]+)\)$/)?.[1];
137
+ return rgb ? parseRgbParts(rgb) : null;
138
+ }
139
+
140
+ function parseColor(value: string): Color | null {
141
+ const trimmed = value.trim().toLowerCase();
142
+ const fallback = trimmed.match(/^var\([^,]+,\s*(.+)\)$/)?.[1];
143
+ if (fallback) return parseColor(fallback);
144
+ if (trimmed === 'white') return { r: 255, g: 255, b: 255, a: 1 };
145
+ if (trimmed === 'black') return { r: 0, g: 0, b: 0, a: 1 };
146
+ if (trimmed === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
147
+ return parseHexColor(trimmed) ?? parseRgbColor(trimmed);
148
+ }
149
+
150
+ function composite(foreground: Color, background: Color): Color | null {
151
+ if (background.a < 1) return null;
152
+ if (foreground.a >= 1) return foreground;
153
+ const alpha = foreground.a;
154
+ return {
155
+ r: foreground.r * alpha + background.r * (1 - alpha),
156
+ g: foreground.g * alpha + background.g * (1 - alpha),
157
+ b: foreground.b * alpha + background.b * (1 - alpha),
158
+ a: 1,
159
+ };
160
+ }
161
+
162
+ function relativeLuminance(color: Color): number {
163
+ const linear = [color.r, color.g, color.b].map((value) => {
164
+ const channelValue = value / 255;
165
+ return channelValue <= 0.03928
166
+ ? channelValue / 12.92
167
+ : ((channelValue + 0.055) / 1.055) ** 2.4;
168
+ });
169
+ return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
170
+ }
171
+
172
+ function contrastRatio(foreground: Color, background: Color): number | null {
173
+ const compositedForeground = composite(foreground, background);
174
+ if (!compositedForeground) return null;
175
+ const foregroundLuminance = relativeLuminance(compositedForeground);
176
+ const backgroundLuminance = relativeLuminance(background);
177
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
178
+ / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
179
+ }
180
+
181
+ function lastDeclaration(declarations: Declaration[], names: string[]): Declaration | undefined {
182
+ return [...declarations].reverse().find((declaration) => names.includes(declaration.name));
183
+ }
184
+
185
+ function simpleBackground(value: string): Color | null {
186
+ if (value.includes('gradient') || value.includes('url(') || value.includes('color-mix(')) return null;
187
+ return parseColor(value);
188
+ }
189
+
190
+ function isThemeSelector(selector: string, theme: Theme): boolean {
191
+ return selector.includes(`.theme-${theme}`);
192
+ }
193
+
194
+ function isActiveRule(selector: string, theme: Theme): boolean {
195
+ const otherTheme: Theme = theme === 'dark' ? 'light' : 'dark';
196
+ return !isThemeSelector(selector, otherTheme);
197
+ }
198
+
199
+ function addVariables(target: Variables, rule: CssRule): void {
200
+ rule.declarations
201
+ .filter((declaration) => declaration.name.startsWith('--'))
202
+ .forEach((declaration) => { target[declaration.name] = declaration.value; });
203
+ }
204
+
205
+ function collectVariables(rules: CssRule[], theme: Theme): Variables {
206
+ const variables: Variables = {};
207
+ rules.filter((rule) => !isThemeSelector(rule.selector, 'dark') && !isThemeSelector(rule.selector, 'light'))
208
+ .forEach((rule) => addVariables(variables, rule));
209
+ rules.filter((rule) => isThemeSelector(rule.selector, theme))
210
+ .forEach((rule) => addVariables(variables, rule));
211
+ return variables;
212
+ }
213
+
214
+ function resolveCssValue(value: string, variables: Variables): string {
215
+ let resolved = value;
216
+ for (let attempt = 0; attempt < 10; attempt += 1) {
217
+ const next = resolved.replace(/var\(\s*(--[\w-]+)(?:\s*,\s*([^()]+))?\s*\)/g, (_, name, fallback) => variables[name] ?? fallback ?? '');
218
+ if (next === resolved) return resolved;
219
+ resolved = next;
220
+ }
221
+ return resolved;
222
+ }
223
+
224
+ function contrastFinding(path: string, rule: CssRule, theme: Theme, variables: Variables): string | null {
225
+ const foreground = lastDeclaration(rule.declarations, ['color']);
226
+ const background = lastDeclaration(rule.declarations, ['background-color', 'background']);
227
+ const foregroundColor = foreground ? parseColor(resolveCssValue(foreground.value, variables)) : null;
228
+ const backgroundColor = background ? simpleBackground(resolveCssValue(background.value, variables)) : null;
229
+ const ratio = foregroundColor && backgroundColor ? contrastRatio(foregroundColor, backgroundColor) : null;
230
+ return ratio !== null && ratio < minimumTextContrast
231
+ ? `[A11Y-06][${theme}] ${relativePath(path)}:${rule.line} ${rule.selector} — ${ratio.toFixed(2)}:1 `
232
+ + `(mínimo ${minimumTextContrast}:1)`
233
+ : null;
234
+ }
235
+
236
+ function fontSizeFinding(path: string, rule: CssRule): string | null {
237
+ const rem = lastDeclaration(rule.declarations, ['font-size'])?.value.match(/^(-?(?:\d+\.?\d*|\.\d+))rem$/i)?.[1];
238
+ if (!rem || Number.parseFloat(rem) >= minimumReviewFontSizeRem || /(svg|icon|logo|decorative|sr-only)/i.test(rule.selector)) {
239
+ return null;
240
+ }
241
+ return `[A11Y-06] ${relativePath(path)}:${rule.line} ${rule.selector} — font-size ${rem}rem `
242
+ + `(revisar por debajo de ${minimumReviewFontSizeRem}rem)`;
243
+ }
244
+
245
+ function inspectSource(path: string, source: string): { contrastFailures: string[]; fontSizeCandidates: string[] } {
246
+ const rules = parseRules(source);
247
+ const contrastFailures = themes.flatMap((theme) => {
248
+ const variables = collectVariables(rules, theme);
249
+ return rules
250
+ .filter((rule) => isActiveRule(rule.selector, theme))
251
+ .map((rule) => contrastFinding(path, rule, theme, variables))
252
+ .filter((finding): finding is string => finding !== null);
253
+ });
254
+ const fontSizeCandidates = rules
255
+ .map((rule) => fontSizeFinding(path, rule))
256
+ .filter((finding): finding is string => finding !== null);
257
+ return {
258
+ contrastFailures,
259
+ fontSizeCandidates,
260
+ };
261
+ }
262
+
263
+ function inspectStyles(): { contrastFailures: string[]; fontSizeCandidates: string[] } {
264
+ const findings = findFiles(toolRoot, ['.css', '.astro'])
265
+ .flatMap((path) => cssSources(path).map((source) => inspectSource(path, source)));
266
+ return {
267
+ contrastFailures: findings.flatMap((finding) => finding.contrastFailures),
268
+ fontSizeCandidates: findings.flatMap((finding) => finding.fontSizeCandidates),
269
+ };
270
+ }
271
+
272
+ describe('QA: contraste y legibilidad de las tools', () => {
273
+ it('calcula ratios WCAG conocidos correctamente', () => {
274
+ expect(contrastRatio(parseColor('#000')!, parseColor('#fff')!)).toBeCloseTo(21, 5);
275
+ expect(contrastRatio(parseColor('#777')!, parseColor('#fff')!)).toBeCloseTo(4.478, 2);
276
+ expect(contrastRatio(parseColor('rgba(0, 0, 0, 0.5)')!, parseColor('#fff')!)).toBeCloseTo(3.98, 2);
277
+ });
278
+
279
+ it('no contiene pares de texto y fondo explícitos por debajo de 4.5:1', () => {
280
+ const { contrastFailures } = inspectStyles();
281
+ expect(contrastFailures, `Contrastes insuficientes:\n${contrastFailures.join('\n')}`).toEqual([]);
282
+ });
283
+
284
+ it('no usa texto por debajo de 0.75rem fuera de elementos decorativos', () => {
285
+ const { fontSizeCandidates } = inspectStyles();
286
+ expect(
287
+ fontSizeCandidates,
288
+ 'Candidatos de legibilidad. El umbral es una política de revisión del repositorio, no una validación completa de WCAG.',
289
+ ).toEqual([]);
290
+ });
291
+ });
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import { ALL_TOOLS } from '../tools';
3
3
 
4
4
  describe('Locale Completeness Validation', () => {
5
- it('seven tools are registered', () => {
6
- expect(ALL_TOOLS.length).toBe(7);
5
+ it('nine tools are registered', () => {
6
+ expect(ALL_TOOLS.length).toBe(9);
7
7
  });
8
8
  });
@@ -4,8 +4,8 @@ import { gamesCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 7 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(7);
7
+ it('should have 9 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(9);
9
9
  });
10
10
 
11
11
  it('gamesCategory should be defined', () => {
@@ -113,9 +113,9 @@
113
113
  }
114
114
 
115
115
  .igt-chip:hover {
116
- background: var(--color-primary, #6366f1);
116
+ background: var(--color-primary, #4f46e5);
117
117
  color: var(--text-inverse, #fff);
118
- border-color: var(--color-primary, #6366f1);
118
+ border-color: var(--color-primary, #4f46e5);
119
119
  }
120
120
 
121
121
  .igt-chip.active {
@@ -195,7 +195,7 @@
195
195
  padding: 0.75rem 1.25rem;
196
196
  border-radius: 8px;
197
197
  border: none;
198
- background: var(--color-primary, #6366f1);
198
+ background: var(--color-primary, #4f46e5);
199
199
  color: var(--text-inverse, #fff);
200
200
  font-weight: 600;
201
201
  cursor: pointer;
@@ -6,6 +6,7 @@
6
6
  --ls-panel-soft: #f4f7fa;
7
7
  --ls-accent: #e56a47;
8
8
  --ls-accent-dark: #bc4d30;
9
+ --ls-on-accent: #17202f;
9
10
  --ls-cyan: #19a6a0;
10
11
  --ls-yellow: #e7a633;
11
12
  --ls-red: #ce4f61;
@@ -24,6 +25,7 @@
24
25
  --ls-panel-soft: #172232;
25
26
  --ls-accent: #f1845d;
26
27
  --ls-accent-dark: #da6744;
28
+ --ls-on-accent: #111926;
27
29
  --ls-cyan: #42c3b8;
28
30
  --ls-yellow: #efbd57;
29
31
  --ls-red: #f07889;
@@ -73,7 +75,7 @@
73
75
  .ls-kicker,
74
76
  .ls-section-eyebrow {
75
77
  color: var(--ls-accent);
76
- font-size: 0.68rem;
78
+ font-size: 0.75rem;
77
79
  font-weight: 800;
78
80
  letter-spacing: 0.14em;
79
81
  }
@@ -101,7 +103,7 @@
101
103
 
102
104
  .ls-format-label {
103
105
  color: var(--ls-muted);
104
- font-size: 0.72rem;
106
+ font-size: 0.75rem;
105
107
  font-weight: 700;
106
108
  margin-right: auto;
107
109
  text-transform: uppercase;
@@ -162,7 +164,7 @@
162
164
  .ls-drop-mark span {
163
165
  border: 1px solid var(--ls-accent);
164
166
  color: var(--ls-accent-dark);
165
- font-size: 0.58rem;
167
+ font-size: 0.75rem;
166
168
  font-weight: 900;
167
169
  letter-spacing: 0.08em;
168
170
  padding: 0.22rem 0.36rem;
@@ -170,7 +172,7 @@
170
172
 
171
173
  .ls-drop-mark span:last-child {
172
174
  background: var(--ls-accent);
173
- color: var(--ls-panel);
175
+ color: var(--ls-on-accent);
174
176
  transform: translateX(0.4rem);
175
177
  }
176
178
 
@@ -189,7 +191,7 @@
189
191
 
190
192
  .ls-file-type-note {
191
193
  color: var(--ls-cyan);
192
- font-size: 0.69rem;
194
+ font-size: 0.75rem;
193
195
  font-weight: 700;
194
196
  }
195
197
 
@@ -219,7 +221,7 @@
219
221
 
220
222
  .ls-button-primary {
221
223
  background: var(--ls-accent);
222
- color: var(--ls-panel);
224
+ color: var(--ls-on-accent);
223
225
  }
224
226
 
225
227
  .ls-button-primary:hover:not(:disabled) {
@@ -277,7 +279,7 @@
277
279
 
278
280
  .ls-section-index {
279
281
  color: var(--ls-accent);
280
- font-size: 0.72rem;
282
+ font-size: 0.75rem;
281
283
  font-weight: 900;
282
284
  padding-top: 0.16rem;
283
285
  }
@@ -329,7 +331,7 @@
329
331
 
330
332
  .ls-health-card span:last-child {
331
333
  color: var(--ls-muted);
332
- font-size: 0.7rem;
334
+ font-size: 0.75rem;
333
335
  margin-top: 0.22rem;
334
336
  }
335
337
 
@@ -435,14 +437,14 @@
435
437
  }
436
438
 
437
439
  .ls-issue-copy strong {
438
- font-size: 0.7rem;
440
+ font-size: 0.75rem;
439
441
  }
440
442
 
441
443
  .ls-issue-copy span,
442
444
  .ls-issue-more,
443
445
  .ls-no-issues {
444
446
  color: var(--ls-muted);
445
- font-size: 0.66rem;
447
+ font-size: 0.75rem;
446
448
  }
447
449
 
448
450
  .ls-issue-more {
@@ -491,7 +493,7 @@
491
493
  }
492
494
 
493
495
  .ls-empty-state span:last-child {
494
- font-size: 0.73rem;
496
+ font-size: 0.75rem;
495
497
  }
496
498
 
497
499
  .ls-empty-grid {
@@ -505,7 +507,7 @@
505
507
 
506
508
  .ls-table {
507
509
  border-collapse: collapse;
508
- font-size: 0.72rem;
510
+ font-size: 0.75rem;
509
511
  min-width: 100%;
510
512
  table-layout: fixed;
511
513
  }
@@ -524,7 +526,7 @@
524
526
  .ls-table th {
525
527
  background: color-mix(in srgb, var(--ls-accent) 8%, var(--ls-panel));
526
528
  color: var(--ls-accent-dark);
527
- font-size: 0.65rem;
529
+ font-size: 0.75rem;
528
530
  letter-spacing: 0.05em;
529
531
  position: sticky;
530
532
  text-transform: uppercase;
@@ -544,7 +546,7 @@
544
546
  .ls-preview-note {
545
547
  color: var(--ls-muted);
546
548
  display: block;
547
- font-size: 0.68rem;
549
+ font-size: 0.75rem;
548
550
  padding: 0.6rem;
549
551
  text-align: center;
550
552
  }
@@ -568,7 +570,7 @@
568
570
 
569
571
  .ls-live-status {
570
572
  color: var(--ls-cyan);
571
- font-size: 0.72rem;
573
+ font-size: 0.75rem;
572
574
  min-height: 1rem;
573
575
  padding: 0.35rem 0.2rem;
574
576
  }