@gemboss/website-ui 0.11.0 → 0.13.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
@@ -17,6 +17,7 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
17
17
  | `templates/header.html` | the header of every site, with its two options (the drill-down mobile menu, the language switcher): fill it once, in the site's shared partial |
18
18
  | `templates/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
19
19
  | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
20
+ | `templates/site/` | 0.13.0: what a new site owns from the start (the Next.js wrapper that serves its static pages, its page list, its public allow-list, `site.css`, `kit-gaps.css`): `npx gemboss-website init --site` writes it |
20
21
  | `demo/index.html` | every component on one page: open it to see what exists before you build anything |
21
22
  | `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
22
23
  | `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
@@ -60,6 +61,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
60
61
 
61
62
  ## 2. Build a page
62
63
 
64
+ **A new site** (0.13.0, the way gemboss.ai and gemians.com are built): `npx gemboss-website init --site` in an empty
65
+ folder writes the header and footer partials, `index.html` and `404.html`, `assets/site.css`, `assets/kit-gaps.css` and
66
+ the Next.js wrapper that serves the static pages, and adds the scripts to `package.json`. Then each page is a file with
67
+ `<!-- gw:header -->` and `<!-- gw:footer -->` markers, and `npx gemboss-website build` writes the partials into every
68
+ page (with `aria-current="page"` on its own links) and copies the kit into `assets/kit/`. Never hand-copy a header into
69
+ a page. A page of such a site starts from `index.html`, not from `templates/page.html`.
70
+
63
71
  1. Copy `templates/page.html` into the site. It links `website.css` and `website.js`, has the skip link, the
64
72
  header, the `<main>` and the footer. Fill the header and footer once (they are the same on every page).
65
73
  **The footer comes from `templates/footer.html`.** A brand fills its rows (the columns, the family rows, the
@@ -225,7 +233,9 @@ A brand other than GemBoss chooses **only** these, and nothing else:
225
233
  Do not set `--gw-accent` itself: the kit points it at those per theme, and a direct override loses as soon as the
226
234
  theme toggle is pressed. Accent text uses `--gw-accent-strong`, so it must reach 4.5:1 on `--gw-bg` and
227
235
  `--gw-bg-soft`;
228
- - **the wash hues**, **the logo** in the header and footer, and **the favicon**;
236
+ - **the wash hues**, **the logo** in the header and footer (a mark beside the name, or one drawing of both:
237
+ `.gw-brand__logo`, with a second file for dark grounds when the brand has one), and **the favicon**;
238
+ - **the announcement bar's and the Stage's colours** (`--gw-announce-*`, `--gw-stage-*`), only when it shows them;
229
239
  - **three named profiles**, once for the site, never free values:
230
240
  - action colour: `data-gw-action="accent"` on `<html>` fills the primary button with the accent (default `ink`).
231
241
  Its words (`--gw-on-accent`) must reach 4.5:1 on `--gw-accent` and `--gw-accent-strong`, light and dark, or
@@ -242,7 +252,9 @@ examples: `demo/theme-example.html` (accent only) and `demo/brand-profiles.html`
242
252
  - Link the kit, then the site's own CSS after it: `website.css` → `site.css`.
243
253
  - The site's own CSS is for content the kit cannot know (an illustration, a product mock-up) and uses only the
244
254
  tokens. Its classes start with the site's prefix (for example `gp-` on GemPages), never `gw-`.
245
- - Shared header and footer markup lives in one place in the site (a partial or component), not copied per page.
255
+ - Shared header and footer markup lives in one place in the site (a partial or component), not copied per page:
256
+ `partials/header.html` and `partials/footer.html`, written into the pages by `npx gemboss-website build` (CI runs
257
+ `build --check`, which fails when a page is out of date).
246
258
 
247
259
  ## 9. When the kit is missing something
248
260
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,83 @@ 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.13.0 (2026-10-26)
8
+
9
+ What building a second site on the kit taught it. gemians.com was rebuilt on 0.12.0 from npm, kit sections only (Chris,
10
+ 26/10/2026: inherit from the GemBoss website kit, fix the kit where it gets in the way, then update Gemians and
11
+ GemBoss), and every place the kit fell short became one of these. A minor version: nothing a 0.12.0 site uses changes
12
+ its look, except a list right under a section's head (below), which now gets the head's spacing it was always meant to.
13
+
14
+ - **A new site in one command**: `npx gemboss-website init --site` writes what gemboss.ai and gemians.com both had to
15
+ write by hand: the header and footer partials (from the kit's templates), `index.html` and `404.html` with the
16
+ build's markers, `assets/site.css` (the brand's `:root` block, commented) and `assets/kit-gaps.css`, and the Next.js
17
+ wrapper that serves the static pages byte for byte (`app/[[...path]]/route.js`, `lib/pages.mjs`, `next.config.mjs`
18
+ with clean URLs, `stage-public.mjs` with its allow-list, `vercel.json`, `.gitignore`), from the new
19
+ `templates/site/`. It never overwrites a file and adds the scripts a site needs to `package.json`.
20
+ - **`npx gemboss-website build`** (new): copies the kit into `assets/kit/` as published, writes `partials/header.html`
21
+ and `partials/footer.html` into every page between `<!-- gw:header -->` and `<!-- gw:footer -->` (their leading
22
+ comments, the templates' notes, stay out), puts `aria-current="page"` on the header's links to that page, and the
23
+ kit's version on its two links (`?v=0.13.0`). `--check` writes nothing and fails when a page is out of date (CI).
24
+ One build for every site, so a fix reaches all of them with the kit, instead of a script per site that drifts.
25
+ - **A logo drawn as one file** (`.gw-brand__logo`, new): the mark and the wordmark together, 26px tall, in place of
26
+ `__mark` and `__name`, its `alt` the brand's name. A logo made for dark grounds (Gemians' white wordmark) comes with a
27
+ second file, `.gw-brand__logo--dark`, shown in dark only (the system's setting or the toggle). `check`:
28
+ `brand-markup`.
29
+ - **A photo beside words** (`.gw-figure`, new): a `<figure>` with its picture at the card radius and a 14px muted
30
+ caption 12px under it, in a split or under a hero's actions (48px below, as a window). Until now only the prose had
31
+ one; a split could hold a product window but not a photo of the team.
32
+ - **A list under a section's head gets the head's spacing**: `.gw-steps`, `.gw-stats`, `.gw-post-list` and
33
+ `.gw-testimonials` no longer reset their own margin, which beat 0.12.0's "what follows a head starts 40px below it
34
+ (48px from 768px)", so the steps sat right under the heading. The base still zeroes a list's margins, at no
35
+ specificity.
36
+ - **A brand's announcement bar and Stage**: `--gw-announce-*` and `--gw-stage-*` may be set in the brand's `:root` block
37
+ (`system.json` always said "a brand sets its own bar here", and `check` called it `token-redefined`). `check` computes
38
+ the bar's words and link on its ground and the Stage's words on its gradient (`brand-contrast`, 4.5:1).
39
+ - Docs: `docs/theming.md` asks 4.5:1 only of the accent's text form (`--gw-accent-strong-*`); the plain accent is never
40
+ text in the kit (icons, marks, the focus ring), so 3:1 is its bar (WCAG 1.4.11), and a gold brand keeps its gold. A
41
+ brand mark is 34×34 (the docs and `templates/page.html` still said 26). A contact page's form is its first screen
42
+ with the page's one `<h1>` (`templates/sections/form.html`). The demo's announcement pill says "New", in sentence case.
43
+
44
+ ## 0.12.0 (2026-10-26)
45
+
46
+ The grammar of every page, 1:1 with gemboss.ai: the announcement bar, the skip link, the button, the type (headings,
47
+ lead, label, the dim first sentence), a section, its head, the wrap, and the scroll reveal. After the footer (0.10.0)
48
+ and the header (0.11.0), this is the first step of the pages' own bodies onto the kit (Chris, 26/10/2026: every value in
49
+ the kit, so that GemPages, Gemians and GemX inherit them). gemboss.ai's 83 pages now draw these parts with the kit's
50
+ classes, and were measured against the pages before, in Chromium: the computed style of every one of these parts (2,300
51
+ of them) at 320, 390, 768, 1080, 1366 and 1920px, light and dark, and every page pixel by pixel at 390 and 1366px; the
52
+ reveal's delays and counts, a hovered button, the focused skip link.
53
+
54
+ A minor version: a site on 0.11.0 sees these values change (below), none of the markup.
55
+
56
+ - The announcement bar (`.gw-announce`): gemboss.ai's, on its own ground in both themes, from the new tokens
57
+ `--gw-announce-*` (`system.json` `color.announce`: ground, words, hairline, the pill, the link and its underline). A
58
+ small pill may lead the sentence (`.gw-announce__tag`, new); the link's underline is a hairline that brightens on
59
+ hover. 12px words in a slimmer bar up to 520px.
60
+ - The skip link (`.gw-skip`): an accent tab out of the top left corner (was an ink button 8px in).
61
+ - The button (`.gw-btn`): on hover the filled one rises 1px onto the rest shadow (was a darker fill); a framed or
62
+ frameless one does not move. Its face is the new `--gw-font-action` (`type.button.family`, the brand face alone, as
63
+ gemboss.ai names it: a glyph Inter's web subset lacks, the arrow, is then the system's own, the width gemboss.ai's
64
+ buttons have). Its line height is its surroundings'. A label wider than needed stays at the start (centred only in
65
+ `--block`, a phone's row of actions in a hero or CTA, an inline form). An arrow after the label is
66
+ `.gw-btn__arrow` (new): 3px further on hover. Up to 768px every button is at least 44px tall, by the width only (it was
67
+ also by a coarse pointer); a label wraps only up to 480px and only in `<main>`, at 1.35 lines (was up to 768px,
68
+ balanced).
69
+ - Type: h1 at line height 1.1 and h2 at 1.06 (new tokens `--gw-display-h1-leading`, `--gw-display-h2-leading`; at 1.02
70
+ the descenders of a first line touched the capitals of the second). h3 and `.gw-h3` 20px / 600, tracking −0.02em,
71
+ 1.2 lines, as every h3 on gemboss.ai (was 24px / 650). The lead at most 58ch (was 60ch). The label's square is ink
72
+ with 2px corners (was the accent, square) and its line height its surroundings'; the input `--gw-label-mark` (new, on
73
+ the label or anything around it) paints the square another colour, a gradient too.
74
+ - A section's head (`.gw-section__head`) up to 760px (was 660px). A lead under a heading 20px below it (in a head, a
75
+ split column, a CTA; 16px in the hero). What follows a head starts 40px below it, 48px from 768px, as a top margin
76
+ of the next thing at no specificity, so a block that brings its own keeps it (was the head's bottom margin).
77
+ - The scroll reveal: 20px (new token `--gw-reveal-distance`, was 24px), once 12% of it is in view with the bottom 6% of
78
+ the view not counting (was 15%); each `.gw-reveal` after the first among its siblings waits 70ms more, at most 380ms;
79
+ a `[data-gw-count]` number inside counts up from 0 over 1.1s (new); the script's failsafe at 1.6s (was 2.5s). The
80
+ values are `system.json` `motion.reveal`.
81
+ - `check`: `--gw-label-mark` is an input, like a mega panel's widths.
82
+ - `website.js` is 10.4 KB gzipped (the budget is 10.5 KB, was 10 KB: the reveal's stagger and count-up).
83
+
7
84
  ## 0.11.0 (2026-10-26)
8
85
 
9
86
  The header, 1:1 with gemboss.ai. Since the footer (0.10.0), the header was the last part of gemboss.ai not drawn by
package/README.md CHANGED
@@ -19,7 +19,8 @@ it. Made so that a team working with Claude Code, even without a designer, ships
19
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; `header.html` là header của gemboss.ai trên điện thoại (menu đi sâu từng mục, thanh nút dính đáy, nút đổi ngôn ngữ với ba ngôn ngữ), cũng có bản kiểu GemPages; `spotlight.html` là đốm sáng trên thẻ gói giá, thẻ link, thẻ nổi bật của mega menu và nút trong dải tối, cũng có 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
- | CLI `gemboss-website` | `init`, `check`, `gap`, `upgrade` |
22
+ | CLI `gemboss-website` | `init` (và `init --site`), `build`, `check`, `gap`, `upgrade` |
23
+ | `templates/site/` | 0.13.0: bộ khung một site mới (Next.js phục vụ trang HTML tĩnh, danh sách trang, danh sách tệp công khai, `site.css`, `kit-gaps.css`), `init --site` chép ra |
23
24
 
24
25
  Cần Node 18 trở lên cho CLI. CSS và JS chạy trên mọi trình duyệt hiện đại, không cần bước build.
25
26
 
@@ -40,9 +41,9 @@ nó đọc `AGENTS.md` của kit trước, dựng từ mẫu, và chạy `check`
40
41
  **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
42
 
42
43
  ```html
