@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/AGENTS.md CHANGED
@@ -10,14 +10,17 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
10
10
  | file | what it is |
11
11
  |---|---|
12
12
  | `website.css` | the whole kit, one file (tokens, base, layout, components, sections) |
13
- | `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box; no dependencies; `window.gwWebsite.init(root)` |
13
+ | `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box, the footer's folds; no dependencies; `window.gwWebsite.init(root)` |
14
14
  | `js/website.mjs` | the same script as an ES module for apps: `import { init, destroy } from '@gemboss/website-ui/website.mjs'` |
15
15
  | `system.json` | every value of the system: colours, spacing, type, radii, shadows, widths |
16
16
  | `templates/page.html` | the page skeleton: start every new page from it |
17
- | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list… |
17
+ | `templates/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
18
+ | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
18
19
  | `demo/index.html` | every component on one page: open it to see what exists before you build anything |
19
20
  | `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
20
21
  | `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
22
+ | `demo/forms.html` | fields in every state, choices, the footer newsletter, a short form, stars, the testimonial wall and the rating badge |
23
+ | `demo/footer.html` | the full footer (Ask AI, columns that fold on phones, family rows, theme toggle, wordmark), and the same for a GemPages-style brand |
21
24
  | `docs/components.md` | every class, what it is for, its variants |
22
25
  | `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
23
26
 
@@ -56,6 +59,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
56
59
 
57
60
  1. Copy `templates/page.html` into the site. It links `website.css` and `website.js`, has the skip link, the
58
61
  header, the `<main>` and the footer. Fill the header and footer once (they are the same on every page).
62
+ **The footer comes from `templates/footer.html`.** A brand fills its rows (the columns, the family rows, the
63
+ words) and deletes the optional parts it does not use (Ask AI, newsletter, family rows, theme toggle, wordmark);
64
+ it never invents a footer part (a logo strip, badges, a map, a second bottom row). Something missing is a gap.
59
65
  2. Pick sections from `templates/sections/` in the order the page needs. A typical page: hero → proof (logos or
60
66
  stats) → 2 to 4 feature sections → one showcase or split → FAQ → final CTA. Not every page needs every section;
61
67
  fewer, clearer sections beat more. A page that is read rather than scanned (a blog post, docs, terms, privacy) is
@@ -74,7 +80,8 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
74
80
  - **Colour roles** (CSS variables, light and dark): `--gw-bg`, `--gw-bg-soft` (the neutral raised fill),
75
81
  `--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`, `--gw-line-strong`, `--gw-action`,
