@brightsideoy/kama 0.1.0-alpha.12 → 0.1.0-alpha.121

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 (86) hide show
  1. package/README.md +195 -0
  2. package/bin/cli.js +602 -0
  3. package/content/definitions/block-layouts/three-column.yaml +20 -0
  4. package/content/definitions/block-layouts/two-column.yaml +29 -0
  5. package/content/definitions/blocks/button.yaml +24 -0
  6. package/content/definitions/blocks/header.yaml +13 -0
  7. package/content/definitions/blocks/header_image.yaml +21 -0
  8. package/content/definitions/blocks/hero.yaml +28 -0
  9. package/content/definitions/blocks/html.yaml +12 -0
  10. package/content/definitions/blocks/image_card.yaml +56 -0
  11. package/content/definitions/blocks/single_image.yaml +27 -0
  12. package/content/definitions/blocks/text.yaml +15 -0
  13. package/content/definitions/channels/ecommerce.yaml +28 -0
  14. package/content/definitions/channels/website.yaml +17 -0
  15. package/content/definitions/field-sets/access.yaml +27 -0
  16. package/content/definitions/field-sets/channel-base.yaml +50 -0
  17. package/content/definitions/field-sets/channel-seo.yaml +21 -0
  18. package/content/definitions/field-sets/comments.yaml +11 -0
  19. package/content/definitions/field-sets/content-block.yaml +41 -0
  20. package/content/definitions/field-sets/page-base.yaml +44 -0
  21. package/content/definitions/field-sets/page-seo.yaml +34 -0
  22. package/content/definitions/field-sets/page-social.yaml +78 -0
  23. package/content/definitions/field-sets/schema-type.yaml +43 -0
  24. package/content/definitions/field-sets/tree-base.yaml +31 -0
  25. package/content/definitions/field-sets/visibility.yaml +8 -0
  26. package/content/definitions/node-layouts/content.yml +12 -0
  27. package/content/definitions/node-layouts/landing-page.yml +25 -0
  28. package/content/definitions/node-layouts/single-column.yml +44 -0
  29. package/content/definitions/node-layouts/two-column.yml +49 -0
  30. package/content/definitions/nodes/address.yaml +40 -0
  31. package/content/definitions/nodes/article.yaml +48 -0
  32. package/content/definitions/nodes/event.yaml +55 -0
  33. package/content/definitions/nodes/external-link.yaml +29 -0
  34. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  35. package/content/definitions/nodes/page.yaml +57 -0
  36. package/content/definitions/nodes/person.yaml +41 -0
  37. package/content/definitions/nodes/product.yaml +89 -0
  38. package/content/definitions/trees/navigation.yaml +24 -0
  39. package/definitions/editor/button.ts +11 -0
  40. package/definitions/editor/default.ts +21 -0
  41. package/definitions/editor/header.ts +36 -0
  42. package/definitions/editor/minimal.ts +15 -0
  43. package/definitions/validators/iban.ts +12 -0
  44. package/definitions/validators/magic-word.ts +7 -0
  45. package/dist/assets/index-C6GoXL-R.js +162 -0
  46. package/dist/assets/index-Drmp3h6P.css +1 -0
  47. package/dist/index.html +13 -0
  48. package/dist/index.js +12209 -2045
  49. package/documentation/SCHEMAS.md +182 -0
  50. package/package.json +14 -11
  51. package/schema.sql +204 -0
  52. package/templates/astro-starter/astro.config.mjs +40 -0
  53. package/templates/astro-starter/middleware/api-purge.ts +130 -0
  54. package/templates/astro-starter/middleware/index.ts +10 -0
  55. package/templates/astro-starter/middleware/kv-cache.ts +76 -0
  56. package/templates/astro-starter/middleware/response-headers.ts +43 -0
  57. package/templates/astro-starter/package.json.template +20 -0
  58. package/templates/astro-starter/public/css/standard-theme.css +157 -0
  59. package/templates/astro-starter/public/robots.txt +23 -0
  60. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +133 -0
  61. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
  62. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
  63. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  64. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +24 -0
  65. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
  66. package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
  67. package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
  68. package/templates/astro-starter/src/components/global/Footer.astro +48 -0
  69. package/templates/astro-starter/src/components/global/Header.astro +92 -0
  70. package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
  71. package/templates/astro-starter/src/components/seo/SeoHead.astro +150 -0
  72. package/templates/astro-starter/src/components/ui/KamaImage.astro +149 -0
  73. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +45 -0
  74. package/templates/astro-starter/src/layouts/StandardLayout.astro +52 -0
  75. package/templates/astro-starter/src/lib/cms.ts +149 -0
  76. package/templates/astro-starter/src/lib/media/kama-image-service.ts +41 -0
  77. package/templates/astro-starter/src/lib/media/media-proxy.ts +127 -0
  78. package/templates/astro-starter/src/lib/media/media-url.ts +34 -0
  79. package/templates/astro-starter/src/pages/404.astro +46 -0
  80. package/templates/astro-starter/src/pages/[...slug].astro +201 -0
  81. package/templates/astro-starter/src/pages/llms-full.txt.ts +161 -0
  82. package/templates/astro-starter/src/pages/llms.txt.ts +107 -0
  83. package/templates/astro-starter/src/pages/media/[...path].ts +7 -0
  84. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  85. package/templates/astro-starter/tsconfig.json +0 -0
  86. package/test-local.sh +76 -0
