@rathnasgala/theme 0.0.17 → 0.0.19

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.19",
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.17",
5
+ "version": "0.0.19",
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": "ab7cc170dc30c74ea5ef08b8ff6a82f9820998ae4190a06d64375dfd7d515864",
28
+ "lib/site-config.js": "da7d2dffab62c7346c9930494b85fc6ea735d4f50565bbd0454a04393f8cad96",
25
29
  "package-lock.json": "c97a6a2f794aef083a39a8d2628eccc4a3c484e1c6225899b77ec1fb7c466d9e",
26
30
  "package.json": "4c60bea06f39aec755ac68caf8d8841c1d59d0f6121fad7f4396231ce14712dc",
27
31
  "src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
@@ -31,7 +35,7 @@
31
35
  "src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
32
36
  "src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
33
37
  "src/_includes/components/ui.njk": "23eb647d8658868449ec6bb809e965a85302124a97639f94d5d28af72df56e4c",
34
- "src/_includes/layouts/base.njk": "f504b5a80020b90eb47d59db5e626a4554e2f2d4ef64b3b25c98869830581ebe",
38
+ "src/_includes/layouts/base.njk": "e73be30738535902599e1c060c11afbd128c86332913114f643dc56dea99fcba",
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",
46
+ "src/assets/favicon.svg": "8615e3d96fe37ae9ed14afb1886e7c0e690eef5b10be66ca91cccfd085a74bb6",
42
47
  "src/assets/interactions.js": "dd4e343505e528d89596ca67db679b52c989e022f93399193e4377db01fc8a08",
43
48
  "src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
44
49
  "src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
45
- "src/assets/theme-mode.js": "3797c84dbaa090b5b21427a31928b5dd97f13d04d99833f5e1e74c6b50e01dcc",
46
- "src/assets/theme.css": "94839833f93997415f570041a691da0c96c27992f897e116948077a8b25e8dfa",
50
+ "src/assets/theme-mode.js": "944b29db11a577961a60ddcfa52f07ef2411f3f3631bea85bd79be16428c08be",
51
+ "src/assets/theme.css": "cdd41ddb670fb8c823b3490efa2a887d11dd6a1931afa0264deee53283b684d3",
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
  },
@@ -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">
@@ -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>
@@ -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,15 +186,78 @@ 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);
65
217
  }
66
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);
236
+ }
237
+
238
+ /*
239
+ * Stays with the reader. It used to scroll away, so on a long post the only way back to the site
240
+ * — or to sign in — was to scroll to the top. The negative inline margin lets the bar's background
241
+ * reach the edge of the viewport while its contents stay on the page's own measure.
242
+ */
67
243
  .gala-site-header {
244
+ position: sticky;
245
+ inset-block-start: 0;
246
+ z-index: 10;
68
247
  display: flex;
69
248
  align-items: center;
70
249
  justify-content: space-between;
250
+ gap: var(--gala-space-md);
71
251
  max-inline-size: var(--gala-content-width);
72
252
  margin-inline: auto;
73
- margin-block-end: 1.5rem;
253
+ margin-block-end: var(--gala-space-xl);
254
+ padding-block: var(--gala-space-xs);
255
+ background: color-mix(in srgb, var(--gala-color-background) 88%, transparent);
256
+ backdrop-filter: blur(8px);
257
+ border-block-end: 1px solid var(--gala-color-hairline);
258
+ }
259
+ @supports not (backdrop-filter: blur(8px)) {
260
+ .gala-site-header { background: var(--gala-color-background); }
74
261
  }
75
262
 
76
263
  .gala-site-header__actions { display: flex; gap: 0.5rem; }
@@ -85,7 +272,12 @@ body {
85
272
  border: 0.125rem solid transparent;
86
273
  border-radius: 50%;
87
274
  }
88
- .gala-icon-control:hover { background: var(--gala-color-surface); }
275
+ .gala-icon-control {
276
+ transition: background var(--gala-motion-fast) var(--gala-ease),
277
+ color var(--gala-motion-fast) var(--gala-ease);
278
+ color: var(--gala-color-muted);
279
+ }
280
+ .gala-icon-control:hover { background: var(--gala-color-surface); color: var(--gala-color-text); }
89
281
  .gala-icon-control:focus-visible { outline: 0.1875rem solid currentcolor; outline-offset: 0.1875rem; }
90
282
  /* Stroked, not filled: the icons are drawn on one 24px grid at a single weight, so they read as
91
283
  a set rather than as six unrelated silhouettes. */