43
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.11.0/website.min.css">
44
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.13.0/website.min.css">
44
45
  <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.11.0/js/website.js" defer></script>
46
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.13.0/js/website.js" defer></script>
46
47
  ```
47
48
 
48
49
  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,6 +114,8 @@ viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs
113
114
  | lệnh | làm gì |
114
115
  |---|---|
115
116
  | `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
117
+ | `npx gemboss-website init --site` | 0.13.0: dựng một site mới như gemboss.ai và gemians.com: partial header và footer, `index.html` và `404.html` có dấu `<!-- gw:header -->` / `<!-- gw:footer -->`, `site.css`, `kit-gaps.css`, phần Next.js phục vụ trang tĩnh; không ghi đè tệp có sẵn, thêm script còn thiếu vào `package.json` |
118
+ | `npx gemboss-website build` | 0.13.0: chép kit vào `assets/kit/`, ghi partial header và footer vào mọi trang (đánh dấu `aria-current="page"` cho link của chính trang đó); `--check` cho CI, báo đỏ khi trang chưa cập nhật |
116
119
  | `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, mục của menu đi sâu thiếu nút Back hay tiêu đề, nút ngôn ngữ không ẩn sẵn hay bản dịch thiếu `hreflang`… và gợi ý chỗ kit có thể đang thiếu |
117
120
  | `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
