@rathnasgala/theme 0.0.18 → 0.0.20

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.18",
3
+ "version": "0.0.20",
4
4
  "description": "Managed Gala static-site theme payload",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,12 +2,16 @@
2
2
  "schemaVersion": 1,
3
3
  "themePackage": {
4
4
  "name": "@rathnasgala/theme",
5
- "version": "0.0.18",
5
+ "version": "0.0.20",
6
6
  "availableDesignThemes": [
7
7
  "editorial"
8
8
  ],
9
9
  "securityAdvisories": []
10
10
  },
11
+ "requiredBudgets": {
12
+ "managedJavaScriptBytes": 65536,
13
+ "managedCssBytes": 32768
14
+ },
11
15
  "files": {
12
16
  ".gala/publish.yml.template": "73080241d89bd09964b797a8bcc8e18471412e11bc873efad76305b60f497d08",
13
17
  ".gitignore": "b5fa24e10155097a0d372926fd2e59e6072e7cc2cb6399bbfb4beb8b33ea315b",
@@ -21,7 +25,7 @@
21
25
  "lib/render-markdown.js": "40e6d6743b76de6c16d138475f188a4263967530c370d8b26a363f4df58c2caf",
22
26
  "lib/seo.js": "107de8804931945b8e73894877fc394d6b535a9d0d969a4ba5b68f2e136e0a17",
23
27
  "lib/share-targets.js": "458aff86fab2b62a364442cd8c612565c6e0b53eef2f20cb6237cc562b1e0731",
24
- "lib/site-config.js": "da7d2dffab62c7346c9930494b85fc6ea735d4f50565bbd0454a04393f8cad96",
28
+ "lib/site-config.js": "3ed45daef0b7f79d94bf0c648c4fe7d8bf517534e4645a4b43d952dfc434ddc4",
25
29
  "package-lock.json": "c97a6a2f794aef083a39a8d2628eccc4a3c484e1c6225899b77ec1fb7c466d9e",
26
30
  "package.json": "4c60bea06f39aec755ac68caf8d8841c1d59d0f6121fad7f4396231ce14712dc",
27
31
  "src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
@@ -29,9 +33,9 @@
29
33
  "src/_data/engagementSnapshot.js": "9217dacebfaf008f7a0ad67ff4e8d1fa8fba609a9dc7e3430c78c5b32024da0e",
30
34
  "src/_data/feedLinks.js": "06c93352ebd888acb431a1c9a8be6ecff757895468e9f2d336761038751c2d5e",
31
35
  "src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
32
- "src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
36
+ "src/_data/site.js": "6f9b754876ab7cfa93f02a38be2962edce8245423c354426a1465513caec7911",
33
37
  "src/_includes/components/ui.njk": "23eb647d8658868449ec6bb809e965a85302124a97639f94d5d28af72df56e4c",
34
- "src/_includes/layouts/base.njk": "6914da799eb31fea3fc230ab196252129ccca90f27ccb08bbcee81741a91ced4",
38
+ "src/_includes/layouts/base.njk": "638457f6c10f3ba866e8c70715678298096a668568e99456bef4167ebf1111e7",
35
39
  "src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
36
40
  "src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
37
41
  "src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
@@ -39,11 +43,12 @@
39
43
  "src/assets/embed-youtube.svg": "10d192f2b14b67664fea88c8cf246321d20190def51d59fad5762e552219b067",
40
44
  "src/assets/engagement-comments.js": "b20d9268c9d908b4453a42188eaca25349f25534fb04a9293c9e688a982cd8f8",
41
45
  "src/assets/engagement-transport.js": "4f9ca0cd669bf8abc25f2909104e3e4b231657ef97d36a17d06e53454d711786",
42
- "src/assets/interactions.js": "dd4e343505e528d89596ca67db679b52c989e022f93399193e4377db01fc8a08",
46
+ "src/assets/favicon.svg": "8615e3d96fe37ae9ed14afb1886e7c0e690eef5b10be66ca91cccfd085a74bb6",
47
+ "src/assets/interactions.js": "4b65dcfcc23682616b15071d746da053bcc3a6bc5df36e11dc5698dd3348f314",
43
48
  "src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
44
49
  "src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
45
50
  "src/assets/theme-mode.js": "944b29db11a577961a60ddcfa52f07ef2411f3f3631bea85bd79be16428c08be",
46
- "src/assets/theme.css": "2a2367f2c875121cc95bc789a0d5889960813e660ca7722b328b39b4ff5068a8",
51
+ "src/assets/theme.css": "300f6c2ffd7e63a948cfdb46b2d0f4abdbce5f5c4da7ee2cca5633d9795277c4",
47
52
  "src/assets/vendor/hooks.js": "ae6ec58592683ca64fa4d65b20d40b103df62a673bcc4712515cf165950b07f3",
48
53
  "src/assets/vendor/htm.js": "e4dac9bc40a6f2ec590413d787ae30bbcfdfcde4f28971ee65d9578271b7aad4",
49
54
  "src/assets/vendor/preact.js": "712517dd2a42c3c22b5ab0264ad7e9a2b1e8be6cf97fa00dbedd98203617e616",
@@ -58,7 +63,6 @@
58
63
  "src/redirects.11ty.js": "3c163246a59f5364861db6a6f05ae88afc938a5379d86cf42776d8d49b0031f3",
59
64
  "src/search-index.11ty.js": "700dc2a2be0ef20de3a828a4612fcf7a082691eade29c6e1d75f09c24b6c6024",
60
65
  "src/search.njk": "63f2e8aeebb63c90da2e375eece639ce096cad1eff7f28628cafc46faf7ceee6",
61
- "src/settings.njk": "9e6babf4d2774687558618c4fbd109ddfc0f479f52ec2e5d00c373c44ad16676",
62
66
  "src/sitemap.11ty.js": "4f233e94fb4a913ad5659116518432ed264984a7b622eba3558c77b4ec6435dc",
63
67
  "static/robots.txt": "16ceb5ee3e0dc13aa9adf31a3ebbe45a1d965b8c2b9f72eaf84e5911e140ed95"
64
68
  },
