@gemboss/website-ui 0.4.0 → 0.6.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/AGENTS.md +43 -5
- package/CHANGELOG.md +117 -0
- package/README.md +9 -8
- package/bin/lib/check.mjs +77 -0
- package/css/article.css +237 -0
- package/css/components.css +23 -0
- package/css/footer.css +3 -0
- package/css/forms.css +154 -0
- package/css/prose.css +29 -16
- package/css/ratings.css +78 -0
- package/css/tokens.css +25 -0
- package/demo/article.html +359 -0
- package/demo/assets/article-hero.svg +1 -0
- package/demo/assets/avatar-gemboss.svg +1 -0
- package/demo/assets/avatar-sample.svg +1 -0
- package/demo/assets/post-headless.svg +1 -0
- package/demo/assets/post-motion.svg +1 -0
- package/demo/assets/post-output.svg +1 -0
- package/demo/assets/post-speed.svg +1 -0
- package/demo/assets/post-theme.svg +1 -0
- package/demo/assets/post-voice.svg +1 -0
- package/demo/blog.html +314 -0
- package/demo/forms.html +505 -0
- package/demo/index.html +2 -0
- package/docs/components.md +334 -12
- package/docs/markup-contract.md +123 -7
- package/docs/theming.md +4 -3
- package/js/website.js +68 -8
- package/js/website.mjs +80 -7
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/system.json +12 -1
- package/templates/footer.html +15 -0
- package/templates/sections/article.html +93 -0
- package/templates/sections/form.html +42 -0
- package/templates/sections/post-list.html +65 -0
- package/templates/sections/testimonials.html +58 -0
- package/website.css +553 -17
- package/website.min.css +1 -1
package/website.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.6.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;
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
--gw-muted: #686b71;
|
|
91
91
|
--gw-line: #e5e7ea;
|
|
92
92
|
--gw-line-strong: #d5d8dd;
|
|
93
|
+
--gw-line-control: #85888e;
|
|
93
94
|
--gw-action: #1b1c1e;
|
|
94
95
|
--gw-action-hover: #000000;
|
|
95
96
|
--gw-on-action: #ffffff;
|
|
@@ -100,6 +101,10 @@
|
|
|
100
101
|
--gw-success: #007f5f;
|
|
101
102
|
--gw-success-soft: #e3f1df;
|
|
102
103
|
--gw-warning: #9a6700;
|
|
104
|
+
--gw-warning-soft: #fbf1dc;
|
|
105
|
+
--gw-danger: #b42318;
|
|
106
|
+
--gw-danger-soft: #fff0e9;
|
|
107
|
+
--gw-on-danger: #ffffff;
|
|
103
108
|
--gw-night: #0c0e14;
|
|
104
109
|
--gw-night-2: #141722;
|
|
105
110
|
--gw-on-night: #e3e4e8;
|
|
@@ -116,6 +121,7 @@
|
|
|
116
121
|
--gw-muted: #9a9ca6;
|
|
117
122
|
--gw-line: #2a2b33;
|
|
118
123
|
--gw-line-strong: #383944;
|
|
124
|
+
--gw-line-control: #6a6d78;
|
|
119
125
|
--gw-action: #f2f3f5;
|
|
120
126
|
--gw-action-hover: #ffffff;
|
|
121
127
|
--gw-on-action: #0d0d12;
|
|
@@ -126,6 +132,10 @@
|
|
|
126
132
|
--gw-success: #34c38f;
|
|
127
133
|
--gw-success-soft: #132a22;
|
|
128
134
|
--gw-warning: #e0b341;
|
|
135
|
+
--gw-warning-soft: #2b2410;
|
|
136
|
+
--gw-danger: #ff6b5a;
|
|
137
|
+
--gw-danger-soft: #2e1714;
|
|
138
|
+
--gw-on-danger: #0d0d12;
|
|
129
139
|
--gw-night: #0a0b10;
|
|
130
140
|
--gw-night-2: #12141c;
|
|
131
141
|
--gw-on-night: #e3e4e8;
|
|
@@ -142,6 +152,7 @@
|
|
|
142
152
|
--gw-muted: #9a9ca6;
|
|
143
153
|
--gw-line: #2a2b33;
|
|
144
154
|
--gw-line-strong: #383944;
|
|
155
|
+
--gw-line-control: #6a6d78;
|
|
145
156
|
--gw-action: #f2f3f5;
|
|
146
157
|
--gw-action-hover: #ffffff;
|
|
147
158
|
--gw-on-action: #0d0d12;
|
|
@@ -152,6 +163,10 @@
|
|
|
152
163
|
--gw-success: #34c38f;
|
|
153
164
|
--gw-success-soft: #132a22;
|
|
154
165
|
--gw-warning: #e0b341;
|
|
166
|
+
--gw-warning-soft: #2b2410;
|
|
167
|
+
--gw-danger: #ff6b5a;
|
|
168
|
+
--gw-danger-soft: #2e1714;
|
|
169
|
+
--gw-on-danger: #0d0d12;
|
|
155
170
|
--gw-night: #0a0b10;
|
|
156
171
|
--gw-night-2: #12141c;
|
|
157
172
|
--gw-on-night: #e3e4e8;
|
|
@@ -167,6 +182,7 @@
|
|
|
167
182
|
--gw-muted: #686b71;
|
|
168
183
|
--gw-line: #e5e7ea;
|
|
169
184
|
--gw-line-strong: #d5d8dd;
|
|
185
|
+
--gw-line-control: #85888e;
|
|
170
186
|
--gw-action: #1b1c1e;
|
|
171
187
|
--gw-action-hover: #000000;
|
|
172
188
|
--gw-on-action: #ffffff;
|
|
@@ -177,6 +193,10 @@
|
|
|
177
193
|
--gw-success: #007f5f;
|
|
178
194
|
--gw-success-soft: #e3f1df;
|
|
179
195
|
--gw-warning: #9a6700;
|
|
196
|
+
--gw-warning-soft: #fbf1dc;
|
|
197
|
+
--gw-danger: #b42318;
|
|
198
|
+
--gw-danger-soft: #fff0e9;
|
|
199
|
+
--gw-on-danger: #ffffff;
|
|
180
200
|
--gw-night: #0c0e14;
|
|
181
201
|
--gw-night-2: #141722;
|
|
182
202
|
--gw-on-night: #e3e4e8;
|
|
@@ -193,6 +213,7 @@
|
|
|
193
213
|
--gw-muted: #9a9ca6;
|
|
194
214
|
--gw-line: #2a2b33;
|
|
195
215
|
--gw-line-strong: #383944;
|
|
216
|
+
--gw-line-control: #6a6d78;
|
|
196
217
|
--gw-action: #f2f3f5;
|
|
197
218
|
--gw-action-hover: #ffffff;
|
|
198
219
|
--gw-on-action: #0d0d12;
|
|
@@ -203,6 +224,10 @@
|
|
|
203
224
|
--gw-success: #34c38f;
|
|
204
225
|
--gw-success-soft: #132a22;
|
|
205
226
|
--gw-warning: #e0b341;
|
|
227
|
+
--gw-warning-soft: #2b2410;
|
|
228
|
+
--gw-danger: #ff6b5a;
|
|
229
|
+
--gw-danger-soft: #2e1714;
|
|
230
|
+
--gw-on-danger: #0d0d12;
|
|
206
231
|
}
|
|
207
232
|
/* action profile "accent": data-gw-action="accent" on <html>. In night bands too, where the dark palette applies. */
|
|
208
233
|
:root[data-gw-action="accent"],
|
|
@@ -432,6 +457,11 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
|
|
|
432
457
|
.gw-btn--secondary:active { background: var(--gw-bg-soft); border-color: var(--gw-ink); }
|
|
433
458
|
.gw-btn--ghost:active { background: var(--gw-line); }
|
|
434
459
|
.gw-btn--on-night:active { background: var(--gw-on-night); opacity: 0.76; }
|
|
460
|
+
/* destructive (0.6.0, gap #28): delete, cancel a plan, erase my data. The danger fill with on-danger words (6.5:1 or
|
|
461
|
+
more, light and dark); hover and pressed one step stronger (towards the ink: darker in light, lighter in dark). It is
|
|
462
|
+
the action of a page that destroys something, never a second primary beside .gw-btn. */
|
|
463
|
+
.gw-btn--danger { background: var(--gw-danger); color: var(--gw-on-danger); }
|
|
464
|
+
.gw-btn--danger:hover, .gw-btn--danger:active { background: color-mix(in srgb, var(--gw-danger) 84%, var(--gw-ink)); }
|
|
435
465
|
/* loading: aria-busy="true" on the <button>. The label stays (it keeps the width and the accessible name) but is not
|
|
436
466
|
painted; a 16px ring in the button's own ink turns in its place, the dashboard's spinner. ::before, because
|
|
437
467
|
.gw-btn--sm draws its hit area with ::after. website.js ignores presses on a busy button (no double submit). */
|
|
@@ -560,6 +590,9 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
|
|
|
560
590
|
.gw-badge--success { background: var(--gw-success-soft); color: var(--gw-ink); }
|
|
561
591
|
.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); }
|
|
562
592
|
.gw-badge--warning::before { background: var(--gw-warning); }
|
|
593
|
+
/* an error (0.6.0, gap #28): the danger dot, ink words on the danger tint, as success's */
|
|
594
|
+
.gw-badge--danger { background: var(--gw-danger-soft); color: var(--gw-ink); }
|
|
595
|
+
.gw-badge--danger::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-danger); }
|
|
563
596
|
|
|
564
597
|
/* cards sit: white, 1px hairline, the card radius of the site's profile (12, 20 in soft), the dashboard's resting
|
|
565
598
|
shadow (Chris 24/10/2026: one language with the app); no hover lift. A soft card is a tint and carries no shadow. */
|
|
@@ -574,6 +607,21 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
|
|
|
574
607
|
.gw-card--soft { background: var(--gw-bg-soft); box-shadow: none; }
|
|
575
608
|
.gw-card--link { display: block; color: inherit; }
|
|
576
609
|
.gw-card--link:hover { border-color: var(--gw-line-strong); }
|
|
610
|
+
/* a card's picture (0.5.0, gap #33): the first child of the card, edge to edge across its top, its corners the card's
|
|
611
|
+
radius inside the hairline. An aspect-ratio box (16:9; --wide 1.91:1, the share-image shape, so a post's own
|
|
612
|
+
1200 x 630 image fits uncut; --square 1:1) that the image covers, on the soft ground while it loads. */
|
|
613
|
+
.gw-card__media {
|
|
614
|
+
overflow: hidden;
|
|
615
|
+
aspect-ratio: 16 / 9;
|
|
616
|
+
margin: -24px -24px 0;
|
|
617
|
+
border-top-left-radius: calc(var(--gw-radius-card) - 1px);
|
|
618
|
+
border-top-right-radius: calc(var(--gw-radius-card) - 1px);
|
|
619
|
+
background: var(--gw-bg-soft);
|
|
620
|
+
}
|
|
621
|
+
.gw-card__media--wide { aspect-ratio: 1200 / 630; }
|
|
622
|
+
.gw-card__media--square { aspect-ratio: 1 / 1; }
|
|
623
|
+
.gw-card__media :is(img, video) { width: 100%; height: 100%; object-fit: cover; }
|
|
624
|
+
@media (min-width: 768px) { .gw-card__media { margin: -32px -32px 0; } }
|
|
577
625
|
:where(.gw-card, .gw-feature, .gw-plan) > :is(h3, h4) + p { margin-top: 8px; }
|
|
578
626
|
:where(.gw-card, .gw-feature) h3 { font-size: 20px; }
|
|
579
627
|
:where(.gw-card, .gw-feature) p { font-size: 16px; color: var(--gw-muted); }
|
|
@@ -994,6 +1042,9 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
|
|
|
994
1042
|
.gw-footer__brand { grid-column: 1 / -1; max-width: 320px; }
|
|
995
1043
|
.gw-footer__brand p { margin-top: 16px; color: var(--gw-muted); }
|
|
996
1044
|
.gw-footer__heading { margin: 0 0 8px; color: var(--gw-ink); font-size: 13px; font-weight: 600; letter-spacing: 0; }
|
|
1045
|
+
/* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
|
|
1046
|
+
reader agrees to; only when the form is wired to a list that keeps the address */
|
|
1047
|
+
.gw-footer__newsletter { margin-top: 24px; }
|
|
997
1048
|
.gw-footer__col ul { margin: 0; padding: 0; list-style: none; }
|
|
998
1049
|
.gw-footer__col a { display: block; padding-block: 6px; color: var(--gw-muted); }
|
|
999
1050
|
.gw-footer__col a:hover { color: var(--gw-ink); }
|
|
@@ -1224,8 +1275,8 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
|
|
|
1224
1275
|
/* the flow: each block 20px below the one before it (16px in --sm); more air around headings and large blocks */
|
|
1225
1276
|
:where(.gw-prose) > * + * { margin-top: 20px; }
|
|
1226
1277
|
:where(.gw-prose--sm) > * + * { margin-top: 16px; }
|
|
1227
|
-
:where(.gw-prose) > * + :where(figure, img, video, pre, table, hr, .gw-table, .gw-window),
|
|
1228
|
-
:where(.gw-prose) > :where(figure, img, video, pre, table, hr, .gw-table, .gw-window) + * { margin-top: 32px; }
|
|
1278
|
+
:where(.gw-prose) > * + :where(figure, img, video, pre, table, hr, .gw-table, .gw-window, .gw-prose__table, .gw-callout),
|
|
1279
|
+
:where(.gw-prose) > :where(figure, img, video, pre, table, hr, .gw-table, .gw-window, .gw-prose__table, .gw-callout) + * { margin-top: 32px; }
|
|
1229
1280
|
:where(.gw-prose) > * + h2 { margin-top: 48px; }
|
|
1230
1281
|
:where(.gw-prose) > * + h3 { margin-top: 40px; }
|
|
1231
1282
|
:where(.gw-prose) > * + h4 { margin-top: 32px; }
|
|
@@ -1303,26 +1354,511 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
|
|
|
1303
1354
|
:where(.gw-prose) > :is(p, figure) > :is(img, video) { border-radius: var(--gw-radius-card); }
|
|
1304
1355
|
:where(.gw-prose) figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
|
|
1305
1356
|
|
|
1306
|
-
/* a table straight from
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1357
|
+
/* a table straight from the CMS or markdown (gap #63, 0.5.0). website.js puts each bare table in a scroll box of its
|
|
1358
|
+
own, .gw-prose__table: a region named after the table's caption or first header cell, with tabindex="0" so a
|
|
1359
|
+
keyboard can scroll it. The table is marked .is-wrapped and keeps display: table, because some Safari versions stop
|
|
1360
|
+
announcing a table as a table once its display changes. Without the script, or before it runs, the table itself
|
|
1361
|
+
scrolls inside its box, as in 0.2.0. The box looks the same either way. When you write the markup, the .gw-table
|
|
1362
|
+
wrapper (a region with a name and tabindex="0") is still the better choice. */
|
|
1363
|
+
:where(.gw-prose) > table:where(:not(.is-wrapped)),
|
|
1364
|
+
:where(.gw-prose) > :where(.gw-prose__table) {
|
|
1310
1365
|
width: max-content;
|
|
1311
1366
|
max-width: 100%;
|
|
1312
1367
|
overflow-x: auto;
|
|
1313
1368
|
border: 1px solid var(--gw-line);
|
|
1314
1369
|
border-radius: var(--gw-radius-card);
|
|
1315
1370
|
background: var(--gw-surface);
|
|
1316
|
-
border-collapse: collapse;
|
|
1317
|
-
font-size: 14px;
|
|
1318
|
-
line-height: 1.5;
|
|
1319
1371
|
-webkit-overflow-scrolling: touch;
|
|
1320
1372
|
}
|
|
1321
|
-
:where(.gw-prose) > table
|
|
1322
|
-
:where(.gw-prose) > table
|
|
1323
|
-
:where(.gw-prose) >
|
|
1324
|
-
:where(.gw-prose) > table
|
|
1325
|
-
:where(.gw-prose) >
|
|
1326
|
-
:where(.gw-prose) > table
|
|
1373
|
+
:where(.gw-prose) > table:where(:not(.is-wrapped)) { display: block; }
|
|
1374
|
+
:where(.gw-prose) > table,
|
|
1375
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table { border-collapse: collapse; font-size: 14px; line-height: 1.5; }
|
|
1376
|
+
:where(.gw-prose) > table :is(th, td),
|
|
1377
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table :is(th, td) { padding: 12px 16px; border-bottom: 1px solid var(--gw-line); text-align: left; vertical-align: top; }
|
|
1378
|
+
:where(.gw-prose) > table thead th,
|
|
1379
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table thead th { background: var(--gw-bg-soft); color: var(--gw-ink); font-size: 13px; font-weight: 600; white-space: nowrap; }
|
|
1380
|
+
:where(.gw-prose) > table tbody th,
|
|
1381
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table tbody th { font-weight: 500; }
|
|
1382
|
+
:where(.gw-prose) > table td,
|
|
1383
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table td { color: var(--gw-ink-2); font-variant-numeric: tabular-nums; }
|
|
1384
|
+
:where(.gw-prose) > table tbody tr:last-child > *,
|
|
1385
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table tbody tr:last-child > * { border-bottom: 0; }
|
|
1386
|
+
:where(.gw-prose) > table caption,
|
|
1387
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table caption { padding: 12px 16px; color: var(--gw-muted); font-size: 13px; text-align: left; }
|
|
1327
1388
|
/* on a phone a cell keeps room for a few words a line: the table scrolls further instead of squeezing its text */
|
|
1328
|
-
@media (max-width: 767px) {
|
|
1389
|
+
@media (max-width: 767px) {
|
|
1390
|
+
:where(.gw-prose) > table td,
|
|
1391
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table td { min-width: 160px; }
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/* article (0.5.0): the parts around a long read and the blog index. Breadcrumbs, the meta row, the article layout with
|
|
1395
|
+
its contents list (.gw-toc), callouts, the post list and prev / next pagination. The article's own HTML stays in
|
|
1396
|
+
.gw-prose (prose.css); .gw-card__media is in components.css. */
|
|
1397
|
+
|
|
1398
|
+
/* breadcrumbs: a nav with an <ol>, the last item aria-current="page". The separator is drawn here (a chevron mask,
|
|
1399
|
+
no character), so screen readers read the trail and not the punctuation. The current item stays on one line and
|
|
1400
|
+
is cut with an ellipsis on a phone: the h1 right under it says it in full. */
|
|
1401
|
+
.gw-breadcrumbs { color: var(--gw-muted); font-size: 13px; line-height: 1.5; }
|
|
1402
|
+
.gw-breadcrumbs > ol { display: flex; flex-wrap: wrap; align-items: center; gap: 0 8px; margin: 0; padding: 0; list-style: none; }
|
|
1403
|
+
.gw-breadcrumbs li { display: flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; }
|
|
1404
|
+
.gw-breadcrumbs li + li::before {
|
|
1405
|
+
content: "";
|
|
1406
|
+
flex: none;
|
|
1407
|
+
width: 12px;
|
|
1408
|
+
height: 12px;
|
|
1409
|
+
background: currentColor;
|
|
1410
|
+
opacity: 0.6;
|
|
1411
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
1412
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
1413
|
+
}
|
|
1414
|
+
.gw-breadcrumbs a { display: inline-flex; align-items: center; min-height: 24px; color: var(--gw-muted); }
|
|
1415
|
+
.gw-breadcrumbs a:hover { color: var(--gw-ink); text-decoration: underline; text-underline-offset: 4px; }
|
|
1416
|
+
.gw-breadcrumbs [aria-current="page"] {
|
|
1417
|
+
display: block;
|
|
1418
|
+
min-width: 0;
|
|
1419
|
+
overflow: hidden;
|
|
1420
|
+
color: var(--gw-ink);
|
|
1421
|
+
font-weight: 500;
|
|
1422
|
+
line-height: 24px;
|
|
1423
|
+
text-overflow: ellipsis;
|
|
1424
|
+
white-space: nowrap;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/* meta row: author (an optional 24px avatar), category, published or updated <time datetime>, read time, each item a
|
|
1428
|
+
<span> (or a <time>, an <a>) straight in the row. Small muted text on 24px lines that wraps. A dot sits before every
|
|
1429
|
+
item; the row is pulled 16px to the left and clipped there, so the dot before the first item of each line is hidden
|
|
1430
|
+
and a wrapped line never starts with a separator. */
|
|
1431
|
+
.gw-article-meta {
|
|
1432
|
+
display: flex;
|
|
1433
|
+
flex-wrap: wrap;
|
|
1434
|
+
align-items: center;
|
|
1435
|
+
row-gap: 4px;
|
|
1436
|
+
margin-inline-start: -16px;
|
|
1437
|
+
clip-path: inset(-8px -8px -8px 12px);
|
|
1438
|
+
color: var(--gw-muted);
|
|
1439
|
+
font-size: 14px;
|
|
1440
|
+
line-height: 24px;
|
|
1441
|
+
}
|
|
1442
|
+
.gw-article-meta > *::before {
|
|
1443
|
+
content: "";
|
|
1444
|
+
display: inline-block;
|
|
1445
|
+
width: 4px;
|
|
1446
|
+
height: 4px;
|
|
1447
|
+
margin-inline: 6px;
|
|
1448
|
+
border-radius: var(--gw-radius-chip);
|
|
1449
|
+
background: var(--gw-line-strong);
|
|
1450
|
+
vertical-align: middle;
|
|
1451
|
+
}
|
|
1452
|
+
.gw-article-meta a {
|
|
1453
|
+
display: inline-block;
|
|
1454
|
+
color: var(--gw-accent-strong);
|
|
1455
|
+
font-weight: 500;
|
|
1456
|
+
text-decoration: underline;
|
|
1457
|
+
text-decoration-thickness: 1px;
|
|
1458
|
+
text-underline-offset: 4px;
|
|
1459
|
+
text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
|
|
1460
|
+
}
|
|
1461
|
+
.gw-article-meta a:hover { text-decoration-color: currentColor; }
|
|
1462
|
+
.gw-article-meta__avatar { display: inline-block; width: 24px; height: 24px; margin-inline-end: 8px; border-radius: var(--gw-radius-chip); object-fit: cover; vertical-align: top; }
|
|
1463
|
+
|
|
1464
|
+
/* the article page (templates/sections/article.html): <article class="gw-wrap gw-article"> holding __head
|
|
1465
|
+
(breadcrumbs, h1, meta), an optional __hero image, the .gw-toc and the .gw-prose, in that order. On phones one
|
|
1466
|
+
column, the contents folded above the prose. From 1080px (the kit's desktop width, where the header shows its nav)
|
|
1467
|
+
two columns: the reading column (68ch, the prose measure) and the contents beside it, sticky under the header. */
|
|
1468
|
+
.gw-article { padding-block: 32px 64px; }
|
|
1469
|
+
.gw-article__head > * + * { margin-top: 16px; }
|
|
1470
|
+
.gw-article__head h1 { font-size: var(--gw-display-h2); }
|
|
1471
|
+
.gw-article__hero { margin-top: 32px; }
|
|
1472
|
+
.gw-article__hero img { width: 100%; border-radius: var(--gw-radius-card); background: var(--gw-bg-soft); }
|
|
1473
|
+
.gw-article__hero figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
|
|
1474
|
+
.gw-article > .gw-toc { margin-top: 32px; }
|
|
1475
|
+
.gw-article > .gw-prose { margin-top: 40px; }
|
|
1476
|
+
@media (min-width: 768px) {
|
|
1477
|
+
.gw-article { padding-block: 48px 96px; }
|
|
1478
|
+
.gw-article__hero { margin-top: 40px; }
|
|
1479
|
+
}
|
|
1480
|
+
@media (min-width: 1080px) {
|
|
1481
|
+
.gw-article {
|
|
1482
|
+
display: grid;
|
|
1483
|
+
grid-template-columns: minmax(0, 68ch) minmax(200px, 240px);
|
|
1484
|
+
grid-template-rows: auto auto 1fr;
|
|
1485
|
+
justify-content: space-between;
|
|
1486
|
+
column-gap: 64px;
|
|
1487
|
+
}
|
|
1488
|
+
.gw-article > * { grid-column: 1; }
|
|
1489
|
+
.gw-article > .gw-article__head { grid-row: 1; }
|
|
1490
|
+
.gw-article > .gw-article__hero { grid-row: 2; }
|
|
1491
|
+
.gw-article > .gw-prose { grid-row: 3; }
|
|
1492
|
+
.gw-article > .gw-toc { grid-column: 2; grid-row: 2 / 4; margin-top: 40px; }
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/* contents: <nav class="gw-toc" aria-label="On this page" data-gw-toc> holding a <details>, its <summary> and an <ol>
|
|
1496
|
+
of links to the article's h2s (a nested <ol> for h3s). Plain links: they work without the script. website.js marks
|
|
1497
|
+
the section in view (aria-current="true", the accent bar) and opens the <details> from 1080px, where the list sits
|
|
1498
|
+
beside the article and sticks under the header; below 1080px it is folded above the article and a link closes it.
|
|
1499
|
+
The weight never changes with the mark, so the list does not move. */
|
|
1500
|
+
.gw-toc { font-size: 14px; line-height: 1.45; }
|
|
1501
|
+
.gw-toc > details { border: 1px solid var(--gw-line); border-radius: var(--gw-radius-card); background: var(--gw-surface); }
|
|
1502
|
+
.gw-toc summary {
|
|
1503
|
+
display: flex;
|
|
1504
|
+
align-items: center;
|
|
1505
|
+
justify-content: space-between;
|
|
1506
|
+
gap: 12px;
|
|
1507
|
+
min-height: var(--gw-tap-min);
|
|
1508
|
+
padding: 8px 16px;
|
|
1509
|
+
color: var(--gw-ink);
|
|
1510
|
+
font-weight: 600;
|
|
1511
|
+
list-style: none;
|
|
1512
|
+
cursor: pointer;
|
|
1513
|
+
}
|
|
1514
|
+
.gw-toc summary::-webkit-details-marker { display: none; }
|
|
1515
|
+
.gw-toc summary::after {
|
|
1516
|
+
content: "";
|
|
1517
|
+
flex: none;
|
|
1518
|
+
width: 12px;
|
|
1519
|
+
height: 12px;
|
|
1520
|
+
background: currentColor;
|
|
1521
|
+
opacity: 0.6;
|
|
1522
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
1523
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
1524
|
+
transition: transform 0.18s var(--gw-ease);
|
|
1525
|
+
}
|
|
1526
|
+
.gw-toc details[open] > summary::after { transform: rotate(180deg); }
|
|
1527
|
+
.gw-toc ol { margin: 0; padding: 0; list-style: none; }
|
|
1528
|
+
/* the rail is a background, so the list can scroll inside itself without clipping the bar of the marked link */
|
|
1529
|
+
.gw-toc details > ol { margin: 0 16px 16px; background: linear-gradient(var(--gw-line), var(--gw-line)) 0 0 / 1px 100% no-repeat; }
|
|
1530
|
+
.gw-toc a {
|
|
1531
|
+
display: block;
|
|
1532
|
+
padding: 6px 8px 6px 16px;
|
|
1533
|
+
border-left: 2px solid transparent;
|
|
1534
|
+
color: var(--gw-muted);
|
|
1535
|
+
}
|
|
1536
|
+
.gw-toc ol ol a { padding-left: 32px; }
|
|
1537
|
+
.gw-toc a:hover { color: var(--gw-ink); }
|
|
1538
|
+
.gw-toc a:focus-visible { outline-offset: -2px; }
|
|
1539
|
+
.gw-toc a[aria-current="true"] { border-left-color: var(--gw-accent); color: var(--gw-ink); }
|
|
1540
|
+
/* from 1080px: sticky beside the article; a long list scrolls inside itself, under the summary. Under the floating
|
|
1541
|
+
header it sticks below the card. */
|
|
1542
|
+
@media (min-width: 1080px) {
|
|
1543
|
+
.gw-toc { position: sticky; top: calc(var(--gw-header-height) + 32px); align-self: start; }
|
|
1544
|
+
.gw-toc > details { border: 0; border-radius: var(--gw-radius-0); background: transparent; }
|
|
1545
|
+
.gw-toc summary { min-height: 32px; padding: 0; color: var(--gw-muted); font-size: 13px; }
|
|
1546
|
+
.gw-toc details > ol {
|
|
1547
|
+
max-height: calc(100vh - var(--gw-header-height) - 120px);
|
|
1548
|
+
margin: 8px 0 0;
|
|
1549
|
+
overflow-y: auto;
|
|
1550
|
+
overscroll-behavior: contain;
|
|
1551
|
+
}
|
|
1552
|
+
:root:has(.gw-header--float) .gw-toc { top: calc(var(--gw-header-height) + var(--gw-header-gap) + 32px); }
|
|
1553
|
+
:root:has(.gw-header--float) .gw-toc details > ol { max-height: calc(100vh - var(--gw-header-height) - var(--gw-header-gap) - 120px); }
|
|
1554
|
+
}
|
|
1555
|
+
/* a contents link glides to its section, except for a visitor who asked for less motion (base.css stops it too) */
|
|
1556
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1557
|
+
:root:has(.gw-toc) { scroll-behavior: smooth; }
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/* callout: a note, a tip or a warning inside an article. The kind is said in words by __label (Note, Tip, Warning,
|
|
1561
|
+
or the reader's language), with a 16px icon before it; the tint only repeats it. Tints are the kit's: the accent's,
|
|
1562
|
+
success's and warning's -soft grounds; the icon takes the solid hue. role="note" on the box. */
|
|
1563
|
+
.gw-callout {
|
|
1564
|
+
padding: 16px 20px;
|
|
1565
|
+
border-radius: var(--gw-radius-card);
|
|
1566
|
+
background: var(--gw-accent-soft);
|
|
1567
|
+
color: var(--gw-ink);
|
|
1568
|
+
font-size: 16px;
|
|
1569
|
+
line-height: 1.6;
|
|
1570
|
+
}
|
|
1571
|
+
.gw-callout > * + * { margin-top: 8px; }
|
|
1572
|
+
.gw-callout--tip { background: var(--gw-success-soft); }
|
|
1573
|
+
.gw-callout--warning { background: var(--gw-warning-soft); }
|
|
1574
|
+
.gw-callout__label { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; line-height: 1.5; }
|
|
1575
|
+
.gw-callout__label::before {
|
|
1576
|
+
content: "";
|
|
1577
|
+
flex: none;
|
|
1578
|
+
width: 16px;
|
|
1579
|
+
height: 16px;
|
|
1580
|
+
background: var(--gw-accent);
|
|
1581
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11v5M12 7.5v.5'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
1582
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11v5M12 7.5v.5'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
1583
|
+
}
|
|
1584
|
+
.gw-callout--tip > .gw-callout__label::before {
|
|
1585
|
+
background: var(--gw-success);
|
|
1586
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.2h5c0-.9.4-1.7 1.1-2.2A6 6 0 0 0 12 3z'/%3E%3C/svg%3E");
|
|
1587
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.2h5c0-.9.4-1.7 1.1-2.2A6 6 0 0 0 12 3z'/%3E%3C/svg%3E");
|
|
1588
|
+
}
|
|
1589
|
+
.gw-callout--warning > .gw-callout__label::before {
|
|
1590
|
+
background: var(--gw-warning);
|
|
1591
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5 2.8 19.5h18.4z'/%3E%3Cpath d='M12 10v4M12 16.8v.2'/%3E%3C/svg%3E");
|
|
1592
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5 2.8 19.5h18.4z'/%3E%3Cpath d='M12 10v4M12 16.8v.2'/%3E%3C/svg%3E");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/* post list: the blog index and "more from the blog". A <ul role="list"> of post cards: .gw-card with a
|
|
1596
|
+
.gw-card__media first, a .gw-label for the category, the title (its link covers the card, so the card is one target
|
|
1597
|
+
and the link's name is the title alone), one or two lines of excerpt and a .gw-article-meta row at the foot. One
|
|
1598
|
+
column on phones, two from 768px, three from 1080px. --compact: one column of rows, no card and no picture. */
|
|
1599
|
+
.gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin: 0; padding: 0; list-style: none; }
|
|
1600
|
+
.gw-post-list > li { display: flex; min-width: 0; }
|
|
1601
|
+
.gw-post { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
|
|
1602
|
+
.gw-post:hover { border-color: var(--gw-line-strong); }
|
|
1603
|
+
.gw-post > .gw-card__media + * { margin-top: 20px; }
|
|
1604
|
+
.gw-post > .gw-label + .gw-post__title { margin-top: 8px; }
|
|
1605
|
+
.gw-post__title { font-size: 20px; line-height: 1.3; }
|
|
1606
|
+
.gw-post__title a { color: var(--gw-ink); }
|
|
1607
|
+
.gw-post__title a::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--gw-radius-card); }
|
|
1608
|
+
.gw-post:hover .gw-post__title a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
|
|
1609
|
+
.gw-post__title a:focus-visible { outline: 0; }
|
|
1610
|
+
.gw-post__title a:focus-visible::after { outline: 2px solid var(--gw-accent); outline-offset: 2px; }
|
|
1611
|
+
.gw-post__title + p { margin-top: 8px; }
|
|
1612
|
+
.gw-post > .gw-article-meta { margin-top: auto; padding-top: 16px; }
|
|
1613
|
+
@media (min-width: 768px) {
|
|
1614
|
+
.gw-post-list { gap: 24px; }
|
|
1615
|
+
.gw-post-list:not(.gw-post-list--compact) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1616
|
+
}
|
|
1617
|
+
@media (min-width: 1080px) {
|
|
1618
|
+
.gw-post-list:not(.gw-post-list--compact) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1619
|
+
}
|
|
1620
|
+
.gw-post-list--compact { gap: 0; border-top: 1px solid var(--gw-line); }
|
|
1621
|
+
.gw-post-list--compact .gw-post { padding: 20px 0; border: 0; border-bottom: 1px solid var(--gw-line); border-radius: var(--gw-radius-0); background: transparent; box-shadow: none; }
|
|
1622
|
+
.gw-post-list--compact .gw-post__title a::after { border-radius: var(--gw-radius-0); }
|
|
1623
|
+
.gw-post-list--compact .gw-card__media { display: none; }
|
|
1624
|
+
.gw-post-list--compact .gw-post > .gw-article-meta { padding-top: 8px; }
|
|
1625
|
+
|
|
1626
|
+
/* pagination: previous and next as links, a muted "Page 2 of 12" between them. Markup only, no script. */
|
|
1627
|
+
.gw-pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 16px; }
|
|
1628
|
+
* + .gw-pagination { margin-top: 40px; }
|
|
1629
|
+
.gw-pagination__status { color: var(--gw-muted); font-size: 14px; }
|
|
1630
|
+
.gw-pagination > [rel="next"] { margin-inline-start: auto; }
|
|
1631
|
+
|
|
1632
|
+
/* forms (0.6.0, gap #28): a field (label, control, error, hint), the controls, a group of choices, the inline row and
|
|
1633
|
+
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
|
|
1634
|
+
(AGENTS.md section 5). The look is the dashboard's field family (@gemboss/ui gemboss-field, ui-select, ui-textarea,
|
|
1635
|
+
ui-check, ui-help, ui-error) at the website's sizes: as tall as a button (40px, 44px on touch), 16px words (a phone
|
|
1636
|
+
does not zoom the page), the action radius of the site's profile, and a 1px border that reaches 3:1
|
|
1637
|
+
(--gw-line-control). The page owns validation: it sets aria-invalid="true" and writes the message; the kit draws it. */
|
|
1638
|
+
|
|
1639
|
+
/* a field: the label above the control, the error and then the hint under it, 8px apart */
|
|
1640
|
+
.gw-field { display: grid; gap: 8px; min-width: 0; }
|
|
1641
|
+
.gw-field__label { display: block; color: var(--gw-ink); font-size: 14px; font-weight: 600; line-height: 1.5; }
|
|
1642
|
+
/* two classes, so a context that styles its paragraphs (the footer's brand block) does not move or recolour them */
|
|
1643
|
+
:is(.gw-field, .gw-choices) > :is(.gw-field__hint, .gw-field__error) { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.5; }
|
|
1644
|
+
:is(.gw-field, .gw-choices) > .gw-field__hint { color: var(--gw-muted); }
|
|
1645
|
+
/* the error: an icon and words, in the danger colour; never the colour alone. Empty, it takes no room, so a page can
|
|
1646
|
+
keep it in the markup (named by aria-describedby) and fill it when the visitor sends the form. */
|
|
1647
|
+
:is(.gw-field, .gw-choices) > .gw-field__error { display: flex; gap: 6px; color: var(--gw-danger); font-weight: 500; }
|
|
1648
|
+
:is(.gw-field, .gw-choices) > .gw-field__error:empty { display: none; }
|
|
1649
|
+
.gw-field__error::before {
|
|
1650
|
+
content: "";
|
|
1651
|
+
flex: none;
|
|
1652
|
+
width: 16px;
|
|
1653
|
+
height: 16px;
|
|
1654
|
+
margin-top: 2px;
|
|
1655
|
+
margin-top: calc((1lh - 16px) / 2);
|
|
1656
|
+
background: currentColor;
|
|
1657
|
+
-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;
|
|
1658
|
+
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;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/* the controls: one box for a line of text, a choice from a list and a few lines. Full width of their field. */
|
|
1662
|
+
.gw-input,
|
|
1663
|
+
.gw-select,
|
|
1664
|
+
.gw-textarea {
|
|
1665
|
+
display: block;
|
|
1666
|
+
width: 100%;
|
|
1667
|
+
min-width: 0;
|
|
1668
|
+
height: var(--gw-button-height);
|
|
1669
|
+
margin: 0;
|
|
1670
|
+
padding: 0 12px;
|
|
1671
|
+
border: 1px solid var(--gw-line-control);
|
|
1672
|
+
border-radius: var(--gw-radius-action);
|
|
1673
|
+
background-color: var(--gw-surface);
|
|
1674
|
+
color: var(--gw-ink);
|
|
1675
|
+
font-size: 16px;
|
|
1676
|
+
line-height: 1.5;
|
|
1677
|
+
-webkit-appearance: none;
|
|
1678
|
+
appearance: none;
|
|
1679
|
+
}
|
|
1680
|
+
:is(.gw-input, .gw-textarea)::placeholder { color: var(--gw-muted); opacity: 1; }
|
|
1681
|
+
/* a list: the caret is drawn with the muted ink (two gradients, so it follows dark mode), with room for it */
|
|
1682
|
+
.gw-select {
|
|
1683
|
+
padding-right: 40px;
|
|
1684
|
+
background-image: linear-gradient(45deg, transparent 50%, var(--gw-muted) 50%), linear-gradient(135deg, var(--gw-muted) 50%, transparent 50%);
|
|
1685
|
+
background-position: calc(100% - 20px) 50%, calc(100% - 15px) 50%;
|
|
1686
|
+
background-size: 5px 5px;
|
|
1687
|
+
background-repeat: no-repeat;
|
|
1688
|
+
cursor: pointer;
|
|
1689
|
+
}
|
|
1690
|
+
.gw-textarea { height: auto; min-height: 96px; padding-block: 8px; resize: vertical; }
|
|
1691
|
+
/* the states, in this order: hover, focus, read-only, invalid, disabled. Focus is the kit's accent ring. */
|
|
1692
|
+
:is(.gw-input, .gw-select, .gw-textarea):hover { border-color: var(--gw-muted); }
|
|
1693
|
+
:is(.gw-input, .gw-select, .gw-textarea):focus-visible { border-color: var(--gw-ink); outline: 2px solid var(--gw-accent); outline-offset: 1px; }
|
|
1694
|
+
/* read-only: still focusable and readable, visibly not editable (the dashboard's) */
|
|
1695
|
+
:is(.gw-input, .gw-textarea)[readonly] { border-color: var(--gw-line-strong); background-color: var(--gw-bg-soft); cursor: default; }
|
|
1696
|
+
:is(.gw-input, .gw-select, .gw-textarea)[aria-invalid="true"] { border-color: var(--gw-danger); background-color: var(--gw-danger-soft); }
|
|
1697
|
+
:is(.gw-input, .gw-select, .gw-textarea):disabled { opacity: 0.48; cursor: not-allowed; }
|
|
1698
|
+
|
|
1699
|
+
/* a group of choices: a <fieldset> whose <legend> is the question, one choice per row */
|
|
1700
|
+
.gw-choices { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
1701
|
+
.gw-choices > legend { margin-bottom: 4px; padding: 0; }
|
|
1702
|
+
.gw-choices > :is(.gw-check, .gw-radio) { display: flex; }
|
|
1703
|
+
.gw-choices > :is(.gw-field__hint, .gw-field__error) { margin-top: 4px; }
|
|
1704
|
+
|
|
1705
|
+
/* a checkbox or a radio: the <label> is the target (32px, 44px on touch), the box sits on the first line of words */
|
|
1706
|
+
.gw-check,
|
|
1707
|
+
.gw-radio {
|
|
1708
|
+
display: inline-flex;
|
|
1709
|
+
align-items: flex-start;
|
|
1710
|
+
gap: 8px;
|
|
1711
|
+
max-width: 100%;
|
|
1712
|
+
padding-block: 4px;
|
|
1713
|
+
color: var(--gw-ink);
|
|
1714
|
+
font-size: 16px;
|
|
1715
|
+
line-height: 1.5;
|
|
1716
|
+
overflow-wrap: break-word;
|
|
1717
|
+
cursor: pointer;
|
|
1718
|
+
}
|
|
1719
|
+
:is(.gw-check, .gw-radio) > :not(input) { min-width: 0; }
|
|
1720
|
+
:is(.gw-check, .gw-radio) > input {
|
|
1721
|
+
display: inline-grid;
|
|
1722
|
+
flex: none;
|
|
1723
|
+
place-content: center;
|
|
1724
|
+
width: 18px;
|
|
1725
|
+
height: 18px;
|
|
1726
|
+
margin: 3px 0 0;
|
|
1727
|
+
border: 1px solid var(--gw-line-control);
|
|
1728
|
+
border-radius: var(--gw-radius-4);
|
|
1729
|
+
background: var(--gw-surface);
|
|
1730
|
+
-webkit-appearance: none;
|
|
1731
|
+
appearance: none;
|
|
1732
|
+
cursor: inherit;
|
|
1733
|
+
}
|
|
1734
|
+
.gw-radio > input { border-radius: var(--gw-radius-chip); }
|
|
1735
|
+
:is(.gw-check, .gw-radio):hover > input { border-color: var(--gw-muted); }
|
|
1736
|
+
/* chosen: the accent, as every check mark of the kit; the mark in the words-on-accent colour */
|
|
1737
|
+
.gw-check > input:checked { border-color: var(--gw-accent); background: var(--gw-accent); }
|
|
1738
|
+
.gw-check > input:checked::before {
|
|
1739
|
+
content: "";
|
|
1740
|
+
width: 14px;
|
|
1741
|
+
height: 14px;
|
|
1742
|
+
background: var(--gw-on-accent);
|
|
1743
|
+
-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;
|
|
1744
|
+
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;
|
|
1745
|
+
}
|
|
1746
|
+
.gw-radio > input:checked { border: 5px solid var(--gw-accent); }
|
|
1747
|
+
:is(.gw-check, .gw-radio) > input[aria-invalid="true"]:not(:checked) { border-color: var(--gw-danger); }
|
|
1748
|
+
:is(.gw-check, .gw-radio):has(> input:disabled) { opacity: 0.48; cursor: not-allowed; }
|
|
1749
|
+
/* a filter's count: a small pill at the end of the row, the neutral badge's colours */
|
|
1750
|
+
.gw-check__count {
|
|
1751
|
+
flex: none;
|
|
1752
|
+
min-width: 24px;
|
|
1753
|
+
margin: 1px 0 0 auto;
|
|
1754
|
+
padding: 2px 8px;
|
|
1755
|
+
border-radius: var(--gw-radius-chip);
|
|
1756
|
+
background: color-mix(in srgb, var(--gw-ink) 8%, transparent);
|
|
1757
|
+
color: var(--gw-ink-2);
|
|
1758
|
+
font-size: 12px;
|
|
1759
|
+
font-weight: 500;
|
|
1760
|
+
text-align: center;
|
|
1761
|
+
font-variant-numeric: tabular-nums;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/* the inline row: one field and its button (a newsletter, "get the guide"). One line from 480px, the field taking the
|
|
1765
|
+
room; below 480px, and in a box too narrow for both, the field and the button each take a full line. */
|
|
1766
|
+
.gw-form-inline { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
1767
|
+
.gw-form-inline > :not(.gw-btn) { flex: 999 1 160px; min-width: 0; }
|
|
1768
|
+
.gw-form-inline > .gw-btn { flex: 1 0 auto; }
|
|
1769
|
+
@media (max-width: 479px) {
|
|
1770
|
+
.gw-form-inline > :not(.gw-btn) { flex-basis: 100%; }
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/* the form stack: fields 24px apart, the actions 32px below the last one, 560px at most (a field's width hints at its
|
|
1774
|
+
answer: no field runs across a wide screen) */
|
|
1775
|
+
.gw-form { display: grid; gap: 24px; max-width: 560px; }
|
|
1776
|
+
/* the gap is the rhythm, also when the form is the card itself (a card spaces its own children otherwise) */
|
|
1777
|
+
.gw-form > * { margin: 0; }
|
|
1778
|
+
.gw-form > :is(.gw-actions, .gw-btn) { margin-top: 8px; }
|
|
1779
|
+
.gw-form > .gw-btn { justify-self: start; }
|
|
1780
|
+
|
|
1781
|
+
/* touch: every control is at least 44px tall, a choice's row too */
|
|
1782
|
+
@media (max-width: 768px), (pointer: coarse) {
|
|
1783
|
+
.gw-input, .gw-select { height: var(--gw-tap-min); }
|
|
1784
|
+
.gw-check, .gw-radio { padding-block: 10px; }
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/* ratings (0.6.0, gap #56): stars, the testimonial wall and the rating badge. Every rating and every testimonial
|
|
1788
|
+
links to the page it comes from, and says only what that page says (AGENTS.md section 6); `check` warns about one
|
|
1789
|
+
without its link (rating-source). */
|
|
1790
|
+
|
|
1791
|
+
/* stars: a value from 0 to 5 (style="--gw-stars: 4.9") drawn as five 16px stars, the last one cut where the value
|
|
1792
|
+
ends. A screen reader hears the value in words: role="img" and aria-label="4.9 out of 5" (the drawing itself is a
|
|
1793
|
+
background, nothing to read). Ink on the strong hairline: a rating is proof, not a state or an action. */
|
|
1794
|
+
.gw-stars {
|
|
1795
|
+
display: inline-block;
|
|
1796
|
+
flex: none;
|
|
1797
|
+
width: 80px;
|
|
1798
|
+
height: 16px;
|
|
1799
|
+
vertical-align: middle;
|
|
1800
|
+
background: linear-gradient(90deg, var(--gw-ink) calc(clamp(0, var(--gw-stars, 0), 5) * 20%), var(--gw-line-strong) 0);
|
|
1801
|
+
-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;
|
|
1802
|
+
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;
|
|
1803
|
+
}
|
|
1804
|
+
/* a forced-colours screen drops backgrounds: draw the stars in its own text colours */
|
|
1805
|
+
@media (forced-colors: active) {
|
|
1806
|
+
.gw-stars { forced-color-adjust: none; background: linear-gradient(90deg, CanvasText calc(clamp(0, var(--gw-stars, 0), 5) * 20%), GrayText 0); }
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/* the wall: short quotes in cards, one column on phones, two from 768px, three from 1080px. No masonry: the cards of
|
|
1810
|
+
a row share its height, and each card's byline sits at its foot. */
|
|
1811
|
+
.gw-testimonials {
|
|
1812
|
+
display: grid;
|
|
1813
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1814
|
+
gap: 16px;
|
|
1815
|
+
margin: 0;
|
|
1816
|
+
padding: 0;
|
|
1817
|
+
list-style: none;
|
|
1818
|
+
}
|
|
1819
|
+
.gw-testimonials > li { display: flex; min-width: 0; }
|
|
1820
|
+
@media (min-width: 768px) { .gw-testimonials { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } }
|
|
1821
|
+
@media (min-width: 1080px) { .gw-testimonials { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
|
1822
|
+
/* a quote card (with .gw-card): stars, the reviewer's own words, who wrote them and where */
|
|
1823
|
+
.gw-testimonial { display: flex; flex-direction: column; gap: 16px; width: 100%; margin: 0; }
|
|
1824
|
+
.gw-testimonial > * { margin: 0; }
|
|
1825
|
+
.gw-testimonial__quote,
|
|
1826
|
+
.gw-testimonial__quote p { color: var(--gw-ink); font-size: 16px; line-height: 1.6; }
|
|
1827
|
+
.gw-testimonial__quote > * + * { margin-top: 8px; }
|
|
1828
|
+
.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; }
|
|
1829
|
+
.gw-testimonial__by b { display: block; color: var(--gw-ink); font-weight: 600; }
|
|
1830
|
+
.gw-testimonial__by > :not(img) { min-width: 0; }
|
|
1831
|
+
.gw-testimonial__avatar { flex: none; width: 40px; height: 40px; border-radius: var(--gw-radius-chip); background: var(--gw-bg-soft); object-fit: cover; }
|
|
1832
|
+
|
|
1833
|
+
/* the rating badge: one link to the store's review page, with the store's mark, the score, the stars and the count.
|
|
1834
|
+
A small card that sits; the border darkens on hover, as a linked card's. */
|
|
1835
|
+
.gw-rating-badge {
|
|
1836
|
+
display: inline-flex;
|
|
1837
|
+
align-items: center;
|
|
1838
|
+
gap: 12px;
|
|
1839
|
+
max-width: 100%;
|
|
1840
|
+
padding: 8px 16px 8px 8px;
|
|
1841
|
+
border: 1px solid var(--gw-line);
|
|
1842
|
+
border-radius: var(--gw-radius-card);
|
|
1843
|
+
background: var(--gw-surface);
|
|
1844
|
+
color: var(--gw-ink);
|
|
1845
|
+
font-size: 13px;
|
|
1846
|
+
line-height: 1.4;
|
|
1847
|
+
vertical-align: middle;
|
|
1848
|
+
}
|
|
1849
|
+
a.gw-rating-badge:hover { border-color: var(--gw-line-strong); }
|
|
1850
|
+
.gw-rating-badge__icon {
|
|
1851
|
+
display: inline-flex;
|
|
1852
|
+
flex: none;
|
|
1853
|
+
align-items: center;
|
|
1854
|
+
justify-content: center;
|
|
1855
|
+
width: 40px;
|
|
1856
|
+
height: 40px;
|
|
1857
|
+
border-radius: var(--gw-radius-action);
|
|
1858
|
+
background: var(--gw-bg-soft);
|
|
1859
|
+
color: var(--gw-ink);
|
|
1860
|
+
}
|
|
1861
|
+
.gw-rating-badge__icon :is(svg, img) { width: 24px; height: 24px; }
|
|
1862
|
+
.gw-rating-badge__body { display: grid; gap: 2px; min-width: 0; }
|
|
1863
|
+
.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; }
|
|
1864
|
+
.gw-rating-badge__count { color: var(--gw-muted); }
|