121
  | `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 |
@@ -8,10 +8,21 @@ export const USAGE = `gemboss-website (${PKG.name} ${PKG.version})
8
8
  Usage: npx gemboss-website <command> [options] (run it in the site's root folder)
9
9
 
10
10
  Commands
11
- init [--force]
11
+ init [--force] [--site]
12
12
  Add or refresh the kit's block in CLAUDE.md (between <!-- ${PKG.name}:start/end --> markers;
13
13
  text outside them is never touched) and install the Claude Code skill
14
14
  .claude/skills/gemboss-website-ui/SKILL.md (replaced only if init wrote it, or with --force).
15
+ --site also start a whole site, as gemboss.ai and gemians.com are built: the header and
16
+ footer partials, index.html and 404.html with their markers, assets/site.css and
17
+ kit-gaps.css, and the Next.js wrapper that serves the static pages (app/, lib/pages.mjs,
18
+ next.config.mjs, stage-public.mjs, vercel.json, .gitignore). Never overwrites a file;
19
+ adds the missing scripts to package.json.
20
+
21
+ build [--check]
22
+ Copy the kit into assets/kit/ and write partials/header.html and partials/footer.html into every
23
+ page between its <!-- gw:header --> / <!-- gw:footer --> markers, with aria-current="page" on
24
+ the header's links to that page. Pages: lib/pages.mjs pages() and 404.html (else every .html in
25
+ the root). --check writes nothing and exits 1 when a file is out of date (for CI).
15
26
 
16
27
  check [paths...] [--json] [--fix-list] [--ignore <path>]... [--include-kit]
17
28
  Check every .html/.htm/.css (and <style>, style="") under the paths (default: here) against
