@joystyk/catalog 0.0.0 → 0.2.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 (54) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +29 -2
  3. package/dist/advertise.d.ts +82 -0
  4. package/dist/advertise.js +203 -0
  5. package/dist/advertise.js.map +1 -0
  6. package/dist/categories.d.ts +12 -0
  7. package/dist/categories.js +45 -0
  8. package/dist/categories.js.map +1 -0
  9. package/dist/client.d.ts +1 -0
  10. package/dist/client.js +9 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/components/advertise.d.ts +4 -0
  13. package/dist/components/advertise.js +1304 -0
  14. package/dist/components/advertise.js.map +1 -0
  15. package/dist/components/app.d.ts +25 -0
  16. package/dist/components/app.js +106 -0
  17. package/dist/components/app.js.map +1 -0
  18. package/dist/components/leaving.d.ts +2 -0
  19. package/dist/components/leaving.js +104 -0
  20. package/dist/components/leaving.js.map +1 -0
  21. package/dist/components/views.d.ts +13 -0
  22. package/dist/components/views.js +1410 -0
  23. package/dist/components/views.js.map +1 -0
  24. package/dist/head.d.ts +6 -0
  25. package/dist/head.js +33 -0
  26. package/dist/head.js.map +1 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.js +12 -0
  29. package/dist/labels.d.ts +10 -0
  30. package/dist/labels.js +27 -0
  31. package/dist/labels.js.map +1 -0
  32. package/dist/placeholders.d.ts +3 -0
  33. package/dist/placeholders.js +7 -0
  34. package/dist/placeholders.js.map +1 -0
  35. package/dist/registry.d.ts +46 -0
  36. package/dist/registry.js +106 -0
  37. package/dist/registry.js.map +1 -0
  38. package/dist/routes.d.ts +56 -0
  39. package/dist/routes.js +89 -0
  40. package/dist/routes.js.map +1 -0
  41. package/dist/seo.d.ts +31 -0
  42. package/dist/seo.js +174 -0
  43. package/dist/seo.js.map +1 -0
  44. package/dist/server.d.ts +50 -0
  45. package/dist/server.js +162 -0
  46. package/dist/server.js.map +1 -0
  47. package/dist/store.d.ts +202 -0
  48. package/dist/store.js +136 -0
  49. package/dist/store.js.map +1 -0
  50. package/dist/styles.css +1121 -0
  51. package/dist/wire.d.ts +108 -0
  52. package/dist/wire.js +74 -0
  53. package/dist/wire.js.map +1 -0
  54. package/package.json +63 -11
