@gemboss/website-ui 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,46 @@ 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.12.0 (2026-10-26)
8
+
9
+ The grammar of every page, 1:1 with gemboss.ai: the announcement bar, the skip link, the button, the type (headings,
10
+ lead, label, the dim first sentence), a section, its head, the wrap, and the scroll reveal. After the footer (0.10.0)
11
+ 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
12
+ the kit, so that GemPages, Gemians and GemX inherit them). gemboss.ai's 83 pages now draw these parts with the kit's
13
+ classes, and were measured against the pages before, in Chromium: the computed style of every one of these parts (2,300
14
+ of them) at 320, 390, 768, 1080, 1366 and 1920px, light and dark, and every page pixel by pixel at 390 and 1366px; the
15
+ reveal's delays and counts, a hovered button, the focused skip link.
16
+
17
+ A minor version: a site on 0.11.0 sees these values change (below), none of the markup.
18
+
19
+ - The announcement bar (`.gw-announce`): gemboss.ai's, on its own ground in both themes, from the new tokens
20
+ `--gw-announce-*` (`system.json` `color.announce`: ground, words, hairline, the pill, the link and its underline). A
21
+ small pill may lead the sentence (`.gw-announce__tag`, new); the link's underline is a hairline that brightens on
22
+ hover. 12px words in a slimmer bar up to 520px.
23
+ - The skip link (`.gw-skip`): an accent tab out of the top left corner (was an ink button 8px in).
24
+ - The button (`.gw-btn`): on hover the filled one rises 1px onto the rest shadow (was a darker fill); a framed or
25
+ frameless one does not move. Its face is the new `--gw-font-action` (`type.button.family`, the brand face alone, as
26
+ gemboss.ai names it: a glyph Inter's web subset lacks, the arrow, is then the system's own, the width gemboss.ai's
27
+ buttons have). Its line height is its surroundings'. A label wider than needed stays at the start (centred only in
28
+ `--block`, a phone's row of actions in a hero or CTA, an inline form). An arrow after the label is
29
+ `.gw-btn__arrow` (new): 3px further on hover. Up to 768px every button is at least 44px tall, by the width only (it was
30
+ also by a coarse pointer); a label wraps only up to 480px and only in `<main>`, at 1.35 lines (was up to 768px,
31
+ balanced).
32
+ - 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
33
+ the descenders of a first line touched the capitals of the second). h3 and `.gw-h3` 20px / 600, tracking −0.02em,
34
+ 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
35
+ with 2px corners (was the accent, square) and its line height its surroundings'; the input `--gw-label-mark` (new, on
36
+ the label or anything around it) paints the square another colour, a gradient too.
37
+ - A section's head (`.gw-section__head`) up to 760px (was 660px). A lead under a heading 20px below it (in a head, a
38
+ split column, a CTA; 16px in the hero). What follows a head starts 40px below it, 48px from 768px, as a top margin
39
+ of the next thing at no specificity, so a block that brings its own keeps it (was the head's bottom margin).
40
+ - The scroll reveal: 20px (new token `--gw-reveal-distance`, was 24px), once 12% of it is in view with the bottom 6% of
41
+ the view not counting (was 15%); each `.gw-reveal` after the first among its siblings waits 70ms more, at most 380ms;
42
+ 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
43
+ values are `system.json` `motion.reveal`.
44
+ - `check`: `--gw-label-mark` is an input, like a mega panel's widths.
45
+ - `website.js` is 10.4 KB gzipped (the budget is 10.5 KB, was 10 KB: the reveal's stagger and count-up).
46
+
7
47
  ## 0.11.0 (2026-10-26)
8
48
 
9
49
  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
@@ -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.11.0/website.min.css">
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.12.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.11.0/js/website.js" defer></script>
45
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.12.0/js/website.js" defer></script>
46
46
  ```
47
47
 
48
48
  Chèn markup của kit sau khi trang đã tải (block của builder, bản xem trước CMS): gọi `gwWebsite.init(phầnMới)` ngay
package/bin/lib/check.mjs CHANGED
@@ -336,8 +336,9 @@ const ATTR_THEME = String.raw`\[\s*data-theme(?:\s*[~|^$*]?=\s*(?:"[^"]*"|'[^']*
336
336
  const ROOTISH = new RegExp(`^(?::root|html)?(?:${ATTR_THEME}|:not\\(\\s*${ATTR_THEME}\\s*\\))*$`, "i");
337
337
  const isRootish = (sel) => splitTop(sel).every((s) => s && ROOTISH.test(s));
338
338
  // 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"]);
339
+ // 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
340
+ // a label's square (0.12.0: --gw-label-mark, on the label or around it)
341
+ const INPUTS = new Set(["--gw-stars", "--gw-mega-w", "--gw-mega-pv", "--gw-mega-cols", "--gw-label-mark"]);
341
342
  const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?)$/; // docs/theming.md: the eight accent variables (-light/-dark) + wash
342
343
 
343
344
  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)$/;
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
  }
@@ -2,12 +2,12 @@
2
2
 
3
3
  /* actions are rectangles: 40px, the action radius of the site's profile (8, 12 in soft), 14px/600, sentence case.
4
4
  The fill is the action colour: ink, or the brand's accent under data-gw-action="accent" (tokens.css). Never a
5
- gradient, a wash hue or a shadow. */
5
+ gradient or a wash hue. On hover (0.12.0, as gemboss.ai) the filled button rises 1px onto the rest shadow; the framed
6
+ and frameless ones only change their frame or tint. Phones: see "touch" below. */
6
7
  .gw-btn {
7
8
  position: relative;
8
9
  display: inline-flex;
9
10
  align-items: center;
10
- justify-content: center;
11
11
  gap: 8px;
12
12
  max-width: 100%;
13
13
  height: var(--gw-button-height);
@@ -16,26 +16,34 @@
16
16
  border-radius: var(--gw-radius-action);
17
17
  background: var(--gw-action);
18
18
  color: var(--gw-on-action);
19
+ font-family: var(--gw-font-action);
19
20
  font-size: 14px;
20
21
  font-weight: 600;
21
- line-height: 1.4;
22
22
  letter-spacing: 0;
23
23
  text-align: center;
24
24
  white-space: nowrap;
25
25
  cursor: pointer;
26
+ transition: background 0.15s ease, border-color 0.15s ease;
26
27
  }
27
- .gw-btn:hover { background: var(--gw-action-hover); }
28
+ .gw-btn:hover { transform: translateY(-1px); box-shadow: var(--gw-shadow); }
28
29
  .gw-btn svg { flex: none; width: 16px; height: 16px; }
29
30
  .gw-btn--secondary { background: transparent; border-color: var(--gw-line-strong); color: var(--gw-ink); }
30
31
  .gw-btn--secondary:hover { background: transparent; border-color: var(--gw-ink); }
31
32
  .gw-btn--ghost { background: transparent; color: var(--gw-ink); }
32
33
  .gw-btn--ghost:hover { background: var(--gw-bg-soft); }
34
+ .gw-btn:is(.gw-btn--secondary, .gw-btn--ghost):hover { transform: none; box-shadow: none; }
35
+ /* an arrow after the label (→), nudged 3px on hover */
36
+ .gw-btn__arrow { letter-spacing: 0; transition: transform 0.18s ease; }
37
+ .gw-btn:hover .gw-btn__arrow { transform: translateX(3px); }
38
+
33
39
  .gw-btn--on-night { background: var(--gw-on-night); color: var(--gw-night); }
34
40
  .gw-btn--on-night:hover { background: var(--gw-on-night); opacity: 0.88; }
35
41
  .gw-btn--sm { height: 36px; padding: 0 16px; }
36
42
  .gw-btn--sm::after { content: ""; position: absolute; top: -6px; right: 0; bottom: -6px; left: 0; }
37
43
  .gw-btn--lg { height: 48px; padding: 0 24px; font-size: 16px; }
38
- .gw-btn--block { display: flex; width: 100%; }
44
+ /* a button wider than its label keeps the label at its start (as gemboss.ai), unless it is made wide on purpose: full
45
+ width (--block), a phone's row of actions, an inline form */
46
+ .gw-btn--block { display: flex; width: 100%; justify-content: center; }
39
47
  .gw-btn:disabled, .gw-btn[aria-disabled="true"] { opacity: 0.48; cursor: not-allowed; pointer-events: none; }
40
48
  /* pressed: the ink one step darker (the hover ink, as the dashboard's primary), nothing moves; the frameless variants
41
49
  take one more step of tint */
@@ -334,16 +342,21 @@
334
342
  .gw-faq__item[open] > summary::after { transform: rotate(45deg); }
335
343
  .gw-faq__item > :not(summary) { margin: 0 0 20px; max-width: 68ch; color: var(--gw-muted); font-size: 16px; }
336
344
 
337
- /* touch: every control is at least 44px tall, labels may wrap */
338
- @media (max-width: 768px), (pointer: coarse) {
345
+ /* touch: every control is at least 44px tall. A button up to 768px (0.12.0, as gemboss.ai: by the width, not the
346
+ pointer), its label wrapping (and tighter) from 480px down in the page's <main>, so a long one never pushes the page
347
+ sideways (a header or an overlay outside <main> keeps its one line) */
348
+ @media (max-width: 768px) {
339
349
  .gw-btn, .gw-btn--sm, .gw-btn--lg {
340
350
  height: auto;
341
351
  min-height: var(--gw-tap-min);
342
352
  padding-block: 8px;
343
- white-space: normal;
344
- text-wrap: balance;
345
353
  }
346
354
  .gw-btn--lg { min-height: 48px; }
355
+ }
356
+ @media (max-width: 480px) {
357
+ :where(main) .gw-btn { white-space: normal; line-height: 1.35; }
358
+ }
359
+ @media (max-width: 768px), (pointer: coarse) {
347
360
  :is(a, button, label).gw-chip { min-height: var(--gw-tap-min); }
348
361
  .gw-segmented > button { height: auto; min-height: var(--gw-tap-min); padding-block: 8px; white-space: normal; text-wrap: balance; }
349
362
  }
package/css/forms.css CHANGED
@@ -134,7 +134,7 @@
134
134
  room; below 480px, and in a box too narrow for both, the field and the button each take a full line. */
135
135
  .gw-form-inline { display: flex; flex-wrap: wrap; gap: 8px; }
136
136
  .gw-form-inline > :not(.gw-btn) { flex: 999 1 160px; min-width: 0; }
137
- .gw-form-inline > .gw-btn { flex: 1 0 auto; }
137
+ .gw-form-inline > .gw-btn { flex: 1 0 auto; justify-content: center; }
138
138
  @media (max-width: 479px) {
139
139
  .gw-form-inline > :not(.gw-btn) { flex-basis: 100%; }
140
140
  }
package/css/header.css CHANGED
@@ -1,16 +1,50 @@
1
1
  /* header: announcement, a flat 64px bar with one hairline, mega menu, mobile menu up to 1080px.
2
2
  Not a floating pill: no radius, no blur, no shadow on the bar. Nothing tucks under it. A brand may choose the one
3
3
  other shape, .gw-header--float (an inset card from 1081px, at the end of this file). */
4
+ /* the announcement bar (0.12.0, gemboss.ai's): one sentence on its own ground over the header, the same in both themes
5
+ (system.json color.announce). A small pill may lead it (.gw-announce__tag); its link is light, underlined by a hairline
6
+ that brightens on hover. From 520px down, 12px words and a slimmer bar. */
4
7
  .gw-announce {
5
- padding: 8px var(--gw-gutter);
6
- border-bottom: 1px solid var(--gw-line);
7
- background: var(--gw-bg-soft);
8
- color: var(--gw-ink-2);
8
+ position: relative;
9
+ z-index: 60;
10
+ padding: 12px 16px;
11
+ border-bottom: 1px solid var(--gw-announce-line);
12
+ background: var(--gw-announce-bg);
13
+ color: var(--gw-announce-ink);
9
14
  font-size: 13px;
15
+ font-weight: 500;
10
16
  line-height: 1.5;
17
+ letter-spacing: 0.005em;
11
18
  text-align: center;
12
19
  }
13
- .gw-announce a { color: var(--gw-ink); font-weight: 600; text-decoration: underline; text-underline-offset: 4px; }
20
+ /* 2.5px above and below, as gemboss.ai draws the pill (21.5px tall): the one half pixel in the kit */
21
+ .gw-announce__tag {
22
+ display: inline-block;
23
+ margin-right: 12px;
24
+ padding: 2.5px 8px;
25
+ border-radius: var(--gw-radius-chip);
26
+ background: var(--gw-announce-tag);
27
+ color: var(--gw-announce-tag-ink);
28
+ font-size: 11px;
29
+ font-weight: 700;
30
+ letter-spacing: 0.09em;
31
+ vertical-align: 1px;
32
+ }
33
+ .gw-announce a {
34
+ margin-left: 8px;
35
+ padding-bottom: 2px;
36
+ border-bottom: 1px solid var(--gw-announce-link-line);
37
+ color: var(--gw-announce-link);
38
+ font-weight: 600;
39
+ text-decoration: none;
40
+ white-space: nowrap;
41
+ transition: border-color 0.18s;
42
+ }
43
+ .gw-announce a:hover { border-bottom-color: var(--gw-announce-link); }
44
+ @media (max-width: 520px) {
45
+ .gw-announce { padding: 8px 12px; font-size: 12px; }
46
+ .gw-announce__tag { margin-right: 6px; }
47
+ }
14
48
 
15
49
  .gw-header {
16
50
  position: sticky;
package/css/layout.css CHANGED
@@ -12,15 +12,19 @@
12
12
  @media (min-width: 768px) { .gw-section--tight { padding-block: 64px; } }
13
13
 
14
14
  /* label + h2 + one or two lines; the vertical flow of every text block in the kit */
15
- .gw-section__head { max-width: 660px; }
15
+ .gw-section__head { max-width: 760px; }
16
16
  .gw-section__head--center { margin-inline: auto; text-align: center; }
17
17
  .gw-section__head--center .gw-lead { margin-inline: auto; }
18
- .gw-section__head:not(:last-child) { margin-bottom: 40px; }
19
- @media (min-width: 768px) { .gw-section__head:not(:last-child) { margin-bottom: 48px; } }
18
+ /* what follows a head starts 40px under it, 48px from 768px (0.12.0): a top margin on the next thing, at no
19
+ specificity, so a block that brings its own (a FAQ's 40px, say) keeps it */
20
+ :where(.gw-section__head) + * { margin-top: 40px; }
21
+ @media (min-width: 768px) { :where(.gw-section__head) + * { margin-top: 48px; } }
20
22
 
21
23
  :where(.gw-section__head, .gw-hero > .gw-wrap, .gw-split > :not(.gw-window), .gw-cta, .gw-card, .gw-feature, .gw-plan) > * + * { margin-top: 16px; }
22
24
  :where(.gw-section__head, .gw-hero > .gw-wrap, .gw-split > :not(.gw-window), .gw-cta, .gw-card, .gw-plan) > :is(.gw-actions, .gw-segmented) { margin-top: 32px; }
23
25
  :where(.gw-section__head, .gw-hero > .gw-wrap, .gw-split > :not(.gw-window), .gw-cta, .gw-card, .gw-plan) > .gw-list--check { margin-top: 24px; }
26
+ /* a lead under a heading: 20px (0.12.0, as gemboss.ai); in a hero it keeps the 16px of the flow */
27
+ :where(.gw-section__head, .gw-split > :not(.gw-window), .gw-cta) > :where(* + .gw-lead) { margin-top: 20px; }
24
28
 
25
29
  /* a row of actions: one .gw-btn, then a secondary or a link */
26
30
  .gw-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
package/css/sections.css CHANGED
@@ -26,7 +26,7 @@
26
26
  .gw-hero > .gw-wrap > .gw-window { margin-top: 64px; }
27
27
  }
28
28
  @media (max-width: 479px) {
29
- :is(.gw-hero, .gw-cta) .gw-actions > .gw-btn { flex: 1 1 100%; }
29
+ :is(.gw-hero, .gw-cta) .gw-actions > .gw-btn { flex: 1 1 100%; justify-content: center; }
30
30
  }
31
31
 
32
32
  /* logos: a proof strip of real logos or wordmarks */
package/css/tokens.css CHANGED
@@ -3,6 +3,7 @@
3
3
  color-scheme: light;
4
4
  --gw-font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
5
5
  --gw-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
6
+ --gw-font-action: Inter;
6
7
  --gw-text-11: 11px;
7
8
  --gw-text-12: 12px;
8
9
  --gw-text-13: 13px;
@@ -18,6 +19,8 @@
18
19
  --gw-display-wordmark: clamp(40px, 9vw, 128px);
19
20
  --gw-display-weight: 600;
20
21
  --gw-display-tracking: -0.03em;
22
+ --gw-display-h1-leading: 1.1;
23
+ --gw-display-h2-leading: 1.06;
21
24
  --gw-heading-weight: 650;
22
25
  --gw-heading-tracking: -0.022em;
23
26
  --gw-heading-leading: 1.08;
@@ -70,6 +73,7 @@
70
73
  --gw-tap-min: 44px;
71
74
  --gw-ease: cubic-bezier(.22,1,.36,1);
72
75
  --gw-reveal-ms: 700ms;
76
+ --gw-reveal-distance: 20px;
73
77
  --gw-spin-ms: 700ms;
74
78
  --gw-spot-radius: 260px;
75
79
  --gw-spot-radius-action: 120px;
@@ -88,6 +92,13 @@
88
92
  --gw-stage-bottom: #0d0f2e;
89
93
  --gw-stage-dot: rgba(255, 255, 255, 0.1);
90
94
  --gw-stage-ink: #ffffff;
95
+ --gw-announce-bg: #171a38;
96
+ --gw-announce-ink: #b9beda;
97
+ --gw-announce-line: rgba(255, 255, 255, 0.06);
98
+ --gw-announce-tag: rgba(122, 132, 255, 0.2);
99
+ --gw-announce-tag-ink: #a7aeff;
100
+ --gw-announce-link: #ffffff;
101
+ --gw-announce-link-line: rgba(255, 255, 255, 0.28);
91
102
  /* accent primitives: the only colours a brand overrides (docs/theming.md) */
92
103
  --gw-accent-light: #5661f5;
93
104
  --gw-accent-strong-light: #3a44de;
package/demo/header.html CHANGED
@@ -44,7 +44,7 @@
44
44
  <body class="gw-page">
45
45
  <a class="gw-skip" href="#main">Skip to content</a>
46
46
 
47
- <div class="gw-announce" role="region" aria-label="Announcement">The GemBoss community is live. <a href="https://community.gemboss.ai">Join founders building brand-true stores</a></div>
47
+ <div class="gw-announce" role="region" aria-label="Announcement"><span class="gw-announce__tag">NEW</span> The GemBoss community is live. <a href="https://community.gemboss.ai">Join founders building brand-true stores &rarr;</a></div>
48
48
  <header class="gw-header" data-gw-header>
49
49
  <div class="gw-header__bar">
50
50
  <a class="gw-brand" href="https://gemboss.ai/" data-gw-show="brand:gemboss"><svg class="gw-brand__mark" viewBox="0 0 100 116" aria-hidden="true"><polygon points="26,14 74,14 66,50 34,50" style="fill:var(--gw-accent)"/><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="9" stroke-linejoin="round"/><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round"/></svg><span class="gw-brand__name">GemBoss</span></a><a class="gw-brand" href="header.html#gempages" data-gw-show="brand:gempages" hidden><svg class="gw-brand__mark" viewBox="0 0 26 26" aria-hidden="true"><rect width="26" height="26" rx="7" style="fill:var(--gw-accent)"/><path d="M8 9h10M8 13h10M8 17h6" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/></svg><span class="gw-brand__name">Example brand</span></a>
@@ -13,15 +13,16 @@ default `tight` profile, and `soft` makes them 12 / 20 / 24; "ink fill" is the d
13
13
  `data-gw-action="accent"` makes it the accent.
14
14
 
15
15
  Spacing between the children of text blocks is automatic: inside `.gw-section__head`, the hero, a split column,
16
- `.gw-cta`, `.gw-card`, `.gw-feature` and `.gw-plan`, each child is 16px below the previous one, a `.gw-actions` row
17
- 32px, a `.gw-list--check` 24px. Do not add margins to fix rhythm; reorder or remove content instead.
16
+ `.gw-cta`, `.gw-card`, `.gw-feature` and `.gw-plan`, each child is 16px below the previous one, a `.gw-lead` under a
17
+ heading 20px (16px in the hero), a `.gw-actions` row 32px, a `.gw-list--check` 24px. Do not add margins to fix rhythm;
18
+ reorder or remove content instead.
18
19
 
19
20
  ## Page
20
21
 
21
22
  | class | for |
22
23
  |---|---|
23
24
  | `.gw-page` | on `<body>`: the footer stays at the bottom of a short page |
24
- | `.gw-skip` | the first element in `<body>`: "Skip to content", visible only on keyboard focus |
25
+ | `.gw-skip` | the first element in `<body>`: "Skip to content", visible only on keyboard focus (an accent tab from the top left corner) |
25
26
 
26
27
  ```html
27
28
  <body class="gw-page">
@@ -60,11 +61,11 @@ copies of the script and expect both to work (the first owns the page).
60
61
 
61
62
  | class | for |
62
63
  |---|---|
63
- | `h1`, `h2` (and `.gw-h1`, `.gw-h2`) | display sizes: h1 `clamp(40px, 4.6vw, 68px)`, h2 `clamp(32px, 3.4vw, 52px)`, weight 600, tracking −0.03em |
64
- | `h3`, `h4` (and `.gw-h3`, `.gw-h4`) | 24px / 650 and 20px / 600 |
64
+ | `h1`, `h2` (and `.gw-h1`, `.gw-h2`) | display sizes: h1 `clamp(40px, 4.6vw, 68px)` at line height 1.1, h2 `clamp(32px, 3.4vw, 52px)` at 1.06, weight 600, tracking −0.03em |
65
+ | `h3`, `h4` (and `.gw-h3`, `.gw-h4`) | 20px / 600 (tracking −0.02em, line height 1.2, every h3 on gemboss.ai) and 20px / 600 |
65
66
  | `.gw-dim` | the muted first sentence of the house headline shape |
66
- | `.gw-label` | the label above a heading: 13px, muted, led by the 8px accent square |
67
- | `.gw-lead` | the one or two lines under a heading (18px, 20px in the hero, muted, max 60ch) |
67
+ | `.gw-label` | the label above a heading: 13px, muted, led by the 8px square in ink; `--gw-label-mark` paints the square (a product's own colour): `style="--gw-label-mark: var(--gw-accent)"` on the label or around it |
68
+ | `.gw-lead` | the one or two lines under a heading (18px, 20px in the hero, muted, max 58ch) |
68
69
  | `.gw-note` | a short reassurance under actions (14px, muted) |
69
70
  | `.gw-link` | an inline link in the accent, underlined |
70
71
  | `.gw-link--arrow` | a standalone "go there" link with a → that moves 2px on hover |
@@ -92,7 +93,7 @@ Don't: uppercase or letter-spaced labels; a label longer than a few words; the a
92
93
  | `.gw-section` | a section's vertical rhythm (`--gw-space-section`) |
93
94
  | `.gw-section--soft` | on `--gw-bg-soft`, to separate a section without a line |
94
95
  | `.gw-section--tight` | less padding (40px, 64px from 768px), for proof strips and follow-on tables |
95
- | `.gw-section__head` | label + h2 + lead, max 660px; 40–48px above the content |
96
+ | `.gw-section__head` | label + h2 + lead, max 760px; the content after it starts 40px below (48px from 768px), unless it brings its own top margin |
96
97
  | `.gw-section__head--center` | the same, centred |
97
98
  | `.gw-grid`, `.gw-grid--2`, `.gw-grid--3`, `.gw-grid--4` | equal columns: one on phones; 2 and 3 from 768px; 4 is 2 from 480px and 4 from 1080px |
98
99
  | `.gw-actions` | a row of actions that wraps; on phones the hero and CTA buttons go full width |
@@ -118,8 +119,11 @@ Don't: more than three night bands on a page (the closing CTA does not count), o
118
119
  brand's accent with `--gw-on-accent` words under `data-gw-action="accent"` (one colour on the whole site). Variants: `--secondary` (hairline, transparent), `--ghost`
119
120
  (no frame), `--on-night` (light fill for a dark surface that is not a `.gw-band--night`), `--danger` (0.6.0: the
120
121
  danger fill with `--gw-on-danger` words, for the action of a page that destroys something: delete a workspace, erase
121
- my data), `--sm` (36px, with a 44px hit area), `--lg` (48px, 16px type), `--block` (full width). On touch screens every button is at least 44px tall and
122
- its label may wrap. Inside a night band the default button turns light by itself.
122
+ my data), `--sm` (36px, with a 44px hit area), `--lg` (48px, 16px type), `--block` (full width). On hover (0.12.0, as
123
+ gemboss.ai) the filled button rises 1px onto the rest shadow, a framed one darkens its frame; an arrow after the label
124
+ (`<span class="gw-btn__arrow">→</span>`) moves 3px. Up to 768px every button is at least 44px tall; up to 480px a label
125
+ in `<main>` may wrap. The face is `--gw-font-action` (the brand face alone, `system.json` `type.button.family`). Inside a
126
+ night band the default button turns light by itself.
123
127
 
124
128
  ```html
125
129
  <div class="gw-actions">
@@ -332,7 +336,7 @@ level. A brand changes what it fills in and the tokens (colours, the Stage, the
332
336
 
333
337
  | class | for |
334
338
  |---|---|
335
- | `.gw-announce` | optional one-sentence bar above the header, with one link |
339
+ | `.gw-announce` | optional one-sentence bar above the header, with one link, on its own ground in both themes (`color.announce`); `.gw-announce__tag` a small pill before the sentence ("New") |
336
340
  | `.gw-header` (`data-gw-header`) | the sticky, flat 64px bar with one hairline; `.is-scrolled` darkens the hairline |
337
341
  | `.gw-header--float` | with `.gw-header`, a brand's header profile: from 1081px the same bar as an inset card (panel radius, the surface at 85% over a blur, one hairline, the raised shadow, 16px from the top); the flat bar up to 1080px; solid under `prefers-reduced-transparency` |
338
342
  | `.gw-header__bar` | the row inside: the page's wrap + 12px (1132px on GemBoss), 24px inside (6px up to 640px), 32px between its parts |
@@ -838,10 +842,12 @@ not make public with the review; masonry or a carousel that moves by itself.
838
842
 
839
843
  | class | for |
840
844
  |---|---|
841
- | `.gw-reveal` | fades and rises 24px into place when 15% visible (the script adds `.is-in`); shown anyway after 2.5s, and still under `prefers-reduced-motion` |
845
+ | `.gw-reveal` | fades and rises 20px into place once 12% of it is in view (the script adds `.is-in`); each one after the first among its siblings waits 70ms more (at most 380ms); shown anyway after 1.6s, and still under `prefers-reduced-motion` |
846
+ | `[data-gw-count]` | inside a `.gw-reveal`: its number counts up from 0 when the reveal shows (`<span data-gw-count>$1,200+</span>`) |
842
847
 
843
- Put it on a block (a grid, a list, a window), not on each card in a grid. Don't use it on the hero or anything in
844
- the first screen, and don't animate anything but `transform` and `opacity`.
848
+ Put it on a block (a grid, a list, a window), or on each card of one grid to cascade them. Don't use it on the hero
849
+ or anything in the first screen, and don't animate anything but `transform` and `opacity`. A count is for a number
850
+ that is a measure (a total, a percentage), never for a step's ordinal ("01").
845
851
 
846
852
  ### Spotlight `data-gw-spot` (0.9.0)
847
853
 
@@ -44,7 +44,7 @@ the spotlight (0.9.0). It comes in two entries built from one source
44
44
  to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
45
45
  - **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
46
46
  mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
47
- - **`version`**: `'0.11.0'`. The classic script exposes the three as `window.gwWebsite`.
47
+ - **`version`**: `'0.12.0'`. The classic script exposes the three as `window.gwWebsite`.
48
48
  - **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, on the `.gw-header` since 0.11.0, `menu`, `lang`, `theme`, `reveal`,
49
49
  `header`, `segmented`, `toc`, `table`, `fold`, `spot`). A drill-down menu is wired with its toggle (`menu`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
50
50
  is still wired.
@@ -170,9 +170,13 @@ Script behaviour:
170
170
  (`gw-theme`, every access in try/catch), updates `aria-label` on every toggle, fires `gw:theme`. On load, a stored
171
171
  choice is applied before paint by a one-line inline script in `<head>` that the template carries
172
172
  (`templates/page.html`).
173
- - **Reveal**: elements with `.gw-reveal` get `.is-in` when 15% visible (IntersectionObserver, once). Failsafe: 2.5s
174
- after the `init` that found them, or at once if IntersectionObserver is missing, they get `.is-in`. Under
175
- `prefers-reduced-motion` the CSS shows them without motion.
173
+ - **Reveal** (0.12.0, as gemboss.ai): elements with `.gw-reveal` get `.is-in` once 12% of them is in view, the bottom
174
+ 6% of the view not counting (IntersectionObserver, once). Each `.gw-reveal` after the first among its `.gw-reveal`
175
+ siblings waits 70ms more (at most 380ms), so a grid of them cascades. A number in a `[data-gw-count]` element inside
176
+ counts up from 0 to itself over 1.1s when its reveal shows (its first words: `$1,200+` keeps the sign, the grouping
177
+ and the decimals). Failsafe: 1.6s after the `init` that found them, or at once if IntersectionObserver is missing,
178
+ they get `.is-in`. Under `prefers-reduced-motion` the CSS shows them without motion, at once, and nothing counts.
179
+ The values are `system.json` `motion.reveal`.
176
180
  - **Header**: `.is-scrolled` on `[data-gw-header]` once the page scrolled more than 8px (the CSS may use it for the
177
181
  hairline only; the bar does not change height or colour). The floating header needs nothing more from the script:
178
182
  it is the same element with one more class.
@@ -521,7 +525,7 @@ CSS only: the script does not touch a form. The page (or its server) owns valida
521
525
  |---|---|
522
526
  | `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--danger` (0.6.0), `--sm`, `--lg`, `--block`) | actions; a rectangle with the action radius, the action colour as fill (ink, or the accent under `data-gw-action="accent"`); `:active` pressed, `[aria-busy="true"]` loading (script above) |
523
527
  | `.gw-link` (+ `--arrow`) | inline text link in the accent; `--arrow` adds → that moves 2px on hover |
524
- | `.gw-label` | the label above a heading: 13px muted, 8px accent square before it |
528
+ | `.gw-label` | the label above a heading: 13px muted, the 8px square before it in ink; `--gw-label-mark` (on it or around it) paints the square |
525
529
  | `.gw-dim` | the muted first sentence inside a headline |
526
530
  | `.gw-chip` (+ `.is-active`) | selectable token, pill |
527
531
  | `.gw-segmented` (+ `--block`) | one choice out of two to four (script above) |
@@ -553,7 +557,7 @@ CSS only: the script does not touch a form. The page (or its server) owns valida
553
557
  |---|---|
554
558
  | `.gw-section` (+ `--soft`, `--tight`) | vertical rhythm; `--soft` on `--gw-bg-soft` |
555
559
  | `.gw-band--night` | a dark band (max three per page plus the closing CTA, never adjacent) |
556
- | `.gw-section__head` (+ `--center`) | label + h2 + one or two lines, max 660px |
560
+ | `.gw-section__head` (+ `--center`) | label + h2 + one or two lines, max 760px |
557
561
  | `.gw-hero` (+ `__wash`, `--center`, `--compact`) | first screen; `__wash` is the soft colour wash behind it |
558
562
  | `.gw-logos` | proof strip of customer or partner logos (real ones only) |
559
563
  | `.gw-stats` (`__item`, `__value`, `__label`) | 3–4 facts in a row, each with its source |
package/docs/theming.md CHANGED
@@ -98,7 +98,7 @@ the panels; in `tight` that is the 16px the header was designed with.
98
98
  - **Gradient or accent-coloured words in a heading.** A heading is ink, with a muted first sentence (`.gw-dim`).
99
99
  - **Accent-coloured section bands.** A section is white, `--gw-bg-soft` or a night band; the accent only lights a
100
100
  night band's top edge.
101
- - **All-caps labels.** Labels are sentence case, muted, led by the 8px accent square.
101
+ - **All-caps labels.** Labels are sentence case, muted, led by the 8px square (ink, or the colour `--gw-label-mark` gives it).
102
102
  - **A light on a heading, or in more than one colour.** The spotlight lights things you click, in the accent only.
103
103
 
104
104
  These make a page louder without making it clearer, and `check` reports the last one (`uppercase-label`) and any
package/js/website.js CHANGED
@@ -1,9 +1,9 @@
1
- /*! @gemboss/website-ui 0.11.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
1
+ /*! @gemboss/website-ui 0.12.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
2
2
  (function () {
3
3
  'use strict';
4
4
  if (window.gwWebsite) return;
5
5
 
6
- var version = '0.11.0';
6
+ var version = '0.12.0';
7
7
  var api = { version: version, init: init, destroy: destroy };
8
8
  var doc, root, mine, fine, sys, wide, desk, mid, live = [], queued = false;
9
9
  var still, lights;
@@ -483,17 +483,41 @@ function theme(ctx) {
483
483
  relabel();
484
484
  }
485
485
 
486
+ var REVEAL = { threshold: 0.12, margin: '0px 0px -6% 0px', stagger: 70, staggerMax: 380, failsafe: 1600, count: 1100 };
487
+ function countUp(el) {
488
+ var n = el.firstChild, m = n && n.nodeType === 3 && /^(\s*\D*?)(\d[\d,]*(?:\.\d+)?)(.*)$/.exec(n.nodeValue), t0;
489
+ if (!m) return;
490
+ var num = m[2].replace(/,/g, ''), dec = (num.split('.')[1] || '').length;
491
+ function fmt(v) { v = v.toFixed(dec); return m[1] + (num !== m[2] ? Number(v).toLocaleString('en-US') : v) + m[3]; }
492
+ n.nodeValue = fmt(0);
493
+ requestAnimationFrame(function step(ts) {
494
+ var p = Math.min((ts - (t0 = t0 || ts)) / REVEAL.count, 1);
495
+ n.nodeValue = fmt(p < 1 ? num * (1 - Math.pow(1 - p, 3)) : +num);
496
+ if (p < 1) requestAnimationFrame(step);
497
+ });
498
+ }
486
499
  function reveal(ctx) {
487
- var els = [], io = null;
488
- each(ctx, '.gw-reveal', 'reveal', function (el) { els.push(el); return { stop: function () { if (io) io.unobserve(el); } }; });
489
- function showAll() { els.forEach(function (el) { el.classList.add('is-in'); }); if (io) io.disconnect(); }
500
+ var els = [], io, calm = still.matches;
501
+ each(ctx, '.gw-reveal', 'reveal', function (el) {
502
+ els.push(el);
503
+ return { stop: function () { if (io) io.unobserve(el); el.style.transitionDelay = ''; } };
504
+ });
505
+ function show(el) {
506
+ if (el.classList.contains('is-in')) return;
507
+ el.classList.add('is-in');
508
+ if (!calm) [].forEach.call(el.querySelectorAll('[data-gw-count]'), countUp);
509
+ }
490
510
  if (!els.length) return;
491
- if (!window.IntersectionObserver || mq('(prefers-reduced-motion: reduce)').matches) return showAll();
511
+ if (!window.IntersectionObserver || calm) return els.forEach(show);
512
+ els.forEach(function (el) {
513
+ var i = [].filter.call(el.parentNode.children, function (c) { return c.classList.contains('gw-reveal'); }).indexOf(el);
514
+ if (i > 0) el.style.transitionDelay = Math.min(i * REVEAL.stagger, REVEAL.staggerMax) + 'ms';
515
+ });
492
516
  io = new IntersectionObserver(function (list) {
493
- list.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); } });
494
- }, { threshold: 0.15 });
517
+ list.forEach(function (en) { if (en.isIntersecting) { show(en.target); io.unobserve(en.target); } });
518
+ }, { threshold: REVEAL.threshold, rootMargin: REVEAL.margin });
495
519
  els.forEach(function (el) { io.observe(el); });
496
- setTimeout(showAll, 2500);
520
+ setTimeout(function () { els.forEach(show); io.disconnect(); }, REVEAL.failsafe);
497
521
  }
498
522
 
499
523
  function scrolled() {