@@ -59,6 +70,7 @@ async function main(argv) {
59
70
  console.log(PKG.version);
60
71
  return 0;
61
72
  case "init": return (await import("./lib/init.mjs")).cmdInit(rest);
73
+ case "build": return (await import("./lib/build.mjs")).cmdBuild(rest);
62
74
  case "check": return (await import("./lib/check.mjs")).cmdCheck(rest);
63
75
  case "gap": return (await import("./lib/gap.mjs")).cmdGap(rest);
64
76
  case "upgrade": return (await import("./lib/upgrade.mjs")).cmdUpgrade(rest);
@@ -0,0 +1,101 @@
1
+ // `gemboss-website build [--check]`: the parts every page of a static site shares, written into each page in place, so
2
+ // the HTML on disk is the HTML served (0.13.0, from gemboss-www and gemians-www):
3
+ // 1. the kit itself: website.min.css and js/website.js → assets/kit/website.css and assets/kit/website.js, as published;
4
+ // 2. partials/header.html and partials/footer.html → between <!-- gw:header --> … <!-- /gw:header --> (and gw:footer)
5
+ // in every page, with aria-current="page" on the header's links to that page;
6
+ // 3. the kit's version on its two links (?v=0.13.0), so a new kit is never served from an old cache.
7
+ // The pages are the site's lib/pages.mjs (`pages()`: Map of URL → file, the same list its Next route serves) plus
8
+ // 404.html; without that file, every .html in the site's root folder.
9
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import { PKG, PKG_DIR, UsageError, c, parseArgs } from "./util.mjs";
13
+
14
+ export const KIT_DIR = "assets/kit";
15
+ const CURRENT = /\b(?:gw-nav__link|gw-nav__trigger|gw-mobile-menu__top|gw-mobile-menu__link|gw-mega__link)\b/;
16
+
17
+ /** file → clean URL, as Vercel's cleanUrls and the site's Next route: index.html → /, a/index.html → /a, a.html → /a. */
18
+ export function routeOf(file) {
19
+ const stem = file.replace(/\.html$/, "");
20
+ if (stem === "index") return "/";
21
+ return "/" + stem.replace(/\/index$/, "");
22
+ }
23
+
24
+ /** The header for one page: aria-current="page" on each of its links (nav, mobile menu, mega rows) to that URL. */
25
+ export function markCurrent(html, route) {
26
+ return html.replace(/<a\b([^>]*)>/g, (tag, attrs) => {
27
+ const cls = (/\bclass\s*=\s*"([^"]*)"/.exec(attrs) || [])[1] || "";
28
+ const href = (/\bhref\s*=\s*"([^"]*)"/.exec(attrs) || [])[1];
29
+ if (!CURRENT.test(cls) || href !== route || /\baria-current\s*=/.test(attrs)) return tag;
30
+ return `<a${attrs} aria-current="page">`;
31
+ });
32
+ }
33
+
34
+ /** Put `body` between the page's <!-- gw:name --> markers; null when the page has none. */
35
+ export function fillRegion(html, name, body) {
36
+ const re = new RegExp(`(<!-- gw:${name} -->)[\\s\\S]*?(<!-- /gw:${name} -->)`);
37
+ return re.test(html) ? html.replace(re, (_, a, b) => `${a}\n${body}\n${b}`) : null;
38
+ }
39
+
40
+ async function sitePages(root) {
41
+ const lib = join(root, "lib", "pages.mjs");
42
+ let files;
43
+ if (existsSync(lib)) {
44
+ const mod = await import(pathToFileURL(lib).href);
45
+ if (typeof mod.pages !== "function") throw new UsageError("lib/pages.mjs must export pages() (a Map of URL → file)");
46
+ files = [...mod.pages().entries()].map(([route, file]) => ({ route, file }));
47
+ } else {
48
+ files = readdirSync(root).filter((f) => f.endsWith(".html") && f !== "404.html").map((file) => ({ route: routeOf(file), file }));
49
+ }
50
+ if (existsSync(join(root, "404.html"))) files.push({ route: "/404", file: "404.html" });
51
+ return files;
52
+ }
53
+
54
+ export async function cmdBuild(argv) {
55
+ const opts = parseArgs(argv, { check: "bool" });
56
+ if (opts._.length) throw new UsageError(`build takes no arguments (got ${opts._.join(" ")}); run it in the site's root folder`);
57
+ const root = process.cwd();
58
+ const stale = [], wrote = [];
59
+ const put = (file, text) => {
60
+ const abs = resolve(root, file);
61
+ if (existsSync(abs) && readFileSync(abs, "utf8") === text) return;
62
+ if (opts.check) { stale.push(file); return; }
63
+ mkdirSync(dirname(abs), { recursive: true });
64
+ writeFileSync(abs, text);
65
+ wrote.push(file);
66
+ };
67
+
68
+ put(`${KIT_DIR}/website.css`, `/* ${PKG.name} ${PKG.version}, copied by gemboss-website build: do not edit */\n` + readFileSync(join(PKG_DIR, "website.min.css"), "utf8"));
69
+ put(`${KIT_DIR}/website.js`, readFileSync(join(PKG_DIR, "js", "website.js"), "utf8"));
70
+
71
+ // a partial's leading comments are notes for its author (the kit templates' instructions), not for every page
72
+ const partial = (name) => { const p = join(root, "partials", `${name}.html`); return existsSync(p) ? readFileSync(p, "utf8").replace(/^(?:\s*<!--[\s\S]*?-->)+/, "").trim() : null; };
73
+ const header = partial("header"), footer = partial("footer");
74
+ const pages = await sitePages(root);
75
+ const problems = [];
76
+ for (const { route, file } of pages) {
77
+ const abs = join(root, file);
78
+ if (!existsSync(abs)) { problems.push(`${file}: listed by lib/pages.mjs, not found`); continue; }
79
+ let html = readFileSync(abs, "utf8");
80
+ for (const [name, body] of [["header", header && markCurrent(header, route)], ["footer", footer]]) {
81
+ if (!html.includes(`<!-- gw:${name} -->`)) continue;
82
+ if (body === null) { problems.push(`${file}: has <!-- gw:${name} --> but partials/${name}.html is missing`); continue; }
83
+ const next = fillRegion(html, name, body);
84
+ if (next === null) problems.push(`${file}: <!-- gw:${name} --> without its closing <!-- /gw:${name} -->`);
85
+ else html = next;
86
+ }
87
+ html = html.replace(/((?:href|src)\s*=\s*"\/assets\/kit\/website\.(?:css|js))(?:\?v=[^"]*)?"/g, `$1?v=${PKG.version}"`);
88
+ put(file, html);
89
+ }
90
+
91
+ for (const p of problems) console.error(` ${c.red(p)}`);
92
+ if (opts.check) {
93
+ for (const f of stale) console.error(` out of date: ${f}`);
94
+ const bad = stale.length + problems.length;
95
+ console.log(bad ? c.red(`build: RED, ${stale.length} file(s) out of date, ${problems.length} problem(s). Run: npx gemboss-website build`) : c.green(`build: green, ${pages.length} pages, kit ${PKG.version}`));
96
+ return bad ? 1 : 0;
97
+ }
98
+ for (const f of wrote) console.log(` wrote ${f}`);
99
+ console.log(`build: ${pages.length} pages, kit ${PKG.version}${wrote.length ? "" : ", nothing to write"}`);
100
+ return problems.length ? 1 : 0;
101
+ }
package/bin/lib/check.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { basename, resolve } from "node:path";
5
5
  import { PKG, SYSTEM, UsageError, blank, c, isKitPath, lineIndex, parseArgs, rel, walkFiles } from "./util.mjs";
6
- import { MIN_CONTRAST, actionPairs, ratio } from "./contrast.mjs";
6
+ import { MIN_CONTRAST, actionPairs, contrast, ratio } from "./contrast.mjs";
7
7
 
8
8
  const RADII = new Set(SYSTEM.radius.scale);
9
9
  // A brand's named profiles (system.json `profiles`): one attribute on <html> each, and the floating header's class.
@@ -31,7 +31,7 @@ export const RULES = {
31
31
  },
32
32
  "token-redefined": {
33
33
  level: "error", title: "Kit token redefined",
34
- why: "A brand chooses its accent family and wash hues (variables), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
34
+ why: "A brand chooses its accent family and wash hues (variables), the colours of its announcement bar and of its Stage (--gw-announce-*, --gw-stage-*), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
35
35
  fix: `Remove it. The action colour and the corners are profiles, not tokens: ${profileList()} on <html>; the floating header is the class ${FLOAT_CLASS} (docs/theming.md). If the brand needs something else, report a gap: npx gemboss-website gap --kind token.`,
36
36
  },
37
37
  "radius-off-scale": {
@@ -129,6 +129,16 @@ export const RULES = {
129
129
  why: `With data-gw-action="accent" the primary button is filled with the brand's accent. Its words (--gw-on-accent) must reach ${MIN_CONTRAST}:1 on --gw-accent and on --gw-accent-strong (hover and pressed), in light and in dark, or the main action of every page is hard to read.`,
130
130
  fix: "Darken --gw-accent-light and --gw-accent-strong-light (or change --gw-on-accent-light), lighten the -dark ones (dark words on them), until every pair passes; write them as #rrggbb. Or keep the ink button: remove data-gw-action.",
131
131
  },
132
+ "brand-contrast": {
133
+ level: "error", title: "Words on the brand's announcement bar or Stage below 4.5:1",
134
+ why: `A brand that sets its own announcement bar (--gw-announce-*) or Stage (--gw-stage-*) picks colours the kit cannot check for it: the bar's words and its link on its ground, and the Stage's words on both ends of its gradient, must reach ${MIN_CONTRAST}:1, or the sentence over every page and the mega menu's pictures are hard to read.`,
135
+ fix: "Darken the ground (--gw-announce-bg, --gw-stage-top, --gw-stage-bottom) or lighten the words (--gw-announce-ink, --gw-announce-link, --gw-stage-ink) until every pair passes; write them as #rrggbb.",
136
+ },
137
+ "brand-markup": {
138
+ level: "error", title: "Brand logo without its markup",
139
+ why: "A logo drawn as one file (.gw-brand__logo, the mark and the wordmark together) stands in for the brand's name: its alt is that name, or the home link has no name for a screen reader. Its width and height keep the bar from jumping while it loads. A file for dark grounds (.gw-brand__logo--dark) is the same logo in another colour and needs the first one right before it.",
140
+ fix: "Write <a class=\"gw-brand\" href=\"/\"><img class=\"gw-brand__logo\" src=\"/logo.png\" alt=\"Brand\" width=\"150\" height=\"26\"><img class=\"gw-brand__logo gw-brand__logo--dark\" src=\"/logo-on-dark.png\" alt=\"Brand\" width=\"150\" height=\"26\"></a> (docs/theming.md, the logo).",
141
+ },
132
142
  "toc-target": {
133
143
  level: "error", title: "Contents link to a heading that is not on the page",
134
144
  why: "A .gw-toc link whose #id names no element on the page jumps nowhere, and website.js cannot mark its section. It happens when a heading is renamed or removed and the list is not.",
@@ -336,9 +346,12 @@ const ATTR_THEME = String.raw`\[\s*data-theme(?:\s*[~|^$*]?=\s*(?:"[^"]*"|'[^']*
336
346
  const ROOTISH = new RegExp(`^(?::root|html)?(?:${ATTR_THEME}|:not\\(\\s*${ATTR_THEME}\\s*\\))*$`, "i");
337
347
  const isRootish = (sel) => splitTop(sel).every((s) => s && ROOTISH.test(s));
338
348
  // set on one element in the markup, read by the kit's CSS: the stars' value (style="--gw-stars: 4.9"); a mega panel's width,
339
- // its pictures' width and its columns (0.11.0: style="--gw-mega-w: 1120px; --gw-mega-pv: 380px; --gw-mega-cols: 2")
340
- const INPUTS = new Set(["--gw-stars", "--gw-mega-w", "--gw-mega-pv", "--gw-mega-cols"]);
341
- const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?)$/; // docs/theming.md: the eight accent variables (-light/-dark) + wash
349
+ // its pictures' width and its columns (0.11.0: style="--gw-mega-w: 1120px; --gw-mega-pv: 380px; --gw-mega-cols: 2"); the colour of
350
+ // a label's square (0.12.0: --gw-label-mark, on the label or around it)
351
+ const INPUTS = new Set(["--gw-stars", "--gw-mega-w", "--gw-mega-pv", "--gw-mega-cols", "--gw-label-mark"]);
352
+ // docs/theming.md: the eight accent variables (-light/-dark), the wash, and (0.13.0) the announcement bar's and the
353
+ // Stage's colours, which system.json always said a brand sets
354
+ const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?|announce-[\w-]+|stage-[\w-]+)$/;
342
355
 
343
356
  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)$/;
344
357
  const NAMED = new Set(("white black red green blue yellow orange purple pink gray grey silver gold navy teal maroon olive lime aqua " +
@@ -479,6 +492,28 @@ function brandAccent(cssRules, page) {
479
492
  return vals;
480
493
  }
481
494
 
495
+ /** The brand's announcement bar and Stage, when its CSS sets them: their words on their grounds (brand-contrast). */
496
+ function checkBrandContrast(out, add) {
497
+ const vals = {};
498
+ for (const { file, rules } of out.cssRules.filter((x) => !x.inline)) {
499
+ for (const r of rules) {
500
+ if (r.atBlock || r.at.length || !r.selector || !isRootish(r.selector)) continue;
501
+ for (const d of r.decls) if (/^--gw-(?:announce|stage)-/.test(d.prop)) vals[d.prop] = { value: d.value.replace(/!important\s*$/i, "").trim(), at: `${file}:${d.line}` };
502
+ }
503
+ }
504
+ const pairs = [["announce", "ink", "bg"], ["announce", "link", "bg"], ["stage", "ink", "top"], ["stage", "ink", "bottom"]];
505
+ for (const [part, text, ground] of pairs) {
506
+ const t = vals[`--gw-${part}-${text}`], g = vals[`--gw-${part}-${ground}`];
507
+ if (!t && !g) continue; // the kit's own pair, checked by the kit
508
+ const kit = (k) => SYSTEM.color[part][k];
509
+ const tv = t ? t.value : kit(text), gv = g ? g.value : kit(ground);
510
+ const r = contrast(tv, gv);
511
+ if (r !== null && r >= MIN_CONTRAST) continue;
512
+ const at = (t || g).at, [file, line] = [at.slice(0, at.lastIndexOf(":")), Number(at.slice(at.lastIndexOf(":") + 1))];
513
+ add("brand-contrast", { file, line, excerpt: `--gw-${part}-${text} ${tv} on --gw-${part}-${ground} ${gv} ${r === null ? "cannot be read as a colour (write #rrggbb)" : `is ${ratio(r)}, needs ${MIN_CONTRAST}:1`}` });
514
+ }
515
+ }
516
+
482
517
  /** Pages with data-gw-action="accent": the words of the primary button on its fills, light and dark (action-contrast). */
483
518
  function checkActionContrast(out, add) {
484
519
  for (const pg of out.pageProfiles.filter((p) => p.action === "accent")) {
@@ -658,6 +693,15 @@ function analyzeHtml(file, text, out) {
658
693
  ].filter(Boolean);
659
694
  if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
660
695
  }
696
+ if (tag === "img" && cls.includes("gw-brand__logo")) {
697
+ const before = scanText.slice(0, m.index).replace(/\s+$/, "");
698
+ const missing = [
699
+ !(a.alt || "").trim() && "alt with the brand's name (the logo stands in for it)",
700
+ !(a.width && a.height) && "width and height",
701
+ cls.includes("gw-brand__logo--dark") && !/<img\b[^>]*\bclass\s*=\s*["'][^"']*\bgw-brand__logo\b(?![\w-])[^"']*["'][^>]*>$/i.test(before) && "the light file right before it (<img class=\"gw-brand__logo\">)",
702
+ ].filter(Boolean);
703
+ if (missing.length) add("brand-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
704
+ }
661
705
  if (cls.includes("gw-footer__wordmark") && (a["aria-hidden"] || "").trim() !== "true") {
662
706
  add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: aria-hidden="true" (the wordmark is decoration)`));
