@gemboss/website-ui 0.9.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -4,6 +4,56 @@ Every release lists what changed for a site that uses the kit. A release that ad
4
4
  says so with `Closes gap #N` (the GitHub issue number), so `npx gemboss-website upgrade` can tell that site which
5
5
  stopgap (`kitgap-*`) it can now delete.
6
6
 
7
+ ## 0.10.0 (2026-10-26)
8
+
9
+ The footer, 1:1 with gemboss.ai. 0.7.0 drew the footer "as gemboss.ai has it", but not to the pixel: when gemboss.ai
10
+ moved onto the kit (site PR #76) its footer changed in about a dozen places (the gaps, the bottom row, the logo, the
11
+ wordmark, the widths where the columns fold). Chris asked for it identical, with nothing missed, and for the values to
12
+ live in the kit, so every site that uses it inherits them (26/10/2026). 0.10.0 takes every value from the site's own
13
+ footer before #76 and was measured against it in Chromium: part by part (position, size, type, colour of every word)
14
+ and pixel by pixel, at 14 widths from 320 to 1920px, light and dark, folded and open, every part hovered and focused
15
+ from the keyboard, on every page of the site. All of it is pixel-identical but one thing, on purpose: at 320px
16
+ gemboss.ai squeezed the chevron of its longest row to 7.3px, and the kit keeps it 8px.
17
+
18
+ A minor version: a footer that already uses the kit changes to these values. Nothing to change in its markup.
19
+
20
+ - Widths: the columns fold up to 620px and stand open from 621px (were 767 and 768), in the CSS and in `website.js`
21
+ (`matchMedia('(min-width: 621px)')`); the footer is one row from 1081px (was 1080px); two columns between (was three
22
+ from 768px). The wordmark shows from 621px (was 480px), its gap 16px up to 640px.
23
+ - The footer: 64px above and 32px below (was 40px), no ground of its own (`background: transparent`, was `--gw-bg`), 32px
24
+ between columns (24px x 20px up to 620px).
25
+ - The brand block: the mark 34px with 8px corners (`--gw-radius-8`), the row 34px tall, as wide as the block (44px on
26
+ touch); its sentence 30 characters a line at most, 22px lines (was 320px wide). The block keeps its width beside the
27
+ Ask AI row; the 0.7.0 rules that widened it are gone.
28
+ - Headings: 12px over their links (was 8px), 1.6 line height, balanced, whatever the page's `h2` rule says (a site's
29
+ `h2` gave them its display face, and balance on some pages, not on others).
30
+ - The face: the brand's name, the headings and the wordmark are set in `--gw-font` whatever the page's body says (a
31
+ page in Chinese puts PingFang first; gemboss.ai keeps Inter for these three). A brand sets its face with the token.
32
+ - Links in a column: 44px rows up to 768px and on touch (gemboss.ai's), 20px lines.
33
+ - The bottom row: 32px under the columns (was 48px), 12px x 24px between its parts, 16px between its links (8px x 16px on
34
+ touch); no 24px minimum size on a desktop link; a social link at least 44px wide on touch.
35
+ - Folds up to 620px: each column's hairline on top, the last column's and each family row's below; a 52px row without
36
+ padding (was 44px padded to 52px); the chevron is two 2px borders, 8px, turned 45deg and 225deg open, at once (it
37
+ eased 0.18s); a focused row draws 2px of accent 2px out with 6px corners.
38
+ - Family rows: four items a line from 621px, 44px items from 621 to 768px, the list pulled 12px left only from 1081px;
39
+ each item fills its cell.
40
+ - `.gw-footer__ext` takes the glyph: `<span class="gw-footer__ext" aria-hidden="true">↗</span>`, set muted at weight
41
+ 400, as gemboss.ai writes it. An empty one is still drawn (the 12px mask).
42
+ - The Ask AI label has 22px lines; a hovered Ask AI link takes the browser's own underline (was 1px, 4px under).
43
+ - The theme toggle: on hover its hairline turns ink and its icon the accent, its ground stays (colour only, 0.15s; was
44
+ the soft ground). Two icons: `.gw-theme-toggle__moon` shows in light, `.gw-theme-toggle__sun` in dark (a new section
45
+ at the end of `header.css`, for any `.gw-theme-toggle`); a toggle with one plain `<svg>` stays as it was.
46
+ - The wordmark: weight 800 (added to `type.weights`, its one use), 48px above and nothing under but the footer's 32px
47
+ (was 24px), the mark 7vw wide (40 to 96px; was 0.9em tall). Its `<svg>` may carry `width` and `height` for its shape:
48
+ gemboss.ai's gem says `width="86" height="100"`.
49
+ - Token: light `--gw-muted` is `#6a6d73` (was `#686b71`), gemboss.ai's: 5.18:1 on white, 4.67:1 on the soft ground
50
+ (were 5.34 and 4.81), both above 4.5:1.
51
+ - `templates/footer.html` follows: the ↗ glyph, the two toggle icons, a 34px mark, the new widths in its notes.
52
+ - Docs: `components.md` (Footer) and `markup-contract.md` (the folds' widths, the ↗, the toggle's icons, the
53
+ wordmark's mark).
54
+ - Tests: `test/run.mjs` holds the base rules byte for byte again from this version (their hash, renewed once on purpose)
55
+ and pins the values above; the fold tests read 620/621px.
56
+
7
57
  ## 0.9.0 (2026-10-25)
8
58
 
9
59
  The spotlight, a named hover effect for the things you click (Chris, 25/10/2026, chosen over a heading that glows
package/README.md CHANGED
@@ -40,9 +40,9 @@ nó đọc `AGENTS.md` của kit trước, dựng từ mẫu, và chạy `check`
40
40
  **Site HTML tĩnh hoặc không có bước build:** dùng CDN của npm, ghim đúng phiên bản.
41
41
 
42
42
  ```html
43
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.9.0/website.min.css">
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.10.0/website.min.css">
44
44
  <link rel="stylesheet" href="/site.css"> <!-- CSS riêng của site, luôn SAU kit -->
45
- <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.9.0/js/website.js" defer></script>
45
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.10.0/js/website.js" defer></script>
46
46
  ```
47
47
 
48
48
  Chèn markup của kit sau khi trang đã tải (block của builder, bản xem trước CMS): gọi `gwWebsite.init(phầnMới)` ngay
package/css/footer.css CHANGED
@@ -1,19 +1,23 @@
1
1
  /* footer: brand block, link columns, bottom row. One hairline on top, white like the page. */
2
2
  .gw-footer {
3
- padding-block: 64px 40px;
3
+ padding-block: 64px 32px;
4
4
  border-top: 1px solid var(--gw-line);
5
- background: var(--gw-bg);
5
+ background: transparent;
6
6
  color: var(--gw-ink);
7
7
  font-size: 14px;
8
8
  }
9
9
  .gw-footer__grid {
10
10
  display: grid;
11
11
  grid-template-columns: repeat(2, minmax(0, 1fr));
12
- gap: 32px 24px;
13
- }
14
- .gw-footer__brand { grid-column: 1 / -1; max-width: 320px; }
15
- .gw-footer__brand p { margin-top: 16px; color: var(--gw-muted); }
16
- .gw-footer__heading { margin: 0 0 8px; color: var(--gw-ink); font-size: 13px; font-weight: 600; letter-spacing: 0; }
12
+ gap: 32px;
13
+ }
14
+ .gw-footer__brand p { max-width: 30ch; margin-top: 16px; color: var(--gw-muted); line-height: 22px; }
15
+ .gw-footer__brand .gw-brand { display: flex; min-height: 34px; font-family: var(--gw-font); line-height: 1.6; }
16
+ .gw-footer__brand .gw-brand__mark { width: 34px; height: 34px; border-radius: var(--gw-radius-8); }
17
+ /* the brand's name, the column headings and the wordmark are set in the kit's face (--gw-font), whatever the page
18
+ does to its body or its h2 (a page in Chinese puts PingFang first; gemboss.ai keeps Inter for these three); a
19
+ heading is balanced, like gemboss.ai's main pages */
20
+ .gw-footer__heading { margin: 0 0 12px; color: var(--gw-ink); font-family: var(--gw-font); font-size: 13px; font-weight: 600; letter-spacing: 0; line-height: 1.6; text-wrap: balance; }
17
21
  /* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
18
22
  reader agrees to; only when the form is wired to a list that keeps the address */
19
23
  .gw-footer__newsletter { margin-top: 24px; }
@@ -26,33 +30,35 @@
26
30
  flex-wrap: wrap;
27
31
  align-items: center;
28
32
  justify-content: space-between;
29
- gap: 8px 24px;
30
- margin-top: 48px;
33
+ gap: 12px 24px;
34
+ margin-top: 32px;
31
35
  padding-top: 24px;
32
36
  border-top: 1px solid var(--gw-line);
33
37
  color: var(--gw-muted);
34
38
  font-size: 13px;
35
39
  }
36
40
  .gw-footer__bottom:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
37
- .gw-footer__bottom ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 0; padding: 0; list-style: none; }
41
+ .gw-footer__bottom ul { display: flex; flex-wrap: wrap; gap: 16px; margin: 0; padding: 0; list-style: none; }
38
42
  .gw-footer__bottom a {
39
43
  display: inline-flex;
40
44
  align-items: center;
41
45
  justify-content: center;
42
- min-width: 24px;
43
- min-height: 24px;
44
46
  color: var(--gw-muted);
45
47
  }