@@ -0,0 +1,218 @@
1
+ ---
2
+ // src/components/blocks/Hero.astro
3
+ ---
4
+
5
+ <section class="hero-container">
6
+ <div class="hero-content">
7
+ <!-- Sub-badge -->
8
+ <div class="hero-badge">
9
+ <span class="badge-dot"></span>
10
+ Freeware for Education & NGOs
11
+ </div>
12
+
13
+ <!-- H1 Title with Slogan -->
14
+ <h1 class="hero-title">KAMA – Headless CMS on Edge</h1>
15
+
16
+ <!-- Subtitle -->
17
+ <p class="hero-subtitle">
18
+ Deploy sub-millisecond corporate web experiences with zero infrastructure
19
+ overhead. Built natively on Astro, Hono, and Cloudflare Workers (D1/R2).
20
+ </p>
21
+
22
+ <!-- Call to Action Buttons -->
23
+ <div class="hero-actions">
24
+ <a href="/docs/quick-start" class="btn btn-primary">
25
+ Get Started
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="16"
29
+ height="16"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ stroke-width="2"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ class="btn-icon"
37
+ ><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
38
+ >
39
+ </a>
40
+ <a
41
+ href="https://github.com/brightside-ou/kama"
42
+ target="_blank"
43
+ rel="noopener noreferrer"
44
+ class="btn btn-secondary"
45
+ >
46
+ View on GitHub
47
+ </a>
48
+ </div>
49
+
50
+ <!-- Tech Stack Highlights -->
51
+ <div class="hero-stack">
52
+ <span class="stack-label">Powered by edge infrastructure:</span>
53
+ <div class="stack-tags">
54
+ <span class="tag">Cloudflare Workers</span>
55
+ <span class="tag">D1 SQL</span>
56
+ <span class="tag">R2 Storage</span>
57
+ <span class="tag">Astro</span>
58
+ <span class="tag">Hono</span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </section>
63
+
64
+ <style>
65
+ .hero-container {
66
+ padding: 64px 0 48px;
67
+ background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
68
+ border-bottom: 1px solid #e2e8f0;
69
+ }
70
+
71
+ .hero-content {
72
+ max-width: 860px;
73
+ margin: 0 auto;
74
+ text-align: center;
75
+ }
76
+
77
+ .hero-badge {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ padding: 6px 14px;
82
+ background-color: #f0f9ff;
83
+ border: 1px solid #bae6fd;
84
+ color: #0369a1;
85
+ font-size: 0.875rem;
86
+ font-weight: 600;
87
+ margin-bottom: 24px;
88
+ }
89
+
90
+ .badge-dot {
91
+ width: 8px;
92
+ height: 8px;
93
+ background-color: #0284c7;
94
+ border-radius: 50% !important;
95
+ }
96
+
97
+ .hero-title {
98
+ font-size: 3rem;
99
+ font-weight: 800;
100
+ line-height: 1.15;
101
+ color: #0f172a;
102
+ letter-spacing: -0.03em;
103
+ margin-top: 0;
104
+ margin-bottom: 20px;
105
+ }
106
+
107
+ .hero-subtitle {
108
+ font-size: 1.25rem;
109
+ line-height: 1.6;
110
+ color: #475569;
111
+ max-width: 720px;
112
+ margin: 0 auto 36px;
113
+ }
114
+
115
+ .hero-actions {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: 16px;
120
+ margin-bottom: 48px;
121
+ }
122
+
123
+ .btn {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ gap: 8px;
128
+ padding: 12px 28px;
129
+ font-size: 1rem;
130
+ font-weight: 600;
131
+ text-decoration: none;
132
+ transition: all 0.15s ease-in-out;
133
+ }
134
+
135
+ .btn-primary {
136
+ background-color: #0284c7;
137
+ color: #ffffff;
138
+ }
139
+
140
+ .btn-primary:hover {
141
+ background-color: #0369a1;
142
+ color: #ffffff;
143
+ }
144
+
145
+ .btn-secondary {
146
+ background-color: #ffffff;
147
+ color: #334155;
148
+ border: 1px solid #cbd5e1;
149
+ }
150
+
151
+ .btn-secondary:hover {
152
+ background-color: #f8fafc;
153
+ color: #0f172a;
154
+ border-color: #94a3b8;
155
+ }
156
+
157
+ .btn-icon {
158
+ transition: transform 0.15s ease;
159
+ }
160
+
161
+ .btn-primary:hover .btn-icon {
162
+ transform: translateX(4px);
163
+ }
164
+
165
+ .hero-stack {
166
+ display: flex;
167
+ flex-direction: column;
168
+ align-items: center;
169
+ gap: 12px;
170
+ }
171
+
172
+ .stack-label {
173
+ font-size: 0.8125rem;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.05em;
176
+ color: #64748b;
177
+ font-weight: 600;
178
+ }
179
+
180
+ .stack-tags {
181
+ display: flex;
182
+ flex-wrap: wrap;
183
+ justify-content: center;
184
+ gap: 8px;
185
+ }
186
+
187
+ .tag {
188
+ padding: 4px 10px;
189
+ background-color: #f1f5f9;
190
+ color: #334155;
191
+ font-size: 0.8125rem;
192
+ font-weight: 500;
193
+ border: 1px solid #e2e8f0;
194
+ }
195
+
196
+ @media (max-width: 768px) {
197
+ .hero-container {
198
+ padding: 40px 0 32px;
199
+ }
200
+
201
+ .hero-title {
202
+ font-size: 2.125rem;
203
+ }
204
+
205
+ .hero-subtitle {
206
+ font-size: 1.05rem;
207
+ }
208
+
209
+ .hero-actions {
210
+ flex-direction: column;
211
+ width: 100%;
212
+ }
213
+
214
+ .btn {
215
+ width: 100%;
216
+ }
217
+ }
218
+ </style>
@@ -0,0 +1,24 @@
1
+ ---
2
+ // src/components/blocks/HtmlBlock.astro
3
+ interface Props {
4
+ data: {
5
+ body?: string;
6
+ html?: string;
7
+ content?: string;
8
+ text?: string;
9
+ };
10
+ }
11
+
12
+ const { data } = Astro.props;
13
+
14
+ // Fallback chain matching your schema's HTML field key ('body') and legacy aliases
15
+ const rawHtml = data?.body || data?.html || data?.content || data?.text || "";
16
+ ---
17
+
18
+ {
19
+ rawHtml && (
20
+ <div class="cms-block cms-block-html">
21
+ <Fragment set:html={rawHtml} />
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,160 @@
1
+ ---
2
+ // src/components/blocks/ImageBlock.astro
3
+ import KamaImage from "@/components/ui/KamaImage.astro";
4
+
5
+ interface CropVariant {
6
+ id?: string;
7
+ url?: string;
8
+ cropCoordinates?: {
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+ }
13
+
14
+ interface Props {
15
+ data: {
16
+ image?: {
17
+ id?: string;
18
+ url?: string;
19
+ path?: string;
20
+ filename?: string;
21
+ alt?: string;
22
+ width?: number | string;
23
+ height?: number | string;
24
+ cropCoordinates?: {
25
+ width?: number;
26
+ height?: number;
27
+ };
28
+ variants?: {
29
+ standard?: CropVariant;
30
+ tablet?: CropVariant;
31
+ mobile?: CropVariant;
32
+ [key: string]: CropVariant | undefined;
33
+ };
34
+ };
35
+
36
+ image_meta?: {
37
+ cropCoordinates?: {
38
+ width?: number;
39
+ height?: number;
40
+ };
41
+ };
42
+
43
+ width?: number | string;
44
+ height?: number | string;
45
+ };
46
+
47
+ region?: string;
48
+ isFirst?: boolean;
49
+ }
50
+
51
+ const { data, region = "main", isFirst = false } = Astro.props;
52
+
53
+ const img = data?.image;
54
+
55
+ if (!img || (!img.url && !img.path && !img.variants)) {
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Determine whether this is likely to be LCP.
61
+ */
62
+ const isLcp = region === "header" || (region === "main" && isFirst);
63
+
64
+ /**
65
+ * Determine grid column count from the region name.
66
+ */
67
+ let columnCount = 1;
68
+
69
+ if (region.includes("_")) {
70
+ columnCount = 2;
71
+
72
+ if (region.includes("_middle") || region.includes("_col3")) {
73
+ columnCount = 3;
74
+ }
75
+
76
+ if (region.includes("_col4")) {
77
+ columnCount = 4;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Default single-column layout.
83
+ */
84
+ let maxWidth = "1352";
85
+ let targetColumnWidth = 1352;
86
+ let optimizedWidths = [768, 1352];
87
+ let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
88
+
89
+ /**
90
+ * Two-column layout.
91
+ */
92
+ if (columnCount === 2) {
93
+ maxWidth = "660";
94
+ targetColumnWidth = 660;
95
+ optimizedWidths = [400, 660];
96
+ sizes =
97
+ "(max-width: 767px) calc(100vw - 48px), " +
98
+ "(max-width: 1399px) calc((100vw - 80px) / 2), " +
99
+ "660px";
100
+ }
101
+
102
+ /**
103
+ * Three-column layout.
104
+ */
105
+ else if (columnCount === 3) {
106
+ maxWidth = "429";
107
+ targetColumnWidth = 429;
108
+ optimizedWidths = [300, 429];
109
+ sizes =
110
+ "(max-width: 767px) calc(100vw - 48px), " +
111
+ "(max-width: 1399px) calc((100vw - 112px) / 3), " +
112
+ "429px";
113
+ }
114
+
115
+ /**
116
+ * Four-column layout.
117
+ */
118
+ else if (columnCount === 4) {
119
+ maxWidth = "314";
120
+ targetColumnWidth = 314;
121
+ optimizedWidths = [264, 314];
122
+ sizes =
123
+ "(max-width: 767px) calc(100vw - 48px), " +
124
+ "(max-width: 1399px) calc((100vw - 144px) / 4), " +
125
+ "314px";
126
+ }
127
+ ---
128
+
129
+ <div class="cms-block cms-block-image" style={`--max-w: ${maxWidth}px;`}>
130
+ <KamaImage
131
+ image={img}
132
+ sizes={sizes}
133
+ desktopWidths={optimizedWidths}
134
+ idealWidth={targetColumnWidth}
135
+ width={data.width || data.image_meta?.cropCoordinates?.width || img.width}
136
+ height={data.height ||
137
+ data.image_meta?.cropCoordinates?.height ||
138
+ img.height}
139
+ class="free-crop-image"
140
+ loading={isLcp ? "eager" : "lazy"}
141
+ fetchpriority={isLcp ? "high" : "auto"}
142
+ decoding={isLcp ? "sync" : "async"}
143
+ />
144
+ </div>
145
+
146
+ <style>
147
+ .cms-block-image {
148
+ width: 100%;
149
+ max-width: var(--max-w, 1352px);
150
+ margin: 0 auto 1.5rem auto;
151
+ }
152
+
153
+ :global(.free-crop-image) {
154
+ width: 100%;
155
+ height: auto;
156
+ object-fit: contain;
157
+ display: block;
158
+ border-radius: var(--cms-radius-md, 0.5rem);
159
+ }
160
+ </style>
@@ -0,0 +1,13 @@
1
+ ---
2
+ interface Props {
3
+ data: {
4
+ text?: string;
5
+ content?: string;
6
+ };
7
+ }
8
+ const { data } = Astro.props;
9
+ ---
10
+
11
+ <div class="block-text">
12
+ <Fragment set:html={data.text || data.content || ""} />
13
+ </div>
@@ -0,0 +1,62 @@
1
+ ---
2
+ interface Props {
3
+ title: string;
4
+ slug?: string;
5
+ }
6
+
7
+ const { title, slug } = Astro.props;
8
+ const isHome = !slug || slug === "home";
9
+ ---
10
+
11
+ <nav class="breadcrumb-nav" aria-label="Breadcrumb">
12
+ <ol class="breadcrumb-list">
13
+ <li class="breadcrumb-item">
14
+ <a href="/">Home</a>
15
+ </li>
16
+ {
17
+ !isHome && (
18
+ <li class="breadcrumb-item active" aria-current="page">
19
+ <span class="separator">/</span>
20
+ <span>{title}</span>
21
+ </li>
22
+ )
23
+ }
24
+ </ol>
25
+ </nav>
26
+
27
+ <style>
28
+ .breadcrumb-nav {
29
+ margin-bottom: 16px;
30
+ }
31
+
32
+ .breadcrumb-list {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 8px;
36
+ list-style: none;
37
+ margin: 0;
38
+ padding: 0;
39
+ font-size: 13px;
40
+ color: #64748b;
41
+ }
42
+
43
+ .breadcrumb-item a {
44
+ color: #334155;
45
+ text-decoration: none;
46
+ }
47
+
48
+ .breadcrumb-item a:hover {
49
+ color: #0f172a;
50
+ text-decoration: underline;
51
+ }
52
+
53
+ .breadcrumb-item.active {
54
+ color: #0f172a;
55
+ font-weight: 500;
56
+ }
57
+
58
+ .separator {
59
+ margin-right: 8px;
60
+ color: #94a3b8;
61
+ }
62
+ </style>
@@ -0,0 +1,48 @@
1
+ ---
2
+ import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
3
+
4
+ interface Props {
5
+ blocks?: any[];
6
+ contentMap?: Record<string, any[]>;
7
+ }
8
+
9
+ const { blocks = [], contentMap = {} } = Astro.props;
10
+ ---
11
+
12
+ <footer class="site-footer">
13
+ <div class="footer-container">
14
+ {
15
+ blocks && blocks.length > 0 ? (
16
+ <BlockRenderer
17
+ blocks={blocks}
18
+ contentMap={contentMap}
19
+ region="footer"
20
+ />
21
+ ) : (
22
+ <div class="footer-default">
23
+ <p>&copy; {new Date().getFullYear()} all rights reserved.</p>
24
+ </div>
25
+ )
26
+ }
27
+ </div>
28
+ </footer>
29
+
30
+ <style>
31
+ .site-footer {
32
+ background: #ffffff;
33
+ border-top: 1px solid #e2e8f0;
34
+ margin-top: auto;
35
+ }
36
+
37
+ .footer-container {
38
+ max-width: 1400px;
39
+ margin: 0 auto;
40
+ padding: 24px 20px;
41
+ }
42
+
43
+ .footer-default p {
44
+ margin: 0;
45
+ font-size: 14px;
46
+ color: #64748b;
47
+ }
48
+ </style>
@@ -0,0 +1,92 @@
1
+ ---
2
+ // src/components/global/Header.astro
3
+ import NavigationMenu from "@/components/global/NavigationMenu.astro";
4
+ import { getTree, getTreeId, getChannelId } from "@/lib/cms";
5
+ import type { TreeNode } from "@/lib/cms";
6
+
7
+ interface Props {
8
+ node?: Record<string, any>;
9
+ tree?: TreeNode[];
10
+ }
11
+
12
+ const { node, tree } = Astro.props;
13
+
14
+ let rawTree = tree;
15
+
16
+ // Fallback: If the layout didn't pass the tree, fetch it directly
17
+ if (!rawTree || rawTree.length === 0) {
18
+ const locals = (Astro.locals as any) || {};
19
+ const runtimeEnv = (locals.runtime?.env as any) || {};
20
+ const cmsWorker = runtimeEnv.CMS_WORKER;
21
+
22
+ // Safely wrap the fetcher for Service Bindings
23
+ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
24
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
25
+ return cmsWorker.fetch(new Request(input, init));
26
+ }
27
+ return fetch(input, init);
28
+ };
29
+
30
+ rawTree = await getTree(getTreeId(), getChannelId(), customFetch);
31
+ }
32
+
33
+ // Automatically use the configured site name for the text logo
34
+ const siteName = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
35
+ ---
36
+
37
+ <header class="site-header">
38
+ <div class="header-container main-header-row">
39
+ <a href="/" class="logo-block">
40
+ <span class="logo-text">{siteName}</span>
41
+ </a>
42
+ <NavigationMenu tree={rawTree} />
43
+ </div>
44
+ </header>
45
+
46
+ <style>
47
+ .site-header {
48
+ background: #ffffff;
49
+ color: #1e293b;
50
+ position: sticky;
51
+ top: 0;
52
+ z-index: 100;
53
+ border-bottom: 1px solid #e2e8f0;
54
+ }
55
+
56
+ .header-container {
57
+ max-width: 1400px;
58
+ margin: 0 auto;
59
+ padding: 0 20px;
60
+ width: 100%;
61
+ box-sizing: border-box;
62
+ }
63
+
64
+ .main-header-row {
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ height: 64px;
69
+ }
70
+
71
+ .logo-block {
72
+ text-decoration: none;
73
+ color: #0f172a;
74
+ font-weight: 700;
75
+ font-size: 20px;
76
+ display: inline-flex;
77
+ align-items: center;
78
+ z-index: 1001;
79
+ }
80
+
81
+ .logo-text {
82
+ white-space: nowrap;
83
+ }
84
+
85
+ @media (min-width: 768px) {
86
+ .main-header-row {
87
+ height: 64px;
88
+ justify-content: space-between;
89
+ gap: 32px;
90
+ }
91
+ }
92
+ </style>