663
707
  }
@@ -824,6 +868,7 @@ export function runCheck(paths, { ignore = [], includeKit = false } = {}) {
824
868
  }
825
869
 
826
870
  checkActionContrast(out, add);
871
+ checkBrandContrast(out, add);
827
872
 
828
873
  // Classes styled in site CSS: first definition of each.
829
874
  const styled = new Map();
package/bin/lib/init.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // `gemboss-website init`: connect the kit's rules to the project's Claude Code (CLAUDE.md block + skill).
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { PKG_DIR, PKG_NAME, UsageError, parseArgs, rel } from "./util.mjs";
5
5
 
@@ -59,8 +59,87 @@ export function upsertBlock(text, block) {
59
59
  return { text: text + sep + b + eol, action: "added" };
60
60
  }
61
61
 
62
+ // ------------------------------------------------------------------------------------------- init --site
63
+
64
+ /** The files a new site owns from the start, from templates/site/ (gitignore → .gitignore: npm drops dotfiles). */
65
+ function siteTemplateFiles(dir = join(PKG_DIR, "templates", "site"), base = "") {
66
+ const out = [];
67
+ for (const name of readdirSync(dir)) {
68
+ const abs = join(dir, name), r = base ? `${base}/${name}` : name;
69
+ if (statSync(abs).isDirectory()) out.push(...siteTemplateFiles(abs, r));
70
+ else out.push({ from: abs, to: r === "gitignore" ? ".gitignore" : r });
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * A page of the site from templates/page.html: the header and footer become the build's markers, the kit's paths the
77
+ * site's (assets/kit/), and kit-gaps.css follows site.css. `main` replaces the template's <main> when given.
78
+ */
79
+ export function sitePage(main) {
80
+ let html = readFileSync(join(PKG_DIR, "templates", "page.html"), "utf8");
81
+ const cut = (re, what) => { if (!re.test(html)) throw new Error(`templates/page.html: no ${what} to replace`); html = html.replace(re, ""); };
82
+ html = html.replace(/<!--\s*\n\s*Page skeleton:[\s\S]*?-->\n/, `<!-- A page of this site. Its header and footer are partials/header.html and partials/footer.html, written between the
83
+ markers by \`npx gemboss-website build\`. Compose <main> from node_modules/${PKG_NAME}/templates/sections/. -->\n`);
84
+ cut(/\n\s*<div class="gw-announce"[\s\S]*?<\/header>\n/, "header");
85
+ html = html.replace(/(<a class="gw-skip"[^\n]*\n)/, "$1 <!-- gw:header -->\n <!-- /gw:header -->\n\n");
86
+ cut(/\n\s*<!-- the short footer[^\n]*-->\s*<footer class="gw-footer">[\s\S]*?<\/footer>\n/, "footer");
87
+ html = html.replace(/(\s*<\/main>\n)/, "$1\n <!-- gw:footer -->\n <!-- /gw:footer -->\n");
88
+ html = html.split("[/kit]").join("/assets/kit").replace("[/assets]/site.css", "/assets/site.css")
89
+ .replace(/(<link rel="stylesheet" href="\/assets\/site\.css">)/, '$1\n <link rel="stylesheet" href="/assets/kit-gaps.css">')
90
+ .replace("[/favicon.svg]", "/favicon.svg");
91
+ if (main) html = html.replace(/<main id="main">[\s\S]*?<\/main>/, `<main id="main">\n${main}\n </main>`);
92
+ return html;
93
+ }
94
+
95
+ const NOT_FOUND_MAIN = ` <section class="gw-hero gw-hero--compact">
96
+ <div class="gw-wrap">
97
+ <p class="gw-label">404</p>
98
+ <h1><span class="gw-dim">This page is not here.</span> The rest of the site is.</h1>
99
+ <div class="gw-actions">
100
+ <a class="gw-btn" href="/">Go to the home page</a>
101
+ </div>
102
+ </div>
103
+ </section>`;
104
+
105
+ export const SITE_SCRIPTS = {
106
+ "build:site": "gemboss-website build",
107
+ build: "gemboss-website build && node stage-public.mjs && next build",
108
+ start: "next start",
109
+ check: "gemboss-website build --check && node stage-public.mjs --check && gemboss-website check . --ignore node_modules --ignore public",
110
+ };
111
+ export const SITE_DEPS = "next@16.4.2 react@19.2.8 react-dom@19.2.8";
112
+
113
+ function initSite(cwd, done) {
114
+ const write = (to, text) => {
115
+ const abs = resolve(cwd, to);
116
+ if (existsSync(abs)) { done.push([to, "kept (exists)"]); return; }
117
+ mkdirSync(dirname(abs), { recursive: true });
118
+ writeFileSync(abs, text);
119
+ done.push([to, "created"]);
120
+ };
121
+ for (const { from, to } of siteTemplateFiles()) write(to, readFileSync(from, "utf8"));
122
+ write("partials/header.html", readFileSync(join(PKG_DIR, "templates", "header.html"), "utf8"));
123
+ write("partials/footer.html", readFileSync(join(PKG_DIR, "templates", "footer.html"), "utf8"));
124
+ write("index.html", sitePage());
125
+ write("404.html", sitePage(NOT_FOUND_MAIN).replace("<title>[Page title] · [Brand]</title>", "<title>Page not found · [Brand]</title>"));
126
+
127
+ const pkgPath = resolve(cwd, "package.json");
128
+ const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")) : { name: "[site]-www", private: true, type: "module" };
129
+ pkg.scripts = pkg.scripts || {};
130
+ const added = Object.keys(SITE_SCRIPTS).filter((k) => !(k in pkg.scripts));
131
+ for (const k of added) pkg.scripts[k] = SITE_SCRIPTS[k];
132
+ const typed = pkg.type === "module";
133
+ if (!typed && !pkg.type) pkg.type = "module";
134
+ if (added.length || !existsSync(pkgPath) || !typed) {
135
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
136
+ done.push(["package.json", added.length ? `scripts added: ${added.join(", ")}` : "written"]);
137
+ } else done.push(["package.json", "scripts already there"]);
138
+ if (pkg.type !== "module") done.push(["package.json", `"type" is "${pkg.type}": the .mjs files work, next.config.mjs too`]);
139
+ }
140
+
62
141
  export function cmdInit(argv) {
63
- const opts = parseArgs(argv, { force: "bool" });
142
+ const opts = parseArgs(argv, { force: "bool", site: "bool" });
64
143
  if (opts._.length) throw new UsageError(`init takes no arguments (got ${opts._.join(" ")}); run it in the project's root folder`);
65
144
  const cwd = process.cwd();
66
145
  const done = [];
@@ -89,11 +168,22 @@ export function cmdInit(argv) {
89
168
  }
90
169
  }
91
170
 
171
+ if (opts.site) initSite(cwd, done);
172
+
92
173
  const w = Math.max(...done.map(([p]) => p.length));
93
174
  console.log(`gemboss-website init (${PKG_NAME})`);
94
175
  for (const [p, what] of done) console.log(` ${p.padEnd(w)} ${what}`);
95
176
  console.log("");
96
177
  console.log(`Claude Code now reads ${KIT}/AGENTS.md before website work in this project.`);
97
- console.log("Next: start a page from the kit's templates/page.html, then run: npx gemboss-website check");
178
+ if (opts.site) {
179
+ console.log("Next:");
180
+ console.log(` 1. npm install ${SITE_DEPS}`);
181
+ console.log(" 2. Fill partials/header.html and partials/footer.html once (delete what the site does not use),");
182
+ console.log(" the brand's :root block in assets/site.css, and add favicon.svg");
183
+ console.log(" 3. npm run build:site, then compose index.html from the kit's templates/sections/");
184
+ console.log(" 4. npm run check, and look at every page at 320px and 1366px");
185
+ } else {
186
+ console.log("Next: start a page from the kit's templates/page.html, then run: npx gemboss-website check");
187
+ }
98
188
  return 0;
99
189
  }
package/css/article.css CHANGED
@@ -203,7 +203,7 @@
203
203
  .gw-card__media first, a .gw-label for the category, the title (its link covers the card, so the card is one target
204
204
  and the link's name is the title alone), one or two lines of excerpt and a .gw-article-meta row at the foot. One
205
205
  column on phones, two from 768px, three from 1080px. --compact: one column of rows, no card and no picture. */
206
- .gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin: 0; padding: 0; list-style: none; }
206
+ .gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 0; list-style: none; }
207
207
  .gw-post-list > li { display: flex; min-width: 0; }