76
82
  `--gw-on-action`, `--gw-accent`, `--gw-accent-strong`, `--gw-accent-soft`, `--gw-success`, `--gw-warning` (and
77
- their tints `--gw-success-soft`, `--gw-warning-soft`), `--gw-night` (the dark band). Wash hues `--gw-wash-pink|violet|sky|mint` exist for the hero wash and illustrations only.
83
+ their tints `--gw-success-soft`, `--gw-warning-soft`), `--gw-danger` (an error, a destructive button; `--gw-danger-soft`,
84
+ `--gw-on-danger`), `--gw-line-control` (a form control's border, 3:1), `--gw-night` (the dark band). Wash hues `--gw-wash-pink|violet|sky|mint` exist for the hero wash and illustrations only.
78
85
  - **Dark mode** follows the visitor's system and the theme toggle (`data-theme` on `<html>`). Use the variables and
79
86
  it works; a hard-coded colour breaks it.
80
87
  - **Accent as text** (links, the active menu item, step numbers) is `--gw-accent-strong`; the plain accent is for
@@ -85,6 +92,17 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
85
92
  - **A button that waits** (a form sent, an audit started) gets `aria-busy="true"` from the page's script until the
86
93
  answer comes: the kit keeps its label and width, draws the ring and ignores a second press. Nothing else: no
87
94
  `disabled`, no loading class, no spinner of your own, no "Loading…" label.
95
+ - **Forms** (0.6.0): a field is `.gw-field` (its `<label>` above, the control, the error, the hint), the controls
96
+ `.gw-input`, `.gw-select`, `.gw-textarea`, `.gw-check`, `.gw-radio` (a group in `.gw-choices`), one field and its
97
+ button in a row `.gw-form-inline`, a short form `.gw-form`. Ask as little as you can: one field beats a form, and a
98
+ form keeps two or three fields (`templates/sections/form.html`). A field in error is `aria-invalid="true"` plus its
99
+ `.gw-field__error` in words; the page decides when, the kit draws it.
100
+ - **Footer** (0.7.0): `templates/footer.html`. Columns are `<details class="gw-footer__fold">` with the heading in the
101
+ `<summary>`: rows that fold on phones, open columns from 768px, no script needed. A link that leaves the site gets the
102
+ ↗ (`.gw-footer__ext`); one with `target="_blank"` also `<span class="gw-visually-hidden"> (opens in a new tab)</span>`.
103
+ The wordmark is `aria-hidden="true"`.
104
+ - **Ratings** (0.6.0): stars `.gw-stars` (`role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"`), the
105
+ wall `.gw-testimonials` of `.gw-card.gw-testimonial`, the badge `.gw-rating-badge` (`templates/sections/testimonials.html`).
88
106
  - **One choice out of two to four** (Monthly / Yearly) is `.gw-segmented`, a radio group; what each option shows
89
107
  carries `data-gw-show="group:value"`. Markup: `docs/components.md`, `templates/sections/pricing.html`.
90
108
  - **Type**: Inter. Headings weight 650, tracking −0.022em; display h1/h2 weight 600, tracking −0.03em. Body 18px,
@@ -119,11 +137,19 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
119
137
  - Don't write "Click here", "Learn more" as the only words of a link; say where it goes.
120
138
  - Don't use the em dash (—) in visible text. Use a comma, a colon or a full stop.
121
139
  - Don't invent numbers, customer names, logos, testimonials, ratings, awards or certifications.
140
+ - Don't show a rating or a testimonial without a link to the page it comes from, and don't reword a review, round a
141
+ score up or average one yourself (section 6).
142
+ - Don't build a form of many fields where one question does; don't use the placeholder as the label; don't tick a
143
+ consent box for the reader or word a refusal to shame them ("No thanks, I like losing sales").
144
+ - Don't tell an error by its colour alone: `aria-invalid="true"` and words in `.gw-field__error` that say how to fix it.
122
145
  - Don't write fake numbers on a mock-up without the caption "Illustrative data." under it, and don't reword it.
123
146
  - Don't style an article's headings, lists or code in site CSS; wrap the article in `.gw-prose`.
124
147
  - Don't make a callout whose kind only its colour tells: `.gw-callout` starts with its `.gw-callout__label` in words.
125
148
  - Don't type separators (›, /, >) between breadcrumbs, and don't build a contents list whose links point at ids the
126
149
  page does not have: build it from the article's own h2s.
150
+ - Don't add a footer part the template does not have, and don't build footer columns that fold with your own script:
151
+ fill `templates/footer.html` (its family rows take another brand's siblings: GemBoss, GemX, GemUpsells on a
152
+ GemPages site). Don't put the assistants' logos in the Ask AI row.
127
153
  - Don't animate layout properties (width, height, top, margin), and don't autoplay motion that cannot be stopped.
128
154
  - Don't ship a page you have not seen at 320px.
129
155
  - Don't write CSS that overrides a `.gw-*` class to change how it looks. If the kit's version is wrong for you,
@@ -144,6 +170,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
144
170
  (a `.gw-note` or a `role="status"` line). Under `prefers-reduced-motion` the kit's ring stands still.
145
171
  - A single choice is a radio group (`.gw-segmented`): one Tab stop, the arrows move and choose. Never a row of
146
172
  `aria-pressed` buttons for it.
173
+ - A form field has a visible `<label for>` above it; its error and hint sit under it and are named by
174
+ `aria-describedby` (the error first). An error is `aria-invalid="true"` on the control plus words that say how to
175
+ fix it ("Enter your work email, like name@example.com"); after a failed send, move the focus to the first field in
176
+ error. Mark the optional fields "(optional)". A group of checkboxes or radios is a `<fieldset class="gw-choices">`
177
+ with its question in the `<legend>`.
178
+ - Stars are `role="img"` with the value in words (`aria-label="4.9 out of 5"`); a number shown beside them is
179
+ `aria-hidden="true"`.
147
180
  - A wide table gets a scroll box a keyboard can reach: the `.gw-table` wrapper, or, for a bare table in `.gw-prose`,
148
181
  the box `website.js` adds. Give the table a `<caption>`: it names the box.
149
182
 
@@ -159,6 +192,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
159
192
  - Made-up numbers in a drawn mock-up: `.gw-window__caption` says exactly "Illustrative data." (vi: "Dữ liệu minh
160
193
  hoạ.", zh: "示意数据。"), plus one sentence only when a demo brand needs naming ("Vital Glow is a GemBoss demo
161
194
  brand."). Real numbers in a window: the caption names their source.
195
+ - **Ratings and testimonials**: every one links to the page it comes from (the store's review page, the review
196
+ itself), and says only what that page says: the score and the count as shown there on the day you copy them, the
197
+ reviewer's own words (cut with an ellipsis, never reworded), their name and store as they wrote them. No made-up
198
+ review, no quote you wrote for a customer, no score you averaged yourself, no rating without its link (`check`
199
+ warns: `rating-source`). Do not name internal tools as the source.
162
200
  - Image sources: record where each image comes from (the brand's own screen, a licensed photo). No photos of real
163
201
  people or other brands' logos without permission.
164
202
  - Other languages: rewrite each page for its reader (transcreation), do not translate sentence by sentence. Facts
@@ -223,7 +261,9 @@ uppercase labels, em dashes in visible text, images without `alt`, overrides of
223
261
  a waiting button not marked with `aria-busy`, a segmented control without its radio-group markup, a drawn mock-up
224
262
  with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
225
263
  words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
226
- the page, a callout without its label, and patterns that repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
264
+ the page, a callout without its label, stars without their value in words, a rating or a testimonial without a link to
265
+ its source, a footer fold without its heading in the summary or a wordmark a screen reader reads, and patterns that
266
+ repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
227
267
 
228
268
  ## 11. Upgrading the kit
229
269
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,122 @@ 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.7.0 (2026-10-25)
8
+
9
+ The footer as gemboss.ai has it today (gap #71): the kit's footer, built on 24/10 from gemboss.ai, had fallen behind
10
+ the site's. Every new part is optional, per site, and selects only its own markup: a page with the 0.6.0 footer looks
11
+ exactly as before. The demo index (without its new link), `brand-profiles.html`, `article.html`, `blog.html`,
12
+ `forms.html` and `theme-example.html` are pixel-identical to 0.6.0 at 390 and 1366px, light and dark.
13
+
14
+ Closes #71
15
+
16
+ - Ask AI `.gw-footer__ask` (+ `__ask-label`) in the brand block: "Ask AI about [Brand]" and four links (ChatGPT,
17
+ Claude, Perplexity, Google AI Mode) that open the assistant with one neutral question already typed, URL-encoded
18
+ after `?q=` (Google: `search?udm=50&q=`), as gemboss.ai builds them. Words, not the assistants' logos. 28px rows,
19
+ 44px on touch; the row may run wider than the brand's sentence (four names on one line at 390px).
20
+ - Columns that fold: each `.gw-footer__col` holds a `<details class="gw-footer__fold">` whose `<summary>` is the
21
+ column's `<h2 class="gw-footer__heading">`. Below 768px the columns are one track of 52px rows with a chevron,
22
+ folded, and the browser opens and closes them (no script needed); the open list's links are 44px. From 768px they
23
+ are the columns as before: the summary is only its heading (`display: contents`: no box, no Tab stop) and the list
24
+ shows even while the `<details>` is closed (`::details-content`, in a rule of its own so an older browser drops only
25
+ it). `website.js` sets `open` from 768px, takes the summary out of the Tab order there and ignores a click on it,
26
+ gives back the markup's state below 768px (again on every change of width), and is wired like every other part
27
+ (`data-gw-ready="fold"`, `init` once per element, `destroy` takes it back). From 1080px the brand's track is as
28
+ wide as gemboss.ai draws it beside five columns (193.5px at 1120px), whatever the number of columns, and the columns
29
+ share the rest: three to six fit. With the fold markup only; the 0.6.0 columns keep their 1.6fr.
30
+ Closes gap #40 (footer accordion on phones)
31
+ - Product family `.gw-footer__family`: a `<nav aria-label>` holding a fold whose heading is the row's label, and a
32
+ list of products, name and one line each (`<b>` and `<i>`, a space between). Repeat it for a second family ("More
33
+ from GemBoss", "From the same team, on the Shopify App Store"). Two items a line on phones (one more folded row),
34
+ four from 768px, and from 1080px the label in the brand's track and the items starting where the first column
35
+ starts. Items take the action radius (12px in `soft`) and the soft ground on hover.
36
+ - External links: `.gw-footer__ext`, a drawn ↗ after a link that leaves the site (aria-hidden). A link with
37
+ `target="_blank"` also carries `<span class="gw-visually-hidden"> (opens in a new tab)</span>`; one without it does
38
+ not. `.gw-visually-hidden` is new in `base.css`: words a screen reader hears and the page does not show.
39
+ - Theme toggle in the bottom row: `.gw-theme-toggle.gw-footer__toggle` with `data-gw-theme-toggle`, wired by
40
+ `website.js` like the header's (both relabel together). A 32px square with one hairline and the action radius, a
41
+ 40px hit area, 44px on touch; it pushes itself and the social links to the right. Below 768px the copyright and the
42
+ toggle share the first line, the legal and social links the second.
43
+ - Wordmark `.gw-footer__wordmark` (`aria-hidden="true"`): the brand's mark (an inline SVG in `currentColor`) and name,
44
+ ink at 10%, at `--gw-display-wordmark` (`clamp(40px, 9vw, 128px)`, new in `system.json` `type.display`), the
45
+ display tracking, weight 700, centred, 48px under the bottom row. A name too long for the line is cut, never a
46
+ sideways scroll. Not shown below 480px, as gemboss.ai hides it on phones.
47
+ - Spacing as measured on gemboss.ai and on the kit's scale: family rows 48px apart with 32px under their hairline,
48
+ 32px from the last family row to the bottom row (24px on phones), headings 12px above their links, 32px link rows.
49
+ - Templates: `templates/footer.html` is now the full footer, every optional part marked (Ask AI, the 0.6.0
50
+ newsletter, the family row, the toggle, the wordmark), five columns; `templates/page.html` uses the folding columns.
51
+ Demo: `demo/footer.html`, the gemboss.ai footer with gemboss.ai's words, and a switch to the same footer for a
52
+ GemPages-style brand (`data-gw-radius="soft"`, `data-gw-action="accent"`, the GemPages accent of
53
+ `docs/theming.md`, a family row listing GemBoss, GemX and GemUpsells, the newsletter instead of Ask AI), linked
54
+ from `demo/index.html`.
55
+ - `check`: `footer-markup` (an error): a `.gw-footer__fold` that is not a `<details>`, has no `<summary>`, or whose
56
+ summary does not hold the `.gw-footer__heading`; a `.gw-footer__wordmark` without `aria-hidden="true"`.
57
+ - `AGENTS.md`, the skill and the docs: the footer comes from `templates/footer.html`; a brand fills its rows and
58
+ never invents a footer part.
59
+ - Budget: `website.min.css` is 65.8 KB, 11.4 KB gzipped (0.6.0: 59.3 KB, 10.5 KB), most of it the three drawn icons'
60
+ masks. `js/website.js` is 14.5 KB unminified, 4.7 KB gzipped (0.6.0: 13.8 KB, 4.5 KB), under the test's 15 KB and
61
+ 5 KB.
62
+ - Tests: `test/run.mjs` holds the 0.6.0 rules of `footer.css` byte for byte and every 0.7.0 rule to a 0.7.0 part, and
63
+ covers the folds in its fake DOM and the new rule; `test/browser.mjs` drives `demo/footer.html` in Chromium (folded
64
+ at 390px, open at 1366px, Tab, Enter and Space, a click on a desktop heading, resizing both ways, without the
65
+ script, the wordmark out of the accessibility tree, no sideways scroll at 320 and 390px, the toggle, the
66
+ GemPages-style switch light and dark, and the column widths measured on gemboss.ai).
67
+
68
+ ## 0.6.0 (2026-10-25)
69
+
70
+ Forms and ratings: the fields a contact page, a sign-up or a footer newsletter needs, with their error state, and the
71
+ proof a product site on the Shopify App Store shows (stars, a wall of short reviews, the store's rating badge). Markup
72
+ that worked in 0.5.0 still works, and a page without the new parts looks exactly as before: the demo index (without its
73
+ new link), `brand-profiles.html`, `article.html`, `blog.html` and `theme-example.html` are pixel-identical to 0.5.0 at
74
+ 390 and 1366px, light and dark, with and without reduced motion.
75
+
76
+ - Fields `.gw-field` (`__label`, `__error`, `__hint`): the label above, the control, the error in words after an icon,
77
+ then the hint, 8px apart, tied with `for`/`id` and `aria-describedby` (the error first). Controls `.gw-input`,
78
+ `.gw-select` (a drawn caret that follows dark mode) and `.gw-textarea`: the dashboard's field family at the
79
+ website's sizes, as tall as a button (40px, 44px on touch), 16px words so a phone does not zoom, the action radius of
80
+ the site's profile, a 1px border that reaches 3:1. States by attribute: hover, focus (the accent ring and an ink
81
+ border), `readonly` (the soft ground), `disabled` (faded, its reason in the hint), `aria-invalid="true"` (the danger
82
+ border and tint, and the message in the danger colour with its icon: never the colour alone).
83
+ Closes gap #28 (form fields and the error state)
84
+ - Choices `.gw-choices` (a `<fieldset>` and its `<legend>`), `.gw-check` and `.gw-radio` (the `<label>` is the row and
85
+ the target: 32px, 44px on touch; chosen, the box takes the accent and the mark `--gw-on-accent`), and a filter's
86
+ count `.gw-check__count`, for the template gallery filters the gempages.net research found.
87
+ - One field and its button `.gw-form-inline` (one line from 480px, each a full line below it or in a narrow box), and
88
+ a short form's stack `.gw-form` (fields 24px apart, actions 32px below, 560px at most; on a card too).
89
+ - Footer newsletter: `templates/footer.html` has an optional `<form class="gw-footer__newsletter">` in the brand block
90
+ (label, the inline row with a secondary button, a note that says how often and how to stop and links to the privacy
91
+ policy). Delete it unless it is wired to a list. Existing footers are unchanged.
92
+ - `.gw-btn--danger` (the danger fill, `--gw-on-danger` words, hover and pressed one step towards the ink) for the action
93
+ of a page that destroys something, and `.gw-badge--danger` (a danger dot, ink words on the danger tint), as the gap
94
+ asked.
95
+ - Tokens: `--gw-danger` (`#b42318`, dark `#ff6b5a`: the dashboard's `--gemboss-danger` family; 5.92:1 or more on every
96
+ ground, light, dark and in night bands), `--gw-danger-soft` (`#fff0e9`, dark `#2e1714`), `--gw-on-danger` (`#ffffff`,
97
+ dark `#0d0d12`; 6.57:1 and 6.92:1 on the fill) and `--gw-line-control` (`#85888e`, dark `#6a6d78`): a form control's
98
+ border, 3.20:1 or more on `bg`, `bg-soft` and `surface` and in night bands, where `--gw-line-strong` (1.28:1) is a
99
+ hairline. A brand does not change them.
100
+ - Stars `.gw-stars`: a value from 0 to 5 in `style="--gw-stars: 4.9"`, drawn as five 16px ink stars with a partial
101
+ last one (a mask over a hard-stop gradient; system colours on a forced-colours screen), and heard as its name:
102
+ `role="img"`, `aria-label="4.9 out of 5"`.
103
+ - Testimonial wall `.gw-testimonials` (one, two from 768px, three from 1080px; no masonry, a row's cards share its
104
+ height) of `.gw-card.gw-testimonial` (`__quote`, `__by` with the name, the store and the link to the review,
105
+ `__avatar` optional), and the rating badge `.gw-rating-badge` (an `<a>` to the store's review page: `__icon`,
106
+ `__score` with the stars, `__count`).
107
+ Closes gap #56 (testimonial wall with star rating, App Store rating badge)
108
+ - Content rule (`AGENTS.md` sections 4 to 6, `docs/components.md`): every rating and testimonial links to the page it
109
+ comes from and copies only what it shows; no made-up review, no reworded quote, no score averaged by hand.
110
+ - `check`: `stars-markup` (an error: `.gw-stars` without `role="img"` or its name, a value outside 0 to 5, a name that
111
+ does not carry the drawn number) and `rating-source` (a warning: a `.gw-rating-badge` or a `.gw-testimonial` without
112
+ a link). `--gw-stars` set on the element is an input, not a redefined token.
113
+ - No script: the page owns validation (the kit ships no validator), so `website.js` only carries the new version.
114
+ - Templates: `templates/sections/form.html` (a short form beside its reason: two fields and a choice) and
115
+ `templates/sections/testimonials.html` (the badge and the wall). Demo: `demo/forms.html` (every field state, the
116
+ choices, the inline row, the danger button, stars from 0 to 5, a short form, the badge and the wall with sample data,
117
+ the footer newsletter), linked from `demo/index.html`.
118
+ - Budget: `website.min.css` is 59.3 KB, 10.5 KB gzipped (0.5.0: 50.5 KB, 9.1 KB). `js/website.js` is unchanged in size.
119
+ - Tests: `test/run.mjs` covers the tokens and their contrast, the new CSS and the two `check` rules; `test/browser.mjs`
120
+ drives `demo/forms.html` in Chromium (Tab through a form, a label click, the error in the accessibility tree,
121
+ 44px targets and the newsletter row at 390px, the stars' names, the soft and accent profiles).
122
+
7
123
  ## 0.5.0 (2026-10-25)
8
124
 
9
125
  The blog release: the parts around a long article and the blog index, so a blog (gempages.net's 1,290 posts first,
package/README.md CHANGED
@@ -12,11 +12,11 @@ it. Made so that a team working with Claude Code, even without a designer, ships
12
12
 
13
13
  | | |
14
14
  |---|---|
15
- | `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 68 KB, 51 KB bản min, 9 KB khi nén gzip): token sáng và tối, chữ, bố cục, nút (cả trạng thái nhấn và đang chờ), link, nhãn, chip, segmented control, badge, card (kèm ảnh trên đầu thẻ), khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), bộ khung bài blog (breadcrumb, dòng tác giả và ngày, mục lục dính, hộp lưu ý, danh sách bài, phân trang), header + mega menu + menu điện thoại, footer, 14 section, 3 hồ sơ thương hiệu |
16
- | `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại, đổi giao diện sáng/tối, hiện dần khi cuộn, đánh dấu trang hiện tại, segmented control, chặn bấm lần hai khi nút đang chờ, đánh dấu mục đang đọc trong mục lục bài viết, khung cuộn cho bảng trong bài; không phụ thuộc thư viện nào, khoảng 4,5 KB khi nén gzip. `website.js` là script thường (tự chạy khi trang tải xong), `website.mjs` là bản ES module cho app; cả hai có `init(root)` để gắn lại phần markup chèn sau (`docs/markup-contract.md`, mục "Script") |
15
+ | `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 92 KB, 66 KB bản min, 11,4 KB khi nén gzip): token sáng và tối, chữ, bố cục, nút (cả trạng thái nhấn, đang chờ và nút xoá), link, nhãn, chip, segmented control, badge, card (kèm ảnh trên đầu thẻ), khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), bộ khung bài blog (breadcrumb, dòng tác giả và ngày, mục lục dính, hộp lưu ý, danh sách bài, phân trang), ô nhập và trạng thái lỗi (ô chữ, danh sách chọn, ô nhiều dòng, checkbox, radio, hàng một ô một nút cho newsletter), sao đánh giá, tường review, huy hiệu điểm App Store, header + mega menu + menu điện thoại, footer như gemboss.ai (cột gập lại trên điện thoại, hàng Ask AI, hàng sản phẩm cùng nhà, nút sáng/tối, wordmark; ô newsletter tuỳ chọn), 16 section, 3 hồ sơ thương hiệu |
16
+ | `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại, đổi giao diện sáng/tối, hiện dần khi cuộn, đánh dấu trang hiện tại, segmented control, chặn bấm lần hai khi nút đang chờ, đánh dấu mục đang đọc trong mục lục bài viết, khung cuộn cho bảng trong bài, cột footer gập/mở theo khổ màn; không phụ thuộc thư viện nào, khoảng 4,7 KB khi nén gzip. `website.js` là script thường (tự chạy khi trang tải xong), `website.mjs` là bản ES module cho app; cả hai có `init(root)` để gắn lại phần markup chèn sau (`docs/markup-contract.md`, mục "Script") |
17
17
  | `AGENTS.md` | luật mà Claude Code (và người) phải theo: 10 luật, cách dựng trang, điều cấm, chữ, khả năng truy cập, đổi thương hiệu, khi kit thiếu |
18
- | `templates/` | khung một trang và 14 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose, article, post-list) |
19
- | `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn; `brand-profiles.html` là một thương hiệu kiểu GemPages bật cả 3 hồ sơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài |
18
+ | `templates/` | khung một trang, header, footer (`footer.html`: footer của gemboss.ai, mọi phần tuỳ chọn đều ghi rõ) và 16 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose, article, post-list, form, testimonials) |
19
+ | `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn; `brand-profiles.html` là một thương hiệu kiểu GemPages bật cả 3 hồ sơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài; `forms.html` là mọi trạng thái của ô nhập, newsletter ở footer, tường review và huy hiệu điểm (dữ liệu mẫu); `footer.html` là footer đầy đủ như gemboss.ai, kèm nút chuyển sang bản kiểu GemPages |
20
20
  | `docs/` | danh mục class (`components.md`), đổi thương hiệu (`theming.md`), triết lý (`philosophy.md`), hợp đồng markup |
21
21
  | `system.json` | mọi giá trị của hệ thống (màu, khoảng cách, chữ, bo góc, bóng, khung): nguồn duy nhất |
22
22
  | CLI `gemboss-website` | `init`, `check`, `gap`, `upgrade` |
@@ -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.5.0/website.min.css">
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.7.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.5.0/js/website.js" defer></script>
45
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.7.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
@@ -113,7 +113,7 @@ viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs
113
113
  | lệnh | làm gì |
114
114
  |---|---|
115
115
  | `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
116
- | `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tự tô màu, nhãn viết hoa giãn chữ, đè class `gw-`, đổi token của kit, hồ sơ thương hiệu sai tên hoặc sai chỗ, chữ trên nút màu nhấn dưới 4,5:1, dấu gạch dài, ảnh thiếu `alt`, link mơ hồ… và gợi ý chỗ kit có thể đang thiếu |
116
+ | `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tự tô màu, nhãn viết hoa giãn chữ, đè class `gw-`, đổi token của kit, hồ sơ thương hiệu sai tên hoặc sai chỗ, chữ trên nút màu nhấn dưới 4,5:1, dấu gạch dài, ảnh thiếu `alt`, link mơ hồ, sao thiếu giá trị bằng chữ, điểm hay review không có link nguồn, cột footer gập thiếu tiêu đề trong summary, wordmark không ẩn với trình đọc màn hình… và gợi ý chỗ kit có thể đang thiếu |
117
117
  | `npx gemboss-website gap` | báo một chỗ kit còn thiếu: mở GitHub Issue ở repo của kit (nếu `gh` đã đăng nhập và có quyền), không thì ghi `kit-gaps/<tên>.md` để gửi |
118
118
  | `npx gemboss-website upgrade` | so bản đang dùng với bản mới nhất, in thay đổi, gap đã có trong kit, và lệnh nâng cấp |
119
119
 
@@ -141,7 +141,7 @@ gemboss-www; mọi thay đổi đi qua PR, giá trị của hệ thống chỉ s
141
141
  cd packages/website-ui
142
142
  node scripts/build.mjs # sinh css/tokens.css từ system.json, ghép website.css và website.min.css, sinh js/website.js từ js/website.mjs
143
143
  node test/run.mjs # tự kiểm CLI, CSS và website.js (DOM giả)
144
- node test/browser.mjs # bài viết demo trong Chromium thật: cuộn, Tab, mục lục gập, bảng (cần .ci của repo)
144
+ node test/browser.mjs # demo trong Chromium thật: bài viết (cuộn, Tab, mục lục, bảng), form, sao, review và footer (gập, Tab, nút sáng/tối) (cần .ci của repo)
145
145
  node scripts/gate-demo.mjs # cổng thị giác và khoảng thở của gemboss.ai trên demo (cần .ci của repo)
146
146
  ```
147
147
 
package/bin/lib/check.mjs CHANGED
@@ -139,6 +139,16 @@ export const RULES = {
139
139
  why: "A screen reader announces the trail from its markup: a <nav> with a name around an <ol>, the last item marked aria-current=\"page\". The kit draws the separators; a › or / typed between the links is read out as a word.",
140
140
  fix: "Copy the markup from docs/components.md (Breadcrumbs) or templates/sections/article.html: <nav class=\"gw-breadcrumbs\" aria-label=\"Breadcrumb\"><ol><li><a href=\"/blog\">Blog</a></li><li><a href=\"…\" aria-current=\"page\">This page</a></li></ol></nav>.",
141
141
  },
142
+ "stars-markup": {
143
+ level: "error", title: "Stars without their value in words",
144
+ why: "The kit draws .gw-stars from style=\"--gw-stars: N\" (a number from 0 to 5), and a screen reader hears only its name: role=\"img\" and aria-label=\"N out of 5\". Without them the stars are empty or silent, and a label that names another number than the drawing says two things at once.",
145
+ fix: "Write <span class=\"gw-stars\" role=\"img\" aria-label=\"4.9 out of 5\" style=\"--gw-stars: 4.9\"></span>: the same number in the style and in the label, from 0 to 5 (docs/components.md, Ratings).",
146
+ },
147
+ "footer-markup": {
148
+ level: "error", title: "Footer fold or wordmark without its markup",
149
+ why: "A footer column folds on phones as a <details class=\"gw-footer__fold\"> whose <summary> holds the column's heading (<h2 class=\"gw-footer__heading\">): the summary is the row a reader taps, and a screen reader names the row by that heading. The wordmark (.gw-footer__wordmark) repeats the brand's name as decoration; without aria-hidden=\"true\" a screen reader reads the name a second time.",
150
+ fix: "Copy the markup from templates/footer.html: <details class=\"gw-footer__fold\"><summary><h2 class=\"gw-footer__heading\">Product</h2></summary><ul>…</ul></details>, and <div class=\"gw-footer__wordmark\" aria-hidden=\"true\">.",
151
+ },
142
152
  "uppercase-text": {
143
153
  level: "warning", title: "Uppercase text",
144
154
  why: "Headings, buttons, labels and menu items are sentence case.",
@@ -154,6 +164,11 @@ export const RULES = {
154
164
  why: "A note, a tip and a warning differ by their tint, which a colour-blind reader, a screen reader and a printed page do not get. The kind has to be in words.",
155
165
  fix: "Start the callout with <p class=\"gw-callout__label\">Note</p> (Tip, Warning, or the same word in the page's language).",
156
166
  },
167
+ "rating-source": {
168
+ level: "warning", title: "Rating or testimonial without a link to its source",
169
+ why: "A score, a review count or a quote is a claim. Linked to the page it comes from (the store's review page, the review itself), a reader can check it; without the link it reads as made up, and a made-up review makes every other claim on the page doubtful.",
170
+ fix: "Make the .gw-rating-badge itself the <a href> to the review page, and give every .gw-testimonial a link to its review (in the byline: <a href=\"…\">Shopify App Store</a>). If there is no source to link, do not show the rating or the quote.",
171
+ },
157
172
  "possible-gap": {
158
173
  level: "note", title: "Possible kit gap: a site class used on several pages",
159
174
  why: "A site class styled in site CSS and used on 3 or more pages is often a reusable part the kit does not have yet.",
@@ -305,6 +320,7 @@ export function subjectOf(sel) {
305
320
  const ATTR_THEME = String.raw`\[\s*data-theme(?:\s*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[\w-]+))?\s*\]`;
306
321
  const ROOTISH = new RegExp(`^(?::root|html)?(?:${ATTR_THEME}|:not\\(\\s*${ATTR_THEME}\\s*\\))*$`, "i");
307
322
  const isRootish = (sel) => splitTop(sel).every((s) => s && ROOTISH.test(s));
323
+ const INPUTS = new Set(["--gw-stars"]); // set on one element in the markup (style="--gw-stars: 4.9"), read by the kit's CSS
308
324
  const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?)$/; // docs/theming.md: the eight accent variables (-light/-dark) + wash
309
325
 
310
326
  const COLOR_PROPS = /^(?:color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left|block|inline)(?:-(?:start|end))?)?(?:-color)?|outline(?:-color)?|fill|stroke|text-decoration(?:-color)?|caret-color|accent-color|column-rule(?:-color)?|text-emphasis(?:-color)?|-webkit-text-fill-color|-webkit-text-stroke(?:-color)?|scrollbar-color|stop-color|flood-color|lighting-color)$/;
@@ -361,6 +377,8 @@ function checkRule(rule, ctx, add) {
361
377
  const where = { file: ctx.file, line: d.line, excerpt: excerptOf(sel, d) };
362
378
  if (d.prop.startsWith("--")) {
363
379
  if (d.prop.startsWith("--gw-")) {
380
+ // an input a component reads from its own markup (the stars' value), not a token: stars-markup checks it
381
+ if (INPUTS.has(d.prop)) continue;
364
382
  if (!(THEMABLE.test(d.prop) && rootish)) add("token-redefined", where);
365
383
  } else if (!ctx.svg && colorLiterals(value).length) add("color-literal", where);
366
384
  continue;
@@ -604,6 +622,35 @@ function analyzeHtml(file, text, out) {
604
622
  if (cls.includes("gw-callout") && !/\bclass\s*=\s*["'][^"']*\bgw-callout__label\b/i.test(inner())) {
605
623
  add("callout-label", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${decode(visible.slice(m.index, elementEnd(scanText, m.index, tag))).replace(/\s+/g, " ").trim().slice(0, 60)}`));
606
624
  }
625
+ if (cls.includes("gw-stars")) {
626
+ const v = (/(?:^|;)\s*--gw-stars\s*:\s*([^;]*)/.exec(decode(a.style || "")) || [])[1];
627
+ const n = v !== undefined && /^\s*\d+(?:\.\d+)?\s*$/.test(v) ? Number(v) : null;
628
+ const label = a["aria-label"] || "";
629
+ const said = [...label.replace(/(\d),(\d)/g, "$1.$2").matchAll(/\d+(?:\.\d+)?/g)].map((x) => Number(x[0]));
630
+ const missing = [
631
+ a.role !== "img" && 'role="img"',
632
+ !(label.trim() || a["aria-labelledby"]) && 'aria-label with the value in words ("4.9 out of 5")',
633
+ v === undefined ? 'style="--gw-stars: N"' : (n === null || n > 5) && `a value from 0 to 5 (--gw-stars: ${v.trim()})`,
634
+ n !== null && n <= 5 && label.trim() && !said.includes(n) && `the same number in aria-label as in --gw-stars (${n})`,
635
+ ].filter(Boolean);
636
+ if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
637
+ }
638
+ if (cls.includes("gw-footer__wordmark") && (a["aria-hidden"] || "").trim() !== "true") {
639
+ add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: aria-hidden="true" (the wordmark is decoration)`));
640
+ }
641
+ if (cls.includes("gw-footer__fold")) {
642
+ const sum = /<summary\b[^>]*>([\s\S]*?)<\/summary\s*>/i.exec(inner());
643
+ const missing = [
644
+ tag !== "details" && `a <details> (the class is on <${tag}>)`,
645
+ !sum && "a <summary> as its first child",
646
+ sum && !/\bclass\s*=\s*["'][^"']*\bgw-footer__heading\b/i.test(sum[1]) && "the column's heading inside the <summary> (<h2 class=\"gw-footer__heading\">)",
647
+ ].filter(Boolean);
648
+ if (missing.length) add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
649
+ }
650
+ if (cls.includes("gw-rating-badge") || cls.includes("gw-testimonial")) {
651
+ const linked = (tag === "a" && "href" in a) || /<a\b[^>]*\bhref\s*=/i.test(inner());
652
+ if (!linked) add("rating-source", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${decode(visible.slice(m.index, elementEnd(scanText, m.index, tag))).replace(/\s+/g, " ").trim().slice(0, 60)}`));
653
+ }
607
654
  if (cls.includes("gw-window")) {
608
655
  const end = elementEnd(scanText, m.index, tag);
609
656
  const body = end < 0 ? "" : scanText.slice(m.index, end);
package/css/base.css CHANGED
@@ -91,6 +91,18 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
91
91
  }
92
92
  .gw-skip:focus { left: 16px; }
93
93
 
94
+ /* words for a screen reader only (0.7.0): " (opens in a new tab)" in a link with target="_blank" */
95
+ .gw-visually-hidden {
96
+ position: absolute;
97
+ width: 1px;
98
+ height: 1px;
99
+ padding: 0;
100
+ overflow: hidden;
101
+ clip-path: inset(50%);
102
+ white-space: nowrap;
103
+ border: 0;
104
+ }
105
+
94
106
  /* scroll reveal: the script adds .is-in; the animation is a failsafe that shows it anyway after 2.5s */
95
107
  .gw-reveal {
96
108
  opacity: 0;
@@ -43,6 +43,11 @@
43
43
  .gw-btn--secondary:active { background: var(--gw-bg-soft); border-color: var(--gw-ink); }
44
44
  .gw-btn--ghost:active { background: var(--gw-line); }
45
45
  .gw-btn--on-night:active { background: var(--gw-on-night); opacity: 0.76; }
46
+ /* destructive (0.6.0, gap #28): delete, cancel a plan, erase my data. The danger fill with on-danger words (6.5:1 or
47
+ more, light and dark); hover and pressed one step stronger (towards the ink: darker in light, lighter in dark). It is
48
+ the action of a page that destroys something, never a second primary beside .gw-btn. */
49
+ .gw-btn--danger { background: var(--gw-danger); color: var(--gw-on-danger); }
50
+ .gw-btn--danger:hover, .gw-btn--danger:active { background: color-mix(in srgb, var(--gw-danger) 84%, var(--gw-ink)); }
46
51
  /* loading: aria-busy="true" on the <button>. The label stays (it keeps the width and the accessible name) but is not
47
52
  painted; a 16px ring in the button's own ink turns in its place, the dashboard's spinner. ::before, because
48
53
  .gw-btn--sm draws its hit area with ::after. website.js ignores presses on a busy button (no double submit). */
@@ -171,6 +176,9 @@
171
176
  .gw-badge--success { background: var(--gw-success-soft); color: var(--gw-ink); }
172
177
  .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); }
173
178
  .gw-badge--warning::before { background: var(--gw-warning); }
179
+ /* an error (0.6.0, gap #28): the danger dot, ink words on the danger tint, as success's */
180
+ .gw-badge--danger { background: var(--gw-danger-soft); color: var(--gw-ink); }
181
+ .gw-badge--danger::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-danger); }
174
182
 
175
183
  /* cards sit: white, 1px hairline, the card radius of the site's profile (12, 20 in soft), the dashboard's resting
176
184
  shadow (Chris 24/10/2026: one language with the app); no hover lift. A soft card is a tint and carries no shadow. */
package/css/footer.css CHANGED
@@ -14,6 +14,9 @@
14
14
  .gw-footer__brand { grid-column: 1 / -1; max-width: 320px; }
15
15
  .gw-footer__brand p { margin-top: 16px; color: var(--gw-muted); }
16
16
  .gw-footer__heading { margin: 0 0 8px; color: var(--gw-ink); font-size: 13px; font-weight: 600; letter-spacing: 0; }
17
+ /* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
18
+ reader agrees to; only when the form is wired to a list that keeps the address */
19
+ .gw-footer__newsletter { margin-top: 24px; }
17
20
  .gw-footer__col ul { margin: 0; padding: 0; list-style: none; }
18
21
  .gw-footer__col a { display: block; padding-block: 6px; color: var(--gw-muted); }
19
22
  .gw-footer__col a:hover { color: var(--gw-ink); }
@@ -51,3 +54,181 @@
51
54
  @media (max-width: 768px), (pointer: coarse) {
52
55
  .gw-footer__bottom a { min-height: var(--gw-tap-min); }
53
56
  }
57
+
58
+ /* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
59
+ Every part below is optional, per site, and styles only its own markup: a 0.6.0 footer looks exactly as before.
60
+ In page order: the Ask AI row in the brand block, columns that fold on phones (gap #40), product-family rows, the
61
+ theme toggle in the bottom row, and the wordmark at the very end. Values measured on gemboss.ai on 25/10/2026 and
62
+ put on the kit's scales; the full markup is templates/footer.html. */
63
+
64
+ /* Ask AI: a label and links that open an assistant with one neutral question already typed (a URL-encoded ?q=), so
65
+ the answer comes from what the web says, not from a claim the brand wrote. Words, not logos. 28px rows, 44px on
66
+ touch. */
67
+ .gw-footer__ask { margin-top: 24px; }
68
+ /* 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; }
72
+ .gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
73
+ .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; }
75
+
76
+ /* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
77
+ .gw-visually-hidden span, only when the link has target="_blank" */
78
+ .gw-footer__ext {
79
+ display: inline-block;
80
+ width: 12px;
81
+ height: 12px;
82
+ margin-left: 4px;
83
+ vertical-align: -1px;
84
+ background: var(--gw-muted);
85
+ -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
+ 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
+ }
88
+
89
+ /* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
90
+ .gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
91
+ `open`; the browser opens and closes it, no script needed. From 768px it is the column as before: the summary is
92
+ only the heading (display: contents: no box, no Tab stop), and the list shows even while the <details> is closed
93
+ (::details-content; website.js also sets `open` there, for browsers without it). */
94
+ .gw-footer__fold > summary { list-style: none; }
95
+ .gw-footer__fold > summary::-webkit-details-marker { display: none; }
96
+ .gw-footer__fold .gw-footer__heading { margin: 0; line-height: 20px; }
97
+ .gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
98
+ .gw-footer__col > .gw-footer__fold a { line-height: 20px; }
99
+ @media (min-width: 768px) {
100
+ .gw-footer__fold > summary { display: contents; }
101
+ .gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
102
+ }
103
+ @media (min-width: 768px) {
104
+ .gw-footer__fold::details-content { content-visibility: visible; }
105
+ }
106
+ @media (max-width: 767px) {
107
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
108
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
109
+ .gw-footer__col:has(> .gw-footer__fold),
110
+ .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
+ .gw-footer__fold > summary {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ gap: 16px;
117
+ min-height: var(--gw-tap-min);
118
+ padding-block: 16px;
119
+ cursor: pointer;
120
+ }
121
+ .gw-footer__fold > summary::after {
122
+ content: "";
123
+ 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);
130
+ }
131
+ .gw-footer__fold[open] > summary::after { transform: rotate(180deg); }
132
+ .gw-footer__fold > ul { padding-bottom: 12px; }
133
+ .gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
134
+ }
135
+ /* 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
+ 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) {
138
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
139
+ }
140
+
141
+ /* Product family: a labelled row of products, name and one line each (<b> and <i>), as many rows as the brand needs
142
+ ("More from GemBoss", "From the same team, on the Shopify App Store"). A <nav aria-label> holding a fold whose
143
+ heading is the label. Two items a row on phones, four from 768px; from 1080px the label sits in the brand's track and
144
+ the items start where the first column starts. */
145
+ .gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
146
+ .gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
147
+ .gw-footer__family a {
148
+ display: flex;
149
+ flex-direction: column;
150
+ justify-content: center;
151
+ gap: 2px;
152
+ min-height: var(--gw-tap-min);
153
+ padding: 8px 12px 8px 0;
154
+ border-radius: var(--gw-radius-action);
155
+ color: var(--gw-ink);
156
+ }
157
+ .gw-footer__family a:hover { background: var(--gw-bg-soft); }
158
+ .gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
159
+ .gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
160
+ @media (min-width: 768px) {
161
+ .gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
162
+ .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; }
164
+ .gw-footer__family a { min-height: 0; padding: 8px 12px; }
165
+ .gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
166
+ }
167
+ @media (min-width: 1080px) {
168
+ .gw-footer__family { display: flow-root; }
169
+ .gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
170
+ .gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
171
+ }
172
+
173
+ /* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
174
+ 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. */
176
+ .gw-footer__toggle {
177
+ position: relative;
178
+ width: 32px;
179
+ height: 32px;
180
+ margin-inline: auto 16px;
181
+ border: 1px solid var(--gw-line-strong);
182
+ background: var(--gw-surface);
183
+ }
184
+ .gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
185
+ .gw-footer__toggle svg { width: 18px; height: 18px; }
186
+ @media (max-width: 768px), (pointer: coarse) {
187
+ .gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
188
+ }
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); }
192
+ .gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
193
+ .gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
194
+ .gw-footer__family + .gw-footer__bottom,
195
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) + .gw-footer__bottom { margin-top: 24px; }
196
+ }
197
+
198
+ /* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
199
+ the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
200
+ 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). */
204
+ .gw-footer__wordmark {
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ gap: 24px;
209
+ padding-block: 48px 24px;
210
+ color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
211
+ font-size: var(--gw-display-wordmark);
212
+ font-weight: 700;
213
+ letter-spacing: var(--gw-display-tracking);
214
+ line-height: 1;
215
+ white-space: nowrap;
216
+ user-select: none;
217
+ }
218
+ .gw-footer__wordmark > svg,
219
+ .gw-footer__wordmark > img { flex: none; width: auto; height: 0.9em; }
220
+ .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; } }
223
+
224
+ @media (max-width: 768px), (pointer: coarse) {
225
+ .gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
226
+ }
227
+ @media (prefers-reduced-motion: reduce) {
228
+ .gw-footer__fold > summary::after { transition: none; }
229
+ }
230
+ /* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
231
+ @media (forced-colors: active) {
232
+ .gw-footer__ext,
233
+ .gw-footer__fold > summary::after { forced-color-adjust: none; background: CanvasText; }
234
+ }