@@ -98,12 +290,21 @@ body {
98
290
  padding: 1.5rem;
99
291
  color: var(--gala-color-text);
100
292
  background: var(--gala-color-background);
101
- border: 0.125rem solid var(--gala-color-border);
102
- border-radius: var(--gala-radius-control);
293
+ border: 1px solid var(--gala-surface-border);
294
+ border-radius: var(--gala-radius-surface);
295
+ box-shadow: var(--gala-shadow-high);
103
296
  }
104
297
  .gala-dialog::backdrop { background: rgb(0 0 0 / 65%); }
105
- .gala-dialog__close { float: inline-end; }
106
- .gala-account-dialog iframe { inline-size: 100%; min-block-size: 22rem; border: 0; }
298
+ /* Sized to what it holds. 22rem was a box for a name and one button, and it showed. */
299
+ .gala-account-dialog { inline-size: min(26rem, calc(100% - 2rem)); }
300
+ .gala-account-dialog iframe {
301
+ display: block;
302
+ inline-size: 100%;
303
+ min-block-size: 7rem;
304
+ border: 0;
305
+ }
306
+ /* The close control belongs on the same line as the title, not floated above it. */
307
+ .gala-dialog__close { float: inline-end; margin-inline-start: var(--gala-space-md); }
107
308
 
108
309
  main,