208
208
  .gw-post { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
209
209
  .gw-post:hover { border-color: var(--gw-line-strong); }
package/css/base.css CHANGED
@@ -32,33 +32,34 @@ h1, h2, h3, h4, h5, h6, .gw-h1, .gw-h2, .gw-h3, .gw-h4 {
32
32
  text-wrap: balance;
33
33
  }
34
34
  h1, .gw-h1, h2, .gw-h2 { font-weight: var(--gw-display-weight); letter-spacing: var(--gw-display-tracking); }
35
- h1, .gw-h1 { font-size: var(--gw-display-h1); }
36
- h2, .gw-h2 { font-size: var(--gw-display-h2); }
37
- h3, .gw-h3 { font-size: 24px; }
35
+ h1, .gw-h1 { font-size: var(--gw-display-h1); line-height: var(--gw-display-h1-leading); }
36
+ h2, .gw-h2 { font-size: var(--gw-display-h2); line-height: var(--gw-display-h2-leading); }
37
+ /* h3 (0.12.0): 20px/600, as every h3 on gemboss.ai */
38
+ h3, .gw-h3 { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; }
38
39
  h4, .gw-h4 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }
39
40
  h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
40
41
 
41
- .gw-lead { font-size: 18px; line-height: 1.55; color: var(--gw-muted); max-width: 60ch; }
42
+ .gw-lead { font-size: 18px; line-height: 1.55; color: var(--gw-muted); max-width: 58ch; }
42
43
  .gw-note { font-size: 14px; color: var(--gw-muted); }