46
48
  .gw-footer__bottom a:hover { color: var(--gw-ink); }
47
- @media (min-width: 768px) {
48
- .gw-footer__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
49
+ @media (max-width: 620px) {
50
+ .gw-footer__grid { gap: 24px 20px; }
51
+ .gw-footer__brand { grid-column: 1 / -1; }
49
52
  }
50
- @media (min-width: 1080px) {
51
- .gw-footer__grid { grid-auto-flow: column; grid-template-columns: minmax(0, 1.6fr); grid-auto-columns: minmax(0, 1fr); }
52
- .gw-footer__brand { grid-column: auto; }
53
+ @media (min-width: 1081px) {
54
+ .gw-footer__grid { grid-auto-flow: column; grid-template-columns: minmax(0, 1.6fr); grid-auto-columns: minmax(0, 1fr); gap: 32px 24px; }
53
55
  }
54
56
  @media (max-width: 768px), (pointer: coarse) {
57
+ .gw-footer__brand .gw-brand { min-height: var(--gw-tap-min); }
58
+ .gw-footer__col a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
55
59
  .gw-footer__bottom a { min-height: var(--gw-tap-min); }
60
+ .gw-footer__bottom ul:last-child a { min-width: var(--gw-tap-min); }
61
+ .gw-footer__bottom ul:not(:last-child) { gap: 8px 16px; }
56
62
  }
57
63
 
58
64
  /* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
@@ -66,16 +72,14 @@
66
72
  touch. */
67
73
  .gw-footer__ask { margin-top: 24px; }
68
74
  /* the row may run wider than the brand's sentence (four names on one line at 390px) */
69
- .gw-footer__brand:has(> .gw-footer__ask) { max-width: none; }
70
- .gw-footer__brand:has(> .gw-footer__ask) > :not(.gw-footer__ask) { max-width: 320px; }
71
- .gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; }
75
+ .gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; line-height: 22px; }
72
76
  .gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