@@ -13,13 +13,9 @@ const BUDGET_KEYS = Object.freeze([
13
13
  */
14
14
  const DESIGN_VALUES = Object.freeze({
15
15
  layout: Object.freeze(['article-first', 'portfolio']),
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'])
16
+ palette: Object.freeze(['default', 'ocean', 'forest', 'plum']),
17
+ theme: Object.freeze(['editorial', 'modern', 'technical']),
18
+ colorMode: Object.freeze(['system', 'light', 'dark'])
23
19
  });
24
20
  const CONTACT_KEYS = Object.freeze([
25
21
  'enabled', 'websiteEnabled', 'phoneEnabled'
@@ -33,7 +29,14 @@ const CONTACT_KEYS = Object.freeze([
33
29
  */
34
30
  const REQUIRED_DESIGN = Object.freeze(['layout', 'palette']);
35
31
 
32
+ /** A free colour, not a menu — but still a colour. */
33
+ const ACCENT = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
34
+
36
35
  function validateImplementedDesign(config) {
36
+ const accent = config?.design?.accent;
37
+ if (accent !== undefined && !(typeof accent === 'string' && ACCENT.test(accent.trim()))) {
38
+ throw new TypeError(`Unsupported design.accent: ${accent}`);
39
+ }
37
40
  if (config?.design == null) return;
38
41
  for (const [field, allowed] of Object.entries(DESIGN_VALUES)) {
39
42
  const value = config.design[field];
@@ -1,5 +1,16 @@
1
+ import { accentPair } from '../../lib/accent.js';
1
2
  import { loadSiteConfiguration } from '../../lib/site-config.js';
2
3
 
3
4
  export default async function siteConfiguration() {
4
- return loadSiteConfiguration();
5
+ const configuration = await loadSiteConfiguration();
6
+ /*
7
+ * The writer picks one colour; the page needs two, because the same colour cannot be readable on
8
+ * both a light and a dark ground. Derived here rather than in CSS so the contrast is exact — a
9
+ * `color-mix()` approximation can land under 4.5:1 and nobody would know until a reader could
10
+ * not read a link.
11
+ *
12
+ * An absent or unparseable value leaves this undefined, and the look's own accent stands.
13
+ */
14
+ const accent = accentPair(configuration?.design?.accent);
15
+ return accent ? { ...configuration, accent } : configuration;
5
16
  }
@@ -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-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') }}">
2
+ <html lang="{{ post.language or site.site.defaultLanguage }}" data-theme="{{ site.design.theme | default('modern') }}" data-layout="{{ site.design.layout }}" data-palette="{{ site.design.palette | default('default') }}" data-mode="{{ site.design.colorMode | default('system') }}"{% if site.accent %} style="--gala-accent-light: {{ site.accent.light }}; --gala-accent-dark: {{ site.accent.dark }}"{% endif %}>
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -26,6 +26,7 @@
26
26
  <link rel="stylesheet" href="{{ '/custom.css' | url }}">
27
27
  <script src="{{ '/assets/theme-mode.js' | url }}" defer></script>
28
28
  <script src="{{ '/assets/preferences.js' | url }}" defer></script>
29
+ <link rel="icon" href="{{ '/assets/favicon.svg' | url }}" type="image/svg+xml">
29
30
  <script src="{{ '/assets/interactions.js' | url }}" defer></script>
30
31
  <script type="module" src="{{ '/assets/engagement-comments.js' | url }}"></script>
31
32
  <script src="{{ '/assets/search.js' | url }}" defer></script>
@@ -69,17 +70,12 @@
69
70
  <dialog id="gala-settings-dialog" class="gala-dialog" aria-labelledby="gala-settings-title">
70
71
  <form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close settings">Close</button></form>
71
72
  <h2 id="gala-settings-title">Reader settings</h2>
72
- <section aria-labelledby="gala-dialog-theme-setting">
73
- <h3 id="gala-dialog-theme-setting">Appearance</h3>
74
- <button type="button" data-theme-mode-toggle aria-label="Color mode: system. Activate for light."><span data-theme-mode-label>Theme: system</span></button>
75
- </section>
76
73
  <section aria-labelledby="gala-dialog-language-setting">
77
74
  <h3 id="gala-dialog-language-setting">Language preference</h3>
78
75
  <label>Preferred language
79
76
  <select data-language-preference>{% for language in languages %}<option value="{{ language | escape }}">{{ language }}</option>{% endfor %}</select>
80
77
  </label>
81
78
  </section>
82
- <p><a href="{{ '/settings/' | url }}">Open settings page</a></p>
83
79
  </dialog>
84
80
  <dialog id="gala-search-dialog" class="gala-dialog" aria-labelledby="gala-search-title">
85
81
  <form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close search">Close</button></form>
@@ -98,7 +94,7 @@
98
94
  {% if site.site.id and site.site.id != "unavailable" %}
99
95
  <dialog id="gala-account-dialog" class="gala-dialog gala-account-dialog" aria-labelledby="gala-account-title">
100
96
  <form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close account">Close</button></form>
101
- <h2 id="gala-account-title">Gala account</h2>
97
+ <h2 id="gala-account-title" class="gala-visually-hidden">Gala account</h2>
102
98
  <iframe data-gala-session-frame title="Gala account and sign in" allow="identity-credentials-get" src="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/widget/session?siteId={{ site.site.id | escape }}"></iframe>
103
99
  </dialog>
104
100
  {% endif %}
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#8a4b2a"/><path d="M22 11.5c-1.1-1.3-2.8-2-4.6-2-3.6 0-6.4 2.9-6.4 6.5s2.8 6.5 6.4 6.5c3.2 0 5.6-2 6-5h-5.6" fill="none" stroke="#fdfcfb" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -392,6 +392,16 @@ if (sessionFrame) {
392
392
  else pending.reject(new Error(event.data.error?.code || 'ENGAGEMENT_WRITE_FAILED'));
393
393
  return;
394
394
  }
395
+ /* The frame is cross-origin, so its content height is not readable from here — it reports
396
+ its own, and the box is sized to it. Without this the account panel scrolled inside a fixed
397
+ box, clipping the first line of its own text. */
398
+ if (event.data?.type === 'gala-session-height') {
399
+ const height = Number(event.data.height);
400
+ if (Number.isFinite(height) && height > 0 && height < 2000) {
401
+ sessionFrame.style.height = `${Math.ceil(height)}px`;
402
+ }
403
+ return;
404
+ }
395
405
  if (event.data?.type !== 'gala-session') return;
396
406
  sessionUser = event.data.user && typeof event.data.user.id === 'string' ? event.data.user : null;
397
407
  const control = document.querySelector('[data-user-control]');
@@ -18,7 +18,9 @@
18
18
  --gala-color-border: light-dark(#968e86, #6b645e);
19
19
  --gala-color-hairline: light-dark(#e8e3dd, #2a2724);
20
20
  --gala-color-border-strong: light-dark(#c9c2ba, #46413d);
21
- --gala-color-accent: light-dark(#8a4b2a, #e8a87c);
21
+ --gala-color-accent: light-dark(
22
+ var(--gala-accent-light, #8a4b2a),
23
+ var(--gala-accent-dark, #e8a87c));
22
24
 
23
25
  /* Type. One scale, used everywhere, fluid between a phone and a desk. */
24
26
  --gala-font-body: ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
@@ -48,7 +50,6 @@
48
50
  --gala-space-page: clamp(1.25rem, 5vw, 4.5rem);
49
51
  --gala-radius-control: 0.625rem;
50
52
  --gala-radius-surface: 1rem;
51
- --gala-shadow-low: 0 1px 2px light-dark(rgb(26 24 23 / 6%), rgb(0 0 0 / 40%));
52
53
  --gala-shadow-high:
53
54
  0 1px 2px light-dark(rgb(26 24 23 / 5%), rgb(0 0 0 / 35%)),
54
55
  0 12px 28px -12px light-dark(rgb(26 24 23 / 18%), rgb(0 0 0 / 60%));
@@ -73,7 +74,7 @@
73
74
  --gala-color-border: #968e86;
74
75
  --gala-color-hairline: #e8e3dd;
75
76
  --gala-color-border-strong: #c9c2ba;
76
- --gala-color-accent: #8a4b2a;
77
+ --gala-color-accent: var(--gala-accent-light, #8a4b2a);
77
78
  }
78
79
  :root[data-mode='dark'] {
79
80
  color-scheme: dark;
@@ -85,7 +86,7 @@
85
86
  --gala-color-border: #6b645e;
86
87
  --gala-color-hairline: #2a2724;
87
88
  --gala-color-border-strong: #46413d;
88
- --gala-color-accent: #e8a87c;
89
+ --gala-color-accent: var(--gala-accent-dark, #e8a87c);
89
90
  }
90
91
 
91
92
  /* ---------------------------------------------------------------- palettes */
@@ -110,7 +111,7 @@
110
111
  --gala-color-border: #7d8d99;
111
112
  --gala-color-hairline: #dde7ee;
112
113
  --gala-color-border-strong: #bdccd7;
113
- --gala-color-accent: #0f5f7a;
114
+ --gala-color-accent: var(--gala-accent-light, #0f5f7a);
114
115
  }
115
116
  :root[data-palette='ocean'][data-mode='dark'] {
116
117
  color-scheme: dark;
@@ -122,62 +123,150 @@
122
123
  --gala-color-border: #5f7d8c;
123
124
  --gala-color-hairline: #1d3644;
124
125
  --gala-color-border-strong: #33505f;
125
- --gala-color-accent: #7fd0e8;
126
+ --gala-color-accent: var(--gala-accent-dark, #7fd0e8);
126
127
  }
127
128
 
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'] {
129
+
130
+ /* Two more, because colour is the choice a writer actually wants to make — and the one that is
131
+ safe to offer, since a palette only swaps values every rule already reads and each is checked
132
+ for contrast in both modes. */
133
+ :root[data-palette='forest'] {
134
+ --gala-color-background: light-dark(#fbfcfa, #101410);
135
+ --gala-color-surface: light-dark(#f0f4ee, #182018);
136
+ --gala-color-raised: light-dark(#ffffff, #1f271f);
137
+ --gala-color-text: light-dark(#18201a, #eef3ec);
138
+ --gala-color-muted: light-dark(#5b665d, #9aa89b);
139
+ --gala-color-border: light-dark(#849084, #5e685f);
140
+ --gala-color-hairline: light-dark(#e2e9df, #232b23);
141
+ --gala-color-border-strong: light-dark(#5b665d, #7d897e);
142
+ --gala-color-accent: light-dark(#2f6b3f, #8fd3a1);
143
+ }
144
+ :root[data-palette='forest'][data-mode='light'] {
145
+ color-scheme: light;
146
+ --gala-color-background: #fbfcfa;
147
+ --gala-color-surface: #f0f4ee;
148
+ --gala-color-raised: #ffffff;
149
+ --gala-color-text: #18201a;
150
+ --gala-color-muted: #5b665d;
151
+ --gala-color-border: #849084;
152
+ --gala-color-hairline: #e2e9df;
153
+ --gala-color-border-strong: #5b665d;
154
+ --gala-color-accent: var(--gala-accent-light, #2f6b3f);
155
+ }
156
+ :root[data-palette='forest'][data-mode='dark'] {
157
+ color-scheme: dark;
158
+ --gala-color-background: #101410;
159
+ --gala-color-surface: #182018;
160
+ --gala-color-raised: #1f271f;
161
+ --gala-color-text: #eef3ec;
162
+ --gala-color-muted: #9aa89b;
163
+ --gala-color-border: #5e685f;
164
+ --gala-color-hairline: #232b23;
165
+ --gala-color-border-strong: #7d897e;
166
+ --gala-color-accent: var(--gala-accent-dark, #8fd3a1);
167
+ }
168
+ :root[data-palette='plum'] {
169
+ --gala-color-background: light-dark(#fdfbfd, #141016);
170
+ --gala-color-surface: light-dark(#f5eff5, #1d1720);
171
+ --gala-color-raised: light-dark(#ffffff, #241d28);
172
+ --gala-color-text: light-dark(#1f1820, #f2ecf3);
173
+ --gala-color-muted: light-dark(#665c69, #a99dac);
174
+ --gala-color-border: light-dark(#948a95, #6b606d);
175
+ --gala-color-hairline: light-dark(#ebe2ec, #281f2b);
176
+ --gala-color-border-strong: light-dark(#665c69, #8b7f8d);
177
+ --gala-color-accent: light-dark(#7a3d76, #dda6d8);
178
+ }
179
+ :root[data-palette='plum'][data-mode='light'] {
180
+ color-scheme: light;
181
+ --gala-color-background: #fdfbfd;
182
+ --gala-color-surface: #f5eff5;
183
+ --gala-color-raised: #ffffff;
184
+ --gala-color-text: #1f1820;
185
+ --gala-color-muted: #665c69;
186
+ --gala-color-border: #948a95;
187
+ --gala-color-hairline: #ebe2ec;
188
+ --gala-color-border-strong: #665c69;
189
+ --gala-color-accent: var(--gala-accent-light, #7a3d76);
190
+ }
191
+ :root[data-palette='plum'][data-mode='dark'] {
192
+ color-scheme: dark;
193
+ --gala-color-background: #141016;
194
+ --gala-color-surface: #1d1720;
195
+ --gala-color-raised: #241d28;
196
+ --gala-color-text: #f2ecf3;
197
+ --gala-color-muted: #a99dac;
198
+ --gala-color-border: #6b606d;
199
+ --gala-color-hairline: #281f2b;
200
+ --gala-color-border-strong: #8b7f8d;
201
+ --gala-color-accent: var(--gala-accent-dark, #dda6d8);
202
+ }
203
+
204
+ /* ---------------------------------------------------------------- the looks
205
+ *
206
+ * Three, each designed whole.
207
+ *
208
+ * This replaced nine independent knobs — typeface, spacing, density, radius, motion, surfaces,
209
+ * palette — which a writer set one at a time, blind, and only saw combined after a deploy. Nine
210
+ * knobs is several hundred looks, and nobody had seen most of them: `mono` + `round` + `spacious`
211
+ * + `raised` produced monospace headings inside page-wide pills, and it was reachable in four
212
+ * clicks. A look is a set of decisions that agree with each other, so it is chosen as a set.
213
+ */
214
+
215
+ /* Calm, serif, generous. For essays and long reading. */
216
+ :root[data-theme='editorial'] {
133
217
  --gala-font-body: ui-serif, "Iowan Old Style", Charter, "Source Serif 4", Georgia, serif;
134
218
  --gala-font-display: var(--gala-font-body);
135
219
  --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;
220
+ --gala-step: 1.1rem;
221
+ --gala-leading-body: 1.7;
222
+ --gala-radius-control: 0.5rem;
223
+ --gala-radius-surface: 0.75rem;
224
+ --gala-measure: 66ch;
225
+ --gala-surface-border: var(--gala-color-hairline);
226
+ --gala-surface-shadow: none;
145
227
  }
146
228
 
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;
229
+ /* Crisp, sans, a little tighter. The default, and the safe one. */
230
+ :root[data-theme='modern'] {
231
+ --gala-font-body: ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
232
+ --gala-font-display: var(--gala-font-body);
233
+ --gala-weight-display: 640;
234
+ --gala-step: 1rem;
235
+ --gala-leading-body: 1.65;
236
+ --gala-radius-control: 0.625rem;
237
+ --gala-radius-surface: 1rem;
238
+ --gala-measure: 70ch;
239
+ --gala-surface-border: transparent;
240
+ --gala-surface-shadow: var(--gala-shadow-high);
155
241
  }
156
242
 
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; }
243
+ /* For writing about code: mono for the furniture, never for the prose or the headings. */
244
+ :root[data-theme='technical'] {
245
+ --gala-font-body: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
246
+ --gala-font-display: var(--gala-font-body);
247
+ --gala-weight-display: 660;
248
+ --gala-tracking-tight: -0.022em;
249
+ --gala-step: 0.9rem;
250
+ --gala-leading-body: 1.6;
251
+ --gala-radius-control: 0.375rem;
252
+ --gala-radius-surface: 0.5rem;
253
+ --gala-measure: 74ch;
254
+ --gala-surface-border: var(--gala-color-border-strong);
255
+ --gala-surface-shadow: none;
256
+ }
257
+ /* Mono is an accent here — labels, counts, metadata — not a face to set a page in. */
258
+ :root[data-theme='technical'] .gala-eyebrow,
259
+ :root[data-theme='technical'] .gala-engagement dt,
260
+ :root[data-theme='technical'] .gala-comment__meta time {
261
+ font-family: var(--gala-font-mono);
262
+ }
263
+
264
+ /* Motion is not a choice a writer has to make; it is a property of a considered page. */
166
265
  @media (prefers-reduced-motion: reduce) {
167
266
  :root { --gala-motion-fast: 0ms; --gala-motion-base: 0ms; }
168
267
  *, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
169
268
  }
170
269
 
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);
179
- }
180
-
181
270
  :root[data-layout='portfolio'] { --gala-content-width: 82rem; }
182
271
 
183
272
  body {
@@ -235,13 +324,29 @@ blockquote {
235
324
  color: var(--gala-color-muted);
236
325
  }
237
326
 
327
+ /*
328
+ * Stays with the reader. It used to scroll away, so on a long post the only way back to the site
329
+ * — or to sign in — was to scroll to the top. The negative inline margin lets the bar's background
330
+ * reach the edge of the viewport while its contents stay on the page's own measure.
331
+ */
238
332
  .gala-site-header {
333
+ position: sticky;
334
+ inset-block-start: 0;
335
+ z-index: 10;
239
336
  display: flex;
240
337
  align-items: center;
241
338
  justify-content: space-between;
339
+ gap: var(--gala-space-md);
242
340
  max-inline-size: var(--gala-content-width);
243
341
  margin-inline: auto;
244
- margin-block-end: 1.5rem;
342
+ margin-block-end: var(--gala-space-xl);
343
+ padding-block: var(--gala-space-xs);
344
+ background: color-mix(in srgb, var(--gala-color-background) 88%, transparent);
345
+ backdrop-filter: blur(8px);
346
+ border-block-end: 1px solid var(--gala-color-hairline);
347
+ }
348
+ @supports not (backdrop-filter: blur(8px)) {
349
+ .gala-site-header { background: var(--gala-color-background); }
245
350
  }
246
351
 
247
352
  .gala-site-header__actions { display: flex; gap: 0.5rem; }
@@ -279,8 +384,21 @@ blockquote {
279
384
  box-shadow: var(--gala-shadow-high);
280
385
  }
281
386
  .gala-dialog::backdrop { background: rgb(0 0 0 / 65%); }
282
- .gala-dialog__close { float: inline-end; }
283
- .gala-account-dialog iframe { inline-size: 100%; min-block-size: 22rem; border: 0; }
387
+ /* Sized to what it holds. 22rem was a box for a name and one button, and it showed. */
388
+ .gala-account-dialog { inline-size: min(26rem, calc(100% - 2rem)); }
389
+ .gala-account-dialog iframe {
390
+ display: block;
391
+ inline-size: 100%;
392
+ /* Height is set from the frame's own report; this is only what it shows before that arrives. */
393
+ block-size: 7rem;
394
+ border: 0;
395
+ /* The frame owns its layout and reports its size, so it never needs to scroll inside the box. */
396
+ overflow: hidden;
397
+ }
398
+ /* The close control belongs on the same line as the title, not floated above it. */
399
+ .gala-dialog { display: grid; gap: var(--gala-space-sm); }
400
+ .gala-dialog__close { justify-self: end; margin: 0; }
401
+ .gala-dialog > h2 { margin-block: 0; }
284
402
 
285
403
  main,
286
404
  .gala-page-footer {
@@ -350,14 +468,11 @@ button:focus-visible {
350
468
  border-radius: var(--gala-radius-surface);
351
469
  box-shadow: var(--gala-surface-shadow);
352
470
  }
353
- [data-component-style='raised'] .gala-card { background: var(--gala-color-raised); }
354
471
  /* The one place the page moves of its own accord, and so the one place `motion` is legible:
355
472
  `subtle` is a short settle, `expressive` a longer one, `none` no movement at all. */
356
473
  .gala-card { transition: transform var(--gala-motion-base) var(--gala-ease),
357
474
  box-shadow var(--gala-motion-base) var(--gala-ease); }
358
475
  .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; }
361
476
  .gala-stats-graph { min-block-size: var(--gala-widget-min-block-size); margin-inline: 0; }
362
477
  .gala-stats-graph svg { inline-size: 100%; block-size: 8rem; }
363
478
  .gala-stats-graph polyline { fill: none; stroke: var(--gala-color-accent); stroke-width: 2; }
@@ -372,23 +487,14 @@ button:focus-visible {
372
487
  .gala-search-results h2 { margin-block-start: 0; }
373
488
  .gala-toc { padding: 1rem; background: var(--gala-color-surface); }
374
489
  .gala-share__fallback { inline-size: min(100%, 36rem); }
375
- .gala-engagement { min-block-size: var(--gala-widget-min-block-size); }
376
- .gala-engagement dl { display: flex; flex-wrap: wrap; gap: 1rem 2rem; }
377
- .gala-engagement dl > div { display: grid; gap: 0.25rem; }
378
- .gala-engagement dd { margin: 0; font-size: 1.25rem; font-weight: 700; }
379
490
  .gala-engagement__placeholder { display: grid; min-block-size: var(--gala-widget-min-block-size); align-content: center; margin: 0; }
380
- .gala-engagement-profile { margin-block: 1.5rem; padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
381
491
  .gala-engagement-profile > * { margin-block: 0 0.5rem; }
382
- .gala-comments { display: grid; gap: 1rem; padding-inline-start: 1.5rem; }
383
- .gala-comments li { padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
384
492
  .gala-engagement-actions { display: grid; gap: 1rem; margin-block-start: 1.5rem; }
385
493
  .gala-engagement-actions[hidden] { display: none; }
386
494
  /* Shown only when signing in interrupted something the reader had already decided to do, so it
387
495
  reassures rather than sells. Hidden for anyone who opened the account dialog deliberately. */
388
496
  .gala-account-dialog__invite { margin-block: 0 1rem; max-inline-size: 34rem; }
389
497
  .gala-account-dialog__invite[hidden] { display: none; }
390
- .gala-reaction-actions,
391
- .gala-comment-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
392
498
  .gala-engagement-actions form,
393
499
  [data-comment-editor] { display: grid; gap: 0.5rem; max-inline-size: 42rem; }
394
500
  .gala-engagement-actions label { display: grid; gap: 0.5rem; }
@@ -522,3 +628,49 @@ button:focus-visible {
522
628
  font-size: var(--gala-text-sm);
523
629
  }
524
630
  .gala-comments__more { margin-block-start: var(--gala-space-md); }
631
+
632
+ /* ---------------------------------------------------------------- where the article ends
633
+ The writing and the conversation about it are different things, and the page has to say so.
634
+ A footnote used to run straight into the author card with nothing between them. */
635
+ .gala-engagement {
636
+ /* Space is reserved before the counts arrive, so the page does not jump under the reader. */
637
+ min-block-size: var(--gala-widget-min-block-size);
638
+ max-inline-size: var(--gala-content-width);
639
+ margin-block-start: var(--gala-space-xl);
640
+ padding-block-start: var(--gala-space-lg);
641
+ border-block-start: 1px solid var(--gala-color-border);
642
+ }
643
+ .gala-engagement-profile {
644
+ display: flex;
645
+ align-items: baseline;
646
+ gap: var(--gala-space-sm);
647
+ padding: 0;
648
+ background: none;
649
+ }
650
+ .gala-engagement-profile h2 { font-size: var(--gala-text-lg); margin: 0; }
651
+ .gala-engagement-profile p { margin: 0; color: var(--gala-color-muted); font-size: var(--gala-text-sm); }
652
+
653
+ /* Counts read as data, not as three stacked paragraphs. */
654
+ .gala-engagement dl {
655
+ display: flex;
656
+ flex-wrap: wrap;
657
+ gap: var(--gala-space-lg);
658
+ margin-block: var(--gala-space-md);
659
+ }
660
+ .gala-engagement dl > div { display: flex; flex-direction: column-reverse; gap: 0.1rem; }
661
+ .gala-engagement dt {
662
+ font-size: var(--gala-text-xs);
663
+ letter-spacing: var(--gala-tracking-wide);
664
+ text-transform: uppercase;
665
+ color: var(--gala-color-muted);
666
+ }
667
+ .gala-engagement dd {
668
+ margin: 0;
669
+ font-size: var(--gala-text-lg);
670
+ font-variant-numeric: tabular-nums;
671
+ }
672
+
673
+ .gala-reaction-actions { display: flex; flex-wrap: wrap; gap: var(--gala-space-2xs); }
674
+ .gala-engagement-actions { display: grid; gap: var(--gala-space-sm); justify-items: start; }
675
+ .gala-engagement-actions > [data-follow-article] { inline-size: auto; }
676
+ .gala-share { margin-block-start: var(--gala-space-lg); }
@@ -1,19 +0,0 @@
1
- ---
2
- layout: layouts/base.njk
3
- title: Settings
4
- permalink: /settings/index.html
5
- ---
6
- <h1>Reader settings</h1>
7
- <section aria-labelledby="theme-setting">
8
- <h2 id="theme-setting">Theme</h2>
9
- <button type="button" data-theme-mode-toggle aria-label="Color mode: system. Activate for light.">Theme: system</button>
10
- </section>
11
- <section aria-labelledby="language-setting">
12
- <h2 id="language-setting">Language preference</h2>
13
- <p>Your preference highlights matching choices. The language in the page URL always controls the content.</p>
14
- <label>Preferred language
15
- <select data-language-preference>
16
- {% for language in languages %}<option value="{{ language | escape }}">{{ language }}</option>{% endfor %}
17
- </select>
18
- </label>
19
- </section>