pandorga 1.8.1 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03c8db4a237e9092a2f0d9256c118a162476cb1d056fc88518cfeccd60379bed
4
- data.tar.gz: e8a2725fbc96d9c02ad55e39dce2c8e98d1eec14f20996ae5636d60f2d54181e
3
+ metadata.gz: 120307b06e8e2a6083eb4684bef760944c0fa334155262ea202be58493e0cdd6
4
+ data.tar.gz: e17010ea2587987f38dc6236944c99c4790a380d87fcd994cc1462f5d184bee0
5
5
  SHA512:
6
- metadata.gz: a4e9108eac5982492245285b4954dc1e43a0ca10534411a8a221af35c5520a5a65c3a0fc9e8700bfc9c27588e41ee332a0204ebb697b9339514128e70b5237d1
7
- data.tar.gz: 8ae715831b65d21cf206f071841f6c444f0b99598d93a2917ccaa09f4eecce658e2e93c0b61a75e375afa9b9a9baf0a6f17a47c70584f4ac6c5f656f8a728e75
6
+ metadata.gz: ef13097e5f083e7741ecfddbce6b994b84697f1a5bc593e43cdce93bd41bab6b94da9864555a12908a419d4e4ff0836b2230085f495f30d155810bac87f0ef0b
7
+ data.tar.gz: 1c0e9c4a2e1f8d3cc6fdf3cd58103261f6271e445629e5f4e661f5d402f3525cc36fe717f1b86204640a0ffaeb25cb5171086645df15bab958781226f5fb2e75
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.4] — 2026-10-12
4
+
5
+ ### Fixed
6
+
7
+ - Metric-matched local fallbacks for Cinzel Decorative, Cinzel, Newsreader,
8
+ Ancizar Serif, and Marcellus SC so `display=swap` does not reflow the home
9
+ hero (CLS)
10
+ - Home hero `portraits.json` includes `srcset` / `sizes` when
11
+ `variants/<stem>-{480,960,1440}.jpg` exist under the hero folder
12
+
13
+ No `pandorga:` config keys were added or renamed. Font roles and the
14
+ ledger fallbacks are unchanged.
15
+
16
+ ## [1.8.3] — 2026-10-12
17
+
18
+ ### Fixed
19
+
20
+ - Detail pages load MathJax only when a TeX block nears the viewport (not on
21
+ every article open), then typeset visible blocks first; below-fold formulas
22
+ follow via IntersectionObserver with yields between blocks
23
+ - Listing layouts (articles / blog) no longer pull the MathJax script
24
+ - Long detail bodies mount in batches so the opening can paint before the
25
+ full DOM lands
26
+ - Figure and cut-in images use `decoding="async"` plus width/height hints
27
+ with existing `loading="lazy"`
28
+
29
+ No `pandorga:` config keys were added or renamed. Font roles and the
30
+ ledger fallbacks are unchanged.
31
+
32
+
33
+ ## [1.8.2] — 2026-10-12
34
+
35
+ ### Fixed
36
+
37
+ - Home listing CTAs: more space between the label and its underline
38
+ - Home index panels (Sources / Network / Bibliography): shared plate flex and
39
+ card-scale titles so the three cards align
40
+ - Articles archive: `random` sort shows **Order: random** (was falling back to
41
+ last revised)
42
+ - Listing masthead tag lines (Rascunhos, Sources, peers) use body size instead
43
+ of micro / mono
44
+ - CV contact tiles: quieter badge and name type
45
+ - Dates / meta (`--type-meta`) +2pt; content titles (`--type-title`) +10%;
46
+ top-bar wordmark +25%
47
+ - Theorem / proof heads sit inline with the opening sentence (no line break
48
+ after the name / **Proof.**)
49
+ - Proof QED square (`□`) is +2pt over the body size
50
+
51
+ No `pandorga:` config keys were added or renamed. Font roles and the
52
+ ledger fallbacks are unchanged.
53
+
3
54
  ## [1.8.1] — 2026-10-12
4
55
 
5
56
  ### Fixed
data/DESIGN.md CHANGED
@@ -779,12 +779,12 @@ those tokens — no one-off rem sizes for roles that already have a step.
779
779
  | `--type-display-lg` | 3.6515 | ~64 | display-hero |
780
780
  | `--type-page-lg` | 2.9015 | ~51 | headline-lg |
781
781
  | `--type-section-lg` | 2.1515 | ~38 | headline-md |
782
- | `--type-title-lg` | 2.4015 | ~42 | detail H1 |
782
+ | `--type-title-lg` | 2.64165 | ~46 | detail H1 |
783
783
  | `--type-card` | 1.4015 | ~25 | headline-sm |
784
784
  | `--type-prose` | 1.2 | ~21.1 | reading body, under 1024px |
785
785
  | `--type-prose` (≥1024px) | 1.25 | ~22 | reading body, desktop |
786
786
  | `--type-body` | 1.0758 | ~19 | body-md |
787
- | `--type-meta` | 0.8125 | ~14 | body-sm |
787
+ | `--type-meta` | calc(0.8125rem + 2pt) | ~16 | body-sm |
788
788
  | `--type-label` | 0.8485 | ~15 | label-md |
789
789
  | `--type-mono` | 0.8504 | ~15 | notation |
790
790
 
