@gemboss/website-ui 0.5.0 → 0.7.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.
package/website.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! @gemboss/website-ui 0.5.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */
1
+ /*! @gemboss/website-ui 0.7.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */
2
2
  /* @gemboss/website-ui tokens. GENERATED from system.json by scripts/build.mjs: do not edit by hand. */
3
3
  :root {
4
4
  color-scheme: light;
@@ -16,6 +16,7 @@
16
16
  --gw-text-30: 30px;
17
17
  --gw-display-h1: clamp(40px, 4.6vw, 68px);
18
18
  --gw-display-h2: clamp(32px, 3.4vw, 52px);
19
+ --gw-display-wordmark: clamp(40px, 9vw, 128px);
19
20
  --gw-display-weight: 600;
20
21
  --gw-display-tracking: -0.03em;
21
22
  --gw-heading-weight: 650;
@@ -90,6 +91,7 @@
90
91
  --gw-muted: #686b71;
91
92
  --gw-line: #e5e7ea;
92
93
  --gw-line-strong: #d5d8dd;
94
+ --gw-line-control: #85888e;
93
95
  --gw-action: #1b1c1e;
94
96
  --gw-action-hover: #000000;
95
97
  --gw-on-action: #ffffff;
@@ -101,6 +103,9 @@
101
103
  --gw-success-soft: #e3f1df;
102
104
  --gw-warning: #9a6700;
103
105
  --gw-warning-soft: #fbf1dc;
106
+ --gw-danger: #b42318;
107
+ --gw-danger-soft: #fff0e9;
108
+ --gw-on-danger: #ffffff;
104
109
  --gw-night: #0c0e14;
105
110
  --gw-night-2: #141722;
106
111
  --gw-on-night: #e3e4e8;
@@ -117,6 +122,7 @@
117
122
  --gw-muted: #9a9ca6;
118
123
  --gw-line: #2a2b33;
119
124
  --gw-line-strong: #383944;
125
+ --gw-line-control: #6a6d78;
120
126
  --gw-action: #f2f3f5;
121
127
  --gw-action-hover: #ffffff;
122
128
  --gw-on-action: #0d0d12;
@@ -128,6 +134,9 @@
128
134
  --gw-success-soft: #132a22;
129
135
  --gw-warning: #e0b341;
130
136
  --gw-warning-soft: #2b2410;
137
+ --gw-danger: #ff6b5a;
138
+ --gw-danger-soft: #2e1714;
139
+ --gw-on-danger: #0d0d12;
131
140
  --gw-night: #0a0b10;
132
141
  --gw-night-2: #12141c;
133
142
  --gw-on-night: #e3e4e8;
@@ -144,6 +153,7 @@
144
153
  --gw-muted: #9a9ca6;
145
154
  --gw-line: #2a2b33;
146
155
  --gw-line-strong: #383944;
156
+ --gw-line-control: #6a6d78;
147
157
  --gw-action: #f2f3f5;
148
158
  --gw-action-hover: #ffffff;
149
159
  --gw-on-action: #0d0d12;
@@ -155,6 +165,9 @@
155
165
  --gw-success-soft: #132a22;
156
166
  --gw-warning: #e0b341;
157
167
  --gw-warning-soft: #2b2410;
168
+ --gw-danger: #ff6b5a;
169
+ --gw-danger-soft: #2e1714;
170
+ --gw-on-danger: #0d0d12;
158
171
  --gw-night: #0a0b10;
159
172
  --gw-night-2: #12141c;
160
173
  --gw-on-night: #e3e4e8;
@@ -170,6 +183,7 @@
170
183
  --gw-muted: #686b71;
171
184
  --gw-line: #e5e7ea;
172
185
  --gw-line-strong: #d5d8dd;
186
+ --gw-line-control: #85888e;
173
187
  --gw-action: #1b1c1e;
174
188
  --gw-action-hover: #000000;
175
189
  --gw-on-action: #ffffff;
@@ -181,6 +195,9 @@
181
195
  --gw-success-soft: #e3f1df;
182
196
  --gw-warning: #9a6700;
183
197
  --gw-warning-soft: #fbf1dc;
198
+ --gw-danger: #b42318;
199
+ --gw-danger-soft: #fff0e9;
200
+ --gw-on-danger: #ffffff;
184
201
  --gw-night: #0c0e14;
185
202
  --gw-night-2: #141722;
186
203
  --gw-on-night: #e3e4e8;
@@ -197,6 +214,7 @@
197
214
  --gw-muted: #9a9ca6;
198
215
  --gw-line: #2a2b33;
199
216
  --gw-line-strong: #383944;
217
+ --gw-line-control: #6a6d78;
200
218
  --gw-action: #f2f3f5;
201
219
  --gw-action-hover: #ffffff;
202
220
  --gw-on-action: #0d0d12;
@@ -208,6 +226,9 @@
208
226
  --gw-success-soft: #132a22;
209
227
  --gw-warning: #e0b341;
210
228
  --gw-warning-soft: #2b2410;
229
+ --gw-danger: #ff6b5a;
230
+ --gw-danger-soft: #2e1714;
231
+ --gw-on-danger: #0d0d12;
211
232
  }