43
44
  .gw-dim { color: var(--gw-muted); }
44
45
 
45
- /* the label above a heading: sentence case, muted, led by the 8px accent square */
46
+ /* the label above a heading: sentence case, muted, led by the 8px square, ink by default (0.12.0, as gemboss.ai). The
47
+ input --gw-label-mark paints the square another colour (a product's own, say): set it on the label or on anything
48
+ around it, e.g. style="--gw-label-mark: var(--gw-accent)". */
46
49
  .gw-label {
47
50
  display: inline-flex;
48
51
  align-items: center;
49
52
  gap: 8px;
50
53
  font-size: 13px;
51
54
  font-weight: 500;
52
- line-height: 1.4;
53
55
  color: var(--gw-muted);
54
56
  }
55
57
  .gw-label::before {
56
58
  content: "";
57
- flex: none;
58
59
  width: 8px;
59
60
  height: 8px;
60
- border-radius: var(--gw-radius-0);
61
- background: var(--gw-accent);
61
+ border-radius: var(--gw-radius-2);
62
+ background: var(--gw-label-mark, var(--gw-ink));
62
63
  }
63
64
 
64
65
  /* links inside running text (and anywhere in .gw-prose) get a visible link style; everything else opts in with .gw-link */
@@ -78,19 +79,22 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
78
79
  :focus-visible { border-radius: var(--gw-radius-4); outline: 2px solid var(--gw-accent); outline-offset: 3px; }