73
77
  .gw-footer__ask a { display: block; padding-block: 4px; color: var(--gw-ink); line-height: 20px; }
74
- .gw-footer__ask a:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
78
+ .gw-footer__ask a:hover { text-decoration: underline; }
75
79
 
76
80
  /* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
77
81
  .gw-visually-hidden span, only when the link has target="_blank" */
78
- .gw-footer__ext {
82
+ .gw-footer__ext:empty {
79
83
  display: inline-block;
80
84
  width: 12px;
81
85
  height: 12px;
@@ -85,6 +89,7 @@
85
89
  -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;
86
90
  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;
87
91
  }
92
+ .gw-footer__ext:not(:empty) { color: var(--gw-muted); font-weight: 400; }
88
93
 
89
94
  /* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
90
95
  .gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
@@ -93,48 +98,52 @@
93
98
  (::details-content; website.js also sets `open` there, for browsers without it). */
94
99
  .gw-footer__fold > summary { list-style: none; }
95
100
  .gw-footer__fold > summary::-webkit-details-marker { display: none; }
96
- .gw-footer__fold .gw-footer__heading { margin: 0; line-height: 20px; }
101
+ .gw-footer__fold .gw-footer__heading { margin: 0; }
97
102
  .gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
98
103
  .gw-footer__col > .gw-footer__fold a { line-height: 20px; }
99
- @media (min-width: 768px) {
104
+ @media (min-width: 621px) {
100
105
  .gw-footer__fold > summary { display: contents; }
101
106
  .gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
102
107
  }
103
- @media (min-width: 768px) {
108
+ @media (min-width: 621px) {
104
109
  .gw-footer__fold::details-content { content-visibility: visible; }
105
110
  }
106
- @media (max-width: 767px) {
111
+ @media (max-width: 620px) {
107
112
  .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
108
113
  .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
109
- .gw-footer__col:has(> .gw-footer__fold),
114
+ /* a column's hairline on top, the last column's below too, a family row's below (gemboss.ai's order: a focused
115
+ row's ring passes under the next row's line) */
116
+ .gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
117
+ .gw-footer__col:has(> .gw-footer__fold):last-child,
110
118
  .gw-footer__family { border-bottom: 1px solid var(--gw-line); }
111
- .gw-footer__brand + .gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
112
119
  .gw-footer__fold > summary {
113
120
  display: flex;
114
121
  align-items: center;
115
122
  justify-content: space-between;
116
123
  gap: 16px;
117
- min-height: var(--gw-tap-min);
118
- padding-block: 16px;
124
+ min-height: 52px;
125
+ padding: 0;
119
126
  cursor: pointer;
120
127
  }
121
128
  .gw-footer__fold > summary::after {
122
129
  content: "";
123
130
  flex: none;
124
- width: 16px;
125
- height: 16px;
126
- background: var(--gw-muted);
127
- -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;
128
- 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;
129
- transition: transform 0.18s var(--gw-ease);
131
+ width: 8px;
132
+ height: 8px;
133
+ margin-right: 4px;
134
+ border-right: 2px solid var(--gw-muted);
135
+ border-bottom: 2px solid var(--gw-muted);
136
+ rotate: 45deg;
137
+ translate: 0 -2px;
130
138
  }
131
- .gw-footer__fold[open] > summary::after { transform: rotate(180deg); }
139
+ .gw-footer__fold[open] > summary::after { rotate: 225deg; translate: 0 2px; }
140
+ .gw-footer__fold > summary:focus-visible { outline: 2px solid var(--gw-accent); outline-offset: 2px; border-radius: var(--gw-radius-6); }
132
141
  .gw-footer__fold > ul { padding-bottom: 12px; }
133
142
  .gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
134
143
  }
135
144
  /* 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
136
145
  gaps), whatever the number of columns, so a family row can line up with it; the columns share the rest */
137
- @media (min-width: 1080px) {
146
+ @media (min-width: 1081px) {
138
147
  .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
139
148
  }
140
149
 
@@ -144,6 +153,8 @@
144
153
  the items start where the first column starts. */
145
154
  .gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
146
155
  .gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
156
+ .gw-footer__family .gw-footer__fold > ul > li { display: flex; }
157
+ .gw-footer__family .gw-footer__fold > ul > li > a { flex: 1 1 auto; }
147
158
  .gw-footer__family a {
148
159
  display: flex;
149
160
  flex-direction: column;
@@ -157,14 +168,18 @@
157
168
  .gw-footer__family a:hover { background: var(--gw-bg-soft); }
158
169
  .gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
159
170
  .gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
160
- @media (min-width: 768px) {
171
+ @media (min-width: 621px) {
161
172
  .gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
162
173
  .gw-footer__family .gw-footer__heading { margin-bottom: 12px; }
163
- .gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px; margin-left: -12px; }
174
+ .gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px; }
164
175
  .gw-footer__family a { min-height: 0; padding: 8px 12px; }
165
176
  .gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
166
177
  }
167
- @media (min-width: 1080px) {
178
+ @media (min-width: 621px) and (max-width: 768px) {
179
+ .gw-footer__family a { min-height: var(--gw-tap-min); }
180
+ }
181
+ @media (min-width: 1081px) {
182
+ .gw-footer__family .gw-footer__fold > ul { margin-left: -12px; }
168
183
  .gw-footer__family { display: flow-root; }
169
184
  .gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
170
185
  .gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
@@ -172,7 +187,7 @@
172
187
 
173
188
  /* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
174
189
  square with one hairline, its hit area 40px, 44px on touch. It pushes itself and the social links to the right.
175
- Below 768px the copyright and the toggle share the first line, legal and social links the second. */
190
+ Up to 620px the copyright and the toggle share the first line, legal and social links the second. */
176
191
  .gw-footer__toggle {
177
192
  position: relative;
178
193
  width: 32px;
@@ -183,12 +198,15 @@
183
198
  }
184
199
  .gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
185
200
  .gw-footer__toggle svg { width: 18px; height: 18px; }
201
+ /* on hover the hairline turns ink and the icon the accent, the ground stays (gemboss.ai's); colour only, 0.15s */
202
+ .gw-footer__toggle { transition: border-color 0.15s, color 0.15s; }
203
+ .gw-footer__toggle:hover { border-color: var(--gw-ink); background: var(--gw-surface); color: var(--gw-accent); }
186
204
  @media (max-width: 768px), (pointer: coarse) {
187
205
  .gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
188
206
  }
189
- @media (max-width: 767px) {
190
- .gw-footer__bottom:has(> .gw-footer__toggle) { column-gap: 16px; }
191
- .gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - var(--gw-tap-min) - 16px); }
207
+ @media (max-width: 620px) {
208
+ .gw-footer__bottom:has(> .gw-footer__toggle) { gap: 8px 16px; }
209
+ .gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - 64px); }
192
210
  .gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
193
211
  .gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
194
212
  .gw-footer__family + .gw-footer__bottom,
@@ -198,37 +216,35 @@
198
216
  /* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
199
217
  the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
200
218
  page scroll sideways: a name too long for the line is cut. The mark is drawn in currentColor (an inline SVG); an
201
- <img> mark keeps its own colours, so leave it out and keep the name. Not on phones, as on gemboss.ai. 24px more room
202
- under it than gemboss.ai gives: the kit's footer is a painted band (white like the page), and the breathing gate
203
- wants half the letters' size between them and its edge (67px under 123px letters at 1366px). */
219
+ <img> mark keeps its own colours, so leave it out and keep the name. Values are gemboss.ai's, 1:1 (Chris 26/10/2026):
220
+ weight 800, 48px above, nothing below but the footer's own 32px, the mark 7vw wide (40–96px), hidden up to 620px.
221
+ The footer is not painted (transparent on the page), so the breathing gate reads it as part of the page. */
204
222
  .gw-footer__wordmark {
205
223
  display: flex;
206
224
  align-items: center;
207
225
  justify-content: center;
208
226
  gap: 24px;
209
- padding-block: 48px 24px;
227
+ padding-block: 48px 0;
210
228
  color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
229
+ font-family: var(--gw-font);
211
230
  font-size: var(--gw-display-wordmark);
212
- font-weight: 700;
231
+ font-weight: 800;
213
232
  letter-spacing: var(--gw-display-tracking);
214
233
  line-height: 1;
215
234
  white-space: nowrap;
216
235
  user-select: none;
217
236
  }
218
237
  .gw-footer__wordmark > svg,
219
- .gw-footer__wordmark > img { flex: none; width: auto; height: 0.9em; }
238
+ .gw-footer__wordmark > img { flex: none; width: clamp(40px, 7vw, 96px); height: auto; }
220
239
  .gw-footer__wordmark > span { min-width: 0; overflow-x: clip; }
221
- @media (max-width: 767px) { .gw-footer__wordmark { gap: 16px; } }
222
- @media (max-width: 479px) { .gw-footer__wordmark { display: none; } }
240
+ @media (max-width: 640px) { .gw-footer__wordmark { gap: 16px; } }
241
+ @media (max-width: 620px) { .gw-footer__wordmark { display: none; } }
223
242
 
224
243
  @media (max-width: 768px), (pointer: coarse) {
225
244
  .gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
226
245
  }
227
- @media (prefers-reduced-motion: reduce) {
228
- .gw-footer__fold > summary::after { transition: none; }
229
- }
230
246
  /* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
231
247
  @media (forced-colors: active) {
232
- .gw-footer__ext,
233
- .gw-footer__fold > summary::after { forced-color-adjust: none; background: CanvasText; }
248
+ .gw-footer__ext:empty { forced-color-adjust: none; background: CanvasText; }
249
+ .gw-footer__fold > summary::after { border-color: CanvasText; }
234
250
  }
package/css/header.css CHANGED
@@ -437,3 +437,15 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
437
437
  @media (min-width: 1080px) {
438
438
  .gw-header__actions > .gw-lang { display: block; }
439
439
  }
440
+
441
+ /* ---- 0.10.0: the theme toggle's two icons ---------------------------------------------------------------------------
442
+ A toggle may hold two icons: .gw-theme-toggle__moon, shown in light, and .gw-theme-toggle__sun, shown in dark, as
443
+ gemboss.ai draws them. The theme in force picks one: data-theme on <html>, else the system's. A toggle with one plain
444
+ <svg> stays as it is. */
445
+ .gw-theme-toggle__sun { display: none; }
446
+ :root[data-theme="dark"] .gw-theme-toggle__sun { display: block; }
447
+ :root[data-theme="dark"] .gw-theme-toggle__moon { display: none; }
448
+ @media (prefers-color-scheme: dark) {
449
+ :root:not([data-theme="light"]) .gw-theme-toggle__sun { display: block; }
450
+ :root:not([data-theme="light"]) .gw-theme-toggle__moon { display: none; }
451
+ }
package/css/tokens.css CHANGED
@@ -94,7 +94,7 @@
94
94
  --gw-surface: #ffffff;
95
95
  --gw-ink: #1b1c1e;
96
96
  --gw-ink-2: #3b3d42;
97
- --gw-muted: #686b71;
97
+ --gw-muted: #6a6d73;
98
98
  --gw-line: #e5e7ea;
99
99
  --gw-line-strong: #d5d8dd;
100
100
  --gw-line-control: #85888e;
@@ -186,7 +186,7 @@
186
186
  --gw-surface: #ffffff;
187
187
  --gw-ink: #1b1c1e;
188
188
  --gw-ink-2: #3b3d42;
189
- --gw-muted: #686b71;
189
+ --gw-muted: #6a6d73;
190
190
  --gw-line: #e5e7ea;
191
191
  --gw-line-strong: #d5d8dd;
192
192
  --gw-line-control: #85888e;
package/demo/footer.html CHANGED
@@ -312,7 +312,7 @@
312
312
  <li><a href="https://www.gempages.net" target="_blank" rel="noopener">GemPages<span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
313
313
  </ul>
314
314
  </div>
315
- <div class="gw-footer__wordmark" aria-hidden="true" data-gw-show="brand:gemboss"><svg viewBox="0 0 100 116"><polygon points="26,14 74,14 66,50 34,50" fill="currentColor"/><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round"/><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="3.8" stroke-linejoin="round"/></svg><span>GemBoss</span></div>
315
+ <div class="gw-footer__wordmark" aria-hidden="true" data-gw-show="brand:gemboss"><svg width="86" height="100" viewBox="0 0 100 116"><polygon points="26,14 74,14 66,50 34,50" fill="currentColor"/><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round"/><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="3.8" stroke-linejoin="round"/></svg><span>GemBoss</span></div>
316
316
  <div class="gw-footer__wordmark" aria-hidden="true" data-gw-show="brand:gempages" hidden><svg viewBox="0 0 26 26"><rect x="1" y="1" width="24" height="24" rx="6" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8 9h10M8 13h10M8 17h6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg><span>Example brand</span></div>
317
317
  </div>
318
318
  </footer>
@@ -415,19 +415,24 @@ The footer of gemboss.ai (0.7.0): one per site, the same on every page, from `te
415
415
  its rows (its columns, its family, its words) and deletes the optional parts it does not use; it never adds a footer
416
416
  part of its own. See it in `demo/footer.html`, with a switch to the same footer for a GemPages-style brand.
417
417
 
418
+ Since 0.10.0 every value is gemboss.ai's, 1:1: measured against the site's own footer before it moved to the kit, part
419
+ by part and pixel by pixel, at 14 widths from 320 to 1920px, light and dark, folded and open, hovered and focused from
420
+ the keyboard. Its widths are the site's: folds up to 620px, columns open from 621px, one row from 1081px. A brand
421
+ changes what it fills in and the tokens (colours, the radius profile), never these values.
422
+
418
423
  | class | for |
419
424
  |---|---|
420
- | `.gw-footer` | the footer: one hairline on top, white like the page |
421
- | `.gw-footer__grid` | brand block and link columns: 2 columns on phones, 3 from 768px, one row from 1080px. With folding columns (0.7.0): one track of rows below 768px, and from 1080px the brand's track as on gemboss.ai (193.5px beside 1120px), the columns sharing the rest: three to six |
422
- | `.gw-footer__brand` | the brand and one sentence, full width on phones |
423
- | `.gw-footer__col`, `__heading` | a `<nav>` column: a sentence-case heading and a `<ul>` of links |
424
- | `.gw-footer__fold` | 0.7.0: the column's `<details>`, its `<summary>` holding the `__heading`. Below 768px a 52px row with a chevron, folded until tapped (44px links); from 768px the open column, the summary only its heading (no box, no Tab stop). Works without the script |
425
+ | `.gw-footer` | the footer: one hairline on top, on the page's own ground (transparent), 64px above and 32px below |
426
+ | `.gw-footer__grid` | brand block and link columns: 2 columns up to 1080px (the brand block full width up to 620px), one row from 1081px. With folding columns (0.7.0): one track of rows up to 620px, and from 1081px the brand's track as on gemboss.ai (193.5px beside 1120px), the columns sharing the rest: three to six |
427
+ | `.gw-footer__brand` | the brand (a 34px mark) and one sentence of 30 characters a line at most |
428
+ | `.gw-footer__col`, `__heading` | a `<nav>` column: a sentence-case heading and a `<ul>` of links (44px rows up to 768px and on touch). The heading is set in `--gw-font` and balanced, whatever the page does to its body or its `h2` |
429
+ | `.gw-footer__fold` | 0.7.0: the column's `<details>`, its `<summary>` holding the `__heading`. Up to 620px a 52px row with a drawn chevron that turns at once, folded until tapped; from 621px the open column, the summary only its heading (no box, no Tab stop). Works without the script |
425
430
  | `.gw-footer__ask`, `__ask-label` | 0.7.0, optional, in the brand block: "Ask AI about [Brand]" and links that open ChatGPT, Claude, Perplexity and Google AI Mode with one neutral question typed in |
426
- | `.gw-footer__family` | 0.7.0, optional, after the grid: a `<nav>` row of products, a fold whose heading is its label, a list of `<a><b>Name</b> <i>One line</i></a>`; two a line on phones, four from 768px, from 1080px the label beside the items. Repeat it for a second family |
427
- | `.gw-footer__ext` | 0.7.0: the drawn ↗ after a link that leaves the site, `aria-hidden="true"` |
431
+ | `.gw-footer__family` | 0.7.0, optional, after the grid: a `<nav>` row of products, a fold whose heading is its label, a list of `<a><b>Name</b> <i>One line</i></a>`; two a line on phones, four from 621px, from 1081px the label beside the items. Repeat it for a second family |
432
+ | `.gw-footer__ext` | 0.7.0: the ↗ after a link that leaves the site, `aria-hidden="true"`: empty, the kit draws it; holding the glyph `↗` (0.10.0, as gemboss.ai writes it), it is set muted at weight 400 |
428
433
  | `.gw-footer__bottom` | copyright, legal links, social links; alone (no grid above), it drops its top rule |
429
- | `.gw-footer__toggle` | 0.7.0, optional: with `.gw-theme-toggle` and `data-gw-theme-toggle`, the theme toggle in the bottom row, a 32px square with one hairline (44px on touch), before the social links |
430
- | `.gw-footer__wordmark` | 0.7.0, optional, last: the brand's mark (inline SVG in `currentColor`) and name, ink at 10%, `--gw-display-wordmark`; `aria-hidden="true"`; not on phones |
434
+ | `.gw-footer__toggle` | 0.7.0, optional: with `.gw-theme-toggle` and `data-gw-theme-toggle`, the theme toggle in the bottom row, a 32px square with one hairline (44px on touch), before the social links. On hover its hairline turns ink and its icon the accent. Two icons (0.10.0): `.gw-theme-toggle__moon` shows in light, `.gw-theme-toggle__sun` in dark |
435
+ | `.gw-footer__wordmark` | 0.7.0, optional, last: the brand's mark (inline SVG in `currentColor`, 7vw wide) and name (weight 800), ink at 10%, `--gw-display-wordmark`; `aria-hidden="true"`; not up to 620px |
431
436
  | `.gw-footer__newsletter` | 0.6.0, optional: a `<form>` in the brand block, 24px under its sentence: a `.gw-field` with its label, a `.gw-form-inline` row (the email field and a `--secondary` button) and the note |
432
437
  | `.gw-visually-hidden` | 0.7.0, anywhere: words for a screen reader only, " (opens in a new tab)" in a link with `target="_blank"` |
433
438
 
@@ -44,7 +44,7 @@ the spotlight (0.9.0). It comes in two entries built from one source
44
44
  to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
45
45
  - **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
46
46
  mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
47
- - **`version`**: `'0.9.0'`. The classic script exposes the three as `window.gwWebsite`.
47
+ - **`version`**: `'0.10.0'`. The classic script exposes the three as `window.gwWebsite`.
48
48
  - **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `lang`, `theme`, `reveal`,
49
49
  `header`, `segmented`, `toc`, `table`, `fold`, `spot`). A drill-down menu is wired with its toggle (`menu`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
50
50
  is still wired.
@@ -282,26 +282,31 @@ top-level links and the actions are children of the sheet's `<nav>`.
282
282
  <details class="gw-footer__fold">
283
283
  <summary><h2 class="gw-footer__heading">More from Brand</h2></summary>
284
284
  <ul><li><a href="…"><b>Name<span class="gw-footer__ext" aria-hidden="true"></span>?</b> <i>One line</i></a></li> …</ul>
285
+ <!-- the ↗: empty, the kit draws it (a 12px mask); or the glyph itself, <span class="gw-footer__ext" aria-hidden="true">↗</span>,
286
+ set in the muted colour at weight 400, as gemboss.ai writes it (0.10.0) -->
285
287
  </details>
286
288
  </nav>
287
289
  <div class="gw-footer__bottom">
288
290
  <span>© …</span><ul aria-label="Legal">…</ul>
289
291
  <button class="gw-theme-toggle gw-footer__toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme">svg</button> <!-- optional -->
292
+ <!-- or two icons (0.10.0), the one for the theme in force shown: <svg class="gw-theme-toggle__moon" …>…</svg><svg class="gw-theme-toggle__sun" …>…</svg> -->
290
293
  <ul aria-label="Social">…</ul>
291
294
  </div>
292
295
  <div class="gw-footer__wordmark" aria-hidden="true"><svg viewBox="…">…currentColor…</svg><span>Brand</span></div> <!-- optional -->
296
+ <!-- the mark is 7vw wide (40-96px), its height from its width and height attributes when it has them, else from
297
+ its viewBox: gemboss.ai's gem says width="86" height="100" -->
293
298
  </div>
294
299
  </footer>
295
300
  ```
296
301
 
297
302
  - **Folds** (`.gw-footer__fold`, a `<details>` whose `<summary>` holds the `.gw-footer__heading`): the CSS folds them
298
- below 768px (52px rows, the browser opens and closes them) and draws the open column from 768px, where the summary
303
+ up to 620px (52px rows, the browser opens and closes them) and draws the open column from 621px, where the summary
299
304
  is `display: contents` and the list shows even while the `<details>` is closed (`::details-content`). The script,
300
305
  for each fold with a summary:
301
- - from 768px (`matchMedia('(min-width: 768px)')`) sets `open`, puts `tabindex="-1"` on the summary and cancels a
306
+ - from 621px (`matchMedia('(min-width: 621px)')`) sets `open`, puts `tabindex="-1"` on the summary and cancels a
302
307
  click on it (`preventDefault`), so a heading never folds its column away, in a browser without
303
308
  `::details-content` too;
304
- - below 768px gives back the markup's state: `open` as the markup had it (folded unless it said `open`), the
309
+ - up to 620px gives back the markup's state: `open` as the markup had it (folded unless it said `open`), the
305
310
  summary's own `tabindex` (none, usually); again on every change of width;
306
311
  - `destroy`: the markup's state, its listener and the `fold` mark go. No event.
307
312
  - **Theme toggle**: a `[data-gw-theme-toggle]` in the bottom row is wired like the header's (above); with both on the
package/js/website.js CHANGED
@@ -1,9 +1,9 @@
1
- /*! @gemboss/website-ui 0.9.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
1
+ /*! @gemboss/website-ui 0.10.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
2
2
  (function () {
3
3
  'use strict';
4
4
  if (window.gwWebsite) return;
5
5
 
6
- var version = '0.9.0';
6
+ var version = '0.10.0';
7
7
  var api = { version: version, init: init, destroy: destroy };
8
8
  var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
9
9
  var still, lights;
@@ -428,7 +428,7 @@ function install() {
428
428
  fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
429
429
  var stored = null;
430
430
  wide = mq('(min-width: 1080px)');
431
- mid = mq('(min-width: 768px)');
431
+ mid = mq('(min-width: 621px)');
432
432
  on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
433
433
  on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
434
434
  on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
package/js/website.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  // the export line and the // comments, plus auto-init): edit this file, then run the build. Keep comments on lines of
7
7
  // their own, so the build can leave them out.
8
8
 
9
- var version = '0.9.0';
9
+ var version = '0.10.0';
10
10
  var api = { version: version, init: init, destroy: destroy };
11
11
  // Page state. Set up by the first init(), never at import: a server can import this file.
12
12
  var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
@@ -425,11 +425,11 @@ function tables(ctx) {
425
425
  });
426
426
  }
427
427
 
428
- // Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row). Below 768px it keeps
429
- // the markup's own state (folded unless the markup says open) and the browser opens and closes it. From 768px it is a
428
+ // Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row). Up to 620px it keeps
429
+ // the markup's own state (folded unless the markup says open) and the browser opens and closes it. From 621px it is a
430
430
  // plain column: the script sets `open`, takes the <summary> out of the Tab order (tabindex="-1") and ignores a click on
431
431
  // it, so the heading cannot fold the list away. Again on every change of width; destroy gives back the markup's state.
432
- // The CSS shows the list from 768px without the script too (::details-content), so this is for state and for browsers
432
+ // The CSS shows the list from 621px without the script too (::details-content), so this is for state and for browsers
433
433
  // without that.
434
434
  function folds(ctx) {
435
435
  each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
@@ -510,8 +510,8 @@ function install() {
510
510
  var stored = null;
511
511
  // 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
512
512
  wide = mq('(min-width: 1080px)');
513
- // 768px: the footer's columns stand open from here, and fold below it
514
- mid = mq('(min-width: 768px)');
513
+ // 621px: the footer's columns stand open from here, and fold up to 620px, as on gemboss.ai (1:1, 26/10/2026)
514
+ mid = mq('(min-width: 621px)');
515
515
  on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
516
516
  on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
517
517
  on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/website-ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/system.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "surface": "#ffffff",
9
9
  "ink": "#1b1c1e",
10
10
  "ink-2": "#3b3d42",
11
- "muted": "#686b71",
11
+ "muted": "#6a6d73",
12
12
  "line": "#e5e7ea",
13
13
  "line-strong": "#d5d8dd",
14
14
  "line-control": "#85888e",
@@ -89,14 +89,14 @@
89
89
  "type": {
90
90
  "family": "Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
91
91
  "mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
92
- "weights": [400, 500, 600, 650, 700],
92
+ "weights": [400, 500, 600, 650, 700, 800],
93
93
  "scale": [11, 12, 13, 14, 16, 18, 20, 22, 24, 30],
94
94
  "display_above": 30,
95
95
  "display": {
96
96
  "h1": "clamp(40px, 4.6vw, 68px)",
97
97
  "h2": "clamp(32px, 3.4vw, 52px)",
98
98
  "wordmark": "clamp(40px, 9vw, 128px)",
99
- "$comment": "wordmark (0.7.0): the footer's last line on gemboss.ai, the brand's name, faint, at the end of the page. Decoration only, never a heading.",
99
+ "$comment": "wordmark (0.7.0): the footer's last line on gemboss.ai, the brand's name, faint, at the end of the page. Decoration only, never a heading. Weight 800 (0.10.0, as gemboss.ai draws it): the one use of 800 in the kit.",
100
100
  "weight": 600,
101
101
  "tracking": "-0.03em"
102
102
  },