@@ -0,0 +1,1121 @@
1
+ /*
2
+ * The catalog's document stylesheet: the light-DOM page shell (<jy-app>) and its pages.
3
+ * Load it after @joystyk/ui/tokens.css, which declares the layer order and the base tokens;
4
+ * the server inlines both into every page. Billboard slots are <ad-slot> elements from
5
+ * @joystyk/ui, which reserve their own boxes from the slot registry.
6
+ *
7
+ * Direction: 80s and 90s arcade nostalgia (Press start, Insert coin, high-score tables),
8
+ * kept quiet enough to read. Neon accents live on "screens": panels pinned to the dark
9
+ * scheme, so the bright colours always sit on a dark surface in both themes. Decorative
10
+ * motion runs only when the visitor has not asked for reduced motion.
11
+ */
12
+
13
+ @layer tokens {
14
+ :root {
15
+ /* Neon accents, darker in the light theme so text and borders keep their contrast. */
16
+ --jy-arcade-magenta: light-dark(oklch(50% 0.22 350), oklch(74% 0.19 350));
17
+ --jy-arcade-cyan: light-dark(oklch(47% 0.1 220), oklch(83% 0.13 210));
18
+ --jy-arcade-yellow: light-dark(oklch(50% 0.11 75), oklch(88% 0.16 95));
19
+ --jy-arcade-ink: oklch(19% 0.03 285);
20
+ --jy-shell-max: 76rem;
21
+ --jy-gutter: clamp(1rem, 0.6rem + 1.6vw, 2rem);
22
+ }
23
+ }
24
+
25
+ @layer reset {
26
+ *,
27
+ *::before,
28
+ *::after {
29
+ box-sizing: border-box;
30
+ }
31
+ body,
32
+ h1,
33
+ h2,
34
+ h3,
35
+ p,
36
+ ol,
37
+ ul,
38
+ dl,
39
+ dd,
40
+ figure {
41
+ margin: 0;
42
+ }
43
+ svg {
44
+ display: block;
45
+ }
46
+ button,
47
+ input,
48
+ textarea {
49
+ font: inherit;
50
+ }
51
+ }
52
+
53
+ @layer base {
54
+ html {
55
+ scrollbar-gutter: stable;
56
+ }
57
+ @media (prefers-reduced-motion: no-preference) {
58
+ html {
59
+ scroll-behavior: smooth;
60
+ }
61
+ }
62
+ body {
63
+ min-block-size: 100dvh;
64
+ -webkit-font-smoothing: antialiased;
65
+ }
66
+ h1,
67
+ h2,
68
+ h3 {
69
+ line-height: 1.15;
70
+ text-wrap: balance;
71
+ }
72
+ h1 {
73
+ font-size: var(--jy-step-3);
74
+ letter-spacing: -0.01em;
75
+ }
76
+ h2 {
77
+ font-size: var(--jy-step-2);
78
+ }
79
+ h3 {
80
+ font-size: var(--jy-step-1);
81
+ }
82
+ p,
83
+ li,
84
+ dd {
85
+ text-wrap: pretty;
86
+ }
87
+ p {
88
+ max-inline-size: 65ch;
89
+ }
90
+ a {
91
+ color: inherit;
92
+ text-decoration-thickness: 0.08em;
93
+ text-underline-offset: 0.2em;
94
+ }
95
+ a:hover {
96
+ text-decoration-thickness: 0.16em;
97
+ }
98
+ ::selection {
99
+ background: color-mix(in oklch, var(--jy-arcade-magenta) 35%, transparent);
100
+ }
101
+ /* Targets of programmatic focus (after navigation, the skip link) need no ring for mouse users. */
102
+ [tabindex='-1']:focus:not(:focus-visible) {
103
+ outline: none;
104
+ }
105
+ }
106
+
107
+ @layer layout {
108
+ jy-app {
109
+ display: flex;
110
+ flex-direction: column;
111
+ min-block-size: 100dvh;
112
+ container: shell / inline-size;
113
+ }
114
+
115
+ .masthead,
116
+ jy-app > main,
117
+ .site-footer {
118
+ inline-size: 100%;
119
+ max-inline-size: var(--jy-shell-max);
120
+ margin-inline: auto;
121
+ padding-inline: var(--jy-gutter);
122
+ }
123
+
124
+ jy-app > main {
125
+ flex: 1;
126
+ padding-block: var(--jy-space-s) var(--jy-space-l);
127
+ }
128
+
129
+ .with-rail {
130
+ display: grid;
131
+ grid-template-columns: minmax(0, 1fr);
132
+ gap: var(--jy-space-l);
133
+ }
134
+
135
+ /* Single-column grids get a zero minimum, so a wide child (the scrolling lineup table)
136
+ never widens the page past the screen. */
137
+ .column,
138
+ .categories,
139
+ .game-page {
140
+ grid-template-columns: minmax(0, 1fr);
141
+ }
142
+
143
+ .column {
144
+ display: grid;
145
+ gap: var(--jy-space-l);
146
+ align-content: start;
147
+ min-inline-size: 0;
148
+ }
149
+
150
+ /* The rail column appears on wide shells; its 160px column fits the rail slot's box. */
151
+ .rail {
152
+ display: none;
153
+ }
154
+ @container shell (inline-size >= 60rem) {
155
+ .with-rail {
156
+ grid-template-columns: minmax(0, 1fr) 160px;
157
+ }
158
+ .rail {
159
+ display: block;
160
+ position: sticky;
161
+ inset-block-start: var(--jy-space-s);
162
+ align-self: start;
163
+ }
164
+ }
165
+ }
166
+
167
+ @layer components {
168
+ /* --- Accents per category ---------------------------------------------------------- */
169
+ [data-category] {
170
+ --accent: var(--jy-accent);
171
+ }
172
+ [data-category='arcade'] {
173
+ --accent: var(--jy-arcade-magenta);
174
+ }
175
+ [data-category='cards'] {
176
+ --accent: var(--jy-arcade-cyan);
177
+ }
178
+ [data-category='dice'] {
179
+ --accent: var(--jy-arcade-yellow);
180
+ }
181
+
182
+ /* --- Screen-reader text and the skip link ------------------------------------------ */
183
+ .visually-hidden,
184
+ .skip-link:not(:focus) {
185
+ position: absolute;
186
+ inline-size: 1px;
187
+ block-size: 1px;
188
+ overflow: hidden;
189
+ clip-path: inset(50%);
190
+ white-space: nowrap;
191
+ }
192
+ .skip-link:focus {
193
+ position: fixed;
194
+ inset-block-start: var(--jy-space-2xs);
195
+ inset-inline-start: var(--jy-space-2xs);
196
+ z-index: 10;
197
+ padding: var(--jy-space-2xs) var(--jy-space-s);
198
+ border-radius: var(--jy-radius-s);
199
+ background: var(--jy-surface-raised);
200
+ box-shadow: var(--jy-shadow);
201
+ }
202
+
203
+ /* --- Wordmark, kicker and the arcade type ------------------------------------------- */
204
+ .wordmark,
205
+ .kicker,
206
+ .cta,
207
+ .badges,
208
+ .crumbs,
209
+ .score-table,
210
+ .cabinet > h2,
211
+ .facts dt {
212
+ font-family: var(--jy-font-mono);
213
+ }
214
+ .wordmark {
215
+ font-weight: 800;
216
+ letter-spacing: 0.14em;
217
+ text-transform: uppercase;
218
+ color: var(--jy-arcade-magenta);
219
+ }
220
+ @supports (background-clip: text) {
221
+ .wordmark {
222
+ background: linear-gradient(in oklch 90deg, var(--jy-arcade-magenta), var(--jy-arcade-cyan));
223
+ background-clip: text;
224
+ color: transparent;
225
+ }
226
+ }
227
+ .kicker {
228
+ font-size: var(--jy-step--1);
229
+ font-weight: 700;
230
+ letter-spacing: 0.16em;
231
+ text-transform: uppercase;
232
+ color: var(--accent, var(--jy-arcade-magenta));
233
+ }
234
+
235
+ /* --- Masthead and footer --------------------------------------------------------- */
236
+ .masthead {
237
+ display: flex;
238
+ flex-wrap: wrap;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ gap: var(--jy-space-2xs) var(--jy-space-m);
242
+ padding-block: var(--jy-space-s);
243
+ }
244
+ .brand {
245
+ display: inline-flex;
246
+ align-items: center;
247
+ gap: var(--jy-space-2xs);
248
+ min-block-size: var(--jy-target-min);
249
+ font-size: var(--jy-step-1);
250
+ text-decoration: none;
251
+ }
252
+ .logo {
253
+ inline-size: 2rem;
254
+ block-size: 2rem;
255
+ }
256
+ .logo-base {
257
+ fill: var(--jy-text);
258
+ }
259
+ .logo-stick {
260
+ fill: var(--jy-text-muted);
261
+ }
262
+ .logo-ball {
263
+ fill: var(--jy-arcade-magenta);
264
+ }
265
+ .logo-button {
266
+ fill: var(--jy-arcade-cyan);
267
+ }
268
+ .main-nav ul {
269
+ display: flex;
270
+ flex-wrap: wrap;
271
+ gap: 0 var(--jy-space-s);
272
+ padding: 0;
273
+ list-style: none;
274
+ }
275
+ .main-nav a {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ min-block-size: var(--jy-target-min);
279
+ text-decoration: none;
280
+ font-weight: 600;
281
+ }
282
+ .main-nav a:hover,
283
+ .main-nav a[aria-current='page'] {
284
+ text-decoration: underline;
285
+ text-decoration-thickness: 0.16em;
286
+ text-decoration-color: var(--jy-arcade-magenta);
287
+ }
288
+ .site-footer {
289
+ display: grid;
290
+ gap: var(--jy-space-3xs);
291
+ padding-block: 0 var(--jy-space-l);
292
+ color: var(--jy-text-muted);
293
+ }
294
+ /* The rule spans the content column, not the gutters. */
295
+ .site-footer::before {
296
+ content: '';
297
+ margin-block-end: var(--jy-space-s);
298
+ border-block-start: 2px dashed var(--jy-border);
299
+ }
300
+ .wordmark.small {
301
+ margin-inline-end: 0.5em;
302
+ }
303
+ .footer-nav ul {
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ gap: 0 var(--jy-space-s);
307
+ padding: 0;
308
+ list-style: none;
309
+ }
310
+ .footer-nav a {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ min-block-size: var(--jy-target-min);
314
+ color: var(--jy-text);
315
+ }
316
+
317
+ /* --- Screens: panels pinned to the dark scheme ------------------------------------- */
318
+ .screen,
319
+ .score-screen,
320
+ .cabinet {
321
+ color-scheme: dark;
322
+ position: relative;
323
+ background: var(--jy-surface);
324
+ color: var(--jy-text);
325
+ border-radius: var(--jy-radius-m);
326
+ overflow: clip;
327
+ }
328
+ /* Scanlines and a soft vignette: decoration only, so they ignore the pointer. */
329
+ .screen::after,
330
+ .score-screen::after {
331
+ content: '';
332
+ position: absolute;
333
+ inset: 0;
334
+ pointer-events: none;
335
+ background:
336
+ repeating-linear-gradient(to bottom, oklch(100% 0 0 / 0.035) 0 1px, transparent 1px 3px),
337
+ radial-gradient(ellipse at center, transparent 55%, oklch(0% 0 0 / 0.35));
338
+ }
339
+ @media (prefers-contrast: more) {
340
+ .screen::after,
341
+ .score-screen::after {
342
+ display: none;
343
+ }
344
+ }
345
+
346
+ /* --- Hero --------------------------------------------------------------------------- */
347
+ .hero {
348
+ margin-block-end: var(--jy-space-l);
349
+ }
350
+ .hero .screen {
351
+ display: grid;
352
+ gap: var(--jy-space-s);
353
+ padding: clamp(1.5rem, 1rem + 4cqi, 4rem) clamp(1.25rem, 0.5rem + 4cqi, 4rem);
354
+ border: 3px solid color-mix(in oklch, var(--jy-arcade-magenta) 55%, transparent);
355
+ box-shadow:
356
+ 0 0 0 6px var(--jy-arcade-ink),
357
+ 0 18px 40px oklch(20% 0.05 285 / 0.25);
358
+ background:
359
+ radial-gradient(ellipse at 20% 0%, oklch(40% 0.15 350 / 0.35), transparent 60%),
360
+ radial-gradient(ellipse at 90% 100%, oklch(45% 0.12 210 / 0.3), transparent 55%),
361
+ var(--jy-surface);
362
+ }
363
+ .hero h1 {
364
+ max-inline-size: 22ch;
365
+ font-size: var(--jy-step-3);
366
+ text-shadow: 0 0 24px oklch(74% 0.19 350 / 0.45);
367
+ }
368
+ .hero .lede {
369
+ font-size: var(--jy-step-1);
370
+ color: var(--jy-text-muted);
371
+ }
372
+ .hero .kicker {
373
+ color: var(--jy-arcade-yellow);
374
+ }
375
+ .blink::after {
376
+ content: '';
377
+ display: inline-block;
378
+ inline-size: 0.6em;
379
+ block-size: 1em;
380
+ margin-inline-start: 0.35em;
381
+ vertical-align: -0.12em;
382
+ background: currentColor;
383
+ }
384
+ @media (prefers-reduced-motion: no-preference) {
385
+ .blink::after {
386
+ animation: jy-blink 1.1s steps(1, end) infinite;
387
+ }
388
+ }
389
+ @keyframes jy-blink {
390
+ 50% {
391
+ opacity: 0;
392
+ }
393
+ }
394
+ .hero-actions {
395
+ margin-block-start: var(--jy-space-2xs);
396
+ }
397
+ .cta {
398
+ display: inline-flex;
399
+ align-items: center;
400
+ min-block-size: var(--jy-target-min);
401
+ padding: 0.6em 1.5em;
402
+ border-radius: var(--jy-radius-s);
403
+ background: var(--jy-arcade-yellow);
404
+ color: var(--jy-arcade-ink);
405
+ box-shadow: 0 0.3em 0 color-mix(in oklch, var(--jy-arcade-yellow) 50%, black);
406
+ font-weight: 800;
407
+ letter-spacing: 0.12em;
408
+ text-decoration: none;
409
+ text-transform: uppercase;
410
+ }
411
+ .cta:hover {
412
+ background: color-mix(in oklch, var(--jy-arcade-yellow) 85%, white);
413
+ }
414
+ @media (prefers-reduced-motion: no-preference) {
415
+ .cta {
416
+ transition: translate var(--jy-duration-fast) var(--jy-ease-out);
417
+ }
418
+ .cta:active {
419
+ translate: 0 0.2em;
420
+ }
421
+ }
422
+
423
+ /* --- Section headings ---------------------------------------------------------------- */
424
+ .featured > h2,
425
+ .section-head h2,
426
+ .lineup > h2,
427
+ .more > h2,
428
+ .game-page section > h2 {
429
+ display: flex;
430
+ align-items: center;
431
+ gap: 0.5em;
432
+ margin-block-end: var(--jy-space-s);
433
+ }
434
+ .featured > h2::before,
435
+ .section-head h2::before,
436
+ .lineup > h2::before {
437
+ content: '';
438
+ flex: none;
439
+ inline-size: 0.55em;
440
+ block-size: 0.7em;
441
+ background: var(--accent, var(--jy-arcade-magenta));
442
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
443
+ }
444
+ .section-head {
445
+ display: grid;
446
+ gap: var(--jy-space-3xs);
447
+ margin-block-end: var(--jy-space-s);
448
+ }
449
+ .section-head h2 {
450
+ margin-block-end: 0;
451
+ }
452
+ .section-head h2 a {
453
+ text-decoration: none;
454
+ }
455
+ .section-head h2 a:hover {
456
+ text-decoration: underline;
457
+ }
458
+ .section-head p,
459
+ .lede {
460
+ color: var(--jy-text-muted);
461
+ }
462
+ .categories {
463
+ display: grid;
464
+ gap: var(--jy-space-l);
465
+ }
466
+
467
+ /* --- Game grid and cards ------------------------------------------------------------- */
468
+ .game-grid {
469
+ display: grid;
470
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
471
+ gap: var(--jy-space-m) var(--jy-space-s);
472
+ padding: 0;
473
+ list-style: none;
474
+ }
475
+ /* Cards share rows (subgrid), so titles, summaries and badges line up across a row. */
476
+ .game-grid > li {
477
+ display: grid;
478
+ grid-row: span 3;
479
+ grid-template-rows: subgrid;
480
+ row-gap: 0;
481
+ }
482
+ .game-card {
483
+ position: relative;
484
+ display: grid;
485
+ grid-row: span 3;
486
+ grid-template-rows: subgrid;
487
+ row-gap: var(--jy-space-2xs);
488
+ container-type: inline-size;
489
+ padding: var(--jy-space-s);
490
+ border: 2px solid var(--jy-border);
491
+ border-block-start: 6px solid var(--accent);
492
+ border-radius: var(--jy-radius-s);
493
+ background: var(--jy-surface-raised);
494
+ box-shadow: 5px 5px 0 color-mix(in oklch, var(--accent) 22%, transparent);
495
+ }
496
+ .game-card h2,
497
+ .game-card h3 {
498
+ font-size: var(--jy-step-1);
499
+ }
500
+ .game-card a {
501
+ text-decoration: none;
502
+ }
503
+ /* The whole card is the link's hit area; the ring moves to the card. */
504
+ .game-card a::after {
505
+ content: '';
506
+ position: absolute;
507
+ inset: 0;
508
+ }
509
+ .game-card a:focus-visible {
510
+ outline: none;
511
+ }
512
+ .game-card:has(a:focus-visible) {
513
+ outline: 3px solid var(--jy-focus-ring);
514
+ outline-offset: 3px;
515
+ }
516
+ .game-card:hover {
517
+ border-color: var(--accent);
518
+ }
519
+ .game-card:hover a {
520
+ text-decoration: underline;
521
+ }
522
+ @media (hover: hover) and (prefers-reduced-motion: no-preference) {
523
+ .game-card {
524
+ transition:
525
+ translate var(--jy-duration) var(--jy-ease-out),
526
+ box-shadow var(--jy-duration) var(--jy-ease-out);
527
+ }
528
+ .game-card:hover {
529
+ translate: -2px -2px;
530
+ box-shadow: 7px 7px 0 color-mix(in oklch, var(--accent) 30%, transparent);
531
+ }
532
+ }
533
+ .card-summary {
534
+ color: var(--jy-text-muted);
535
+ }
536
+
537
+ /* --- Badges -------------------------------------------------------------------------- */
538
+ .badges {
539
+ display: flex;
540
+ flex-wrap: wrap;
541
+ align-content: start;
542
+ gap: var(--jy-space-3xs);
543
+ padding: 0;
544
+ list-style: none;
545
+ font-size: 0.78rem;
546
+ letter-spacing: 0.04em;
547
+ text-transform: uppercase;
548
+ }
549
+ .badges li {
550
+ padding: 0.2em 0.55em;
551
+ border: 1px solid var(--jy-border);
552
+ border-radius: 0.25rem;
553
+ background: color-mix(in oklch, var(--jy-text) 4%, transparent);
554
+ }
555
+ .badges .badge-status {
556
+ border-color: var(--accent, var(--jy-border));
557
+ font-weight: 700;
558
+ }
559
+ @container (inline-size < 15rem) {
560
+ .badges li:not(.badge-status) {
561
+ font-size: 0.72rem;
562
+ }
563
+ }
564
+
565
+ /* --- The lineup: a high-score table -------------------------------------------------- */
566
+ .score-screen {
567
+ padding: var(--jy-space-s) var(--jy-space-s) var(--jy-space-2xs);
568
+ border: 3px solid color-mix(in oklch, var(--jy-arcade-cyan) 45%, transparent);
569
+ overflow-x: auto;
570
+ }
571
+ .score-screen:focus-visible {
572
+ outline: 3px solid var(--jy-focus-ring);
573
+ outline-offset: 3px;
574
+ }
575
+ .score-table {
576
+ inline-size: 100%;
577
+ border-collapse: collapse;
578
+ font-size: var(--jy-step--1);
579
+ letter-spacing: 0.04em;
580
+ }
581
+ .score-table caption {
582
+ caption-side: bottom;
583
+ padding-block: var(--jy-space-2xs);
584
+ color: var(--jy-text-muted);
585
+ text-align: start;
586
+ }
587
+ .score-table th,
588
+ .score-table td {
589
+ padding: 0.55em 0.75em;
590
+ text-align: start;
591
+ white-space: nowrap;
592
+ }
593
+ /* On phones the cells wrap rather than scroll; the screen still scrolls if it must. */
594
+ @container shell (inline-size < 34rem) {
595
+ .score-screen {
596
+ padding-inline: var(--jy-space-2xs);
597
+ }
598
+ .score-table {
599
+ font-size: 0.8rem;
600
+ letter-spacing: 0;
601
+ }
602
+ .score-table th,
603
+ .score-table td {
604
+ padding-inline: 0.35em;
605
+ white-space: normal;
606
+ }
607
+ }
608
+ .score-table thead th {
609
+ color: var(--jy-arcade-yellow);
610
+ text-transform: uppercase;
611
+ border-block-end: 2px solid color-mix(in oklch, var(--jy-arcade-yellow) 50%, transparent);
612
+ }
613
+ .score-table tbody tr + tr > * {
614
+ border-block-start: 1px dotted color-mix(in oklch, var(--jy-text) 25%, transparent);
615
+ }
616
+ .score-table tbody th {
617
+ font-weight: 700;
618
+ }
619
+ .score-table tbody th a {
620
+ color: var(--jy-arcade-magenta);
621
+ }
622
+ .score-table .rank {
623
+ color: var(--jy-arcade-cyan);
624
+ font-variant-numeric: tabular-nums;
625
+ }
626
+ .score-table .num {
627
+ text-align: end;
628
+ font-variant-numeric: tabular-nums;
629
+ }
630
+
631
+ /* --- Breadcrumbs and page heads ------------------------------------------------------ */
632
+ .crumbs ol {
633
+ display: flex;
634
+ flex-wrap: wrap;
635
+ gap: 0 0.5em;
636
+ padding: 0;
637
+ list-style: none;
638
+ font-size: var(--jy-step--1);
639
+ }
640
+ .crumbs li + li::before {
641
+ content: '/' / '';
642
+ margin-inline-end: 0.5em;
643
+ color: var(--jy-text-muted);
644
+ }
645
+ .crumbs a {
646
+ display: inline-flex;
647
+ align-items: center;
648
+ min-block-size: var(--jy-target-min);
649
+ }
650
+ .crumbs a[aria-current='page'] {
651
+ text-decoration: none;
652
+ color: var(--jy-text-muted);
653
+ }
654
+ .page-head {
655
+ display: grid;
656
+ gap: var(--jy-space-2xs);
657
+ margin-block: var(--jy-space-2xs) var(--jy-space-l);
658
+ }
659
+ .page-head .lede {
660
+ font-size: var(--jy-step-1);
661
+ }
662
+
663
+ /* --- Game page -------------------------------------------------------------------- */
664
+ .game-page {
665
+ display: grid;
666
+ gap: var(--jy-space-l);
667
+ container-type: inline-size;
668
+ }
669
+ .game-page > .crumbs,
670
+ .game-page > .page-head {
671
+ margin-block: 0;
672
+ }
673
+ .facts {
674
+ display: flex;
675
+ flex-wrap: wrap;
676
+ gap: var(--jy-space-2xs);
677
+ margin-block-start: var(--jy-space-2xs);
678
+ }
679
+ .facts > div {
680
+ padding: 0.45em 0.8em;
681
+ border: 1px solid var(--jy-border);
682
+ border-inline-start: 4px solid var(--accent);
683
+ border-radius: 0.25rem;
684
+ background: var(--jy-surface-raised);
685
+ }
686
+ .facts dt {
687
+ font-size: 0.75rem;
688
+ letter-spacing: 0.08em;
689
+ text-transform: uppercase;
690
+ color: var(--jy-text-muted);
691
+ }
692
+ .facts dd {
693
+ font-weight: 600;
694
+ }
695
+ .cabinet {
696
+ display: grid;
697
+ gap: var(--jy-space-s);
698
+ padding: var(--jy-space-m);
699
+ border: 3px solid color-mix(in oklch, var(--accent) 60%, transparent);
700
+ box-shadow: 0 0 0 6px var(--jy-arcade-ink);
701
+ background:
702
+ radial-gradient(ellipse at 100% 0%, color-mix(in oklch, var(--accent) 22%, transparent), transparent 60%),
703
+ var(--jy-surface);
704
+ }
705
+ .cabinet > h2 {
706
+ margin: 0;
707
+ font-size: var(--jy-step-0);
708
+ letter-spacing: 0.14em;
709
+ text-transform: uppercase;
710
+ color: var(--jy-arcade-yellow);
711
+ }
712
+ /* The PWA's "Make available offline" control fills in after a service worker is active;
713
+ its box is reserved so nothing shifts when it appears. */
714
+ jy-offline-toggle {
715
+ display: block;
716
+ min-block-size: var(--jy-target-min);
717
+ }
718
+ @container (inline-size >= 44rem) {
719
+ .game-page > section:not(.cabinet) {
720
+ display: grid;
721
+ grid-template-columns: 14rem minmax(0, 1fr);
722
+ gap: 0 var(--jy-space-l);
723
+ align-items: start;
724
+ }
725
+ .game-page > section:not(.cabinet) > h2 {
726
+ grid-row: span 3;
727
+ font-size: var(--jy-step-1);
728
+ }
729
+ }
730
+ .rules {
731
+ display: grid;
732
+ gap: var(--jy-space-2xs);
733
+ padding-inline-start: 1.5em;
734
+ }
735
+ .rules li::marker {
736
+ font-family: var(--jy-font-mono);
737
+ font-weight: 700;
738
+ color: var(--accent);
739
+ }
740
+ .note {
741
+ margin-block-end: var(--jy-space-2xs);
742
+ color: var(--jy-text-muted);
743
+ font-style: italic;
744
+ }
745
+ .more ul {
746
+ display: grid;
747
+ gap: var(--jy-space-2xs);
748
+ padding-inline-start: 1.25em;
749
+ }
750
+ .more .count {
751
+ color: var(--jy-text-muted);
752
+ }
753
+
754
+ /* --- Not found ---------------------------------------------------------------------- */
755
+ .not-found {
756
+ display: grid;
757
+ gap: var(--jy-space-s);
758
+ justify-items: start;
759
+ padding-block: var(--jy-space-l);
760
+ }
761
+ .not-found .kicker {
762
+ font-size: var(--jy-step-1);
763
+ color: var(--jy-arcade-magenta);
764
+ }
765
+
766
+ /* --- Advertise with us, the creative policy and the sent page ------------------------- */
767
+ .advertise-page {
768
+ display: grid;
769
+ gap: var(--jy-space-l);
770
+ container-type: inline-size;
771
+ }
772
+ .advertise-page > .page-head,
773
+ .advertise-page > .crumbs {
774
+ margin-block: 0;
775
+ }
776
+ .advertise-section {
777
+ display: grid;
778
+ gap: var(--jy-space-s);
779
+ align-content: start;
780
+ }
781
+ .advertise-section > h2 {
782
+ display: flex;
783
+ align-items: center;
784
+ gap: 0.5em;
785
+ }
786
+ .advertise-section > h2::before {
787
+ content: '';
788
+ flex: none;
789
+ inline-size: 0.55em;
790
+ block-size: 0.7em;
791
+ background: var(--jy-arcade-magenta);
792
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
793
+ }
794
+ .points,
795
+ .steps {
796
+ display: grid;
797
+ gap: var(--jy-space-2xs);
798
+ max-inline-size: 70ch;
799
+ padding-inline-start: 1.25em;
800
+ }
801
+ .steps li::marker {
802
+ font-family: var(--jy-font-mono);
803
+ font-weight: 700;
804
+ color: var(--jy-arcade-magenta);
805
+ }
806
+
807
+ /* The slot gallery: one card per slot, its shape drawn to a common scale. */
808
+ .slot-gallery {
809
+ display: grid;
810
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr));
811
+ gap: var(--jy-space-s);
812
+ padding: 0;
813
+ list-style: none;
814
+ }
815
+ .slot-card {
816
+ display: grid;
817
+ gap: var(--jy-space-2xs);
818
+ align-content: start;
819
+ block-size: 100%;
820
+ padding: var(--jy-space-s);
821
+ border: 2px solid var(--jy-border);
822
+ border-block-start: 6px solid var(--jy-arcade-cyan);
823
+ border-radius: var(--jy-radius-s);
824
+ background: var(--jy-surface-raised);
825
+ }
826
+ .slot-card h3 {
827
+ font-size: var(--jy-step-1);
828
+ }
829
+ .slot-preview {
830
+ display: grid;
831
+ place-items: center;
832
+ min-block-size: 9rem;
833
+ padding: var(--jy-space-2xs);
834
+ border-radius: var(--jy-radius-s);
835
+ background: color-mix(in oklch, var(--jy-text) 5%, transparent);
836
+ }
837
+ .slot-shape {
838
+ max-inline-size: 100%;
839
+ }
840
+ .slot-shape rect {
841
+ fill: color-mix(in oklch, var(--jy-arcade-cyan) 25%, transparent);
842
+ stroke: var(--jy-arcade-cyan);
843
+ stroke-width: 2;
844
+ stroke-dasharray: 6 4;
845
+ }
846
+ /* Terms share one column, as wide as the longest term. */
847
+ .slot-facts {
848
+ display: grid;
849
+ grid-template-columns: max-content minmax(0, 1fr);
850
+ gap: var(--jy-space-3xs) var(--jy-space-s);
851
+ font-size: var(--jy-step--1);
852
+ }
853
+ .slot-facts > div {
854
+ display: grid;
855
+ grid-column: span 2;
856
+ grid-template-columns: subgrid;
857
+ }
858
+ .slot-facts dt,
859
+ .policy-list dt {
860
+ font-weight: 700;
861
+ }
862
+
863
+ /* The policy page: lists of products, and the spec table. */
864
+ .policy-list {
865
+ display: grid;
866
+ gap: var(--jy-space-2xs);
867
+ max-inline-size: 70ch;
868
+ }
869
+ .policy-list > div {
870
+ padding-inline-start: var(--jy-space-xs);
871
+ border-inline-start: 4px solid var(--jy-border);
872
+ }
873
+ .table-scroll {
874
+ max-inline-size: 100%;
875
+ overflow-x: auto;
876
+ }
877
+ .table-scroll:focus-visible {
878
+ outline: 3px solid var(--jy-focus-ring);
879
+ outline-offset: 3px;
880
+ }
881
+ .spec-table {
882
+ border-collapse: collapse;
883
+ font-size: var(--jy-step--1);
884
+ }
885
+ .spec-table caption {
886
+ caption-side: bottom;
887
+ padding-block: var(--jy-space-2xs);
888
+ color: var(--jy-text-muted);
889
+ text-align: start;
890
+ }
891
+ .spec-table th,
892
+ .spec-table td {
893
+ padding: 0.5em 0.75em;
894
+ border-block-end: 1px solid var(--jy-border);
895
+ text-align: start;
896
+ }
897
+ .spec-table thead th {
898
+ border-block-end-width: 2px;
899
+ }
900
+
901
+ /* The contact form. */
902
+ .contact-form {
903
+ display: grid;
904
+ gap: var(--jy-space-s);
905
+ max-inline-size: 40rem;
906
+ }
907
+ .field {
908
+ display: grid;
909
+ gap: var(--jy-space-3xs);
910
+ min-inline-size: 0;
911
+ margin: 0;
912
+ padding: 0;
913
+ border: 0;
914
+ }
915
+ .field > label,
916
+ .field > legend {
917
+ padding: 0;
918
+ font-weight: 700;
919
+ }
920
+ .optional,
921
+ .hint {
922
+ font-weight: 400;
923
+ color: var(--jy-text-muted);
924
+ }
925
+ .contact-form input:not([type='checkbox']),
926
+ .contact-form textarea {
927
+ inline-size: 100%;
928
+ min-block-size: var(--jy-target-min);
929
+ padding: 0.5em 0.65em;
930
+ border: 2px solid color-mix(in oklch, var(--jy-text) 45%, var(--jy-surface));
931
+ border-radius: var(--jy-radius-s);
932
+ background: var(--jy-surface-raised);
933
+ color: var(--jy-text);
934
+ }
935
+ .contact-form textarea {
936
+ resize: vertical;
937
+ field-sizing: content;
938
+ min-block-size: 8lh;
939
+ }
940
+ .contact-form input[type='date'] {
941
+ inline-size: auto;
942
+ max-inline-size: 100%;
943
+ }
944
+ .contact-form :is(input, textarea):focus-visible {
945
+ outline: 3px solid var(--jy-focus-ring);
946
+ outline-offset: 2px;
947
+ }
948
+ .contact-form :is(input, textarea)[aria-invalid='true'],
949
+ .contact-form :is(input, textarea):user-invalid {
950
+ border-color: var(--jy-danger);
951
+ }
952
+ .field > legend + .hint,
953
+ .field > legend + .field-error {
954
+ margin-block-start: var(--jy-space-3xs);
955
+ }
956
+ .choices {
957
+ display: grid;
958
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
959
+ gap: 0 var(--jy-space-s);
960
+ padding: 0;
961
+ list-style: none;
962
+ }
963
+ .choice {
964
+ display: flex;
965
+ align-items: center;
966
+ gap: var(--jy-space-2xs);
967
+ min-block-size: var(--jy-target-min);
968
+ }
969
+ .choice input {
970
+ flex: none;
971
+ inline-size: 1.25rem;
972
+ block-size: 1.25rem;
973
+ accent-color: var(--jy-arcade-magenta);
974
+ }
975
+ .date-pair {
976
+ display: flex;
977
+ flex-wrap: wrap;
978
+ gap: var(--jy-space-s);
979
+ margin-block-start: var(--jy-space-2xs);
980
+ }
981
+ .field-error {
982
+ font-weight: 700;
983
+ color: var(--jy-danger);
984
+ }
985
+ .form-errors {
986
+ display: grid;
987
+ gap: var(--jy-space-2xs);
988
+ padding: var(--jy-space-s);
989
+ border: 3px solid var(--jy-danger);
990
+ border-radius: var(--jy-radius-s);
991
+ background: var(--jy-surface-raised);
992
+ }
993
+ .form-errors h3 {
994
+ font-size: var(--jy-step-1);
995
+ }
996
+ .form-errors ul {
997
+ display: grid;
998
+ gap: var(--jy-space-3xs);
999
+ padding-inline-start: 1.25em;
1000
+ }
1001
+ .form-errors a {
1002
+ color: var(--jy-danger);
1003
+ font-weight: 700;
1004
+ }
1005
+ /* The honeypot: people never see it, so they never fill it. */
1006
+ .trap {
1007
+ display: none;
1008
+ }
1009
+ .form-note {
1010
+ color: var(--jy-text-muted);
1011
+ font-size: var(--jy-step--1);
1012
+ }
1013
+ /* The arcade button, in the bright yellow of the dark screens in both schemes: the light
1014
+ scheme's darker yellow is too dark for the ink text. */
1015
+ .contact-form .cta,
1016
+ .leaving .cta {
1017
+ border: 0;
1018
+ background: oklch(88% 0.16 95);
1019
+ box-shadow: 0 0.3em 0 oklch(62% 0.13 85);
1020
+ cursor: pointer;
1021
+ }
1022
+ .contact-form .cta:hover,
1023
+ .leaving .cta:hover {
1024
+ background: oklch(93% 0.11 95);
1025
+ }
1026
+ .contact-form .cta:focus-visible,
1027
+ .leaving .cta:focus-visible {
1028
+ outline: 3px solid var(--jy-focus-ring);
1029
+ outline-offset: 3px;
1030
+ }
1031
+
1032
+ .sent {
1033
+ display: grid;
1034
+ gap: var(--jy-space-s);
1035
+ justify-items: start;
1036
+ padding-block: var(--jy-space-l);
1037
+ }
1038
+ .sent-links {
1039
+ display: flex;
1040
+ flex-wrap: wrap;
1041
+ gap: 0 var(--jy-space-m);
1042
+ padding: 0;
1043
+ list-style: none;
1044
+ }
1045
+ .sent-links a {
1046
+ display: inline-flex;
1047
+ align-items: center;
1048
+ min-block-size: var(--jy-target-min);
1049
+ }
1050
+
1051
+ /* --- You're leaving Joystyk ---------------------------------------------------------- */
1052
+ .leaving {
1053
+ display: grid;
1054
+ gap: var(--jy-space-s);
1055
+ justify-items: start;
1056
+ padding-block: var(--jy-space-l);
1057
+ }
1058
+ /* A long domain breaks rather than pushing the page sideways on a phone. */
1059
+ .leaving-domain {
1060
+ overflow-wrap: anywhere;
1061
+ }
1062
+ .leaving-links {
1063
+ display: flex;
1064
+ flex-wrap: wrap;
1065
+ align-items: center;
1066
+ gap: var(--jy-space-s) var(--jy-space-m);
1067
+ padding: 0;
1068
+ list-style: none;
1069
+ }
1070
+ .leaving-links .cta {
1071
+ overflow-wrap: anywhere;
1072
+ }
1073
+ .leaving-links .back {
1074
+ display: inline-flex;
1075
+ align-items: center;
1076
+ min-block-size: var(--jy-target-min);
1077
+ font-weight: 700;
1078
+ }
1079
+ .leaving-links .back:focus-visible {
1080
+ outline: 3px solid var(--jy-focus-ring);
1081
+ outline-offset: 3px;
1082
+ }
1083
+
1084
+ /* --- Loading line ------------------------------------------------------------------- */
1085
+ .loading {
1086
+ padding-block: var(--jy-space-l);
1087
+ color: var(--jy-text-muted);
1088
+ }
1089
+
1090
+ @media (forced-colors: active) {
1091
+ .game-card,
1092
+ .facts > div,
1093
+ .badges li,
1094
+ .slot-card {
1095
+ border-color: CanvasText;
1096
+ }
1097
+ .slot-shape rect {
1098
+ fill: Canvas;
1099
+ stroke: CanvasText;
1100
+ }
1101
+ .wordmark {
1102
+ color: LinkText;
1103
+ background: none;
1104
+ }
1105
+ }
1106
+ }
1107
+ @layer components {
1108
+ /* Sponsorships disappear throughout play, including its pause menu. */
1109
+ .game-page:has(jy-launcher:state(playing)) > ad-slot {
1110
+ display: none;
1111
+ }
1112
+ }
1113
+
1114
+ @layer components {
1115
+ jy-offline-availability:not([hidden]) {
1116
+ display: block;
1117
+ font-size: var(--jy-font-small, .875rem);
1118
+ color: var(--jy-text-muted);
1119
+ }
1120
+ a[aria-disabled="true"] { cursor: not-allowed; }
1121
+ }