@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.
@@ -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 */
@@ -258,6 +266,13 @@
258
266
  }
259
267
  .gw-window__body:has(> img:only-child) + .gw-window__caption { padding-top: 12px; }
260
268
 
269
+ /* figure (0.13.0, Gemians): a photo and its caption where a section shows one outside the prose (beside words in a
270
+ split, under a hero's actions): the picture at the card radius, the caption 14px, muted, 12px under it, as a figure
271
+ in .gw-prose. A screen of the product goes in .gw-window instead. */
272
+ .gw-figure > :is(img, video, picture) { display: block; width: 100%; height: auto; border-radius: var(--gw-radius-card); }
273
+ .gw-figure > picture > img { display: block; width: 100%; height: auto; border-radius: inherit; }
274
+ .gw-figure > figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
275
+
261
276
  /* icon: 20px inline stroke SVG (1.7 to 2px stroke, currentColor) */
262
277
  .gw-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 20px; height: 20px; }
263
278
  .gw-icon svg { width: 20px; height: 20px; }
@@ -334,16 +349,21 @@
334
349
  .gw-faq__item[open] > summary::after { transform: rotate(45deg); }
335
350
  .gw-faq__item > :not(summary) { margin: 0 0 20px; max-width: 68ch; color: var(--gw-muted); font-size: 16px; }
336
351
 
337
- /* touch: every control is at least 44px tall, labels may wrap */
338
- @media (max-width: 768px), (pointer: coarse) {
352
+ /* touch: every control is at least 44px tall. A button up to 768px (0.12.0, as gemboss.ai: by the width, not the
353
+ pointer), its label wrapping (and tighter) from 480px down in the page's <main>, so a long one never pushes the page
354
+ sideways (a header or an overlay outside <main> keeps its one line) */
355
+ @media (max-width: 768px) {
339
356
  .gw-btn, .gw-btn--sm, .gw-btn--lg {
340
357
  height: auto;
341
358
  min-height: var(--gw-tap-min);
342
359
  padding-block: 8px;
343
- white-space: normal;
344
- text-wrap: balance;
345
360
  }
346
361
  .gw-btn--lg { min-height: 48px; }
362
+ }
363
+ @media (max-width: 480px) {
364
+ :where(main) .gw-btn { white-space: normal; line-height: 1.35; }
365
+ }
366
+ @media (max-width: 768px), (pointer: coarse) {
347
367
  :is(a, button, label).gw-chip { min-height: var(--gw-tap-min); }
348
368
  .gw-segmented > button { height: auto; min-height: var(--gw-tap-min); padding-block: 8px; white-space: normal; text-wrap: balance; }
349
369
  }
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;
@@ -669,3 +703,16 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
669
703
  :root:not([data-theme="light"]) .gw-theme-toggle__sun { display: block; }
670
704
  :root:not([data-theme="light"]) .gw-theme-toggle__moon { display: none; }
671
705
  }
706
+
707
+ /* ---- 0.13.0: the brand's logo as one drawing (Gemians): the mark and the wordmark in one file, in place of the mark and
708
+ the name, 26px tall, its width the file's. Its alt is the brand's name. A logo drawn for dark grounds only (a white
709
+ wordmark) comes as a second file, .gw-brand__logo--dark, right after the first: it shows in dark (the system's or the
710
+ toggle's) and the first one hides, the toggle icons' rule. One file that reads on both: no --dark. */
711
+ .gw-brand__logo { display: block; flex: none; width: auto; height: 26px; }
712
+ .gw-brand__logo--dark { display: none; }
713
+ :root[data-theme="dark"] .gw-brand__logo--dark { display: block; }
714
+ :root[data-theme="dark"] .gw-brand__logo:has(+ .gw-brand__logo--dark) { display: none; }
715
+ @media (prefers-color-scheme: dark) {
716
+ :root:not([data-theme="light"]) .gw-brand__logo--dark { display: block; }
717
+ :root:not([data-theme="light"]) .gw-brand__logo:has(+ .gw-brand__logo--dark) { display: none; }
718
+ }
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/ratings.css CHANGED
@@ -26,7 +26,6 @@
26
26
  display: grid;