212
233
  /* action profile "accent": data-gw-action="accent" on <html>. In night bands too, where the dark palette applies. */
213
234
  :root[data-gw-action="accent"],
@@ -317,6 +338,18 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
317
338
  }
318
339
  .gw-skip:focus { left: 16px; }
319
340
 
341
+ /* words for a screen reader only (0.7.0): " (opens in a new tab)" in a link with target="_blank" */
342
+ .gw-visually-hidden {
343
+ position: absolute;
344
+ width: 1px;
345
+ height: 1px;
346
+ padding: 0;
347
+ overflow: hidden;
348
+ clip-path: inset(50%);
349
+ white-space: nowrap;
350
+ border: 0;
351
+ }
352
+
320
353
  /* scroll reveal: the script adds .is-in; the animation is a failsafe that shows it anyway after 2.5s */
321
354
  .gw-reveal {
322
355
  opacity: 0;
@@ -437,6 +470,11 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
437
470
  .gw-btn--secondary:active { background: var(--gw-bg-soft); border-color: var(--gw-ink); }
438
471
  .gw-btn--ghost:active { background: var(--gw-line); }
439
472
  .gw-btn--on-night:active { background: var(--gw-on-night); opacity: 0.76; }
473
+ /* destructive (0.6.0, gap #28): delete, cancel a plan, erase my data. The danger fill with on-danger words (6.5:1 or
474
+ more, light and dark); hover and pressed one step stronger (towards the ink: darker in light, lighter in dark). It is
475
+ the action of a page that destroys something, never a second primary beside .gw-btn. */
476
+ .gw-btn--danger { background: var(--gw-danger); color: var(--gw-on-danger); }
477
+ .gw-btn--danger:hover, .gw-btn--danger:active { background: color-mix(in srgb, var(--gw-danger) 84%, var(--gw-ink)); }
440
478
  /* loading: aria-busy="true" on the <button>. The label stays (it keeps the width and the accessible name) but is not
441
479
  painted; a 16px ring in the button's own ink turns in its place, the dashboard's spinner. ::before, because
442
480
  .gw-btn--sm draws its hit area with ::after. website.js ignores presses on a busy button (no double submit). */
@@ -565,6 +603,9 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
565
603
  .gw-badge--success { background: var(--gw-success-soft); color: var(--gw-ink); }
566
604
  .gw-badge--success::before, .gw-badge--warning::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-success); }
567
605
  .gw-badge--warning::before { background: var(--gw-warning); }
606
+ /* an error (0.6.0, gap #28): the danger dot, ink words on the danger tint, as success's */
607
+ .gw-badge--danger { background: var(--gw-danger-soft); color: var(--gw-ink); }
608
+ .gw-badge--danger::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-danger); }
568
609
 
569
610
  /* cards sit: white, 1px hairline, the card radius of the site's profile (12, 20 in soft), the dashboard's resting
570
611
  shadow (Chris 24/10/2026: one language with the app); no hover lift. A soft card is a tint and carries no shadow. */