109
310
  .gala-page-footer {
@@ -115,14 +316,19 @@ a { color: var(--gala-color-accent); }
115
316
 
116
317
  button {
117
318
  min-block-size: 2.75rem;
118
- padding-inline: 1rem;
319
+ padding-inline: var(--gala-space-md);
119
320
  color: inherit;
120
- background: transparent;
121
- border: 0.125rem solid currentcolor;
321
+ background: var(--gala-color-surface);
322
+ border: 1px solid var(--gala-color-border);
122
323
  border-radius: var(--gala-radius-control);
123
324
  font: inherit;
325
+ font-size: var(--gala-text-sm);
124
326
  cursor: pointer;
327
+ transition: background var(--gala-motion-fast) var(--gala-ease),
328
+ border-color var(--gala-motion-fast) var(--gala-ease);
125
329
  }
330
+ button:hover { border-color: var(--gala-color-border-strong); background: var(--gala-color-raised); }
331
+ button[disabled] { opacity: 0.55; cursor: not-allowed; }
126
332
 
127
333
  button:focus-visible {
128
334
  outline: 0.1875rem solid currentcolor;
@@ -135,8 +341,9 @@ button:focus-visible {
135
341
  display: inline-flex;
136
342
  align-items: center;
137
343
  min-block-size: 2.75rem;
138
- border: 0.125rem solid var(--gala-color-border);
344
+ border: 1px solid var(--gala-color-border);
139
345
  border-radius: var(--gala-radius-control);
346
+ font-size: var(--gala-text-sm);
140
347
  }
141
348
 
142
349
  .gala-badge,
@@ -146,7 +353,7 @@ button:focus-visible {
146
353
  .gala-section-heading > :where(h2, p) { margin-block: 0 0.5rem; }
147
354
  .gala-hero { display: grid; gap: 1.5rem; align-items: center; }
148
355
  .gala-hero__image { inline-size: 100%; block-size: auto; border-radius: var(--gala-radius-control); }
149
- .gala-page-content { max-inline-size: 70ch; }
356
+ .gala-page-content { max-inline-size: var(--gala-measure); }
150
357
  .gala-page-content :where(img, video) { max-inline-size: 100%; block-size: auto; }
151
358
  .gala-author-profile { display: flex; gap: 1rem; padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
152
359
  .gala-author-profile img { border-radius: 50%; object-fit: cover; }
@@ -156,7 +363,25 @@ button:focus-visible {
156
363
  .gala-card-index { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; padding: 0; list-style: none; }
157
364
  [data-layout='portfolio'] .gala-card-index { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1.5rem; }
158
365
  [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); }
366
+ /* The surfaces are where `componentStyle` becomes visible: quiet sits on the page, outlined draws
367
+ its edge, raised lifts. One variable decides, so cards and dialogs cannot drift apart. */
368
+ .gala-card {
369
+ block-size: 100%;
370
+ box-sizing: border-box;
371
+ padding: var(--gala-space-lg);
372
+ background: var(--gala-color-surface);
373
+ border: 1px solid var(--gala-surface-border);
374
+ border-radius: var(--gala-radius-surface);
375
+ box-shadow: var(--gala-surface-shadow);
376
+ }
377
+ [data-component-style='raised'] .gala-card { background: var(--gala-color-raised); }
378
+ /* The one place the page moves of its own accord, and so the one place `motion` is legible:
379
+ `subtle` is a short settle, `expressive` a longer one, `none` no movement at all. */
380
+ .gala-card { transition: transform var(--gala-motion-base) var(--gala-ease),
381
+ box-shadow var(--gala-motion-base) var(--gala-ease); }
382
+ .gala-card-index li:hover .gala-card { transform: translateY(-2px); box-shadow: var(--gala-shadow-high); }
383
+ [data-motion='none'] .gala-card { transition: none; }
384
+ [data-motion='none'] .gala-card-index li:hover .gala-card { transform: none; }
160
385
  .gala-stats-graph { min-block-size: var(--gala-widget-min-block-size); margin-inline: 0; }
161
386
  .gala-stats-graph svg { inline-size: 100%; block-size: 8rem; }
162
387
  .gala-stats-graph polyline { fill: none; stroke: var(--gala-color-accent); stroke-width: 2; }
@@ -171,12 +396,7 @@ button:focus-visible {
171
396
  .gala-search-results h2 { margin-block-start: 0; }
172
397
  .gala-toc { padding: 1rem; background: var(--gala-color-surface); }
173
398
  .gala-share__fallback { inline-size: min(100%, 36rem); }
174
- .gala-engagement { min-block-size: var(--gala-widget-min-block-size); }
175
- .gala-engagement dl { display: flex; flex-wrap: wrap; gap: 1rem 2rem; }
176
- .gala-engagement dl > div { display: grid; gap: 0.25rem; }
177
- .gala-engagement dd { margin: 0; font-size: 1.25rem; font-weight: 700; }
178
399
  .gala-engagement__placeholder { display: grid; min-block-size: var(--gala-widget-min-block-size); align-content: center; margin: 0; }
179
- .gala-engagement-profile { margin-block: 1.5rem; padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
180
400
  .gala-engagement-profile > * { margin-block: 0 0.5rem; }
181
401
  .gala-comments { display: grid; gap: 1rem; padding-inline-start: 1.5rem; }
182
402
  .gala-comments li { padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
@@ -186,8 +406,6 @@ button:focus-visible {
186
406
  reassures rather than sells. Hidden for anyone who opened the account dialog deliberately. */
187
407
  .gala-account-dialog__invite { margin-block: 0 1rem; max-inline-size: 34rem; }
188
408
  .gala-account-dialog__invite[hidden] { display: none; }
189
- .gala-reaction-actions,
190
- .gala-comment-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
191
409
  .gala-engagement-actions form,
192
410
  [data-comment-editor] { display: grid; gap: 0.5rem; max-inline-size: 42rem; }
193
411
  .gala-engagement-actions label { display: grid; gap: 0.5rem; }
@@ -249,3 +467,122 @@ button:focus-visible {
249
467
  @media (prefers-reduced-motion: no-preference) {
250
468
  @view-transition { navigation: auto; }
251
469
  }
470
+
471
+ /* ---------------------------------------------------------------- the conversation
472
+ A thread is a hierarchy, and the indent is the only thing that says so. It is drawn with a
473
+ rule rather than padding alone, so five levels deep still reads as five levels and not as an
474
+ accident of margin. */
475
+ .gala-comments-island { margin-block-start: var(--gala-space-xl); }
476
+ .gala-comments__heading {
477
+ font-size: var(--gala-text-lg);
478
+ margin-block-end: var(--gala-space-md);
479
+ }
480
+ .gala-comments, .gala-comment-replies { list-style: none; margin: 0; padding: 0; }
481
+ .gala-comment-replies {
482
+ margin-block-start: var(--gala-space-sm);
483
+ margin-inline-start: var(--gala-space-xs);
484
+ padding-inline-start: var(--gala-space-md);
485
+ border-inline-start: 1px solid var(--gala-color-hairline);
486
+ }
487
+ .gala-comment { padding-block: var(--gala-space-sm); }
488
+ .gala-comment + .gala-comment { border-block-start: 1px solid var(--gala-color-hairline); }
489
+ .gala-comment-replies .gala-comment + .gala-comment { border-block-start: 0; }
490
+ .gala-comment__meta {
491
+ display: flex;
492
+ align-items: baseline;
493
+ gap: var(--gala-space-xs);
494
+ margin-block-end: var(--gala-space-2xs);
495
+ font-size: var(--gala-text-sm);
496
+ }
497
+ .gala-comment__meta strong { font-weight: 600; }
498
+ .gala-comment__pending { font-size: var(--gala-text-xs); color: var(--gala-color-muted); }
499
+ .gala-comment__body { margin-block: 0; overflow-wrap: anywhere; }
500
+
501
+ /* The controls stay out of the way until the comment is worth acting on. */
502
+ .gala-comment-actions {
503
+ display: flex;
504
+ gap: var(--gala-space-2xs);
505
+ margin-block-start: var(--gala-space-2xs);
506
+ opacity: 0.72;
507
+ transition: opacity var(--gala-motion-fast) var(--gala-ease);
508
+ }
509
+ .gala-comment:hover > .gala-comment-actions,
510
+ .gala-comment:focus-within > .gala-comment-actions { opacity: 1; }
511
+ .gala-comment-actions button {
512
+ min-block-size: 2rem;
513
+ padding-inline: var(--gala-space-xs);
514
+ font-size: var(--gala-text-xs);
515
+ background: transparent;
516
+ border-color: transparent;
517
+ color: var(--gala-color-muted);
518
+ }
519
+ .gala-comment-actions button:hover { color: var(--gala-color-text); background: var(--gala-color-surface); }
520
+
521
+ .gala-comment__form { display: grid; gap: var(--gala-space-xs); margin-block: var(--gala-space-sm); }
522
+ .gala-comment__form textarea {
523
+ inline-size: 100%;
524
+ box-sizing: border-box;
525
+ padding: var(--gala-space-sm);
526
+ color: inherit;
527
+ background: var(--gala-color-raised);
528
+ border: 1px solid var(--gala-color-border);
529
+ border-radius: var(--gala-radius-control);
530
+ font: inherit;
531
+ font-size: var(--gala-text-sm);
532
+ line-height: var(--gala-leading-body);
533
+ resize: vertical;
534
+ }
535
+ .gala-comment__form textarea:focus-visible { outline: 2px solid var(--gala-color-accent); outline-offset: 1px; }
536
+ .gala-comment__controls { display: flex; gap: var(--gala-space-xs); }
537
+ .gala-comments__status, .gala-comments__empty, .gala-comments__prompt {
538
+ color: var(--gala-color-muted);
539
+ font-size: var(--gala-text-sm);
540
+ }
541
+ .gala-comments__more { margin-block-start: var(--gala-space-md); }
542
+
543
+ /* ---------------------------------------------------------------- where the article ends
544
+ The writing and the conversation about it are different things, and the page has to say so.
545
+ A footnote used to run straight into the author card with nothing between them. */
546
+ .gala-engagement {
547
+ /* Space is reserved before the counts arrive, so the page does not jump under the reader. */
548
+ min-block-size: var(--gala-widget-min-block-size);
549
+ max-inline-size: var(--gala-content-width);
550
+ margin-block-start: var(--gala-space-xl);
551
+ padding-block-start: var(--gala-space-lg);
552
+ border-block-start: 1px solid var(--gala-color-border);
553
+ }
554
+ .gala-engagement-profile {
555
+ display: flex;
556
+ align-items: baseline;
557
+ gap: var(--gala-space-sm);
558
+ padding: 0;
559
+ background: none;
560
+ }
561
+ .gala-engagement-profile h2 { font-size: var(--gala-text-lg); margin: 0; }
562
+ .gala-engagement-profile p { margin: 0; color: var(--gala-color-muted); font-size: var(--gala-text-sm); }
563
+
564
+ /* Counts read as data, not as three stacked paragraphs. */
565
+ .gala-engagement dl {
566
+ display: flex;
567
+ flex-wrap: wrap;
568
+ gap: var(--gala-space-lg);
569
+ margin-block: var(--gala-space-md);
570
+ }
571
+ .gala-engagement dl > div { display: flex; flex-direction: column-reverse; gap: 0.1rem; }
572
+ .gala-engagement dt {
573
+ font-size: var(--gala-text-xs);
574
+ letter-spacing: var(--gala-tracking-wide);
575
+ text-transform: uppercase;
576
+ color: var(--gala-color-muted);
577
+ }
578
+ .gala-engagement dd {
579
+ margin: 0;
580
+ font-size: var(--gala-text-lg);
581
+ font-variant-numeric: tabular-nums;
582
+ }
583
+
584
+ .gala-reaction-actions { display: flex; flex-wrap: wrap; gap: var(--gala-space-2xs); }
585
+ .gala-comment-actions { display: flex; flex-wrap: wrap; gap: var(--gala-space-2xs); }
586
+ .gala-engagement-actions { display: grid; gap: var(--gala-space-sm); justify-items: start; }
587
+ .gala-engagement-actions > [data-follow-article] { inline-size: auto; }
588
+ .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>