27
27
  grid-template-columns: minmax(0, 1fr);
28
28
  gap: 16px;
29
- margin: 0;
30
29
  padding: 0;
31
30
  list-style: none;
32
31
  }
package/css/sections.css CHANGED
@@ -13,7 +13,7 @@
13
13
  radial-gradient(420px 320px at 52% 118%, color-mix(in srgb, var(--gw-wash-mint) 10%, transparent), transparent 62%);
14
14
  }
15
15
  .gw-hero h1 .gw-dim { display: block; }
16
- .gw-hero > .gw-wrap > .gw-window { margin-top: 48px; }
16
+ .gw-hero > .gw-wrap > :is(.gw-window, .gw-figure) { margin-top: 48px; }
17
17
  .gw-hero--center { text-align: center; }
18
18
  .gw-hero--center > .gw-wrap > * { margin-inline: auto; }
19
19
  .gw-hero--center .gw-actions { justify-content: center; }
@@ -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 */
@@ -46,7 +46,7 @@
46
46
  .gw-logos img { width: auto; height: 28px; }
47
47
 
48
48
  /* stats: three or four facts, each with its source */
49
- .gw-stats { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; margin: 0; padding: 0; list-style: none; }
49
+ .gw-stats { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; padding: 0; list-style: none; }
50
50
  .gw-stats__value { font-size: var(--gw-display-h2); font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
51
51
  .gw-stats__label { margin-top: 8px; max-width: 28ch; color: var(--gw-muted); font-size: 14px; }
52
52
  .gw-stats__source { margin-top: 8px; color: var(--gw-muted); font-size: 12px; }
@@ -79,7 +79,6 @@
79
79
  display: grid;
80
80
  grid-template-columns: minmax(0, 1fr);
81
81
  gap: 32px 24px;
82
- margin: 0;
83
82
  padding: 0;
84
83
  list-style: none;
85
84
  counter-reset: gw-step;
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">
@@ -270,6 +274,24 @@ the mock-up shows a demo brand ("Vital Glow is a GemBoss demo brand."). Under re
270
274
  Don't: put interactive controls inside a mock-up; show made-up numbers without the caption; write the caption in
271
275
  other words ("Sample", "Demo numbers") or leave it in English on a translated page.
272
276
 
277
+ ### Figure `.gw-figure`
278
+
279
+ 0.13.0 (Gemians): a photo and its caption outside the prose, where a section shows one: beside the words of a split,
280
+ under a hero's actions (48px below, as a window). The picture takes the card radius; the `<figcaption>` is 14px,
281
+ muted, 12px under it, the look of a figure inside `.gw-prose`. A screen of the product goes in `.gw-window`; a photo
282
+ of people, a place or a product goes here.
283
+
284
+ ```html
285
+ <figure class="gw-figure">
286
+ <img src="/img/team-2025.webp" alt="The team together in the office, 2025" width="1000" height="471" loading="lazy">
287
+ <figcaption>The GemCommerce team, 2025.</figcaption>
288
+ </figure>
289
+ ```
290
+
291
+ Don't: a photo of people without their permission, or of another brand without its; a caption that repeats the alt
292
+ word for word (the alt says what is in the picture, the caption says what it is); a frame, a shadow or a radius of
293
+ your own.
294
+
273
295
  ### Icon `.gw-icon`
274
296
 
275
297
  A 20px box for an inline stroke SVG (1.7–2px stroke, `currentColor`, `aria-hidden="true"`). Inside `.gw-feature` it
@@ -332,11 +354,12 @@ level. A brand changes what it fills in and the tokens (colours, the Stage, the
332
354
 
333
355
  | class | for |
334
356
  |---|---|
335
- | `.gw-announce` | optional one-sentence bar above the header, with one link |
357
+ | `.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
358
  | `.gw-header` (`data-gw-header`) | the sticky, flat 64px bar with one hairline; `.is-scrolled` darkens the hairline |
337
359
  | `.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
360
  | `.gw-header__bar` | the row inside: the page's wrap + 12px (1132px on GemBoss), 24px inside (6px up to 640px), 32px between its parts |
339
361
  | `.gw-brand`, `__mark`, `__name` | logo (34px, its corners clipped at 8px) and name, linking home; a 44px row up to 768px |
362
+ | `.gw-brand__logo`, `--dark` | 0.13.0: a brand's logo drawn as one file (mark and wordmark), 26px tall, in place of `__mark` and `__name`, `alt` the brand's name; `--dark` a second file shown in dark only, right after the first (docs/theming.md) |
340
363
  | `.gw-nav`, `__list`, `__item`, `__trigger`, `__chev`, `__link` | the desktop nav (from 1081px), 14px weight 500, 0.005em tighter; `__trigger` is a section's link that opens its mega panel (its 12px `__chev` turns while open), `__link` a plain link; the current page (`aria-current="page"`) or section (`aria-current` on a trigger) in the accent's strong ink, weight 600 |
341
364
  | `.gw-mega` (`--gw-mega-w`, `--gw-mega-pv`, `--gw-mega-cols`), `__body`, `__grid`, `__col`, `__heading`, `__link`, `__aside`, `__card`, `__art`, `__card-text`, `__go`, `__foot`, `__foot-start`, `__foot-end`, `__cta`, `__new` | 0.11.0, gemboss.ai's mega menu: a panel on one surface that morphs between panels (the popover radius, raised shadow), a caret at the open name, a pill under the hovered name, a highlight under the hovered row; columns of rows (a name and one line), the pictures beside them (`__aside`: the panel's own `__card`, a row's preview from its `<template>`), each picture on the Stage (`__art`: the deep iris ground, `--gw-stage-*`); an optional strip under it. The page behind dims and blurs (`.gw-scrim`). Script: `docs/markup-contract.md` |
342
365
  | `.gw-header__actions` | Sign in, a second text action, Start free, the menu button (4px apart; the button 8px further) |
@@ -838,10 +861,12 @@ not make public with the review; masonry or a carousel that moves by itself.
838
861
 
839
862
  | class | for |
840
863
  |---|---|
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` |
864
+ | `.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` |
865
+ | `[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
866
 
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`.
867
+ 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
868
+ or anything in the first screen, and don't animate anything but `transform` and `opacity`. A count is for a number
869
+ that is a measure (a total, a percentage), never for a step's ordinal ("01").
845
870
 
846
871
  ### Spotlight `data-gw-spot` (0.9.0)
847
872
 
@@ -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
@@ -3,8 +3,9 @@
3
3
  A GemCommerce brand (GemPages, Gemians…) uses the kit as it is and chooses six things, **and nothing else**:
4
4
 
5
5
  1. **the accent variables** (eight, light and dark),
6
- 2. **the wash hues** (only if the brand has its own),
7
- 3. **the logo** (and the favicon),
6
+ 2. **the wash hues** (only if the brand has its own), and the colours of its **announcement bar** and **Stage** (0.13.0,
7
+ only if it uses them and wants its own),
8
+ 3. **the logo** (and the favicon): a mark beside the name, or one drawing of both (0.13.0),
8
9
  4. **the action colour mode**: `data-gw-action="ink|accent"` on `<html>`,
9
10
  5. **the radius profile**: `data-gw-radius="tight|soft"` on `<html>`,
10
11
  6. **the header style**: the flat bar, or `.gw-header--float`.
@@ -46,9 +47,30 @@ brand's page lights in the brand's own colour, in light, in dark and in night ba
46
47
  takes the corners of the element it lights, so both radius profiles hold. A brand does not choose its colour, its
47
48
  strength or its size (they are tokens), and never a second colour.
48
49
 
49
- The logo and the favicon are files, not variables: the `<img class="gw-brand__mark">` (26×26, SVG preferred, with
50
- `alt=""` because the name is written next to it) and the `<span class="gw-brand__name">` in the header and footer
51
- partials, and `<link rel="icon">` in the page head.
50
+ The logo and the favicon are files, not variables, in the header and footer partials and `<link rel="icon">` in the
51
+ page head. The logo is one of two shapes:
52
+
53
+ - **a mark beside the name** (GemBoss): `<img class="gw-brand__mark">` (34×34, SVG preferred, `alt=""` because the name
54
+ is written next to it) and `<span class="gw-brand__name">`;
55
+ - **one drawing of both** (0.13.0, Gemians: the mark and the wordmark in one file): `<img class="gw-brand__logo">`, 26px
56
+ tall, its width the file's, its `alt` the brand's name. A logo that only works on a dark ground (a white wordmark)
57
+ comes with a second file in ink for the light theme, or the other way round: the file for light grounds first, then
58
+ the file for dark grounds as `class="gw-brand__logo gw-brand__logo--dark"`. The kit shows one per theme (the system's
59
+ setting or the toggle). Keep the brand's own drawing: recolour the wordmark, never redraw it in the kit's type.
60
+
61
+ ```html
62
+ <a class="gw-brand" href="/"><img class="gw-brand__logo" src="/assets/img/logo.png" alt="Gemians" width="150" height="26"><img class="gw-brand__logo gw-brand__logo--dark" src="/assets/img/logo-on-dark.png" alt="Gemians" width="150" height="26"></a>
63
+ ```
64
+
65
+ `check` reports a logo without its name in `alt`, without `width` and `height`, or a dark file without the light one
66
+ right before it (`brand-markup`).
67
+
68
+ **The announcement bar and the Stage** (0.13.0). The bar over the header (`--gw-announce-*`, `system.json`
69
+ `color.announce`) and the Stage the mega menu's pictures sit on (`--gw-stage-*`, `color.stage`) are GemBoss's iris by
70
+ default. A brand that shows them may set its own in the same `:root` block: the bar's `bg`, `ink`, `line`, `tag`,
71
+ `tag-ink`, `link`, `link-line`; the Stage's `glow`, `glow-2`, `top`, `bottom`, `dot`, `ink`. Both stay the same in
72
+ light and dark. `check` computes the bar's words and link on its ground and the Stage's words on both ends of its
73
+ gradient, and fails below 4.5:1 (`brand-contrast`).
52
74
 
53
75
  The footer (0.7.0, `templates/footer.html`) is the same for every brand; a brand fills it. Its family row lists the
54
76
  brand's siblings (a GemPages site: GemBoss, GemX, GemUpsells), its Ask AI row asks about the brand, and its wordmark
@@ -98,7 +120,7 @@ the panels; in `tight` that is the 16px the header was designed with.
98
120
  - **Gradient or accent-coloured words in a heading.** A heading is ink, with a muted first sentence (`.gw-dim`).
99
121
  - **Accent-coloured section bands.** A section is white, `--gw-bg-soft` or a night band; the accent only lights a
100
122
  night band's top edge.
101
- - **All-caps labels.** Labels are sentence case, muted, led by the 8px accent square.
123
+ - **All-caps labels.** Labels are sentence case, muted, led by the 8px square (ink, or the colour `--gw-label-mark` gives it).
102
124
  - **A light on a heading, or in more than one colour.** The spotlight lights things you click, in the accent only.
103
125
 
104
126
  These make a page louder without making it clearer, and `check` reports the last one (`uppercase-label`) and any
@@ -106,8 +128,9 @@ button filled from site CSS (`accent-button`).
106
128
 
107
129
  `check` also reports: colours outside the tokens, a redefined kit token (`token-redefined`, including the radius and
108
130
  action tokens), overrides of `.gw-*` classes, an unknown profile or one set on another element than `<html>`
109
- (`profile-unknown`), `.gw-header--float` anywhere but the header itself (`header-float-markup`), and, on a page with
110
- `data-gw-action="accent"`, accent button words below 4.5:1 (`action-contrast`).
131
+ (`profile-unknown`), `.gw-header--float` anywhere but the header itself (`header-float-markup`), on a page with
132
+ `data-gw-action="accent"`, accent button words below 4.5:1 (`action-contrast`), a brand's own announcement bar or
133
+ Stage with words below 4.5:1 (`brand-contrast`), and a one-file logo without its markup (`brand-markup`).
111
134
 
112
135
  ## A full example
113
136
 
@@ -136,7 +159,7 @@ action tokens), overrides of `.gw-*` classes, an unknown profile or one set on a
136
159
  …
137
160
  <header class="gw-header gw-header--float" data-gw-header>
138
161
  <div class="gw-wrap gw-header__bar">
139
- <a class="gw-brand" href="/"><img class="gw-brand__mark" src="/logo-mark.svg" alt="" width="26" height="26"><span class="gw-brand__name">GemPages</span></a>
162
+ <a class="gw-brand" href="/"><img class="gw-brand__mark" src="/logo-mark.svg" alt="" width="34" height="34"><span class="gw-brand__name">GemPages</span></a>
140
163
  ```
141
164
 
142
165
  ## Check the contrast before you ship
@@ -145,8 +168,10 @@ The accent family is read as text: the kit draws links, step numbers, active men
145
168
  `--gw-accent-strong`, and the label square, focus ring, check marks and the featured plan's hairline in `--gw-accent`.
146
169
  So:
147
170
 
148
- 1. **Accent text on white at least 4.5:1**: `--gw-accent-light` and `--gw-accent-strong-light` on `#ffffff`.
149
- Check `--gw-accent-strong-light` on `--gw-bg-soft` (`#f2f3f5`) too, where links sit in soft sections.
171
+ 1. **Accent text on white at least 4.5:1**: `--gw-accent-strong-light` on `#ffffff` and on `--gw-bg-soft`
172
+ (`#f2f3f5`), where links sit in soft sections. The plain `--gw-accent-light` is never text (icons, marks, the focus
173
+ ring, the featured plan's hairline): **3:1** on `#ffffff` is enough (WCAG 1.4.11), so a gold or a light green can
174
+ stay bright there (0.13.0; this line asked 4.5:1 of it before, which no gold passes).
150
175
  2. `--gw-accent-strong-light` on `--gw-accent-soft-light`: at least 4.5:1 (the accent badge, the active chip and the
151
176
  chosen option of a segmented control).
152
177
  3. `--gw-accent-dark` and `--gw-accent-strong-dark` on `#0d0d12` (dark page) and on `#0c0e14` (night band): at
@@ -158,7 +183,9 @@ So:
158
183
  build does the same for its own colours.
159
184
 
160
185
  Any WCAG contrast checker works (paste the two hex values). A bright brand colour (yellow, light green, cyan) almost
161
- never passes on white: darken it for `-light` and keep the bright one for `-dark`. If you cannot get 4.5:1 without
186
+ never passes as text on white: darken it for `-strong-light`, keep it near the brand for `-light` (3:1) and bright
187
+ for `-dark`. A colour that needs dark words on it (gold, yellow) cannot be the button's fill and its text colour at
188
+ once: keep the ink button (Gemians does). If you cannot get 4.5:1 without
162
189
  changing the brand colour beyond recognition, keep the kit's accent and use the brand colour only in the logo and
163
190
  illustrations, and keep the ink button.
164
191
 
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.13.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.13.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() {