@@ -1014,6 +1055,9 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
1014
1055
  .gw-footer__brand { grid-column: 1 / -1; max-width: 320px; }
1015
1056
  .gw-footer__brand p { margin-top: 16px; color: var(--gw-muted); }
1016
1057
  .gw-footer__heading { margin: 0 0 8px; color: var(--gw-ink); font-size: 13px; font-weight: 600; letter-spacing: 0; }
1058
+ /* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
1059
+ reader agrees to; only when the form is wired to a list that keeps the address */
1060
+ .gw-footer__newsletter { margin-top: 24px; }
1017
1061
  .gw-footer__col ul { margin: 0; padding: 0; list-style: none; }
1018
1062
  .gw-footer__col a { display: block; padding-block: 6px; color: var(--gw-muted); }
1019
1063
  .gw-footer__col a:hover { color: var(--gw-ink); }
@@ -1052,6 +1096,184 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
1052
1096
  .gw-footer__bottom a { min-height: var(--gw-tap-min); }
1053
1097
  }
1054
1098
 
1099
+ /* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
1100
+ Every part below is optional, per site, and styles only its own markup: a 0.6.0 footer looks exactly as before.
1101
+ In page order: the Ask AI row in the brand block, columns that fold on phones (gap #40), product-family rows, the
1102
+ theme toggle in the bottom row, and the wordmark at the very end. Values measured on gemboss.ai on 25/10/2026 and
1103
+ put on the kit's scales; the full markup is templates/footer.html. */
1104
+
1105
+ /* Ask AI: a label and links that open an assistant with one neutral question already typed (a URL-encoded ?q=), so
1106
+ the answer comes from what the web says, not from a claim the brand wrote. Words, not logos. 28px rows, 44px on
1107
+ touch. */
1108
+ .gw-footer__ask { margin-top: 24px; }
1109
+ /* the row may run wider than the brand's sentence (four names on one line at 390px) */
1110
+ .gw-footer__brand:has(> .gw-footer__ask) { max-width: none; }
1111
+ .gw-footer__brand:has(> .gw-footer__ask) > :not(.gw-footer__ask) { max-width: 320px; }
1112
+ .gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; }
1113
+ .gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
1114
+ .gw-footer__ask a { display: block; padding-block: 4px; color: var(--gw-ink); line-height: 20px; }
1115
+ .gw-footer__ask a:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
1116
+
1117
+ /* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
1118
+ .gw-visually-hidden span, only when the link has target="_blank" */
1119
+ .gw-footer__ext {
1120
+ display: inline-block;
1121
+ width: 12px;
1122
+ height: 12px;
1123
+ margin-left: 4px;
1124
+ vertical-align: -1px;
1125
+ background: var(--gw-muted);
1126
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
1127
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
1128
+ }
1129
+
1130
+ /* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
1131
+ .gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
1132
+ `open`; the browser opens and closes it, no script needed. From 768px it is the column as before: the summary is
1133
+ only the heading (display: contents: no box, no Tab stop), and the list shows even while the <details> is closed
1134
+ (::details-content; website.js also sets `open` there, for browsers without it). */
1135
+ .gw-footer__fold > summary { list-style: none; }
1136
+ .gw-footer__fold > summary::-webkit-details-marker { display: none; }
1137
+ .gw-footer__fold .gw-footer__heading { margin: 0; line-height: 20px; }
1138
+ .gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
1139
+ .gw-footer__col > .gw-footer__fold a { line-height: 20px; }
1140
+ @media (min-width: 768px) {
1141
+ .gw-footer__fold > summary { display: contents; }
1142
+ .gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
1143
+ }
1144
+ @media (min-width: 768px) {
1145
+ .gw-footer__fold::details-content { content-visibility: visible; }
1146
+ }
1147
+ @media (max-width: 767px) {
1148
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
1149
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
1150
+ .gw-footer__col:has(> .gw-footer__fold),
1151
+ .gw-footer__family { border-bottom: 1px solid var(--gw-line); }
1152
+ .gw-footer__brand + .gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
1153
+ .gw-footer__fold > summary {
1154
+ display: flex;
1155
+ align-items: center;
1156
+ justify-content: space-between;
1157
+ gap: 16px;
1158
+ min-height: var(--gw-tap-min);
1159
+ padding-block: 16px;
1160
+ cursor: pointer;
1161
+ }
1162
+ .gw-footer__fold > summary::after {
1163
+ content: "";
1164
+ flex: none;
1165
+ width: 16px;
1166
+ height: 16px;
1167
+ background: var(--gw-muted);
1168
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
1169
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
1170
+ transition: transform 0.18s var(--gw-ease);
1171
+ }
1172
+ .gw-footer__fold[open] > summary::after { transform: rotate(180deg); }
1173
+ .gw-footer__fold > ul { padding-bottom: 12px; }
1174
+ .gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
1175
+ }
1176
+ /* desktop: the brand's track is as wide as gemboss.ai draws it beside five columns (1.2 parts of 6.2, after five 24px
1177
+ gaps), whatever the number of columns, so a family row can line up with it; the columns share the rest */
1178
+ @media (min-width: 1080px) {
1179
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
1180
+ }
1181
+
1182
+ /* Product family: a labelled row of products, name and one line each (<b> and <i>), as many rows as the brand needs
1183
+ ("More from GemBoss", "From the same team, on the Shopify App Store"). A <nav aria-label> holding a fold whose
1184
+ heading is the label. Two items a row on phones, four from 768px; from 1080px the label sits in the brand's track and
1185
+ the items start where the first column starts. */
1186
+ .gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
1187
+ .gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1188
+ .gw-footer__family a {
1189
+ display: flex;
1190
+ flex-direction: column;
1191
+ justify-content: center;
1192
+ gap: 2px;
1193
+ min-height: var(--gw-tap-min);
1194
+ padding: 8px 12px 8px 0;
1195
+ border-radius: var(--gw-radius-action);
1196
+ color: var(--gw-ink);
1197
+ }
1198
+ .gw-footer__family a:hover { background: var(--gw-bg-soft); }
1199
+ .gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
1200
+ .gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
1201
+ @media (min-width: 768px) {
1202
+ .gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
1203
+ .gw-footer__family .gw-footer__heading { margin-bottom: 12px; }
1204
+ .gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px; margin-left: -12px; }
1205
+ .gw-footer__family a { min-height: 0; padding: 8px 12px; }
1206
+ .gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
1207
+ }
1208
+ @media (min-width: 1080px) {
1209
+ .gw-footer__family { display: flow-root; }
1210
+ .gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
1211
+ .gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
1212
+ }
1213
+
1214
+ /* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
1215
+ square with one hairline, its hit area 40px, 44px on touch. It pushes itself and the social links to the right.
1216
+ Below 768px the copyright and the toggle share the first line, legal and social links the second. */
1217
+ .gw-footer__toggle {
1218
+ position: relative;
1219
+ width: 32px;
1220
+ height: 32px;
1221
+ margin-inline: auto 16px;
1222
+ border: 1px solid var(--gw-line-strong);
1223
+ background: var(--gw-surface);
1224
+ }
1225
+ .gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
1226
+ .gw-footer__toggle svg { width: 18px; height: 18px; }
1227
+ @media (max-width: 768px), (pointer: coarse) {
1228
+ .gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
1229
+ }
1230
+ @media (max-width: 767px) {
1231
+ .gw-footer__bottom:has(> .gw-footer__toggle) { column-gap: 16px; }
1232
+ .gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - var(--gw-tap-min) - 16px); }
1233
+ .gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
1234
+ .gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
1235
+ .gw-footer__family + .gw-footer__bottom,
1236
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) + .gw-footer__bottom { margin-top: 24px; }
1237
+ }
1238
+
1239
+ /* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
1240
+ the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
1241
+ page scroll sideways: a name too long for the line is cut. The mark is drawn in currentColor (an inline SVG); an
1242
+ <img> mark keeps its own colours, so leave it out and keep the name. Not on phones, as on gemboss.ai. 24px more room
1243
+ under it than gemboss.ai gives: the kit's footer is a painted band (white like the page), and the breathing gate
1244
+ wants half the letters' size between them and its edge (67px under 123px letters at 1366px). */
1245
+ .gw-footer__wordmark {
1246
+ display: flex;
1247
+ align-items: center;
1248
+ justify-content: center;
1249
+ gap: 24px;
1250
+ padding-block: 48px 24px;
1251
+ color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
1252
+ font-size: var(--gw-display-wordmark);
1253
+ font-weight: 700;
1254
+ letter-spacing: var(--gw-display-tracking);
1255
+ line-height: 1;
1256
+ white-space: nowrap;
1257
+ user-select: none;
1258
+ }
1259
+ .gw-footer__wordmark > svg,
1260
+ .gw-footer__wordmark > img { flex: none; width: auto; height: 0.9em; }
1261
+ .gw-footer__wordmark > span { min-width: 0; overflow-x: clip; }
1262
+ @media (max-width: 767px) { .gw-footer__wordmark { gap: 16px; } }
1263
+ @media (max-width: 479px) { .gw-footer__wordmark { display: none; } }
1264
+
1265
+ @media (max-width: 768px), (pointer: coarse) {
1266
+ .gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
1267
+ }
1268
+ @media (prefers-reduced-motion: reduce) {
1269
+ .gw-footer__fold > summary::after { transition: none; }
1270
+ }
1271
+ /* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
1272
+ @media (forced-colors: active) {
1273
+ .gw-footer__ext,
1274
+ .gw-footer__fold > summary::after { forced-color-adjust: none; background: CanvasText; }
1275
+ }
1276
+
1055
1277
  /* sections: hero, logos, stats, features, split, showcase, steps, quote, pricing, CTA. */
