@rathnasgala/theme 0.0.17 → 0.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala/theme",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Managed Gala static-site theme payload",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "themePackage": {
4
4
  "name": "@rathnasgala/theme",
5
- "version": "0.0.17",
5
+ "version": "0.0.18",
6
6
  "availableDesignThemes": [
7
7
  "editorial"
8
8
  ],
@@ -21,7 +21,7 @@
21
21
  "lib/render-markdown.js": "40e6d6743b76de6c16d138475f188a4263967530c370d8b26a363f4df58c2caf",
22
22
  "lib/seo.js": "107de8804931945b8e73894877fc394d6b535a9d0d969a4ba5b68f2e136e0a17",
23
23
  "lib/share-targets.js": "458aff86fab2b62a364442cd8c612565c6e0b53eef2f20cb6237cc562b1e0731",
24
- "lib/site-config.js": "ab7cc170dc30c74ea5ef08b8ff6a82f9820998ae4190a06d64375dfd7d515864",
24
+ "lib/site-config.js": "da7d2dffab62c7346c9930494b85fc6ea735d4f50565bbd0454a04393f8cad96",
25
25
  "package-lock.json": "c97a6a2f794aef083a39a8d2628eccc4a3c484e1c6225899b77ec1fb7c466d9e",
26
26
  "package.json": "4c60bea06f39aec755ac68caf8d8841c1d59d0f6121fad7f4396231ce14712dc",
27
27
  "src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
@@ -31,7 +31,7 @@
31
31
  "src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
32
32
  "src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
33
33
  "src/_includes/components/ui.njk": "23eb647d8658868449ec6bb809e965a85302124a97639f94d5d28af72df56e4c",
34
- "src/_includes/layouts/base.njk": "f504b5a80020b90eb47d59db5e626a4554e2f2d4ef64b3b25c98869830581ebe",
34
+ "src/_includes/layouts/base.njk": "6914da799eb31fea3fc230ab196252129ccca90f27ccb08bbcee81741a91ced4",
35
35
  "src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
36
36
  "src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
37
37
  "src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
@@ -42,8 +42,8 @@
42
42
  "src/assets/interactions.js": "dd4e343505e528d89596ca67db679b52c989e022f93399193e4377db01fc8a08",
43
43
  "src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
44
44
  "src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
45
- "src/assets/theme-mode.js": "3797c84dbaa090b5b21427a31928b5dd97f13d04d99833f5e1e74c6b50e01dcc",
46
- "src/assets/theme.css": "94839833f93997415f570041a691da0c96c27992f897e116948077a8b25e8dfa",
45
+ "src/assets/theme-mode.js": "944b29db11a577961a60ddcfa52f07ef2411f3f3631bea85bd79be16428c08be",
46
+ "src/assets/theme.css": "2a2367f2c875121cc95bc789a0d5889960813e660ca7722b328b39b4ff5068a8",
47
47
  "src/assets/vendor/hooks.js": "ae6ec58592683ca64fa4d65b20d40b103df62a673bcc4712515cf165950b07f3",
48
48
  "src/assets/vendor/htm.js": "e4dac9bc40a6f2ec590413d787ae30bbcfdfcde4f28971ee65d9578271b7aad4",
49
49
  "src/assets/vendor/preact.js": "712517dd2a42c3c22b5ab0264ad7e9a2b1e8be6cf97fa00dbedd98203617e616",
@@ -7,19 +7,39 @@ const BUDGET_KEYS = Object.freeze([
7
7
  'managedCssBytes',
8
8
  'ordinaryHtmlBytes'
9
9
  ]);
10
+ /*
11
+ * Only what the theme actually implements. A value here is a promise that choosing it changes how
12
+ * a publication looks; anything the CSS does not answer to has no business being offered.
13
+ */
10
14
  const DESIGN_VALUES = Object.freeze({
11
15
  layout: Object.freeze(['article-first', 'portfolio']),
12
- palette: Object.freeze(['default', 'ocean'])
16
+ palette: Object.freeze(['default', 'ocean']),
17
+ typography: Object.freeze(['system', 'editorial', 'humanist', 'mono']),
18
+ density: Object.freeze(['compact', 'comfortable', 'spacious']),
19
+ spacing: Object.freeze(['compact', 'comfortable', 'spacious']),
20
+ radius: Object.freeze(['sharp', 'soft', 'round']),
21
+ motion: Object.freeze(['none', 'subtle', 'expressive']),
22
+ componentStyle: Object.freeze(['quiet', 'outlined', 'raised'])
13
23
  });
14
24
  const CONTACT_KEYS = Object.freeze([
15
25
  'enabled', 'websiteEnabled', 'phoneEnabled'
16
26
  ]);
17
27
 
28
+ /*
29
+ * `layout` and `palette` have always been required. The rest were added later and are optional:
30
+ * a publication written before they existed is still a valid publication, and the theme falls
31
+ * back to its own defaults for anything absent. Present-but-unknown is still an error, because
32
+ * that is a writer expecting something the CSS will not do.
33
+ */
34
+ const REQUIRED_DESIGN = Object.freeze(['layout', 'palette']);
35
+
18
36
  function validateImplementedDesign(config) {
19
37
  if (config?.design == null) return;
20
38
  for (const [field, allowed] of Object.entries(DESIGN_VALUES)) {
21
- if (!allowed.includes(config.design[field])) {
22
- throw new TypeError(`Unsupported design.${field}: ${config.design[field]}`);
39
+ const value = config.design[field];
40
+ if (value === undefined && !REQUIRED_DESIGN.includes(field)) continue;
41
+ if (!allowed.includes(value)) {
42
+ throw new TypeError(`Unsupported design.${field}: ${value}`);
23
43
  }
24
44
  }
25
45
  }
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="{{ post.language or site.site.defaultLanguage }}" data-theme="{{ site.design.theme }}" data-layout="{{ site.design.layout }}" data-palette="{{ site.design.palette }}" data-mode="system">
2
+ <html lang="{{ post.language or site.site.defaultLanguage }}" data-theme="{{ site.design.theme }}" data-layout="{{ site.design.layout }}" data-palette="{{ site.design.palette }}" data-typography="{{ site.design.typography }}" data-density="{{ site.design.density }}" data-spacing="{{ site.design.spacing }}" data-radius="{{ site.design.radius }}" data-motion="{{ site.design.motion }}" data-component-style="{{ site.design.componentStyle }}" data-mode="{{ site.design.colorMode | default('system') }}">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -1,13 +1,25 @@
1
1
  const MODES = ['system', 'light', 'dark'];
2
2
  const STORAGE_KEY = 'gala-color-mode';
3
3
 
4
+ /*
5
+ * The reader's own choice always wins. Absent one, the publication's `design.colorMode` decides
6
+ * what they see first — it is already on the element, server-rendered. This used to fall back to
7
+ * `system` unconditionally, which meant a writer could set a colour mode and no reader ever saw
8
+ * it: the setting existed everywhere except in the page.
9
+ */
10
+ function publicationDefault() {
11
+ const declared = document.documentElement.dataset.mode;
12
+ return MODES.includes(declared) ? declared : 'system';
13
+ }
14
+
4
15
  function storedMode() {
5
16
  try {
6
17
  const mode = localStorage.getItem(STORAGE_KEY);
7
- return MODES.includes(mode) ? mode : 'system';
18
+ if (MODES.includes(mode)) return mode;
8
19
  } catch {
9
- return 'system';
20
+ return publicationDefault();
10
21
  }
22
+ return publicationDefault();
11
23
  }
12
24
 
13
25
  function applyMode(mode) {
@@ -1,57 +1,181 @@
1
+ /*
2
+ * A publication should read like a well-set page, not like a product.
3
+ *
4
+ * The chrome is quiet, the type carries the hierarchy, and depth comes from surface and space
5
+ * rather than from an outline around everything. Every value here is a token so that the ten
6
+ * `design:` choices in site.config.yml can move the whole page by moving a handful of variables,
7
+ * instead of each component inventing its own spacing and its own grey.
8
+ */
1
9
  :root {
2
10
  color-scheme: light dark;
3
- --gala-color-background: light-dark(#fff, #111);
4
- --gala-color-text: light-dark(#181818, #f5f5f5);
5
- --gala-space-page: clamp(1rem, 4vw, 4rem);
6
- --gala-radius-control: 0.5rem;
7
- --gala-color-surface: light-dark(#f4f4f4, #1d1d1d);
8
- --gala-color-accent: light-dark(#174ea6, #8ab4f8);
9
- --gala-color-border: light-dark(#6b6b6b, #aaa);
10
- --gala-font-body: system-ui, sans-serif;
11
- --gala-content-width: 70rem;
11
+
12
+ /* Neutrals carry a slight warmth. A pure grey reads as unconsidered; these were picked. */
13
+ --gala-color-background: light-dark(#fdfcfb, #131211);
14
+ --gala-color-surface: light-dark(#f6f4f1, #1c1a18);
15
+ --gala-color-raised: light-dark(#fffffe, #24211f);
16
+ --gala-color-text: light-dark(#1a1817, #f2efec);
17
+ --gala-color-muted: light-dark(#6b645e, #a8a099);
18
+ --gala-color-border: light-dark(#968e86, #6b645e);
19
+ --gala-color-hairline: light-dark(#e8e3dd, #2a2724);
20
+ --gala-color-border-strong: light-dark(#c9c2ba, #46413d);
21
+ --gala-color-accent: light-dark(#8a4b2a, #e8a87c);
22
+
23
+ /* Type. One scale, used everywhere, fluid between a phone and a desk. */
24
+ --gala-font-body: ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
25
+ --gala-font-display: var(--gala-font-body);
26
+ --gala-font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
27
+ --gala-text-xs: 0.78rem;
28
+ --gala-text-sm: 0.875rem;
29
+ --gala-text-base: clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
30
+ --gala-text-lg: clamp(1.15rem, 1.09rem + 0.3vw, 1.3rem);
31
+ --gala-text-xl: clamp(1.4rem, 1.28rem + 0.6vw, 1.75rem);
32
+ --gala-text-2xl: clamp(1.75rem, 1.5rem + 1.2vw, 2.4rem);
33
+ --gala-text-3xl: clamp(2.2rem, 1.7rem + 2.4vw, 3.4rem);
34
+ --gala-leading-tight: 1.15;
35
+ --gala-leading-body: 1.65;
36
+ --gala-tracking-tight: -0.018em;
37
+ --gala-tracking-wide: 0.08em;
38
+ --gala-weight-display: 640;
39
+
40
+ /* Space, radius, depth, motion. */
41
+ --gala-step: 1rem;
42
+ --gala-space-2xs: calc(var(--gala-step) * 0.25);
43
+ --gala-space-xs: calc(var(--gala-step) * 0.5);
44
+ --gala-space-sm: calc(var(--gala-step) * 0.75);
45
+ --gala-space-md: var(--gala-step);
46
+ --gala-space-lg: calc(var(--gala-step) * 1.75);
47
+ --gala-space-xl: calc(var(--gala-step) * 3);
48
+ --gala-space-page: clamp(1.25rem, 5vw, 4.5rem);
49
+ --gala-radius-control: 0.625rem;
50
+ --gala-radius-surface: 1rem;
51
+ --gala-shadow-low: 0 1px 2px light-dark(rgb(26 24 23 / 6%), rgb(0 0 0 / 40%));
52
+ --gala-shadow-high:
53
+ 0 1px 2px light-dark(rgb(26 24 23 / 5%), rgb(0 0 0 / 35%)),
54
+ 0 12px 28px -12px light-dark(rgb(26 24 23 / 18%), rgb(0 0 0 / 60%));
55
+ --gala-motion-fast: 120ms;
56
+ --gala-motion-base: 220ms;
57
+ --gala-ease: cubic-bezier(0.2, 0, 0, 1);
58
+
59
+ --gala-content-width: 72rem;
60
+ --gala-measure: 68ch;
12
61
  --gala-widget-min-block-size: 8rem;
13
62
  }
14
63
 
64
+ /* An explicit choice must beat the operating system in both directions, so every colour is
65
+ restated rather than left to `light-dark()`. */
15
66
  :root[data-mode='light'] {
16
67
  color-scheme: light;
17
- --gala-color-background: #fff;
18
- --gala-color-text: #181818;
19
- --gala-color-surface: #f4f4f4;
20
- --gala-color-accent: #174ea6;
21
- --gala-color-border: #6b6b6b;
68
+ --gala-color-background: #fdfcfb;
69
+ --gala-color-surface: #f6f4f1;
70
+ --gala-color-raised: #fffffe;
71
+ --gala-color-text: #1a1817;
72
+ --gala-color-muted: #6b645e;
73
+ --gala-color-border: #968e86;
74
+ --gala-color-hairline: #e8e3dd;
75
+ --gala-color-border-strong: #c9c2ba;
76
+ --gala-color-accent: #8a4b2a;
22
77
  }
23
-
24
78
  :root[data-mode='dark'] {
25
79
  color-scheme: dark;
26
- --gala-color-background: #111;
27
- --gala-color-text: #f5f5f5;
28
- --gala-color-surface: #1d1d1d;
29
- --gala-color-accent: #8ab4f8;
30
- --gala-color-border: #aaa;
80
+ --gala-color-background: #131211;
81
+ --gala-color-surface: #1c1a18;
82
+ --gala-color-raised: #24211f;
83
+ --gala-color-text: #f2efec;
84
+ --gala-color-muted: #a8a099;
85
+ --gala-color-border: #6b645e;
86
+ --gala-color-hairline: #2a2724;
87
+ --gala-color-border-strong: #46413d;
88
+ --gala-color-accent: #e8a87c;
31
89
  }
32
90
 
91
+ /* ---------------------------------------------------------------- palettes */
33
92
  :root[data-palette='ocean'] {
34
- --gala-color-background: light-dark(#f7fbff, #071b2d);
35
- --gala-color-text: light-dark(#102a43, #e8f4ff);
36
- --gala-color-surface: light-dark(#d9eaf7, #102f49);
37
- --gala-color-accent: light-dark(#005ea8, #78c8ff);
38
- --gala-color-border: light-dark(#526d82, #8aa8bf);
93
+ --gala-color-background: light-dark(#f8fbfd, #0b1a24);
94
+ --gala-color-surface: light-dark(#eef4f9, #12252f);
95
+ --gala-color-raised: light-dark(#ffffff, #172d39);
96
+ --gala-color-text: light-dark(#12242f, #eaf3f8);
97
+ --gala-color-muted: light-dark(#5a6f7d, #98aebb);
98
+ --gala-color-border: light-dark(#7d8d99, #5f7d8c);
99
+ --gala-color-hairline: light-dark(#dde7ee, #1d3644);
100
+ --gala-color-border-strong: light-dark(#bdccd7, #33505f);
101
+ --gala-color-accent: light-dark(#0f5f7a, #7fd0e8);
39
102
  }
40
-
41
103
  :root[data-palette='ocean'][data-mode='light'] {
42
- --gala-color-background: #f7fbff;
43
- --gala-color-text: #102a43;
44
- --gala-color-surface: #d9eaf7;
45
- --gala-color-accent: #005ea8;
46
- --gala-color-border: #526d82;
104
+ color-scheme: light;
105
+ --gala-color-background: #f8fbfd;
106
+ --gala-color-surface: #eef4f9;
107
+ --gala-color-raised: #ffffff;
108
+ --gala-color-text: #12242f;
109
+ --gala-color-muted: #5a6f7d;
110
+ --gala-color-border: #7d8d99;
111
+ --gala-color-hairline: #dde7ee;
112
+ --gala-color-border-strong: #bdccd7;
113
+ --gala-color-accent: #0f5f7a;
47
114
  }
48
-
49
115
  :root[data-palette='ocean'][data-mode='dark'] {
50
- --gala-color-background: #071b2d;
51
- --gala-color-text: #e8f4ff;
52
- --gala-color-surface: #102f49;
53
- --gala-color-accent: #78c8ff;
54
- --gala-color-border: #8aa8bf;
116
+ color-scheme: dark;
117
+ --gala-color-background: #0b1a24;
118
+ --gala-color-surface: #12252f;
119
+ --gala-color-raised: #172d39;
120
+ --gala-color-text: #eaf3f8;
121
+ --gala-color-muted: #98aebb;
122
+ --gala-color-border: #5f7d8c;
123
+ --gala-color-hairline: #1d3644;
124
+ --gala-color-border-strong: #33505f;
125
+ --gala-color-accent: #7fd0e8;
126
+ }
127
+
128
+ /* ---------------------------------------------------------------- typography
129
+ System stacks rather than a downloaded face: a publication must build and render with no call
130
+ to anyone, and a webfont is bytes on every reader's first paint. These are the good faces the
131
+ reader's machine already has. */
132
+ :root[data-typography='editorial'] {
133
+ --gala-font-body: ui-serif, "Iowan Old Style", Charter, "Source Serif 4", Georgia, serif;
134
+ --gala-font-display: var(--gala-font-body);
135
+ --gala-weight-display: 600;
136
+ }
137
+ :root[data-typography='humanist'] {
138
+ --gala-font-body: Optima, Candara, "Gill Sans", "Trebuchet MS", sans-serif;
139
+ --gala-font-display: var(--gala-font-body);
140
+ }
141
+ :root[data-typography='mono'] {
142
+ --gala-font-display: var(--gala-font-mono);
143
+ --gala-weight-display: 560;
144
+ --gala-tracking-tight: -0.005em;
145
+ }
146
+
147
+ /* ---------------------------------------------------------------- density */
148
+ :root[data-density='compact'], :root[data-spacing='compact'] {
149
+ --gala-step: 0.8rem;
150
+ --gala-leading-body: 1.5;
151
+ }
152
+ :root[data-density='spacious'], :root[data-spacing='spacious'] {
153
+ --gala-step: 1.25rem;
154
+ --gala-leading-body: 1.75;
155
+ }
156
+
157
+ /* ---------------------------------------------------------------- radius */
158
+ :root[data-radius='sharp'] { --gala-radius-control: 0; --gala-radius-surface: 0; }
159
+ :root[data-radius='round'] { --gala-radius-control: 999px; --gala-radius-surface: 1.5rem; }
160
+
161
+ /* ---------------------------------------------------------------- motion
162
+ `none` is honoured for everyone; a reader who asks the operating system for less motion is
163
+ honoured regardless of what the publication chose. */
164
+ :root[data-motion='none'] { --gala-motion-fast: 0ms; --gala-motion-base: 0ms; }
165
+ :root[data-motion='expressive'] { --gala-motion-base: 340ms; }
166
+ @media (prefers-reduced-motion: reduce) {
167
+ :root { --gala-motion-fast: 0ms; --gala-motion-base: 0ms; }
168
+ *, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
169
+ }
170
+
171
+ /* ---------------------------------------------------------------- surfaces
172
+ `quiet` sits on the page, `outlined` draws its edge, `raised` lifts. One variable decides,
173
+ so every card, dialog and chip agrees. */
174
+ :root { --gala-surface-border: var(--gala-color-hairline); --gala-surface-shadow: var(--gala-shadow-low); }
175
+ :root[data-component-style='outlined'] { --gala-surface-border: var(--gala-color-border-strong); }
176
+ :root[data-component-style='raised'] {
177
+ --gala-surface-border: transparent;
178
+ --gala-surface-shadow: var(--gala-shadow-high);
55
179
  }
56
180
 
57
181
  :root[data-layout='portfolio'] { --gala-content-width: 82rem; }
@@ -62,6 +186,53 @@ body {
62
186
  color: var(--gala-color-text);
63
187
  background: var(--gala-color-background);
64
188
  font-family: var(--gala-font-body);
189
+ font-size: var(--gala-text-base);
190
+ line-height: var(--gala-leading-body);
191
+ -webkit-font-smoothing: antialiased;
192
+ text-rendering: optimizeLegibility;
193
+ }
194
+
195
+ /* Headings carry the hierarchy, so they get the display face, a tighter measure and tighter
196
+ tracking — large type set at body tracking is the single clearest sign of an unconsidered page. */
197
+ :where(h1, h2, h3, h4) {
198
+ font-family: var(--gala-font-display);
199
+ font-weight: var(--gala-weight-display);
200
+ line-height: var(--gala-leading-tight);
201
+ letter-spacing: var(--gala-tracking-tight);
202
+ text-wrap: balance;
203
+ margin-block: 0 var(--gala-space-sm);
204
+ }
205
+ :where(h1) { font-size: var(--gala-text-3xl); }
206
+ :where(h2) { font-size: var(--gala-text-2xl); }
207
+ :where(h3) { font-size: var(--gala-text-xl); }
208
+ :where(h4) { font-size: var(--gala-text-lg); }
209
+ :where(p, ul, ol, blockquote) { margin-block: 0 var(--gala-space-md); }
210
+ :where(p) { text-wrap: pretty; }
211
+
212
+ blockquote {
213
+ margin-inline: 0;
214
+ padding-inline-start: var(--gala-space-md);
215
+ border-inline-start: 2px solid var(--gala-color-accent);
216
+ color: var(--gala-color-muted);
217
+ }
218
+
219
+ :where(code, kbd, samp) { font-family: var(--gala-font-mono); font-size: 0.9em; }
220
+ :where(pre) {
221
+ padding: var(--gala-space-md);
222
+ overflow-x: auto;
223
+ background: var(--gala-color-surface);
224
+ border-radius: var(--gala-radius-control);
225
+ }
226
+ :where(hr) { border: 0; border-block-start: 1px solid var(--gala-color-hairline); margin-block: var(--gala-space-xl); }
227
+ :where(table) { border-collapse: collapse; inline-size: 100%; }
228
+ :where(th, td) { padding: var(--gala-space-xs) var(--gala-space-sm); border-block-end: 1px solid var(--gala-color-hairline); text-align: start; }
229
+
230
+ /* An uppercase label needs air between the letters or it sets as a solid block. */
231
+ .gala-eyebrow, .gala-comment__meta time, .gala-comment__edited {
232
+ font-size: var(--gala-text-xs);
233
+ letter-spacing: var(--gala-tracking-wide);
234
+ text-transform: uppercase;
235
+ color: var(--gala-color-muted);
65
236
  }
66
237
 
67
238
  .gala-site-header {
@@ -85,7 +256,12 @@ body {
85
256
  border: 0.125rem solid transparent;
86
257
  border-radius: 50%;
87
258
  }
88
- .gala-icon-control:hover { background: var(--gala-color-surface); }
259
+ .gala-icon-control {
260
+ transition: background var(--gala-motion-fast) var(--gala-ease),
261
+ color var(--gala-motion-fast) var(--gala-ease);
262
+ color: var(--gala-color-muted);
263
+ }
264
+ .gala-icon-control:hover { background: var(--gala-color-surface); color: var(--gala-color-text); }
89
265
  .gala-icon-control:focus-visible { outline: 0.1875rem solid currentcolor; outline-offset: 0.1875rem; }
90
266
  /* Stroked, not filled: the icons are drawn on one 24px grid at a single weight, so they read as
91
267
  a set rather than as six unrelated silhouettes. */
@@ -98,8 +274,9 @@ body {
98
274
  padding: 1.5rem;
99
275
  color: var(--gala-color-text);
100
276
  background: var(--gala-color-background);
101
- border: 0.125rem solid var(--gala-color-border);
102
- border-radius: var(--gala-radius-control);
277
+ border: 1px solid var(--gala-surface-border);
278
+ border-radius: var(--gala-radius-surface);
279
+ box-shadow: var(--gala-shadow-high);
103
280
  }
104
281
  .gala-dialog::backdrop { background: rgb(0 0 0 / 65%); }
105
282
  .gala-dialog__close { float: inline-end; }
@@ -115,14 +292,19 @@ a { color: var(--gala-color-accent); }
115
292
 
116
293
  button {
117
294
  min-block-size: 2.75rem;
118
- padding-inline: 1rem;
295
+ padding-inline: var(--gala-space-md);
119
296
  color: inherit;
120
- background: transparent;
121
- border: 0.125rem solid currentcolor;
297
+ background: var(--gala-color-surface);
298
+ border: 1px solid var(--gala-color-border);
122
299
  border-radius: var(--gala-radius-control);
123
300
  font: inherit;
301
+ font-size: var(--gala-text-sm);
124
302
  cursor: pointer;
303
+ transition: background var(--gala-motion-fast) var(--gala-ease),
304
+ border-color var(--gala-motion-fast) var(--gala-ease);
125
305
  }
306
+ button:hover { border-color: var(--gala-color-border-strong); background: var(--gala-color-raised); }
307
+ button[disabled] { opacity: 0.55; cursor: not-allowed; }
126
308
 
127
309
  button:focus-visible {
128
310
  outline: 0.1875rem solid currentcolor;
@@ -135,8 +317,9 @@ button:focus-visible {
135
317
  display: inline-flex;
136
318
  align-items: center;
137
319
  min-block-size: 2.75rem;
138
- border: 0.125rem solid var(--gala-color-border);
320
+ border: 1px solid var(--gala-color-border);
139
321
  border-radius: var(--gala-radius-control);
322
+ font-size: var(--gala-text-sm);
140
323
  }
141
324
 
142
325
  .gala-badge,
@@ -146,7 +329,7 @@ button:focus-visible {
146
329
  .gala-section-heading > :where(h2, p) { margin-block: 0 0.5rem; }
147
330
  .gala-hero { display: grid; gap: 1.5rem; align-items: center; }
148
331
  .gala-hero__image { inline-size: 100%; block-size: auto; border-radius: var(--gala-radius-control); }
149
- .gala-page-content { max-inline-size: 70ch; }
332
+ .gala-page-content { max-inline-size: var(--gala-measure); }
150
333
  .gala-page-content :where(img, video) { max-inline-size: 100%; block-size: auto; }
151
334
  .gala-author-profile { display: flex; gap: 1rem; padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
152
335
  .gala-author-profile img { border-radius: 50%; object-fit: cover; }
@@ -156,7 +339,25 @@ button:focus-visible {
156
339
  .gala-card-index { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; padding: 0; list-style: none; }
157
340
  [data-layout='portfolio'] .gala-card-index { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1.5rem; }
158
341
  [data-layout='portfolio'] .gala-card { padding: clamp(1.25rem, 3vw, 2rem); }
159
- .gala-card { block-size: 100%; box-sizing: border-box; padding: 1.25rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
342
+ /* The surfaces are where `componentStyle` becomes visible: quiet sits on the page, outlined draws
343
+ its edge, raised lifts. One variable decides, so cards and dialogs cannot drift apart. */
344
+ .gala-card {
345
+ block-size: 100%;
346
+ box-sizing: border-box;
347
+ padding: var(--gala-space-lg);
348
+ background: var(--gala-color-surface);
349
+ border: 1px solid var(--gala-surface-border);
350
+ border-radius: var(--gala-radius-surface);
351
+ box-shadow: var(--gala-surface-shadow);
352
+ }
353
+ [data-component-style='raised'] .gala-card { background: var(--gala-color-raised); }
354
+ /* The one place the page moves of its own accord, and so the one place `motion` is legible:
355
+ `subtle` is a short settle, `expressive` a longer one, `none` no movement at all. */
356
+ .gala-card { transition: transform var(--gala-motion-base) var(--gala-ease),
357
+ box-shadow var(--gala-motion-base) var(--gala-ease); }
358
+ .gala-card-index li:hover .gala-card { transform: translateY(-2px); box-shadow: var(--gala-shadow-high); }
359
+ [data-motion='none'] .gala-card { transition: none; }
360
+ [data-motion='none'] .gala-card-index li:hover .gala-card { transform: none; }
160
361
  .gala-stats-graph { min-block-size: var(--gala-widget-min-block-size); margin-inline: 0; }
161
362
  .gala-stats-graph svg { inline-size: 100%; block-size: 8rem; }
162
363
  .gala-stats-graph polyline { fill: none; stroke: var(--gala-color-accent); stroke-width: 2; }
@@ -249,3 +450,75 @@ button:focus-visible {
249
450
  @media (prefers-reduced-motion: no-preference) {
250
451
  @view-transition { navigation: auto; }
251
452
  }
453
+
454
+ /* ---------------------------------------------------------------- the conversation
455
+ A thread is a hierarchy, and the indent is the only thing that says so. It is drawn with a
456
+ rule rather than padding alone, so five levels deep still reads as five levels and not as an
457
+ accident of margin. */
458
+ .gala-comments-island { margin-block-start: var(--gala-space-xl); }
459
+ .gala-comments__heading {
460
+ font-size: var(--gala-text-lg);
461
+ margin-block-end: var(--gala-space-md);
462
+ }
463
+ .gala-comments, .gala-comment-replies { list-style: none; margin: 0; padding: 0; }
464
+ .gala-comment-replies {
465
+ margin-block-start: var(--gala-space-sm);
466
+ margin-inline-start: var(--gala-space-xs);
467
+ padding-inline-start: var(--gala-space-md);
468
+ border-inline-start: 1px solid var(--gala-color-hairline);
469
+ }
470
+ .gala-comment { padding-block: var(--gala-space-sm); }
471
+ .gala-comment + .gala-comment { border-block-start: 1px solid var(--gala-color-hairline); }
472
+ .gala-comment-replies .gala-comment + .gala-comment { border-block-start: 0; }
473
+ .gala-comment__meta {
474
+ display: flex;
475
+ align-items: baseline;
476
+ gap: var(--gala-space-xs);
477
+ margin-block-end: var(--gala-space-2xs);
478
+ font-size: var(--gala-text-sm);
479
+ }
480
+ .gala-comment__meta strong { font-weight: 600; }
481
+ .gala-comment__pending { font-size: var(--gala-text-xs); color: var(--gala-color-muted); }
482
+ .gala-comment__body { margin-block: 0; overflow-wrap: anywhere; }
483
+
484
+ /* The controls stay out of the way until the comment is worth acting on. */
485
+ .gala-comment-actions {
486
+ display: flex;
487
+ gap: var(--gala-space-2xs);
488
+ margin-block-start: var(--gala-space-2xs);
489
+ opacity: 0.72;
490
+ transition: opacity var(--gala-motion-fast) var(--gala-ease);
491
+ }
492
+ .gala-comment:hover > .gala-comment-actions,
493
+ .gala-comment:focus-within > .gala-comment-actions { opacity: 1; }
494
+ .gala-comment-actions button {
495
+ min-block-size: 2rem;
496
+ padding-inline: var(--gala-space-xs);
497
+ font-size: var(--gala-text-xs);
498
+ background: transparent;
499
+ border-color: transparent;
500
+ color: var(--gala-color-muted);
501
+ }
502
+ .gala-comment-actions button:hover { color: var(--gala-color-text); background: var(--gala-color-surface); }
503
+
504
+ .gala-comment__form { display: grid; gap: var(--gala-space-xs); margin-block: var(--gala-space-sm); }
505
+ .gala-comment__form textarea {
506
+ inline-size: 100%;
507
+ box-sizing: border-box;
508
+ padding: var(--gala-space-sm);
509
+ color: inherit;
510
+ background: var(--gala-color-raised);
511
+ border: 1px solid var(--gala-color-border);
512
+ border-radius: var(--gala-radius-control);
513
+ font: inherit;
514
+ font-size: var(--gala-text-sm);
515
+ line-height: var(--gala-leading-body);
516
+ resize: vertical;
517
+ }
518
+ .gala-comment__form textarea:focus-visible { outline: 2px solid var(--gala-color-accent); outline-offset: 1px; }
519
+ .gala-comment__controls { display: flex; gap: var(--gala-space-xs); }
520
+ .gala-comments__status, .gala-comments__empty, .gala-comments__prompt {
521
+ color: var(--gala-color-muted);
522
+ font-size: var(--gala-text-sm);
523
+ }
524
+ .gala-comments__more { margin-block-start: var(--gala-space-md); }