79
80
  [id] { scroll-margin-top: calc(var(--gw-header-height) + 16px); }
80
81
 
82
+ /* the skip link (0.12.0, gemboss.ai's): the accent tab in the top left corner, shown while it has the focus */
81
83
  .gw-skip {
82
84
  position: absolute;
83
85
  left: -9999px;
84
- top: 8px;
85
- z-index: 100;
86
- padding: 12px 16px;
87
- border-radius: var(--gw-radius-action);
88
- background: var(--gw-action);
89
- color: var(--gw-on-action);
86
+ top: 0;
87
+ z-index: 200;
88
+ padding: 12px 20px;
89
+ border-radius: 0 0 var(--gw-radius-card) 0;
90
+ background: var(--gw-accent);
91
+ color: var(--gw-on-accent);
92
+ box-shadow: var(--gw-shadow);
90
93
  font-size: 14px;
91
94
  font-weight: 600;
92
95
  }
93
- .gw-skip:focus { left: 16px; }
96
+ /* its corner again with the focus: a page's own :focus-visible corners, loaded after the kit, must not round the tab */
97
+ .gw-skip:focus { left: 0; border-radius: 0 0 var(--gw-radius-card) 0; }
94
98
 
95
99
  /* words for a screen reader only (0.7.0): " (opens in a new tab)" in a link with target="_blank" */
96
100
  .gw-visually-hidden {
@@ -104,10 +108,11 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
104
108
  border: 0;
105
109
  }
106
110
 
107
- /* scroll reveal: the script adds .is-in; the animation is a failsafe that shows it anyway after 2.5s */
111
+ /* scroll reveal: the script adds .is-in (system.json motion.reveal: when, the stagger, the failsafe); the animation is
112
+ a second failsafe, for a page whose script never ran, that shows it anyway after 2.5s */
108
113
  .gw-reveal {
109
114
  opacity: 0;
110
- transform: translateY(24px);
115
+ transform: translateY(var(--gw-reveal-distance));
111
116
  transition: opacity var(--gw-reveal-ms) var(--gw-ease), transform var(--gw-reveal-ms) var(--gw-ease);
112
117
  animation: gw-reveal-failsafe 1ms linear 2.5s forwards;
113
118
  }