1056
1278
 
1057
1279
  /* hero: the first screen. __wash is the one place the wash hues may appear. */
@@ -1597,3 +1819,237 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
1597
1819
  * + .gw-pagination { margin-top: 40px; }
1598
1820
  .gw-pagination__status { color: var(--gw-muted); font-size: 14px; }
1599
1821
  .gw-pagination > [rel="next"] { margin-inline-start: auto; }
1822
+
1823
+ /* forms (0.6.0, gap #28): a field (label, control, error, hint), the controls, a group of choices, the inline row and
1824
+ the form stack. A public site asks as little as it can: one field beats a form, and a form keeps two or three fields
1825
+ (AGENTS.md section 5). The look is the dashboard's field family (@gemboss/ui gemboss-field, ui-select, ui-textarea,
1826
+ ui-check, ui-help, ui-error) at the website's sizes: as tall as a button (40px, 44px on touch), 16px words (a phone
1827
+ does not zoom the page), the action radius of the site's profile, and a 1px border that reaches 3:1
1828
+ (--gw-line-control). The page owns validation: it sets aria-invalid="true" and writes the message; the kit draws it. */
1829
+
1830
+ /* a field: the label above the control, the error and then the hint under it, 8px apart */
1831
+ .gw-field { display: grid; gap: 8px; min-width: 0; }
1832
+ .gw-field__label { display: block; color: var(--gw-ink); font-size: 14px; font-weight: 600; line-height: 1.5; }
1833
+ /* two classes, so a context that styles its paragraphs (the footer's brand block) does not move or recolour them */
1834
+ :is(.gw-field, .gw-choices) > :is(.gw-field__hint, .gw-field__error) { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.5; }
1835
+ :is(.gw-field, .gw-choices) > .gw-field__hint { color: var(--gw-muted); }
1836
+ /* the error: an icon and words, in the danger colour; never the colour alone. Empty, it takes no room, so a page can
1837
+ keep it in the markup (named by aria-describedby) and fill it when the visitor sends the form. */
1838
+ :is(.gw-field, .gw-choices) > .gw-field__error { display: flex; gap: 6px; color: var(--gw-danger); font-weight: 500; }
1839
+ :is(.gw-field, .gw-choices) > .gw-field__error:empty { display: none; }
1840
+ .gw-field__error::before {
1841
+ content: "";
1842
+ flex: none;
1843
+ width: 16px;
1844
+ height: 16px;
1845
+ margin-top: 2px;
1846
+ margin-top: calc((1lh - 16px) / 2);
1847
+ background: currentColor;
1848
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
1849
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
1850
+ }
1851
+
1852
+ /* the controls: one box for a line of text, a choice from a list and a few lines. Full width of their field. */
1853
+ .gw-input,
1854
+ .gw-select,
1855
+ .gw-textarea {
1856
+ display: block;
1857
+ width: 100%;
1858
+ min-width: 0;
1859
+ height: var(--gw-button-height);
1860
+ margin: 0;
1861
+ padding: 0 12px;
1862
+ border: 1px solid var(--gw-line-control);
1863
+ border-radius: var(--gw-radius-action);
1864
+ background-color: var(--gw-surface);
1865
+ color: var(--gw-ink);
1866
+ font-size: 16px;
1867
+ line-height: 1.5;
1868
+ -webkit-appearance: none;
1869
+ appearance: none;
1870
+ }
1871
+ :is(.gw-input, .gw-textarea)::placeholder { color: var(--gw-muted); opacity: 1; }
1872
+ /* a list: the caret is drawn with the muted ink (two gradients, so it follows dark mode), with room for it */
1873
+ .gw-select {
1874
+ padding-right: 40px;
1875
+ background-image: linear-gradient(45deg, transparent 50%, var(--gw-muted) 50%), linear-gradient(135deg, var(--gw-muted) 50%, transparent 50%);
1876
+ background-position: calc(100% - 20px) 50%, calc(100% - 15px) 50%;
1877
+ background-size: 5px 5px;
1878
+ background-repeat: no-repeat;
1879
+ cursor: pointer;
1880
+ }
1881
+ .gw-textarea { height: auto; min-height: 96px; padding-block: 8px; resize: vertical; }
1882
+ /* the states, in this order: hover, focus, read-only, invalid, disabled. Focus is the kit's accent ring. */
1883
+ :is(.gw-input, .gw-select, .gw-textarea):hover { border-color: var(--gw-muted); }
1884
+ :is(.gw-input, .gw-select, .gw-textarea):focus-visible { border-color: var(--gw-ink); outline: 2px solid var(--gw-accent); outline-offset: 1px; }
1885
+ /* read-only: still focusable and readable, visibly not editable (the dashboard's) */
1886
+ :is(.gw-input, .gw-textarea)[readonly] { border-color: var(--gw-line-strong); background-color: var(--gw-bg-soft); cursor: default; }
1887
+ :is(.gw-input, .gw-select, .gw-textarea)[aria-invalid="true"] { border-color: var(--gw-danger); background-color: var(--gw-danger-soft); }
1888
+ :is(.gw-input, .gw-select, .gw-textarea):disabled { opacity: 0.48; cursor: not-allowed; }
1889
+
1890
+ /* a group of choices: a <fieldset> whose <legend> is the question, one choice per row */
1891
+ .gw-choices { min-width: 0; margin: 0; padding: 0; border: 0; }
1892
+ .gw-choices > legend { margin-bottom: 4px; padding: 0; }
1893
+ .gw-choices > :is(.gw-check, .gw-radio) { display: flex; }
1894
+ .gw-choices > :is(.gw-field__hint, .gw-field__error) { margin-top: 4px; }
1895
+
1896
+ /* a checkbox or a radio: the <label> is the target (32px, 44px on touch), the box sits on the first line of words */
1897
+ .gw-check,
1898
+ .gw-radio {
1899
+ display: inline-flex;
1900
+ align-items: flex-start;
1901
+ gap: 8px;
1902
+ max-width: 100%;
1903
+ padding-block: 4px;
1904
+ color: var(--gw-ink);
1905
+ font-size: 16px;
1906
+ line-height: 1.5;
1907
+ overflow-wrap: break-word;
1908
+ cursor: pointer;
1909
+ }
1910
+ :is(.gw-check, .gw-radio) > :not(input) { min-width: 0; }
1911
+ :is(.gw-check, .gw-radio) > input {
1912
+ display: inline-grid;
1913
+ flex: none;
1914
+ place-content: center;
1915
+ width: 18px;
1916
+ height: 18px;
1917
+ margin: 3px 0 0;
1918
+ border: 1px solid var(--gw-line-control);
1919
+ border-radius: var(--gw-radius-4);
1920
+ background: var(--gw-surface);
1921
+ -webkit-appearance: none;
1922
+ appearance: none;
1923
+ cursor: inherit;
1924
+ }
1925
+ .gw-radio > input { border-radius: var(--gw-radius-chip); }
1926
+ :is(.gw-check, .gw-radio):hover > input { border-color: var(--gw-muted); }
1927
+ /* chosen: the accent, as every check mark of the kit; the mark in the words-on-accent colour */
1928
+ .gw-check > input:checked { border-color: var(--gw-accent); background: var(--gw-accent); }
1929
+ .gw-check > input:checked::before {
1930
+ content: "";
1931
+ width: 14px;
1932
+ height: 14px;
1933
+ background: var(--gw-on-accent);
1934
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
1935
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
1936
+ }
1937
+ .gw-radio > input:checked { border: 5px solid var(--gw-accent); }
1938
+ :is(.gw-check, .gw-radio) > input[aria-invalid="true"]:not(:checked) { border-color: var(--gw-danger); }
1939
+ :is(.gw-check, .gw-radio):has(> input:disabled) { opacity: 0.48; cursor: not-allowed; }
1940
+ /* a filter's count: a small pill at the end of the row, the neutral badge's colours */
1941
+ .gw-check__count {
1942
+ flex: none;
1943
+ min-width: 24px;
1944
+ margin: 1px 0 0 auto;
1945
+ padding: 2px 8px;
1946
+ border-radius: var(--gw-radius-chip);
1947
+ background: color-mix(in srgb, var(--gw-ink) 8%, transparent);
1948
+ color: var(--gw-ink-2);
1949
+ font-size: 12px;
1950
+ font-weight: 500;
1951
+ text-align: center;
1952
+ font-variant-numeric: tabular-nums;
1953
+ }
1954
+
1955
+ /* the inline row: one field and its button (a newsletter, "get the guide"). One line from 480px, the field taking the
1956
+ room; below 480px, and in a box too narrow for both, the field and the button each take a full line. */
1957
+ .gw-form-inline { display: flex; flex-wrap: wrap; gap: 8px; }
1958
+ .gw-form-inline > :not(.gw-btn) { flex: 999 1 160px; min-width: 0; }
1959
+ .gw-form-inline > .gw-btn { flex: 1 0 auto; }
1960
+ @media (max-width: 479px) {
1961
+ .gw-form-inline > :not(.gw-btn) { flex-basis: 100%; }
1962
+ }
1963
+
1964
+ /* the form stack: fields 24px apart, the actions 32px below the last one, 560px at most (a field's width hints at its
1965
+ answer: no field runs across a wide screen) */
1966
+ .gw-form { display: grid; gap: 24px; max-width: 560px; }
1967
+ /* the gap is the rhythm, also when the form is the card itself (a card spaces its own children otherwise) */
1968
+ .gw-form > * { margin: 0; }
1969
+ .gw-form > :is(.gw-actions, .gw-btn) { margin-top: 8px; }
1970
+ .gw-form > .gw-btn { justify-self: start; }
1971
+
1972
+ /* touch: every control is at least 44px tall, a choice's row too */
1973
+ @media (max-width: 768px), (pointer: coarse) {
1974
+ .gw-input, .gw-select { height: var(--gw-tap-min); }
1975
+ .gw-check, .gw-radio { padding-block: 10px; }
1976
+ }
1977
+
1978
+ /* ratings (0.6.0, gap #56): stars, the testimonial wall and the rating badge. Every rating and every testimonial
1979
+ links to the page it comes from, and says only what that page says (AGENTS.md section 6); `check` warns about one
1980
+ without its link (rating-source). */
1981
+
1982
+ /* stars: a value from 0 to 5 (style="--gw-stars: 4.9") drawn as five 16px stars, the last one cut where the value
1983
+ ends. A screen reader hears the value in words: role="img" and aria-label="4.9 out of 5" (the drawing itself is a
1984
+ background, nothing to read). Ink on the strong hairline: a rating is proof, not a state or an action. */
1985
+ .gw-stars {
1986
+ display: inline-block;
1987
+ flex: none;
1988
+ width: 80px;
1989
+ height: 16px;
1990
+ vertical-align: middle;
1991
+ background: linear-gradient(90deg, var(--gw-ink) calc(clamp(0, var(--gw-stars, 0), 5) * 20%), var(--gw-line-strong) 0);
1992
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
1993
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
1994
+ }
1995
+ /* a forced-colours screen drops backgrounds: draw the stars in its own text colours */
1996
+ @media (forced-colors: active) {
1997
+ .gw-stars { forced-color-adjust: none; background: linear-gradient(90deg, CanvasText calc(clamp(0, var(--gw-stars, 0), 5) * 20%), GrayText 0); }
1998
+ }
1999
+
2000
+ /* the wall: short quotes in cards, one column on phones, two from 768px, three from 1080px. No masonry: the cards of
2001
+ a row share its height, and each card's byline sits at its foot. */
2002
+ .gw-testimonials {
2003
+ display: grid;
2004
+ grid-template-columns: minmax(0, 1fr);
2005
+ gap: 16px;
2006
+ margin: 0;
2007
+ padding: 0;
2008
+ list-style: none;
2009
+ }
2010
+ .gw-testimonials > li { display: flex; min-width: 0; }
2011
+ @media (min-width: 768px) { .gw-testimonials { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } }
2012
+ @media (min-width: 1080px) { .gw-testimonials { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
2013
+ /* a quote card (with .gw-card): stars, the reviewer's own words, who wrote them and where */
2014
+ .gw-testimonial { display: flex; flex-direction: column; gap: 16px; width: 100%; margin: 0; }
2015
+ .gw-testimonial > * { margin: 0; }
2016
+ .gw-testimonial__quote,
2017
+ .gw-testimonial__quote p { color: var(--gw-ink); font-size: 16px; line-height: 1.6; }
2018
+ .gw-testimonial__quote > * + * { margin-top: 8px; }
2019
+ .gw-testimonial > .gw-testimonial__by { display: flex; align-items: center; gap: 12px; margin-top: auto; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
2020
+ .gw-testimonial__by b { display: block; color: var(--gw-ink); font-weight: 600; }
2021
+ .gw-testimonial__by > :not(img) { min-width: 0; }
2022
+ .gw-testimonial__avatar { flex: none; width: 40px; height: 40px; border-radius: var(--gw-radius-chip); background: var(--gw-bg-soft); object-fit: cover; }
2023
+
2024
+ /* the rating badge: one link to the store's review page, with the store's mark, the score, the stars and the count.
2025
+ A small card that sits; the border darkens on hover, as a linked card's. */
2026
+ .gw-rating-badge {
2027
+ display: inline-flex;
2028
+ align-items: center;
2029
+ gap: 12px;
2030
+ max-width: 100%;
2031
+ padding: 8px 16px 8px 8px;
2032
+ border: 1px solid var(--gw-line);
2033
+ border-radius: var(--gw-radius-card);
2034
+ background: var(--gw-surface);
2035
+ color: var(--gw-ink);
2036
+ font-size: 13px;
2037
+ line-height: 1.4;
2038
+ vertical-align: middle;
2039
+ }
2040
+ a.gw-rating-badge:hover { border-color: var(--gw-line-strong); }
2041
+ .gw-rating-badge__icon {
2042
+ display: inline-flex;
2043
+ flex: none;
2044
+ align-items: center;
2045
+ justify-content: center;
2046
+ width: 40px;
2047
+ height: 40px;
2048
+ border-radius: var(--gw-radius-action);
2049
+ background: var(--gw-bg-soft);
2050
+ color: var(--gw-ink);
2051
+ }
2052
+ .gw-rating-badge__icon :is(svg, img) { width: 24px; height: 24px; }
2053
+ .gw-rating-badge__body { display: grid; gap: 2px; min-width: 0; }
2054
+ .gw-rating-badge__score { display: flex; align-items: center; gap: 8px; color: var(--gw-ink); font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
2055
+ .gw-rating-badge__count { color: var(--gw-muted); }