@@ -115,23 +115,35 @@
115
115
  return m ? String(m[1] != null ? m[1] : m[2] || '') : '';
116
116
  }
117
117
 
118
+ /* Put the head inside the first block so "Corollary 1." / "Proof." sit on
119
+ the same line as the opening sentence (paper style), not above it. */
120
+ function prependInlineHead(html, head) {
121
+ var body = String(html || '');
122
+ var m = body.match(/^(\s*<p\b[^>]*>)/i);
123
+ if (m) return body.replace(m[1], m[1] + head);
124
+ return head + body;
125
+ }
126
+
118
127
  function renderTheoremHtml(openTag, inner) {
119
128
  var name = attrValue(openTag, 'name').trim();
120
129
  var description = attrValue(openTag, 'description').trim();
121
130
  var head = '';
122
131
  if (name) {
123
- head = '<p class="theorem-head"><span class="theorem-name">' + escapeHtml(name) + '</span>';
132
+ head = '<span class="theorem-head"><span class="theorem-name">' + escapeHtml(name) + '</span>';
124
133
  if (description) {
125
134
  head += ' <span class="theorem-description">(' + escapeHtml(description) + ')</span>';
126
135
  }
127
- head += '.</p>';
136
+ head += '. </span>';
128
137
  }
129
- return '<div class="theorem">' + head + '<div class="theorem-body">' + parseInlineMarkdown(inner) + '</div></div>';
138
+ var body = parseInlineMarkdown(inner);
139
+ if (head) body = prependInlineHead(body, head);
140
+ return '<div class="theorem"><div class="theorem-body">' + body + '</div></div>';
130
141
  }
131
142
 
132
143
  function renderProofHtml(inner) {
133
- var head = '<p class="proof-head"><span class="proof-name">Proof</span>.</p>';
134
- return '<div class="proof">' + head + '<div class="proof-body">' + parseInlineMarkdown(inner) + '</div></div>';
144
+ var head = '<span class="proof-head"><span class="proof-name">Proof</span>. </span>';
145
+ var body = prependInlineHead(parseInlineMarkdown(inner), head);
146
+ return '<div class="proof"><div class="proof-body">' + body + '</div></div>';
135
147
  }
136
148
 
137
149
  function preprocessMarkdownDivs(markdown) {
@@ -184,7 +196,104 @@
184
196
  return html;
185
197
  }
186
198
 
