@cogenta/theme-canonical 1.1.3 → 1.3.4

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.
Files changed (71) hide show
  1. package/dist/src/index.d.ts +1 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +1 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/render/arrow-link.d.ts +11 -0
  6. package/dist/src/render/arrow-link.d.ts.map +1 -0
  7. package/dist/src/render/arrow-link.js +29 -0
  8. package/dist/src/render/arrow-link.js.map +1 -0
  9. package/dist/src/render/blocks/collection-list.d.ts +15 -5
  10. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  11. package/dist/src/render/blocks/collection-list.js +36 -13
  12. package/dist/src/render/blocks/collection-list.js.map +1 -1
  13. package/dist/src/render/blocks/embed.d.ts.map +1 -1
  14. package/dist/src/render/blocks/embed.js +3 -2
  15. package/dist/src/render/blocks/embed.js.map +1 -1
  16. package/dist/src/render/blocks/gallery.d.ts.map +1 -1
  17. package/dist/src/render/blocks/gallery.js +4 -1
  18. package/dist/src/render/blocks/gallery.js.map +1 -1
  19. package/dist/src/render/blocks/hero.d.ts.map +1 -1
  20. package/dist/src/render/blocks/hero.js +9 -2
  21. package/dist/src/render/blocks/hero.js.map +1 -1
  22. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
  23. package/dist/src/render/blocks/pricing-table.js +17 -4
  24. package/dist/src/render/blocks/pricing-table.js.map +1 -1
  25. package/dist/src/render/chrome.d.ts +40 -1
  26. package/dist/src/render/chrome.d.ts.map +1 -1
  27. package/dist/src/render/chrome.js +103 -71
  28. package/dist/src/render/chrome.js.map +1 -1
  29. package/dist/src/render/dates.d.ts +9 -0
  30. package/dist/src/render/dates.d.ts.map +1 -0
  31. package/dist/src/render/dates.js +16 -0
  32. package/dist/src/render/dates.js.map +1 -0
  33. package/dist/src/render/render-block.d.ts.map +1 -1
  34. package/dist/src/render/render-block.js +7 -1
  35. package/dist/src/render/render-block.js.map +1 -1
  36. package/dist/src/render/strings.d.ts +25 -0
  37. package/dist/src/render/strings.d.ts.map +1 -0
  38. package/dist/src/render/strings.js +26 -0
  39. package/dist/src/render/strings.js.map +1 -0
  40. package/dist/src/render/term-archive.d.ts +5 -5
  41. package/dist/src/render/term-archive.d.ts.map +1 -1
  42. package/dist/src/render/term-archive.js +14 -12
  43. package/dist/src/render/term-archive.js.map +1 -1
  44. package/dist/src/render/widget-areas.d.ts +20 -0
  45. package/dist/src/render/widget-areas.d.ts.map +1 -0
  46. package/dist/src/render/widget-areas.js +40 -0
  47. package/dist/src/render/widget-areas.js.map +1 -0
  48. package/package.json +5 -5
  49. package/src/index.ts +1 -0
  50. package/src/render/arrow-link.ts +27 -0
  51. package/src/render/blocks/collection-list.ts +53 -15
  52. package/src/render/blocks/embed.ts +3 -2
  53. package/src/render/blocks/gallery.ts +4 -1
  54. package/src/render/blocks/hero.ts +9 -2
  55. package/src/render/blocks/pricing-table.ts +22 -4
  56. package/src/render/chrome.ts +156 -70
  57. package/src/render/dates.ts +14 -0
  58. package/src/render/render-block.ts +6 -0
  59. package/src/render/strings.ts +30 -0
  60. package/src/render/term-archive.ts +54 -32
  61. package/src/render/widget-areas.ts +44 -0
  62. package/src/styles/archive.css +66 -20
  63. package/src/styles/base.css +266 -680
  64. package/src/styles/blocks.css +678 -811
  65. package/src/styles/chrome.css +469 -0
  66. package/src/styles/listings.css +183 -0
  67. package/src/styles/theme.css +28 -14
  68. package/src/styles/tokens.css +192 -205
  69. package/src/styles/utility.css +295 -0
  70. package/src/styles/widgets.css +1064 -0
  71. package/tokens.json +15 -15
@@ -1,8 +1,7 @@
1
1
  /*
2
- * Document, accessibility floor, page frame and the one control the theme
3
- * ships (an action link). Everything here reads the design system from
4
- * `tokens.css`; nothing here reads a `--cogenta-*` token directly, so a value
5
- * that needs changing is changed in one place.
2
+ * Document, accessibility floor, the page frame, page and entry titles,
3
+ * actions and the four per-block variants. Everything here reads the design
4
+ * system from `tokens.css`.
6
5
  */
7
6
 
8
7
  /* --------------------------------------------------------------------------
@@ -15,206 +14,218 @@
15
14
  box-sizing: border-box;
16
15
  }
17
16
 
17
+ html {
18
+ -webkit-text-size-adjust: 100%;
19
+ text-size-adjust: 100%;
20
+ }
21
+
18
22
  body {
19
23
  margin: 0;
20
24
  background: var(--cg-canvas);
21
25
  color: var(--cg-ink);
22
- font-family: var(--cogenta-font-sans);
26
+ font-family: var(--cg-font-text);
23
27
  font-size: var(--cg-text-md);
24
- line-height: var(--cg-leading-normal);
25
- /* Two opt-in font features rather than a font choice: `kern` is what stops
26
- display type looking loose, `liga` what stops "fi" breaking apart. Both
27
- are no-ops on a font that lacks them. */
28
+ line-height: var(--cg-leading-body);
28
29
  font-kerning: normal;
29
30
  font-variant-ligatures: common-ligatures;
30
- text-underline-offset: 0.16em;
31
- text-decoration-thickness: from-font;
32
- -webkit-text-size-adjust: 100%;
31
+ /* A family that lacks a weight or an italic shows the real face, never a
32
+ smeared imitation of one. */
33
+ font-synthesis: none;
34
+ text-rendering: optimizeLegibility;
33
35
  }
34
36
 
35
37
  ::selection {
36
- background: var(--cg-accent-soft);
37
- color: var(--cg-accent-soft-fg);
38
+ background: var(--cg-selection);
39
+ color: var(--cg-ink);
40
+ }
41
+
42
+ :where(h1, h2, h3, h4, h5, h6) {
43
+ margin: 0;
44
+ font-family: var(--cg-font-text);
45
+ font-weight: var(--cg-weight-semibold);
46
+ line-height: var(--cg-leading-heading);
47
+ letter-spacing: var(--cg-tracking-heading);
48
+ text-wrap: balance;
38
49
  }
39
50
 
40
- img,
41
- video,
42
- iframe {
43
- max-width: 100%;
44
- height: auto;
51
+ :where(p, li, dd, figcaption, blockquote) {
52
+ text-wrap: pretty;
53
+ }
54
+
55
+ :where(p, ul, ol, dl, dd, figure, blockquote) {
56
+ margin: 0;
45
57
  }
46
58
 
47
- img {
59
+ /* Dates, years, prices and figures keep their width from one line to the
60
+ next. */
61
+ :where(data, time) {
62
+ font-variant-numeric: tabular-nums;
63
+ }
64
+
65
+ :where(img, video, iframe) {
48
66
  display: block;
67
+ max-inline-size: 100%;
68
+ block-size: auto;
69
+ }
70
+
71
+ :where(img, video) {
49
72
  object-fit: cover;
73
+ filter: brightness(var(--cg-image-brightness));
50
74
  }
51
75
 
52
76
  :where(a) {
53
77
  color: var(--cg-accent);
54
- text-decoration-color: color-mix(in oklab, var(--cg-accent) 45%, transparent);
78
+ text-decoration-line: underline;
79
+ text-decoration-thickness: var(--cg-rule);
80
+ text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);
81
+ text-underline-offset: 0.2em;
82
+ text-decoration-skip-ink: auto;
55
83
  transition:
56
- color var(--cg-duration-fast) var(--cg-ease),
57
- text-decoration-color var(--cg-duration-fast) var(--cg-ease);
84
+ color var(--cg-duration) var(--cg-ease),
85
+ text-decoration-color var(--cg-duration) var(--cg-ease);
58
86
  }
59
87
 
60
- :where(a:hover) {
88
+ :where(a):hover {
61
89
  color: var(--cg-accent-hover);
62
90
  text-decoration-color: currentColor;
63
91
  }
64
92
 
65
93
  :where(hr) {
66
- block-size: 1px;
94
+ block-size: 0;
95
+ margin: 0;
67
96
  border: 0;
68
- margin-block: var(--cg-s10);
69
- background: var(--cg-line);
97
+ border-block-start: var(--cg-rule) solid var(--cg-line);
70
98
  }
71
99
 
72
100
  /* --------------------------------------------------------------------------
73
- Focus, targets, motion — the three that decide whether AA holds
101
+ Focus, targets, the skip link
74
102
  -------------------------------------------------------------------------- */
75
103
 
76
- :where(a, button, summary, [tabindex]):focus-visible {
77
- /* Two solid pixels against the page, offset so it never sits on the glyphs.
78
- `outline`, not `box-shadow`: it survives forced-colors mode. */
79
- outline: 2px solid var(--cg-accent);
104
+ :where(a, button, summary, input, select, textarea, [tabindex]):focus-visible {
105
+ /* An outline, not a shadow: it survives forced-colours mode. */
106
+ outline: var(--cg-rule-strong) solid var(--cg-accent);
80
107
  outline-offset: 3px;
81
- border-radius: var(--cg-radius-sm);
82
- }
83
-
84
- /* WCAG 2.2 asks 24x24 minimum (2.5.8); 44x44 is the comfortable target and
85
- costs nothing here. Applied to the theme's own controls only — an inline link
86
- inside a paragraph is exempt, and padding it would break the text flow. */
87
- :where(.cg-action, .cg-faq__question, .cg-logo__link) {
88
- min-block-size: 44px;
89
- min-inline-size: 44px;
90
- }
91
-
92
- /* The entrance animation the blocks opt into. Scroll-driven, so it plays as a
93
- block arrives rather than firing eleven at once on load, and wrapped in a
94
- feature query because the starting state is `opacity: 0`: a browser without
95
- `animation-timeline` must never be left holding an invisible page. */
96
- @supports (animation-timeline: view()) {
97
- @media (prefers-reduced-motion: no-preference) {
98
- @keyframes cg-rise {
99
- from {
100
- opacity: 0;
101
- transform: translateY(var(--cg-s4));
102
- }
103
-
104
- to {
105
- opacity: 1;
106
- transform: none;
107
- }
108
- }
109
-
110
- .cg-block {
111
- animation: cg-rise linear both;
112
- animation-timeline: view();
113
- /* Fully in by the time the block is a fifth of the way up the viewport;
114
- it never animates back out on the way past. */
115
- animation-range: entry 0% cover 22%;
116
- }
117
- }
118
- }
119
-
120
- @media (prefers-reduced-motion: reduce) {
121
- /* The `motion.reduced` token says the skin's motion may be removed. Nothing
122
- in this theme conveys meaning through movement, so removing all of it is
123
- safe — including the smooth scroll of the carousel, which is the one people
124
- forget and which triggers vestibular symptoms. */
125
- *,
126
- *::before,
127
- *::after {
128
- animation-duration: 1ms !important;
129
- animation-iteration-count: 1 !important;
130
- transition-duration: 1ms !important;
131
- scroll-behavior: auto !important;
132
- }
133
108
  }
134
109
 
135
110
  .cg-skip-link {
136
111
  position: absolute;
137
- inset-inline-start: var(--cg-s3);
138
- inset-block-start: calc(var(--cg-s3) * -8);
112
+ inset-inline-start: var(--cg-gutter);
113
+ inset-block-start: var(--cg-s2);
139
114
  z-index: 10;
140
- padding: var(--cg-s2) var(--cg-s4);
141
- background: var(--cg-accent);
142
- color: var(--cg-accent-fg);
143
- border-radius: var(--cg-radius-md);
144
- box-shadow: var(--cg-elevation-2);
115
+ padding: var(--cg-s3) var(--cg-s5);
116
+ background: var(--cg-action);
117
+ color: var(--cg-action-fg);
145
118
  font-weight: var(--cg-weight-semibold);
146
119
  text-decoration: none;
147
- /* Moved, never hidden: `display:none` would take it out of the tab order,
148
- which is the whole point of a skip link (WCAG 2.4.1). */
149
- transition: inset-block-start var(--cg-duration) var(--cg-ease);
120
+ /* Moved off screen, never hidden: `display: none` would take it out of the
121
+ tab order, which is the whole point of a skip link (WCAG 2.4.1). */
122
+ translate: 0 -200%;
150
123
  }
151
124
 
152
125
  .cg-skip-link:focus-visible {
153
- inset-block-start: var(--cg-s3);
126
+ translate: 0 0;
127
+ }
128
+
129
+ /* The standard clip technique: read by assistive technology, not drawn.
130
+ `renderSocialLinks` (`@cogenta/theme-kit`) depends on this class existing
131
+ in every theme that renders it. */
132
+ .cg-visually-hidden {
133
+ position: absolute;
134
+ inline-size: 1px;
135
+ block-size: 1px;
136
+ padding: 0;
137
+ margin: -1px;
138
+ overflow: hidden;
139
+ clip-path: inset(50%);
140
+ white-space: nowrap;
141
+ border: 0;
154
142
  }
155
143
 
156
144
  /* --------------------------------------------------------------------------
157
- Page frame
145
+ The page frame
146
+ --------------------------------------------------------------------------
147
+
148
+ Every block is as wide as the page and centres its content with side
149
+ padding (`--cg-inset`), so a band colour can reach both edges of the
150
+ window while the content keeps to the page width. A block that lays out
151
+ more than one thing is a twelve-column grid on that content box, which is
152
+ what puts a section title, a card and a caption on the same left edge.
158
153
  -------------------------------------------------------------------------- */
159
154
 
160
- /* `flow-root`, so the first and last block's own margins stay inside `<main>`
161
- instead of collapsing through it and pushing the header and footer around.
162
- That also means no padding is needed here: the last block already carries a
163
- section's worth of space below itself, and adding more produced a dead band
164
- three times too tall between the last block and the footer. */
165
155
  .cg-main {
166
156
  display: flow-root;
167
157
  }
168
158
 
169
- /* The one measurement every block shares. A block that wants to break out of
170
- it says so itself (`.cg-figure[data-align="full"]`), so the default stays
171
- one rule instead of eleven. */
172
159
  .cg-block {
173
- max-inline-size: var(--cg-page);
174
- margin-inline: auto;
175
- margin-block: var(--cg-section-y);
176
- padding-inline: var(--cg-gutter);
160
+ padding-block: calc(var(--cg-section) / 2);
161
+ padding-inline: var(--cg-inset);
162
+ }
163
+
164
+ .cg-grid,
165
+ .cg-features,
166
+ .cg-cta,
167
+ .cg-gallery,
168
+ .cg-quote,
169
+ .cg-faq,
170
+ .cg-stats,
171
+ .cg-logos,
172
+ .cg-collection,
173
+ .cg-embed,
174
+ .cg-testimonial,
175
+ .cg-pricing,
176
+ .cg-accordion,
177
+ .cg-stat-counters,
178
+ .cg-logo-strip,
179
+ .cg-figure,
180
+ .cg-hero {
181
+ display: grid;
182
+ grid-template-columns: repeat(var(--cg-columns), minmax(0, 1fr));
183
+ column-gap: var(--cg-column-gap);
184
+ align-content: start;
177
185
  }
178
186
 
179
- /* --- per-block visual variant (blocks@2.0, RFC 0002) ----------------------
180
- `withBlockVariant` (`@cogenta/theme-kit`) stamps one `data-variant-<axis>`
181
- attribute per axis an author actually set — never all four, never one this
182
- theme does not resolve. Two attribute selectors give every rule here the
183
- same specificity, (0,2,0), which is what lets it override `.cg-block`
184
- above regardless of source order, without `!important` anywhere.
185
-
186
- `background: 'image'` has no theme mechanism to attach an actual image to
187
- a block instance (RFC 0002 adds only the semantic token, not a media
188
- field) — resolved to the same tinted surface as the closest honest
189
- approximation this theme has, rather than silently doing nothing with a
190
- stated author intent.
191
- ------------------------------------------------------------------------- */
192
-
193
- [data-block][data-variant-background="muted"],
194
- [data-block][data-variant-background="image"] {
195
- padding-block: var(--cg-s10);
196
- border-radius: var(--cg-radius-lg);
187
+ .cg-block > * {
188
+ grid-column: 1 / -1;
189
+ min-inline-size: 0;
197
190
  }
198
191
 
199
- [data-block][data-variant-background="muted"] {
200
- background: var(--cg-surface-sunken);
192
+ /* The heading of a section: in the text face, from the first column, under
193
+ a rule in ink that spans what the section spans. */
194
+ .cg-section-title,
195
+ .cg-features__title,
196
+ .cg-faq__title,
197
+ .cg-stats__title,
198
+ .cg-logos__title,
199
+ .cg-collection__title,
200
+ .cg-pricing__title,
201
+ .cg-accordion__title,
202
+ .cg-stat-counters__title {
203
+ padding-block-start: var(--cg-s5);
204
+ margin-block-end: var(--cg-s10);
205
+ border-block-start: var(--cg-rule) solid var(--cg-ink);
206
+ font-size: var(--cg-text-heading);
201
207
  }
202
208
 
209
+ /* --- per-block variant (blocks@2.0, RFC 0002) -----------------------------
210
+ `withBlockVariant` stamps one `data-variant-<axis>` attribute per axis an
211
+ author set. `background: image` has no image to show (RFC 0002 carries the
212
+ intent, not a media field), so it resolves to the same band as `muted`,
213
+ the closest honest rendering this theme has.
214
+ ------------------------------------------------------------------------- */
215
+
216
+ [data-block][data-variant-background="muted"],
203
217
  [data-block][data-variant-background="image"] {
204
- background: var(--cg-surface-tinted);
218
+ background: var(--cg-band);
205
219
  }
206
220
 
207
221
  [data-block][data-variant-spacing="compact"] {
208
- margin-block: calc(var(--cg-section-y) / 2);
222
+ padding-block: calc(var(--cg-section) / 4);
209
223
  }
210
224
 
211
- /* No rule for "comfortable": it names this theme's existing default, so an
212
- author who picks it explicitly gets exactly what an absent `variant`
213
- already gave them — the whole point of "theme default" being a real,
214
- selectable option and not only the absence of a choice. */
225
+ /* No rule for "comfortable": it names this theme's default rhythm. */
215
226
 
216
227
  [data-block][data-variant-spacing="spacious"] {
217
- margin-block: calc(var(--cg-section-y) * 1.6);
228
+ padding-block: calc(var(--cg-section) * 0.8);
218
229
  }
219
230
 
220
231
  [data-block][data-variant-align="start"] {
@@ -230,634 +241,209 @@ img {
230
241
  }
231
242
 
232
243
  [data-block][data-variant-width="full"] {
233
- max-inline-size: none;
234
- padding-inline: 0;
244
+ padding-inline: var(--cg-gutter);
235
245
  }
236
246
 
247
+ /* --------------------------------------------------------------------------
248
+ Page title — a page without a hero and without entry furniture, and the
249
+ search, form and archive pages the host renders
250
+ -------------------------------------------------------------------------- */
251
+
237
252
  .cg-page__title {
238
- max-inline-size: var(--cg-page);
239
- margin-inline: auto;
240
- margin-block: var(--cg-section-y) var(--cg-s6);
241
- padding-inline: var(--cg-gutter);
242
- font-size: var(--cg-text-3xl);
243
- line-height: var(--cg-leading-tight);
244
- letter-spacing: var(--cg-tracking-tight);
245
- font-weight: var(--cg-weight-bold);
246
- text-wrap: balance;
253
+ padding-block: calc(var(--cg-section) * 0.6) calc(var(--cg-section) / 4);
254
+ padding-inline: var(--cg-inset);
255
+ font-family: var(--cg-font-display);
256
+ font-size: var(--cg-text-title);
257
+ font-weight: var(--cg-weight-regular);
258
+ line-height: var(--cg-leading-display);
259
+ letter-spacing: var(--cg-tracking-display);
260
+ }
261
+
262
+ @media (min-width: 64rem) {
263
+ .cg-page__title {
264
+ /* Ten columns at most, so a long title breaks into lines a reader takes
265
+ in at once. */
266
+ padding-inline-end: calc(var(--cg-inset) + (100% - 2 * var(--cg-inset)) / 6);
267
+ }
247
268
  }
248
269
 
249
270
  /* --------------------------------------------------------------------------
250
- Entry header (`renderEntryHeader`, `@cogenta/theme-kit`, contract D
251
- `theme@1.4`) — the article furniture (`PageContent.entry`) a `richText`
252
- entry carries beyond its blocks: an eyebrow of taxonomy terms, the title
253
- (replacing the bare `.cg-page__title` this same page would otherwise get
254
- — `render-block.ts` renders exactly one of the two), an excerpt, a meta
255
- line, a cover image.
271
+ Entry header (`renderEntryHeader`, `@cogenta/theme-kit`, `theme@1.4`)
256
272
  -------------------------------------------------------------------------- */
257
273
 
258
274
  .cg-entry-header {
259
- max-inline-size: var(--cg-page);
260
- margin-inline: auto;
261
- margin-block: var(--cg-section-y) var(--cg-s6);
262
- padding-inline: var(--cg-gutter);
275
+ display: grid;
276
+ grid-template-columns: repeat(var(--cg-columns), minmax(0, 1fr));
277
+ column-gap: var(--cg-column-gap);
278
+ padding-block: calc(var(--cg-section) * 0.6) calc(var(--cg-section) / 4);
279
+ padding-inline: var(--cg-inset);
280
+ }
281
+
282
+ .cg-entry-header > * {
283
+ grid-column: 1 / -1;
263
284
  }
264
285
 
265
286
  .cg-entry-header__terms {
266
287
  display: flex;
267
288
  flex-wrap: wrap;
268
- gap: var(--cg-s2);
269
- margin: 0 0 var(--cg-s3);
289
+ gap: var(--cg-s1) var(--cg-s4);
290
+ margin-block-end: var(--cg-s6);
270
291
  padding: 0;
271
292
  list-style: none;
272
- font-size: var(--cg-text-sm);
273
- font-weight: var(--cg-weight-semibold);
274
- letter-spacing: var(--cg-tracking-wide);
275
- text-transform: uppercase;
276
- color: var(--cg-accent);
293
+ font-size: var(--cg-text-label);
294
+ font-weight: var(--cg-weight-medium);
277
295
  }
278
296
 
279
297
  .cg-entry-header__terms a {
280
- color: inherit;
281
- text-decoration: none;
298
+ color: var(--cg-ink);
299
+ text-decoration-color: var(--cg-underline);
282
300
  }
283
301
 
284
- .cg-entry-header__terms li:not(:last-child)::after {
285
- content: "·";
286
- margin-inline-start: var(--cg-s2);
287
- color: var(--cg-ink-muted);
302
+ .cg-entry-header__terms a:hover {
303
+ color: var(--cg-accent);
304
+ text-decoration-color: currentColor;
288
305
  }
289
306
 
290
307
  .cg-entry-header__title {
291
- margin: 0 0 var(--cg-s4);
292
- font-size: var(--cg-text-3xl);
293
- line-height: var(--cg-leading-tight);
294
- letter-spacing: var(--cg-tracking-tight);
295
- font-weight: var(--cg-weight-bold);
296
- text-wrap: balance;
308
+ font-family: var(--cg-font-display);
309
+ font-size: var(--cg-text-title);
310
+ font-weight: var(--cg-weight-regular);
311
+ line-height: var(--cg-leading-display);
312
+ letter-spacing: var(--cg-tracking-display);
297
313
  }
298
314
 
299
315
  .cg-entry-header__excerpt {
300
- margin: 0 0 var(--cg-s4);
301
- font-size: var(--cg-text-lg);
316
+ margin-block-start: var(--cg-s6);
317
+ max-inline-size: var(--cg-measure);
302
318
  color: var(--cg-ink-muted);
303
- text-wrap: pretty;
319
+ font-size: var(--cg-text-lg);
320
+ line-height: var(--cg-leading-snug);
304
321
  }
305
322
 
306
323
  .cg-entry-header__meta {
307
324
  display: flex;
308
325
  flex-wrap: wrap;
309
- gap: var(--cg-s3);
310
- margin: 0 0 var(--cg-s6);
311
- font-size: var(--cg-text-sm);
326
+ gap: var(--cg-s1) var(--cg-s5);
327
+ margin-block-start: var(--cg-s8);
328
+ padding-block-start: var(--cg-s4);
329
+ border-block-start: var(--cg-rule) solid var(--cg-line);
312
330
  color: var(--cg-ink-muted);
331
+ font-size: var(--cg-text-label);
332
+ font-variant-numeric: tabular-nums;
313
333
  }
314
334
 
315
- .cg-entry-header__meta > :not(:last-child)::after {
316
- content: "·";
317
- margin-inline-start: var(--cg-s3);
318
- color: var(--cg-line-strong);
335
+ .cg-entry-header__author {
336
+ color: var(--cg-ink);
337
+ font-weight: var(--cg-weight-medium);
319
338
  }
320
339
 
321
340
  .cg-entry-header__cover {
322
- margin: 0;
341
+ margin-block-start: var(--cg-s10);
323
342
  }
324
343
 
325
344
  .cg-entry-header__cover img,
326
345
  .cg-entry-header__cover video {
327
346
  inline-size: 100%;
328
- aspect-ratio: 16 / 9;
329
- border-radius: var(--cg-radius-lg);
330
- object-fit: cover;
331
- }
332
-
333
- .cg-site-header {
334
- position: sticky;
335
- inset-block-start: 0;
336
- z-index: 5;
337
- padding: var(--cg-s4) var(--cg-gutter);
338
- /* Flat and fully opaque, not a frosted-glass blur: a sticky header reads
339
- as a solid plane sitting above the page, with a hairline marking where
340
- it ends. */
341
- background: var(--cg-canvas);
342
- border-block-end: 1px solid var(--cg-line-soft);
343
- }
344
-
345
- .cg-site-header__inner,
346
- .cg-site-footer__inner {
347
- display: flex;
348
- flex-wrap: wrap;
349
- align-items: center;
350
- gap: var(--cg-gap);
351
- max-inline-size: var(--cg-page);
352
- margin-inline: auto;
353
- }
354
-
355
- .cg-site-header__home {
356
- color: var(--cg-ink);
357
- font-weight: var(--cg-weight-semibold);
358
- font-size: var(--cg-text-lg);
359
- letter-spacing: var(--cg-tracking-snug);
360
- text-decoration: none;
361
- margin-inline-end: auto;
362
- }
363
-
364
- /* The uploaded site logo, when there is one. Height-constrained rather than
365
- width-constrained: a wordmark logo and a square mark have wildly different
366
- aspect ratios, and only a fixed height keeps the header bar the same
367
- height for both. `max-inline-size` is the guard against a very wide
368
- wordmark pushing the navigation off the row. */
369
- .cg-site-header__logo {
370
- display: block;
371
- block-size: 2rem;
372
- inline-size: auto;
373
- max-inline-size: 14rem;
374
- object-fit: contain;
375
- }
376
-
377
- .cg-site-footer {
378
- padding: var(--cg-s12) var(--cg-gutter);
379
- background: var(--cg-surface-sunken);
380
- color: var(--cg-ink-muted);
381
- border-block-start: 1px solid var(--cg-line-soft);
382
- font-size: var(--cg-text-sm);
383
- }
384
-
385
- /* `theme@1.4` (L25 D2) — a button-styled link at the end of the header nav.
386
- Reuses `.cg-action`'s own look (see "Actions" below) rather than a second
387
- button style. */
388
- .cg-site-header__action {
389
- margin-inline-start: var(--cg-s2);
390
- }
391
-
392
- /* The manual light/dark/system control (`renderThemeToggle`, `@cogenta/
393
- theme-kit`) — a plain icon button, not the block vocabulary's `.cg-action`
394
- style, so it never reads as a call-to-action competing with a real one. */
395
- .cg-theme-toggle {
396
- display: inline-flex;
397
- align-items: center;
398
- justify-content: center;
399
- block-size: 2.25rem;
400
- inline-size: 2.25rem;
401
- flex-shrink: 0;
402
- border-radius: var(--cg-radius-pill);
403
- border: 1px solid var(--cg-line-soft);
404
- background: transparent;
405
- color: var(--cg-ink);
406
- cursor: pointer;
407
- }
408
-
409
- .cg-theme-toggle:hover {
410
- background: var(--cg-surface-sunken);
411
- }
412
-
413
- .cg-theme-toggle__icon {
414
- block-size: 1.1rem;
415
- inline-size: 1.1rem;
347
+ aspect-ratio: 3 / 2;
416
348
  }
417
349
 
418
- .cg-theme-toggle__icon--moon {
419
- display: none;
420
- }
421
-
422
- /* Icon reflects the *current* effective appearance — sun while light, moon
423
- while dark — from the same `[data-theme]`/`prefers-color-scheme` signals
424
- `tokens.css` already keys the whole palette on. */
425
- @media (prefers-color-scheme: dark) {
426
- :where(:root:not([data-theme="light"])) .cg-theme-toggle__icon--sun {
427
- display: none;
350
+ @media (min-width: 64rem) {
351
+ .cg-entry-header__title {
352
+ grid-column: 1 / span 10;
428
353
  }
429
- :where(:root:not([data-theme="light"])) .cg-theme-toggle__icon--moon {
430
- display: block;
431
- }
432
- }
433
-
434
- :where(:root[data-theme="dark"]) .cg-theme-toggle__icon--sun {
435
- display: none;
436
- }
437
-
438
- :where(:root[data-theme="dark"]) .cg-theme-toggle__icon--moon {
439
- display: block;
440
- }
441
-
442
- /* `general.tagline`, shown right after the site name in the footer — prose,
443
- never a heading, so it stays the same visual weight as the credit line
444
- beside it. */
445
- .cg-site-footer__tagline {
446
- margin: var(--cg-s1) 0 0;
447
- max-inline-size: 28rem;
448
- }
449
-
450
- .cg-site-footer__social {
451
- display: flex;
452
- flex-wrap: wrap;
453
- gap: var(--cg-s3);
454
- margin: var(--cg-s4) 0 0;
455
- padding: 0;
456
- list-style: none;
457
- }
458
-
459
- .cg-site-footer__social-item a {
460
- display: flex;
461
- align-items: center;
462
- justify-content: center;
463
- inline-size: 2.25rem;
464
- block-size: 2.25rem;
465
- border-radius: var(--cg-radius-md);
466
- color: var(--cg-ink-muted);
467
- background: var(--cg-surface);
468
- box-shadow: var(--cg-ring);
469
- transition:
470
- color var(--cg-duration-fast) var(--cg-ease),
471
- background-color var(--cg-duration-fast) var(--cg-ease);
472
- }
473
-
474
- .cg-site-footer__social-item a:hover,
475
- .cg-site-footer__social-item a:focus-visible {
476
- color: var(--cg-accent-fg);
477
- background: var(--cg-accent);
478
- }
479
-
480
- .cg-social__icon {
481
- inline-size: 1.1rem;
482
- block-size: 1.1rem;
483
- }
484
-
485
- /* `general.footerNote` — a short "about" column, visually distinct from the
486
- tagline (which sits with the site name) and from Cogenta's own credit
487
- (which stays pinned to the far end of the row, see
488
- `.cg-site-footer__branding` above). */
489
- .cg-site-footer__about {
490
- margin-inline-end: auto;
491
- max-inline-size: 24rem;
492
- }
493
354
 
494
- .cg-site-footer__about p {
495
- margin: 0;
496
- }
497
-
498
- /* The standard clip-to-1px technique — visible to assistive tech, invisible
499
- on screen, unlike `display:none` (which removes it from both). Every
500
- social icon link's own text label (`renderSocialLinks`,
501
- `@cogenta/theme-kit`) depends on this class existing here: theme-kit ships
502
- no CSS of its own (contract D, R5 — a theme owns its own stylesheet). */
503
- .cg-visually-hidden {
504
- position: absolute;
505
- inline-size: 1px;
506
- block-size: 1px;
507
- padding: 0;
508
- margin: -1px;
509
- overflow: hidden;
510
- clip: rect(0, 0, 0, 0);
511
- white-space: nowrap;
512
- border: 0;
513
- }
514
-
515
- /* Cogenta's own credit, or its white-label replacement (fiche L21 task 8) —
516
- pushed to the far end of the footer row, after the site name and its own
517
- navigation, never competing with either for space. */
518
- .cg-site-footer__branding {
519
- margin-inline-start: auto;
520
- display: flex;
521
- align-items: center;
522
- }
523
-
524
- .cg-site-footer__branding a {
525
- display: flex;
526
- align-items: center;
527
- gap: 0.4em;
528
- opacity: 0.7;
529
- transition: opacity 0.15s ease;
530
- }
531
-
532
- /* `@cogenta/core`'s package version, next to the logo (fiche 22 tâche 8) — only rendered at all when Cogenta's own credit is on. */
533
- .cg-site-footer__version {
534
- font-variant-numeric: tabular-nums;
535
- font-size: 0.9em;
536
- }
537
-
538
- .cg-site-footer__branding a:hover,
539
- .cg-site-footer__branding a:focus-visible {
540
- opacity: 1;
541
- }
355
+ .cg-entry-header__excerpt {
356
+ grid-column: 1 / span 8;
357
+ }
542
358
 
543
- .cg-site-footer__brand-logo {
544
- display: block;
545
- /* Explicit pixels, not `em`: a footer's own small font-size cascaded into
546
- this and made the credit mark unreadably tiny — the same class of bug
547
- the admin shell's own logo had before its fix. */
548
- block-size: 25px;
549
- inline-size: auto;
359
+ .cg-entry-header__cover img,
360
+ .cg-entry-header__cover video {
361
+ aspect-ratio: 2 / 1;
362
+ }
550
363
  }
551
364
 
552
365
  /* --------------------------------------------------------------------------
553
- Actions — the theme's only control
366
+ Actions
554
367
  -------------------------------------------------------------------------- */
555
368
 
556
369
  .cg-actions {
557
370
  display: flex;
558
371
  flex-wrap: wrap;
559
- gap: var(--cg-s3);
560
- margin: var(--cg-s8) 0 0;
372
+ align-items: center;
373
+ gap: var(--cg-s3) var(--cg-s8);
374
+ margin-block-start: var(--cg-s8);
561
375
  padding: 0;
562
376
  list-style: none;
563
377
  }
564
378
 
565
- .cg-action {
379
+ /* `emphasis` stays a data attribute: it is the editor's intent, and the
380
+ stylesheet decides what it looks like. */
381
+ .cg-action[data-emphasis="primary"] {
566
382
  display: inline-flex;
567
383
  align-items: center;
568
384
  justify-content: center;
569
- gap: var(--cg-s2);
570
- padding: var(--cg-s3) var(--cg-s6);
571
- border: 0;
572
- border-radius: var(--cg-radius-md);
573
- font-size: var(--cg-text-sm);
385
+ min-block-size: 2.75rem;
386
+ padding: var(--cg-s2) var(--cg-s6);
387
+ border: var(--cg-rule) solid var(--cg-action);
388
+ border-radius: var(--cg-radius-control);
389
+ background: var(--cg-action);
390
+ color: var(--cg-action-fg);
391
+ font-size: var(--cg-text-md);
574
392
  font-weight: var(--cg-weight-semibold);
575
- letter-spacing: var(--cg-tracking-snug);
576
- line-height: var(--cg-leading-snug);
393
+ line-height: var(--cg-leading-heading);
577
394
  text-decoration: none;
578
- white-space: nowrap;
579
395
  transition:
580
396
  background-color var(--cg-duration) var(--cg-ease),
581
- color var(--cg-duration) var(--cg-ease),
582
- box-shadow var(--cg-duration) var(--cg-ease),
583
- transform var(--cg-duration-fast) var(--cg-ease);
584
- }
585
-
586
- /* `emphasis` stays a data attribute, never a class: it is the editor's
587
- semantic intent, and the skin — not the block — decides what it looks like. */
588
- .cg-action[data-emphasis="primary"] {
589
- background: var(--cg-accent);
590
- color: var(--cg-accent-fg);
591
- box-shadow: var(--cg-elevation-1);
397
+ border-color var(--cg-duration) var(--cg-ease);
592
398
  }
593
399
 
594
400
  .cg-action[data-emphasis="primary"]:hover {
595
- background: var(--cg-accent-hover);
596
- box-shadow: var(--cg-elevation-2);
597
- transform: translateY(-1px);
598
- }
599
-
600
- .cg-action[data-emphasis="secondary"] {
601
- background: var(--cg-surface);
602
- color: var(--cg-ink);
603
- box-shadow: var(--cg-ring);
604
- }
605
-
606
- .cg-action[data-emphasis="secondary"]:hover {
607
- background: var(--cg-accent-soft);
608
- color: var(--cg-accent-soft-fg);
609
- box-shadow: var(--cg-ring-accent);
610
- }
611
-
612
- /* The press. Without it a hover lift reads as a bug on touch, where there is
613
- no hover to come back from. */
614
- .cg-action:active {
615
- transform: translateY(0);
616
- box-shadow: var(--cg-elevation-1);
617
- }
618
-
619
- /* --------------------------------------------------------------------------
620
- Shared block furniture
621
- -------------------------------------------------------------------------- */
622
-
623
- /* Every block that carries a heading uses the same one, so eleven blocks do
624
- not drift into eleven title sizes. */
625
- .cg-features__title,
626
- .cg-cta__title,
627
- .cg-faq__title,
628
- .cg-stats__title,
629
- .cg-logos__title,
630
- .cg-collection__title {
631
- margin: 0 0 var(--cg-s8);
632
- font-size: var(--cg-text-xl);
633
- line-height: var(--cg-leading-snug);
634
- letter-spacing: var(--cg-tracking-tight);
635
- font-weight: var(--cg-weight-bold);
636
- text-wrap: balance;
637
- }
638
-
639
- .cg-features__items,
640
- .cg-logos__items,
641
- .cg-gallery__items,
642
- .cg-collection__items,
643
- .cg-faq__items {
644
- margin: 0;
645
- padding: 0;
646
- list-style: none;
647
- }
648
-
649
- /* A scroll-snapping row, used by both `gallery` and `collectionList`. The
650
- container is focusable and labelled, so it is operable from the keyboard —
651
- at zero bytes of JavaScript. */
652
- .cg-gallery__viewport,
653
- .cg-collection__viewport {
654
- overflow-x: auto;
655
- scroll-snap-type: x mandatory;
656
- scroll-behavior: smooth;
657
- overscroll-behavior-inline: contain;
658
- scrollbar-width: thin;
659
- scrollbar-color: var(--cg-line-strong) transparent;
660
- /* Room for the focus ring and the hover lift, which would otherwise be
661
- clipped by the scroll container. */
662
- padding-block: var(--cg-s2);
663
- margin-block: calc(var(--cg-s2) * -1);
664
- }
665
-
666
- /* --------------------------------------------------------------------------
667
- Search, forms, comments — public utility pages and post-content furniture
668
-
669
- `/search` (`search-page.ts`), `/forms/{name}` (`forms-page.ts`) and the
670
- comment thread appended after an entry page (`comments.ts`) all render
671
- real markup with real `cg-*` classes, but none of it had a single rule
672
- here (L20 audit, points 8-9) — the stylesheet loaded, the class names were
673
- right there in the HTML, and nothing matched. These three follow the same
674
- page-width measure `.cg-page__title` already sets, so a search result list
675
- or a form lines up under its own heading rather than running edge to edge.
676
- -------------------------------------------------------------------------- */
677
-
678
- .cg-search__form,
679
- .cg-search__results,
680
- .cg-search__empty,
681
- .cg-search__error,
682
- .cg-form,
683
- .cg-form__confirmation,
684
- .cg-comments {
685
- max-inline-size: var(--cg-page);
686
- margin-inline: auto;
687
- padding-inline: var(--cg-gutter);
688
- }
689
-
690
- .cg-search__form {
691
- display: flex;
692
- flex-wrap: wrap;
693
- align-items: end;
694
- gap: var(--cg-s3);
695
- margin-block-end: var(--cg-s8);
696
- }
697
-
698
- .cg-search__form label {
699
- display: block;
700
- font-size: var(--cg-text-sm);
701
- font-weight: var(--cg-weight-semibold);
702
- margin-block-end: var(--cg-s2);
703
- }
704
-
705
- .cg-search__form input[type="search"] {
706
- flex: 1 1 16rem;
707
- }
708
-
709
- .cg-search__results {
710
- margin: 0 auto;
711
- padding-inline-start: var(--cg-gutter);
712
- display: grid;
713
- gap: var(--cg-s4);
714
- font-size: var(--cg-text-lg);
715
- }
716
-
717
- .cg-search__empty,
718
- .cg-comments__empty,
719
- .cg-comments__closed {
720
- color: var(--cg-ink-muted);
721
- }
722
-
723
- /* No skin exposes a "danger" colour (contract D has none), so an error reads
724
- through weight and a stronger border rather than a hue no skin can change —
725
- the same constraint that keeps every other value in this file a function of
726
- a token. */
727
- .cg-search__error,
728
- .cg-form__error,
729
- .cg-form__field-error {
401
+ border-color: var(--cg-action-hover);
402
+ background: var(--cg-action-hover);
403
+ color: var(--cg-action-fg);
404
+ }
405
+
406
+ /* A secondary action and every arrow link: words in ink under one continuous
407
+ underline, the arrow drawn after them as an atomic inline so the line does
408
+ not run under it, and the label kept on one line so the arrow never wraps
409
+ alone. */
410
+ .cg-action[data-emphasis="secondary"],
411
+ .cg-arrow-link {
412
+ display: inline-block;
413
+ padding-block: var(--cg-s3);
730
414
  color: var(--cg-ink);
731
- font-weight: var(--cg-weight-bold);
732
- }
733
-
734
- /* Text controls, shared by `/forms/{name}` and the comment form — the same
735
- input surface `.cg-search__form input` above uses. */
736
- .cg-search__form input,
737
- .cg-form__field input,
738
- .cg-form__field select,
739
- .cg-form__field textarea,
740
- .cg-comment__form input,
741
- .cg-comment__form textarea {
742
- inline-size: 100%;
743
- padding: var(--cg-s3) var(--cg-s4);
744
- border: 1px solid var(--cg-line);
745
- border-radius: var(--cg-radius-md);
746
- background: var(--cg-surface);
747
- color: var(--cg-ink);
748
- font: inherit;
749
- }
750
-
751
- .cg-form__field input[aria-invalid="true"],
752
- .cg-form__field select[aria-invalid="true"],
753
- .cg-form__field textarea[aria-invalid="true"] {
754
- border-width: 2px;
755
- border-color: var(--cg-line-strong);
756
- }
757
-
758
- .cg-search__form button,
759
- .cg-form button[type="submit"],
760
- .cg-comment__form button[type="submit"] {
761
- padding: var(--cg-s3) var(--cg-s6);
762
- border: 0;
763
- border-radius: var(--cg-radius-md);
764
- background: var(--cg-accent);
765
- color: var(--cg-accent-fg);
766
- font-weight: var(--cg-weight-semibold);
767
- cursor: pointer;
768
- transition: background-color var(--cg-duration) var(--cg-ease);
769
- }
770
-
771
- .cg-search__form button:hover,
772
- .cg-form button[type="submit"]:hover,
773
- .cg-comment__form button[type="submit"]:hover {
774
- background: var(--cg-accent-hover);
775
- }
776
-
777
- .cg-form__field,
778
- .cg-comment__form p {
779
- display: block;
780
- margin-block-end: var(--cg-s5);
781
- }
782
-
783
- .cg-form__field label,
784
- .cg-comment__form label {
785
- display: block;
786
- font-size: var(--cg-text-sm);
787
415
  font-weight: var(--cg-weight-semibold);
788
- margin-block-end: var(--cg-s2);
789
- }
790
-
791
- .cg-form__help {
792
- margin: var(--cg-s2) 0 0;
793
- font-size: var(--cg-text-sm);
794
- color: var(--cg-ink-muted);
795
- }
796
-
797
- .cg-form__choice {
798
- display: inline-flex;
799
- align-items: center;
800
- gap: var(--cg-s2);
801
- margin-inline-end: var(--cg-s4);
802
- font-weight: normal;
803
- }
804
-
805
- /* Visually hidden, not `display:none`: the honeypot must stay reachable by a
806
- naive bot's "fill every input" script while never appearing for anyone
807
- else — the same reasoning the inline style already carried before this
808
- class existed to do it declaratively for `comments.ts`. */
809
- .cg-form__honeypot,
810
- .cg-comment__honeypot {
811
- position: absolute;
812
- inline-size: 1px;
813
- block-size: 1px;
814
- overflow: hidden;
815
- inset-inline-start: -9999px;
816
- }
817
-
818
- .cg-comments {
819
- margin-block-start: var(--cg-section-y);
820
- padding-block-start: var(--cg-s8);
821
- padding-block-end: var(--cg-section-y);
822
- border-block-start: 1px solid var(--cg-line-soft);
416
+ text-decoration-line: underline;
417
+ text-decoration-color: var(--cg-underline);
823
418
  }
824
419
 
825
- .cg-comments__list,
826
- .cg-comment__replies {
827
- margin: 0;
828
- padding: 0;
829
- list-style: none;
420
+ .cg-action[data-emphasis="secondary"] {
421
+ white-space: nowrap;
830
422
  }
831
423
 
832
- .cg-comment {
833
- padding-block: var(--cg-s5);
834
- border-block-end: 1px solid var(--cg-line-soft);
424
+ .cg-arrow-link__end {
425
+ white-space: nowrap;
835
426
  }
836
427
 
837
- .cg-comment__replies {
838
- padding-block-start: var(--cg-s5);
428
+ .cg-action[data-emphasis="secondary"]::after,
429
+ .cg-arrow-link__end::after {
430
+ content: "\2192" / "";
431
+ display: inline-block;
432
+ margin-inline-start: 0.35em;
839
433
  }
840
434
 
841
- .cg-comment__replies .cg-comment {
842
- padding-inline-start: var(--cg-s6);
843
- border-inline-start: 2px solid var(--cg-line-soft);
844
- border-block-end: 0;
435
+ .cg-arrow-link__start::before {
436
+ content: "\2190" / "";
437
+ display: inline-block;
438
+ margin-inline-end: 0.35em;
845
439
  }
846
440
 
847
- .cg-comment__meta {
848
- display: flex;
849
- gap: var(--cg-s3);
850
- align-items: baseline;
851
- font-size: var(--cg-text-sm);
852
- color: var(--cg-ink-muted);
853
- margin-block-end: var(--cg-s2);
854
- }
855
-
856
- .cg-comment__author {
857
- font-weight: var(--cg-weight-semibold);
858
- color: var(--cg-ink);
441
+ .cg-arrow-link__start {
442
+ white-space: nowrap;
859
443
  }
860
444
 
861
- .cg-comment__body {
862
- margin: 0;
445
+ .cg-action[data-emphasis="secondary"]:hover,
446
+ .cg-arrow-link:hover {
447
+ color: var(--cg-accent);
448
+ text-decoration-color: currentColor;
863
449
  }