199
+ var MATHJAX_SRC = 'https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js';
200
+ var mathJaxLoadPromise = null;
201
+
202
+ function rootHasTeX(root) {
203
+ var text = (root && root.textContent) || '';
204
+ return text.indexOf('$') !== -1 || text.indexOf('\\(') !== -1 || text.indexOf('\\[') !== -1;
205
+ }
206
+
207
+ function elementNeedsMath(el) {
208
+ if (!el || el.nodeType !== 1) return false;
209
+ if (el.getAttribute('data-math-done') === '1') return false;
210
+ if (el.querySelector && el.querySelector('mjx-container')) return false;
211
+ return rootHasTeX(el);
212
+ }
213
+
214
+ function collectMathBlocks(root) {
215
+ var out = [];
216
+ if (!root) return out;
217
+ Array.prototype.forEach.call(root.children || [], function (el) {
218
+ if (elementNeedsMath(el)) out.push(el);
219
+ });
220
+ return out;
221
+ }
222
+
223
+ function blockNearViewport(el, marginPx) {
224
+ var rect = el.getBoundingClientRect();
225
+ var vh = window.innerHeight || 800;
226
+ var margin = typeof marginPx === 'number' ? marginPx : vh;
227
+ return rect.top < vh + margin && rect.bottom > -margin;
228
+ }
229
+
230
+ function yieldToMain() {
231
+ return new Promise(function (resolve) {
232
+ if (typeof window.requestIdleCallback === 'function') {
233
+ window.requestIdleCallback(function () { resolve(); }, { timeout: 48 });
234
+ } else {
235
+ window.setTimeout(resolve, 0);
236
+ }
237
+ });
238
+ }
239
+
240
+ function ensureMathJaxConfig() {
241
+ if (window.MathJax && window.MathJax.tex) return;
242
+ window.MathJax = {
243
+ startup: { typeset: false },
244
+ tex: {
245
+ inlineMath: [['$', '$'], ['\\(', '\\)']],
246
+ displayMath: [['$$', '$$'], ['\\[', '\\]']]
247
+ }
248
+ };
249
+ }
250
+
251
+ function loadMathJaxScript() {
252
+ if (mathJaxLoadPromise) return mathJaxLoadPromise;
253
+ ensureMathJaxConfig();
254
+ if (window.MathJax && typeof window.MathJax.typesetPromise === 'function') {
255
+ mathJaxLoadPromise = Promise.resolve(window.MathJax);
256
+ return mathJaxLoadPromise;
257
+ }
258
+ mathJaxLoadPromise = new Promise(function (resolve, reject) {
259
+ var existing = document.getElementById('MathJax-script') ||
260
+ document.querySelector('script[src*="mathjax"][src*="tex-mml-chtml"]');
261
+ if (existing) {
262
+ if (window.MathJax && typeof window.MathJax.typesetPromise === 'function') {
263
+ resolve(window.MathJax);
264
+ return;
265
+ }
266
+ existing.addEventListener('load', function () { resolve(window.MathJax); });
267
+ existing.addEventListener('error', reject);
268
+ return;
269
+ }
270
+ var script = document.createElement('script');
271
+ script.id = 'MathJax-script';
272
+ script.src = MATHJAX_SRC;
273
+ script.async = true;
274
+ script.onload = function () {
275
+ var mj = window.MathJax;
276
+ if (mj && mj.startup && mj.startup.promise) {
277
+ mj.startup.promise.then(function () { resolve(mj); }).catch(reject);
278
+ } else {
279
+ resolve(mj);
280
+ }
281
+ };
282
+ script.onerror = function () {
283
+ reject(new Error('MathJax failed to load'));
284
+ };
285
+ document.head.appendChild(script);
286
+ });
287
+ return mathJaxLoadPromise;
288
+ }
289
+
187
290
  async function waitForMathJax() {
291
+ try {
292
+ await loadMathJaxScript();
293
+ } catch (error) {
294
+ console.error(error);
295
+ return false;
296
+ }
188
297
  var mj = window.MathJax;
189
298
  if (!mj) return false;
190
299
  if (mj.startup && mj.startup.promise) {
@@ -198,10 +307,89 @@
198
307
  return typeof mj.typesetPromise === 'function';
199
308
  }
200
309
 
310
+ async function typesetMathBlock(el) {
311
+ if (!el || el.getAttribute('data-math-done') === '1') return;
312
+ if (el.querySelector && el.querySelector('mjx-container')) {
313
+ el.setAttribute('data-math-done', '1');
314
+ return;
315
+ }
316
+ if (!rootHasTeX(el)) return;
317
+ if (!(await waitForMathJax())) return;
318
+ try {
319
+ await window.MathJax.typesetPromise([el]);
320
+ } catch (error) {
321
+ console.error(error);
322
+ return;
323
+ }
324
+ el.setAttribute('data-math-done', '1');
325
+ }
326
+
327
+ function observeMathBlocks(blocks) {
328
+ if (!blocks.length) return;
329
+ if (typeof IntersectionObserver === 'undefined') {
330
+ (async function () {
331
+ for (var i = 0; i < blocks.length; i++) {
332
+ await yieldToMain();
333
+ await typesetMathBlock(blocks[i]);
334
+ }
335
+ })();
336
+ return;
337
+ }
338
+ var queue = [];
339
+ var draining = false;
340
+
341
+ async function drain() {
342
+ if (draining) return;
343
+ draining = true;
344
+ while (queue.length) {
345
+ var el = queue.shift();
346
+ await typesetMathBlock(el);
347
+ if (queue.length) await yieldToMain();
348
+ }
349
+ draining = false;
350
+ }
351
+
352
+ var observer = new IntersectionObserver(function (entries) {
353
+ entries.forEach(function (entry) {
354
+ if (!entry.isIntersecting) return;
355
+ observer.unobserve(entry.target);
356
+ queue.push(entry.target);
357
+ drain();
358
+ });
359
+ }, { root: null, rootMargin: '120% 0px', threshold: 0 });
360
+
361
+ blocks.forEach(function (el) { observer.observe(el); });
362
+ }
363
+
364
+ /* Load MathJax only when a TeX block is near the viewport. Typeset those
365
+ blocks first, then the rest as the reader scrolls. Returns after the
366
+ visible pass so the detail shell can paint without waiting on below-fold
367
+ formulas. */
201
368
  async function typesetMath(root) {
202
369
  try {
203
- if (!(await waitForMathJax())) return;
204
- await window.MathJax.typesetPromise(root ? [root] : undefined);
370
+ if (!root || !rootHasTeX(root)) return;
371
+
372
+ var blocks = collectMathBlocks(root);
373
+ if (!blocks.length) {
374
+ if (!(await waitForMathJax())) return;
375
+ await window.MathJax.typesetPromise([root]);
376
+ return;
377
+ }
378
+
379
+ var immediate = [];
380
+ var deferred = [];
381
+ var margin = (window.innerHeight || 800) * 1.25;
382
+ blocks.forEach(function (el) {
383
+ if (blockNearViewport(el, margin)) immediate.push(el);
384
+ else deferred.push(el);
385
+ });
386
+
387
+ for (var i = 0; i < immediate.length; i++) {
388
+ await typesetMathBlock(immediate[i]);
389
+ if (i < immediate.length - 1) await yieldToMain();
390
+ }
391
+
392
+ observeMathBlocks(deferred);
205
393
  } catch (error) {
206
394
  console.error(error);
207
395
  }
@@ -26,7 +26,7 @@
26
26
  var src = resolveAssetUrl(attrs.src);
27
27
  var sideClass = attrs.side === 'right' ? ' article-cut-in--right' : '';
28
28
  return '<figure class="article-cut-in-plate' + sideClass + '">' +
29
- '<img class="article-cut-in' + sideClass + '" src="' + escapeAttr(src) + '" alt="' + escapeAttr(attrs.alt || '') + '" width="120" loading="lazy">' +
29
+ '<img class="article-cut-in' + sideClass + '" src="' + escapeAttr(src) + '" alt="' + escapeAttr(attrs.alt || '') + '" width="120" loading="lazy" decoding="async">' +
30
30
  '</figure>';
31
31
  }
32
32
 
@@ -36,9 +36,12 @@
36
36
  var caption = attrs.caption || '';
37
37
  var lbCaption = figureLightboxCaption(caption, attrs.source);
38
38
  var lbAttr = lbCaption ? ' data-lightbox-caption="' + escapeAttr(lbCaption) + '"' : '';
39
+ /* Reserve a 16:9 box until the file paints. Diagrams vary; a stable
40
+ ratio beats an unsized image for CLS on long articles. */
41
+ var sizeAttr = ' width="960" height="540"';
39
42
  return [
40
43
  '<figure class="my-8 mx-auto text-center w-full" style="display:block;margin-left:auto;margin-right:auto;' + width + '">',
41
- '<img src="' + escapeAttr(src) + '" alt="' + escapeAttr(attrs.alt || caption) + '" class="figure-lightbox-trigger mx-auto max-w-full h-auto block" loading="lazy" tabindex="0" role="button" aria-label="Enlarge image"' + lbAttr + '>',
44
+ '<img src="' + escapeAttr(src) + '" alt="' + escapeAttr(attrs.alt || caption) + '" class="figure-lightbox-trigger mx-auto max-w-full h-auto block" loading="lazy" decoding="async"' + sizeAttr + ' tabindex="0" role="button" aria-label="Enlarge image"' + lbAttr + '>',
42
45
  caption || attrs.source ? '<figcaption class="mt-3 font-body">' + includeCaptionHtml(caption) + figureSourceHtml(attrs.source) + '</figcaption>' : '',
43
46
  '</figure>'
44
47
  ].join('');
@@ -813,12 +813,42 @@
813
813
 
814
814
  var body = document.createElement('div');
815
815
  body.className = 'article-content';
816
- body.innerHTML = await renderRichContent(options.bodyMarkdown, fm);
817
- applyDropCapClass(body, fm);
816
+ /* Mount the opening blocks first so title-adjacent prose can paint before
817
+ the rest of a long article lands in the DOM (mobile LCP / TBT). */
818
+ var staging = document.createElement('div');
819
+ staging.innerHTML = await renderRichContent(options.bodyMarkdown, fm);
820
+ var nodes = Array.prototype.slice.call(staging.childNodes);
818
821
  section.appendChild(body);
819
-
820
822
  if (options.mainColumn) options.mainColumn.appendChild(section);
821
823
 
824
+ var firstBatch = 6;
825
+ var batchSize = 12;
826
+ var ni = 0;
827
+ for (; ni < nodes.length && ni < firstBatch; ni++) {
828
+ body.appendChild(nodes[ni]);
829
+ }
830
+ applyDropCapClass(body, fm);
831
+ await new Promise(function (resolve) {
832
+ if (typeof window.requestAnimationFrame === 'function') {
833
+ window.requestAnimationFrame(function () { window.setTimeout(resolve, 0); });
834
+ } else {
835
+ window.setTimeout(resolve, 0);
836
+ }
837
+ });
838
+ while (ni < nodes.length) {
839
+ var end = Math.min(ni + batchSize, nodes.length);
840
+ for (; ni < end; ni++) body.appendChild(nodes[ni]);
841
+ if (ni < nodes.length) {
842
+ await new Promise(function (resolve) {
843
+ if (typeof window.requestIdleCallback === 'function') {
844
+ window.requestIdleCallback(function () { resolve(); }, { timeout: 48 });
845
+ } else {
846
+ window.setTimeout(resolve, 0);
847
+ }
848
+ });
849
+ }
850
+ }
851
+
822
852
  await enhanceArticleContent(body, fm);
823
853
 
824
854
  assignHeadingIds(body);
@@ -885,11 +915,14 @@
885
915
 
886
916
  async function renderRichContent(markdown, frontMatter) {
887
917
  var fm = frontMatter || {};
888
- var index = await ensureXciteIndex();
889
- var bibIndex = await ensureBibliographyIndex();
918
+ /* Kick index fetches immediately so they overlap with front-matter strip. */
919
+ var indexPromise = ensureXciteIndex();
920
+ var bibPromise = ensureBibliographyIndex();
890
921
  var assignPrep = preprocessTranscriptionAssigns(stripFrontMatter(markdown));
891
922
  var prepared = assignPrep.text;
892
923
  var transcriptionAssigns = assignPrep.assignMap;
924
+ var index = await indexPromise;
925
+ var bibIndex = await bibPromise;
893
926
  var replacements = [];
894
927
  prepared = prepared.replace(/\{%-?\s*include\s+transcription\.html\s+([\s\S]*?)-?%\}/g, function (match, raw) {
895
928
  var replacementIndex = replacements.length;
@@ -31,7 +31,7 @@
31
31
  {%- else -%}
32
32
  <figure class="home-index-plate" data-content-band-art="{{ _art_key }}" hidden></figure>
33
33
  {%- endif -%}
34
- <h3 class="section-title home-index-title">{{ _panel_title }}</h3>
34
+ <h3 class="home-index-title">{{ _panel_title }}</h3>
35
35
  <a class="ledger-cta" href="{{ _panel_path }}">
36
36
  <span>Open</span>
37
37
  <span class="material-symbols-outlined ledger-cta-arrow" aria-hidden="true">chevron_right</span>
@@ -1,10 +1,32 @@
1
- <!-- MathJax configuration for Jekyll with inline math support -->
1
+ <!-- MathJax: config only. The content runtime loads the library on demand when
2
+ a detail page actually contains TeX, then typesets visible blocks first. -->
2
3
  <script>
3
4
  window.MathJax = {
4
- tex: {
5
- inlineMath: [['$', '$'], ['\\(', '\\)']],
6
- displayMath: [['$$', '$$'], ['\\[', '\\]']]
7
- }
5
+ startup: { typeset: false },
6
+ tex: {
7
+ inlineMath: [['$', '$'], ['\\(', '\\)']],
8
+ displayMath: [['$$', '$$'], ['\\[', '\\]']]
9
+ }
8
10
  };
11
+
12
+ /* Studio / private text layout: typeset once the runtime exists. Public detail
13
+ pages call typesetMath from enhanceArticleContent instead. */
14
+ (function () {
15
+ function runStaticMath() {
16
+ var root = document.querySelector('.article-content');
17
+ if (!root || !window.ContentRuntime || typeof window.ContentRuntime.typesetMath !== 'function') {
18
+ return;
19
+ }
20
+ var text = root.textContent || '';
21
+ if (text.indexOf('$') === -1 && text.indexOf('\\(') === -1 && text.indexOf('\\[') === -1) {
22
+ return;
23
+ }
24
+ window.ContentRuntime.typesetMath(root);
25
+ }
26
+ if (window.ContentRuntime) {
27
+ runStaticMath();
28
+ } else {
29
+ document.addEventListener('pandorga:runtime', runStaticMath, { once: true });
30
+ }
31
+ })();
9
32
  </script>
10
- <script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js"></script>
@@ -126,8 +126,6 @@ layout: shell
126
126
  </div>
127
127
 
128
128
  </main>
129
- {% include mathjax.html %}
130
-
131
129
  <script>
132
130
  (function bootArticles() {
133
131
  'use strict';
@@ -168,7 +166,8 @@ layout: shell
168
166
 
169
167
  var SORT_LABELS = {
170
168
  updated: 'Order: last revised',
171
- published: 'Order: date published'
169
+ published: 'Order: date published',
170
+ random: 'Order: random'
172
171
  };
173
172
 
174
173
  var allArticles = [];
data/_layouts/blog.html CHANGED
@@ -90,8 +90,6 @@ layout: shell
90
90
  </div>
91
91
 
92
92
  </main>
93
- {% include mathjax.html %}
94
-
95
93
  <script>
96
94
  (function bootBlog() {
97
95
  'use strict';
@@ -1012,8 +1012,9 @@ a.xcite-ref:hover {
1012
1012
  and `<div class="proof" markdown="1">`. Theorems are bare (no plate): the
1013
1013
  runtime builds the head from name / description and italicizes the body.
1014
1014
  Proofs sit on a gray band that bleeds to the edges of `.text-page-main`
1015
- while the text keeps the body measure; the runtime prefixes "Proof." and
1016
- a QED square lands on the last line of the body. */
1015
+ while the text keeps the body measure; the runtime prefixes "Proof." into
1016
+ the first paragraph (same line as the opening sentence) and a QED square
1017
+ lands on the last line of the body. */
1017
1018
  .article-content .theorem {
1018
1019
  display: block;
1019
1020
  width: 100%;
@@ -1026,7 +1027,6 @@ a.xcite-ref:hover {
1026
1027
 
1027
1028
  .article-content .theorem-head,
1028
1029
  .article-content .proof-head {
1029
- margin: 0 0 0.5em;
1030
1030
  font-style: normal;
1031
1031
  font-weight: var(--weight-body);
1032
1032
  }
@@ -1046,6 +1046,12 @@ a.xcite-ref:hover {
1046
1046
  font-style: italic;
1047
1047
  }
1048
1048
 
1049
+ /* Head is injected into the first paragraph; keep it upright against the
1050
+ italic theorem body. */
1051
+ .article-content .theorem-body .theorem-head {
1052
+ font-style: normal;
1053
+ }
1054
+
1049
1055
  .article-content .theorem-body > :first-child,
1050
1056
  .article-content .proof-body > :first-child {
1051
1057
  margin-top: 0;
@@ -1085,7 +1091,7 @@ a.xcite-ref:hover {
1085
1091
  content: "□";
1086
1092
  float: right;
1087
1093
  margin-left: 0.35em;
1088
- font-size: inherit;
1094
+ font-size: calc(1em + 2pt);
1089
1095
  line-height: inherit;
1090
1096
  color: var(--color-on-surface);
1091
1097
  pointer-events: none;
data/assets/css/base.css CHANGED
@@ -1,3 +1,53 @@
1
+ /* ── Metric-matched fallbacks (font-display: swap) ──
2
+ Local Times/Liberation faces scaled to the web fonts that paint the
3
+ home hero before the Google kit arrives. Without these, swap CLS on
4
+ home is ~0.15. Metrics from UPM-normalised average glyph width vs
5
+ Liberation Serif Regular (Times New Roman metric twin). */
6
+ @font-face {
7
+ font-family: "Pandorga Title Fallback";
8
+ src: local("Times New Roman"), local("Times"), local("Liberation Serif");
9
+ size-adjust: 148.98%;
10
+ ascent-override: 65.51%;
11
+ descent-override: 24.97%;
12
+ line-gap-override: 0%;
13
+ }
14
+
15
+ @font-face {
16
+ font-family: "Pandorga Cinzel Fallback";
17
+ src: local("Times New Roman"), local("Times"), local("Liberation Serif");
18
+ size-adjust: 139.21%;
19
+ ascent-override: 70.11%;
20
+ descent-override: 26.72%;
21
+ line-gap-override: 0%;
22
+ }
23
+
24
+ @font-face {
25
+ font-family: "Pandorga Body Fallback";
26
+ src: local("Times New Roman"), local("Times"), local("Liberation Serif");
27
+ size-adjust: 103.26%;
28
+ ascent-override: 71.18%;
29
+ descent-override: 25.66%;
30
+ line-gap-override: 0%;
31
+ }
32
+
33
+ @font-face {
34
+ font-family: "Pandorga Ancizar Fallback";
35
+ src: local("Times New Roman"), local("Times"), local("Liberation Serif");
36
+ size-adjust: 101.28%;
37
+ ascent-override: 88.86%;
38
+ descent-override: 29.62%;
39
+ line-gap-override: 0%;
40
+ }
41
+
42
+ @font-face {
43
+ font-family: "Pandorga Label Fallback";
44
+ src: local("Times New Roman"), local("Times"), local("Liberation Serif");
45
+ size-adjust: 112.98%;
46
+ ascent-override: 86.22%;
47
+ descent-override: 24.76%;
48
+ line-gap-override: 0%;
49
+ }
50
+
1
51
  /* ── Material Symbols ── */
2
52
  /* Doubled class so this beats the Google stylesheet, which arrives later
3
53
  and sets font-size: 24px. That late size change moves the hero. */
@@ -122,8 +172,8 @@ html {
122
172
  --type-section-lg: 2.1515rem;
123
173
 
124
174
  /* Content titles — Newsreader Regular 400 */
125
- --type-title: 2.0015rem;
126
- --type-title-lg: 2.4015rem;
175
+ --type-title: 2.20165rem;
176
+ --type-title-lg: 2.64165rem;
127
177
  --type-card: 1.4015rem;
128
178
  --type-card-lg: 1.5515rem;
129
179
 
@@ -140,7 +190,8 @@ html {
140
190
  /* Card/row blurbs, standfirsts, project/source copy — same as body. */
141
191
  --type-subtitle: var(--type-body);
142
192
  --type-dek: 0.9375rem;
143
- --type-meta: 0.8125rem;
193
+ /* Dates, kickers, quiet meta — +2pt over the prior 0.8125rem step. */
194
+ --type-meta: calc(0.8125rem + 2pt);
144
195
 
145
196
  /* Chrome — quiet relative to body (tuned for Marcellus SC cap-height to match icons) */
146
197
  --type-label: 0.8485rem;
@@ -405,10 +456,13 @@ html body {
405
456
  font-variant-numeric: tabular-nums;
406
457
  }
407
458
 
408
- /* Masthead tag line under the page H1. */
459
+ /* Masthead tag line under the page H1. Body size so "Channels. Sites." /
460
+ "Blog pessoal. …" reads with the standfirst, not as micro-label chrome.
461
+ Beats `.code-technical` when a listing also puts that class on the span. */
409
462
  [data-content-header-tags],
410
- .page-header-tags {
411
- font-size: var(--type-dek);
463
+ .page-header-tags,
464
+ [data-content-header-tags].code-technical {
465
+ font-size: var(--type-body);
412
466
  line-height: 1.45;
413
467
  letter-spacing: 0.04em;
414
468
  }
@@ -950,7 +1004,7 @@ a.ledger-chip:hover {
950
1004
  text-decoration: underline;
951
1005
  text-decoration-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
952
1006
  text-decoration-thickness: 1.5px;
953
- text-underline-offset: 0.28em;
1007
+ text-underline-offset: 0.48em;
954
1008
  text-underline-position: under;
955
1009
  }
956
1010
 
@@ -43,7 +43,7 @@
43
43
  .site-wordmark {
44
44
  display: none;
45
45
  font-family: var(--font-title);
46
- font-size: var(--type-dek);
46
+ font-size: calc(var(--type-dek) * 1.25);
47
47
  line-height: 1;
48
48
  white-space: nowrap;
49
49
  }
@@ -137,6 +137,7 @@ html[data-theme="light"] .cv-product {
137
137
  margin-top: var(--space-md);
138
138
  color: var(--color-on-surface-variant);
139
139
  letter-spacing: 0.06em;
140
+ font-size: var(--type-body);
140
141
  }
141
142
 
142
143
  .cv-dossier-standfirst {
@@ -219,7 +220,19 @@ html[data-theme="light"] .cv-product {
219
220
  }
220
221
 
221
222
  .cv-contacts .group .material-symbols-outlined {
222
- font-size: var(--type-card);
223
+ font-size: var(--type-label-lg);
224
+ }
225
+
226
+ /* Badge (Network / Code) and name (LinkedIn / GitHub) were inheriting body /
227
+ nav sizes and reading louder than the dossier. Quiet label register. */
228
+ .cv-contacts .group .marginalia {
229
+ font-size: var(--type-meta);
230
+ letter-spacing: 0.06em;
231
+ }
232
+
233
+ .cv-contacts .group .font-label {
234
+ font-size: var(--type-label);
235
+ letter-spacing: 0.03em;
223
236
  }
224
237
 
225
238
  /* ── Section register ──────────────────────────────────────────────────
@@ -487,6 +487,7 @@ html[data-theme="light"] .home-hero-plate {
487
487
  vertical tick so each band reads as a ruled plate rather than a soft fade. */
488
488
  .home-band:not(.home-band--hero) .ledger-section-banner {
489
489
  padding-top: var(--space-lg);
490
+ padding-bottom: var(--space-sm);
490
491
  border-top: 1px solid var(--border-gentle);
491
492
  box-shadow: 0 4px 0 -3px var(--border-gentle-faint);
492
493
  position: relative;
@@ -1674,20 +1675,28 @@ button.home-project-inventory-row {
1674
1675
  margin-inline: var(--space-lg);
1675
1676
  }
1676
1677
 
1678
+ /* Card-scale title, not `.section-title`: that face is for band banners and
1679
+ was oversized once the three index panels sat in one row. Locked here so
1680
+ Sources / Network / Bibliography share one title register. */
1677
1681
  .home-index-title {
1678
1682
  margin: 0;
1683
+ font-family: var(--font-title);
1679
1684
  font-size: var(--type-card);
1685
+ font-weight: var(--weight-display);
1686
+ line-height: 1.2;
1687
+ color: var(--color-on-surface);
1688
+ font-synthesis: none;
1689
+ font-optical-sizing: auto;
1680
1690
  }
1681
1691
 
1682
- @media (min-width: 1024px) {
1683
- .home-index-title {
1684
- font-size: var(--type-card-lg);
1685
- }
1686
- }
1687
-
1688
- /* ── The plate ── */
1692
+ /* ── The plate ──
1693
+ Every panel grows its plate so the title + Open row lands on one baseline
1694
+ across Sources, Network, and Bibliography — the bib plate already flexed;
1695
+ the other two used to sit shorter and leave their titles higher. */
1689
1696
  .home-index-plate {
1690
1697
  align-self: stretch;
1698
+ flex: 1 1 auto;
1699
+ min-height: 0;
1691
1700
  margin: 0;
1692
1701
  background: var(--color-background);
1693
1702
  border-bottom: 1px solid var(--border-gentle-subtle);
@@ -1952,8 +1961,6 @@ html[data-theme="light"] .home-featured .ledger-poster {
1952
1961
  #home-bib-panel .home-index-plate {
1953
1962
  display: flex;
1954
1963
  flex-direction: column;
1955
- flex: 1 1 auto;
1956
- min-height: 0;
1957
1964
  }
1958
1965
 
1959
1966
  #home-bib-panel .home-index-plate-head {
@@ -58,6 +58,7 @@
58
58
  is set in. */
59
59
  color: var(--color-primary);
60
60
  letter-spacing: 0.06em;
61
+ font-size: var(--type-body);
61
62
  }
62
63
 
63
64
  .projects-page .projects-state {
@@ -37,6 +37,7 @@
37
37
  the tag line should follow whatever `.page-accent--*` sets. */
38
38
  color: var(--color-primary);
39
39
  letter-spacing: 0.06em;
40
+ font-size: var(--type-body);
40
41
  }
41
42
 
42
43
  /* ══ Featured panel ══
@@ -24,7 +24,7 @@ module Pandorga
24
24
 
25
25
  gem "pandorga", "~> 1.3"
26
26
  # Local checkout: gem "pandorga", path: "../pandorga"
27
- # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.8.1"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.8.4"
28
28
  gem "csv"
29
29
  gem "base64"
30
30
  RUBY
@@ -15,6 +15,15 @@ module Pandorga
15
15
  FOLLOW = { subtitle: :body, code: :mono, cv_body: :body }.freeze
16
16
  MONO_FALLBACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"
17
17
  FAMILY_NAME = /\A[\p{L}\p{N}][\p{L}\p{N} .+\-]*\z/
18
+ # Named faces defined in assets/css/base.css with size-adjust / ascent
19
+ # overrides so display=swap does not reflow the hero (home CLS).
20
+ METRIC_FALLBACK = {
21
+ "Cinzel Decorative" => "Pandorga Title Fallback",
22
+ "Cinzel" => "Pandorga Cinzel Fallback",
23
+ "Newsreader" => "Pandorga Body Fallback",
24
+ "Ancizar Serif" => "Pandorga Ancizar Fallback",
25
+ "Marcellus SC" => "Pandorga Label Fallback"
26
+ }.freeze
18
27
 
19
28
  DEFAULTS = {
20
29
  title: { family: "Cinzel Decorative", spec: "wght@400;700;900", fallback: "serif" },
@@ -78,8 +87,14 @@ module Pandorga
78
87
  face = resolved[role.to_sym]
79
88
  return "" unless face
80
89
 
81
- quoted = face[:family].to_s.empty? ? nil : "'#{escape_css(face[:family])}'"
82
- [quoted, face[:fallback].to_s.strip].compact.reject(&:empty?).join(", ")
90
+ family = face[:family].to_s
91
+ parts = []
92
+ parts << "'#{escape_css(family)}'" unless family.empty?
93
+ metric = METRIC_FALLBACK[family]
94
+ parts << "'#{escape_css(metric)}'" if metric
95
+ fallback = face[:fallback].to_s.strip
96
+ parts << fallback unless fallback.empty?
97
+ parts.join(", ")
83
98
  end
84
99
 
85
100
  def resolve_role(role, override, theme)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.8.1"
4
+ VERSION = "1.8.4"
5
5
  end
@@ -3,17 +3,31 @@
3
3
  # The home hero pool is the folder. The content export writes the public
4
4
  # paths to pages/home/portraits.json. The author uploads a photograph under
5
5
  # content/media/images/hero/ and does not keep a second list.
6
+ # Optional width variants live under variants/ so they are not extra draws.
6
7
 
7
8
  require "pathname"
8
9
 
9
10
  module HeroPortraits
10
11
  IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .gif].freeze
12
+ WIDTHS = [480, 960, 1440].freeze
13
+ SIZES = "(max-width: 767px) 100vw, (max-width: 1023px) 14rem, 16rem"
11
14
 
12
15
  module_function
13
16
 
14
17
  def entries(content_root)
15
- image_files(Pathname.new(content_root)).map do |file|
16
- { "src" => "/media/images/hero/#{file.basename}" }
18
+ root = Pathname.new(content_root)
19
+ image_files(root).map do |file|
20
+ entry = { "src" => "/media/images/hero/#{file.basename}" }
21
+ stem = file.basename.to_s.sub(/\.[^.]+\z/, "")
22
+ parts = WIDTHS.filter_map do |width|
23
+ variant = root.join("media/images/hero/variants/#{stem}-#{width}.jpg")
24
+ next unless variant.file?
25
+
26
+ "/media/images/hero/variants/#{variant.basename} #{width}w"
27
+ end
28
+ next entry if parts.empty?
29
+
30
+ entry.merge("srcset" => parts.join(", "), "sizes" => SIZES)
17
31
  end
18
32
  end
19
33
 
@@ -152,7 +152,7 @@ end
152
152
  warn "FAIL test-examples-build: #{name} home dropped the ledger font request"
153
153
  exit 1
154
154
  end
155
- unless html.include?("--font-title: 'Cinzel Decorative', serif")
155
+ unless html.include?("--font-title: 'Cinzel Decorative', 'Pandorga Title Fallback', serif")
156
156
  warn "FAIL test-examples-build: #{name} home dropped the title face"
157
157
  exit 1
158
158
  end
@@ -28,10 +28,23 @@ from_theme = Pandorga::Fonts.resolve(nil, theme)
28
28
  fail!("unset config dropped the ledger faces") unless Pandorga::Fonts.stylesheet_href(bare) == DEFAULT_HREF
29
29
  fail!("theme tokens drifted from the ledger faces") unless Pandorga::Fonts.stylesheet_href(from_theme) == DEFAULT_HREF
30
30
 
31
- fail!("title stack dropped Cinzel") unless Pandorga::Fonts.css_stack(from_theme, "title") == "'Cinzel Decorative', serif"
32
- fail!("body stack dropped the serif fallback") unless Pandorga::Fonts.css_stack(from_theme, "body") == "'Newsreader', Georgia, serif"
31
+ fail!("title stack dropped Cinzel") unless Pandorga::Fonts.css_stack(from_theme, "title") ==
32
+ "'Cinzel Decorative', 'Pandorga Title Fallback', serif"
33
+ fail!("body stack dropped the serif fallback") unless Pandorga::Fonts.css_stack(from_theme, "body") ==
34
+ "'Newsreader', 'Pandorga Body Fallback', Georgia, serif"
35
+ fail!("label stack dropped the metric fallback") unless Pandorga::Fonts.css_stack(from_theme, "label") ==
36
+ "'Marcellus SC', 'Pandorga Label Fallback', serif"
33
37
  fail!("mono stack dropped Courier Prime") unless Pandorga::Fonts.css_stack(from_theme, "mono").start_with?("'Courier Prime', ui-monospace")
34
38
 
39
+ site_faces = Pandorga::Fonts.resolve(
40
+ { "title" => "Cinzel", "body" => { "family" => "Ancizar Serif", "weights" => [300, 400, 600] } },
41
+ theme
42
+ )
43
+ fail!("Cinzel stack missed its metric fallback") unless Pandorga::Fonts.css_stack(site_faces, "title") ==
44
+ "'Cinzel', 'Pandorga Cinzel Fallback', serif"
45
+ fail!("Ancizar stack missed its metric fallback") unless Pandorga::Fonts.css_stack(site_faces, "body") ==
46
+ "'Ancizar Serif', 'Pandorga Ancizar Fallback', Georgia, serif"
47
+
35
48
  custom = Pandorga::Fonts.resolve(
36
49
  {
37
50
  "title" => "Fraunces",
@@ -54,6 +67,7 @@ broken = []
54
67
  Dir.glob(ROOT.join("**/*.{css,html,rb,js,mjs,md,liquid,yml}")).each do |path|
55
68
  next if path.include?("/studio/assets/")
56
69
  next if path.include?("/node_modules/")
70
+ next if path.include?("/_site/")
57
71
  next if path.end_with?("test-fonts.rb")
58
72
  text = File.read(path)
59
73
  next unless text.include?(token)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors