pandorga 1.8.1 → 1.9.0
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 +4 -4
- data/CHANGELOG.md +81 -0
- data/DESIGN.md +2 -2
- data/_includes/content-runtime/20-markdown.html +195 -7
- data/_includes/content-runtime/30-includes.html +5 -2
- data/_includes/content-runtime/50-detail.html +38 -5
- data/_includes/content-runtime/60-fragments.html +40 -2
- data/_includes/home/band-index.html +1 -1
- data/_includes/mathjax.html +28 -6
- data/_layouts/articles.html +2 -3
- data/_layouts/blog.html +0 -2
- data/_layouts/home.html +10 -2
- data/_layouts/shell.html +9 -1
- data/assets/css/article.css +10 -4
- data/assets/css/base.css +61 -7
- data/assets/css/ledger/chrome.css +1 -1
- data/assets/css/ledger/cv.css +14 -1
- data/assets/css/ledger/home.css +45 -19
- data/assets/css/ledger/projects.css +1 -0
- data/assets/css/ledger/writing.css +1 -0
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/fonts.rb +17 -2
- data/lib/pandorga/registry/schema.yml +3 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/content/hero_portraits.rb +23 -2
- data/scripts/test/test-examples-build.rb +1 -1
- data/scripts/test/test-fonts.rb +16 -2
- data/scripts/test/test-hero-portraits.rb +45 -3
- data/scripts/validate.sh +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ce749863130651664857439b365492b57207e7d0aba994315be703200fe0f48
|
|
4
|
+
data.tar.gz: 1b8f5293944f8cd27c76ee413f69e02869e9dc1cd6619aa48cfde1daf2355b98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 157e81581c2e84cf398ab699902f1a3a17023e26b4fdbc27e76cba5013f1f7e2f5609cafe2dc65cf9bad36c9027bdb520d0ab22891324c9dfc63c3aabda76abc
|
|
7
|
+
data.tar.gz: b016e51119eadedc1ae7b063800580dfa04519cc84f0f9cae4237a4a9e4a356c8d329ac44d8d9da4220e48bab33b68a74c891e59e50a07c947685750d178377f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.0] — 2026-10-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Optional `pandorga.home.hero_landscape` (home front matter
|
|
8
|
+
`hero_landscape` overrides it): a 16:9 image for viewports at or below
|
|
9
|
+
767px, via `<picture>`, shown whole in a 16:9 plate
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Without that key the phone hero plate stays 3:4. It no longer cover-crops
|
|
14
|
+
the portrait into a 4:3 band
|
|
15
|
+
- The hero image keeps `fetchpriority="high"`. A configured landscape file
|
|
16
|
+
is preloaded on narrow viewports. The portrait probe sets `fetchPriority`
|
|
17
|
+
to `high`, applies `srcset` before `src`, and the plate draw starts with
|
|
18
|
+
the other home fetches
|
|
19
|
+
- Home links `tailwind.css` before the icon scripts so that sheet is not
|
|
20
|
+
held behind parser-blocking script
|
|
21
|
+
|
|
22
|
+
Text faces still load after the first paint with `display=swap` and the
|
|
23
|
+
metric-matched fallbacks from 1.8.4. One listing shell still serves every
|
|
24
|
+
detail URL.
|
|
25
|
+
|
|
26
|
+
## [1.8.5] — 2026-10-13
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Home hero `sizes` nudged above the plate width so 1.75–2× DPR picks a
|
|
31
|
+
sharp 3:4 variant; document that hero variants must be portrait fills
|
|
32
|
+
|
|
33
|
+
## [1.8.4] — 2026-10-12
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Metric-matched local fallbacks for Cinzel Decorative, Cinzel, Newsreader,
|
|
38
|
+
Ancizar Serif, and Marcellus SC so `display=swap` does not reflow the home
|
|
39
|
+
hero (CLS)
|
|
40
|
+
- Home hero `portraits.json` includes `srcset` / `sizes` when
|
|
41
|
+
`variants/<stem>-{480,960,1440}.jpg` exist under the hero folder
|
|
42
|
+
|
|
43
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
44
|
+
ledger fallbacks are unchanged.
|
|
45
|
+
|
|
46
|
+
## [1.8.3] — 2026-10-12
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- Detail pages load MathJax only when a TeX block nears the viewport (not on
|
|
51
|
+
every article open), then typeset visible blocks first; below-fold formulas
|
|
52
|
+
follow via IntersectionObserver with yields between blocks
|
|
53
|
+
- Listing layouts (articles / blog) no longer pull the MathJax script
|
|
54
|
+
- Long detail bodies mount in batches so the opening can paint before the
|
|
55
|
+
full DOM lands
|
|
56
|
+
- Figure and cut-in images use `decoding="async"` plus width/height hints
|
|
57
|
+
with existing `loading="lazy"`
|
|
58
|
+
|
|
59
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
60
|
+
ledger fallbacks are unchanged.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## [1.8.2] — 2026-10-12
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- Home listing CTAs: more space between the label and its underline
|
|
68
|
+
- Home index panels (Sources / Network / Bibliography): shared plate flex and
|
|
69
|
+
card-scale titles so the three cards align
|
|
70
|
+
- Articles archive: `random` sort shows **Order: random** (was falling back to
|
|
71
|
+
last revised)
|
|
72
|
+
- Listing masthead tag lines (Rascunhos, Sources, peers) use body size instead
|
|
73
|
+
of micro / mono
|
|
74
|
+
- CV contact tiles: quieter badge and name type
|
|
75
|
+
- Dates / meta (`--type-meta`) +2pt; content titles (`--type-title`) +10%;
|
|
76
|
+
top-bar wordmark +25%
|
|
77
|
+
- Theorem / proof heads sit inline with the opening sentence (no line break
|
|
78
|
+
after the name / **Proof.**)
|
|
79
|
+
- Proof QED square (`□`) is +2pt over the body size
|
|
80
|
+
|
|
81
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
82
|
+
ledger fallbacks are unchanged.
|
|
83
|
+
|
|
3
84
|
## [1.8.1] — 2026-10-12
|
|
4
85
|
|
|
5
86
|
### 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.
|
|
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.
|
|
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 = '<
|
|
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 += '
|
|
136
|
+
head += '. </span>';
|
|
128
137
|
}
|
|
129
|
-
|
|
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 = '<
|
|
134
|
-
|
|
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 (!
|
|
204
|
-
|
|
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
|
-
|
|
817
|
-
|
|
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
|
-
|
|
889
|
-
var
|
|
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;
|
|
@@ -578,10 +578,13 @@
|
|
|
578
578
|
}
|
|
579
579
|
return new Promise(function (resolve) {
|
|
580
580
|
var probe = new Image();
|
|
581
|
+
probe.fetchPriority = 'high';
|
|
582
|
+
applyHeroPortraitSources(probe, chosen);
|
|
581
583
|
probe.onload = function () {
|
|
582
|
-
img.src = nextSrc;
|
|
583
584
|
img.alt = nextAlt;
|
|
584
585
|
applyHeroPortraitSources(img, chosen);
|
|
586
|
+
img.fetchPriority = 'high';
|
|
587
|
+
img.src = nextSrc;
|
|
585
588
|
img.classList.add('is-shown');
|
|
586
589
|
resolve(true);
|
|
587
590
|
};
|
|
@@ -610,9 +613,44 @@
|
|
|
610
613
|
return next();
|
|
611
614
|
}
|
|
612
615
|
|
|
616
|
+
function heroLandscapeActive(img) {
|
|
617
|
+
var url = img && img.getAttribute('data-hero-landscape');
|
|
618
|
+
if (!url || !String(url).trim() || !window.matchMedia) return false;
|
|
619
|
+
return window.matchMedia('(max-width: 767px)').matches;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function clearHeroSources(img) {
|
|
623
|
+
img.removeAttribute('src');
|
|
624
|
+
img.removeAttribute('srcset');
|
|
625
|
+
img.removeAttribute('sizes');
|
|
626
|
+
img.alt = '';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function watchHeroFrame(img) {
|
|
630
|
+
var url = img.getAttribute('data-hero-landscape');
|
|
631
|
+
if (!url || !String(url).trim() || !window.matchMedia) return;
|
|
632
|
+
var mq = window.matchMedia('(max-width: 767px)');
|
|
633
|
+
if (!mq.addEventListener) return;
|
|
634
|
+
mq.addEventListener('change', function () {
|
|
635
|
+
if (mq.matches) {
|
|
636
|
+
clearHeroSources(img);
|
|
637
|
+
img.classList.add('is-shown');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
img.classList.remove('is-shown');
|
|
641
|
+
loadJson('pages/home/portraits.json').then(function (data) {
|
|
642
|
+
return showHeroPortrait(img, heroPortraitEntries(data));
|
|
643
|
+
}).catch(function (error) {
|
|
644
|
+
console.error(error);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
613
649
|
function hydrateHeroPortrait() {
|
|
614
650
|
var img = document.querySelector('[data-content-hero-portrait]');
|
|
615
651
|
if (!img) return Promise.resolve();
|
|
652
|
+
watchHeroFrame(img);
|
|
653
|
+
if (heroLandscapeActive(img)) return Promise.resolve();
|
|
616
654
|
return loadJson('pages/home/portraits.json').then(function (data) {
|
|
617
655
|
return showHeroPortrait(img, heroPortraitEntries(data));
|
|
618
656
|
}).catch(function (error) {
|
|
@@ -621,6 +659,7 @@
|
|
|
621
659
|
}
|
|
622
660
|
|
|
623
661
|
function hydratePageFragments() {
|
|
662
|
+
hydrateHeroPortrait();
|
|
624
663
|
var headers = Array.prototype.slice.call(document.querySelectorAll('[data-content-page-header]'));
|
|
625
664
|
var chain = Promise.resolve();
|
|
626
665
|
headers.forEach(function (el) {
|
|
@@ -644,7 +683,6 @@
|
|
|
644
683
|
hydrateHomeSections();
|
|
645
684
|
hydrateBandArt();
|
|
646
685
|
hydrateContacts();
|
|
647
|
-
hydrateHeroPortrait();
|
|
648
686
|
});
|
|
649
687
|
}
|
|
650
688
|
|
|
@@ -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="
|
|
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>
|
data/_includes/mathjax.html
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
|
-
<!-- MathJax
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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>
|
data/_layouts/articles.html
CHANGED
|
@@ -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
data/_layouts/home.html
CHANGED
|
@@ -17,6 +17,7 @@ layout: shell
|
|
|
17
17
|
{%- assign _hero_name = site.pandorga.identity | pandorga_hero_name: site.author, site.title -%}
|
|
18
18
|
{%- assign _cv_page = site.pages | where: "key", "cv" | first -%}
|
|
19
19
|
{%- assign _hero_art = site.pandorga.home.hero_art | default: "page/hero-default.html" -%}
|
|
20
|
+
{%- assign _hero_landscape = page.hero_landscape | default: site.pandorga.home.hero_landscape | default: "" | strip -%}
|
|
20
21
|
{%- assign _home_bands = site.data.pandorga_home_bands -%}
|
|
21
22
|
|
|
22
23
|
<main class="home-ledger">
|
|
@@ -53,8 +54,15 @@ layout: shell
|
|
|
53
54
|
<span class="shard-accent home-hero-shard" aria-hidden="true"></span>
|
|
54
55
|
<div class="home-hero-plate-wrap">
|
|
55
56
|
<span class="ledger-frame-crosshairs" aria-hidden="true"></span>
|
|
56
|
-
<figure class="home-hero-plate">
|
|
57
|
-
|
|
57
|
+
<figure class="home-hero-plate{% if _hero_landscape != blank %} home-hero-plate--landscape{% endif %}">
|
|
58
|
+
{%- if _hero_landscape != blank -%}
|
|
59
|
+
<picture>
|
|
60
|
+
<source media="(max-width: 767px)" srcset="{{ _hero_landscape | escape }}" />
|
|
61
|
+
<img alt="" width="1600" height="900" fetchpriority="high" decoding="sync" data-content-hero-portrait data-hero-landscape="{{ _hero_landscape | escape }}" />
|
|
62
|
+
</picture>
|
|
63
|
+
{%- else -%}
|
|
64
|
+
<img alt="" width="960" height="1280" fetchpriority="high" decoding="sync" data-content-hero-portrait />
|
|
65
|
+
{%- endif -%}
|
|
58
66
|
<span class="ledger-frame-corners" aria-hidden="true"></span>
|
|
59
67
|
</figure>
|
|
60
68
|
</div>
|
data/_layouts/shell.html
CHANGED
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
{%- endcomment -%}
|
|
67
67
|
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
68
68
|
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
|
|
69
|
+
{%- assign _hero_landscape = page.hero_landscape | default: site.pandorga.home.hero_landscape | default: "" | strip -%}
|
|
70
|
+
{%- if page.layout == "home" and _hero_landscape != blank -%}
|
|
71
|
+
<link rel="preload" as="image" href="{{ _hero_landscape | escape }}" fetchpriority="high" media="(max-width: 767px)" />
|
|
72
|
+
{%- endif -%}
|
|
69
73
|
{% include theme/critical-preload.html %}
|
|
70
74
|
<link rel="stylesheet" href="/assets/css/base.css" />
|
|
71
75
|
<link rel="stylesheet" href="/assets/css/global-link.css" />
|
|
@@ -120,6 +124,11 @@
|
|
|
120
124
|
{%- for _sheet in _ledger -%}
|
|
121
125
|
<link rel="stylesheet" href="/assets/css/ledger/{{ _sheet }}.css" />
|
|
122
126
|
{%- endfor -%}
|
|
127
|
+
{%- comment -%}
|
|
128
|
+
Utilities before the icon scripts. Those scripts are parser-blocking,
|
|
129
|
+
so a sheet after them waits until they finish before the scanner sees it.
|
|
130
|
+
{%- endcomment -%}
|
|
131
|
+
<link rel="stylesheet" href="/assets/css/tailwind.css" />
|
|
123
132
|
{% include theme/font-loader.html %}
|
|
124
133
|
{%- comment -%}
|
|
125
134
|
Named icons only. The full variable face is about a megabyte, and a
|
|
@@ -194,7 +203,6 @@
|
|
|
194
203
|
<style>
|
|
195
204
|
:root { --drop-cap-lines: {{ site.drop_cap_lines | default: 2 }}; }
|
|
196
205
|
</style>
|
|
197
|
-
<link rel="stylesheet" href="/assets/css/tailwind.css" />
|
|
198
206
|
</head>
|
|
199
207
|
|
|
200
208
|
<body class="bg-background text-on-surface">
|
data/assets/css/article.css
CHANGED
|
@@ -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."
|
|
1016
|
-
|
|
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:
|
|
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.
|
|
126
|
-
--type-title-lg: 2.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1007
|
+
text-underline-offset: 0.48em;
|
|
954
1008
|
text-underline-position: under;
|
|
955
1009
|
}
|
|
956
1010
|
|
data/assets/css/ledger/cv.css
CHANGED
|
@@ -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-
|
|
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 ──────────────────────────────────────────────────
|
data/assets/css/ledger/home.css
CHANGED
|
@@ -370,8 +370,10 @@
|
|
|
370
370
|
|
|
371
371
|
/* ── Portrait plate ──
|
|
372
372
|
Beside the copy on a wide page the plate is portrait (3:4): its height
|
|
373
|
-
matches the left column and the width is W = 3/4 × H.
|
|
374
|
-
|
|
373
|
+
matches the left column and the width is W = 3/4 × H. On a phone it stays
|
|
374
|
+
3:4. A wider box would cover-crop that portrait a second time. When
|
|
375
|
+
hero_landscape is set, the phone plate is 16:9 and the picture source is
|
|
376
|
+
that file. */
|
|
375
377
|
.home-hero-plate {
|
|
376
378
|
position: relative;
|
|
377
379
|
z-index: 1;
|
|
@@ -389,12 +391,6 @@
|
|
|
389
391
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-on-surface) 10%, transparent);
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
@media (max-width: 767px) {
|
|
393
|
-
.home-hero-plate {
|
|
394
|
-
aspect-ratio: 4 / 3;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
394
|
:root:not([data-theme="dark"]) .home-hero-plate,
|
|
399
395
|
html[data-theme="light"] .home-hero-plate {
|
|
400
396
|
border-width: 1px;
|
|
@@ -451,6 +447,13 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
451
447
|
}
|
|
452
448
|
}
|
|
453
449
|
|
|
450
|
+
.home-hero-plate picture {
|
|
451
|
+
display: block;
|
|
452
|
+
width: 100%;
|
|
453
|
+
height: 100%;
|
|
454
|
+
margin: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
454
457
|
.home-hero-plate img {
|
|
455
458
|
width: 100%;
|
|
456
459
|
height: 100%;
|
|
@@ -469,6 +472,21 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
469
472
|
opacity: 1;
|
|
470
473
|
}
|
|
471
474
|
|
|
475
|
+
/* 16:9 file in a 16:9 plate. contain shows the whole frame. The image is
|
|
476
|
+
visible without waiting for the portrait runtime, so it can be the LCP. */
|
|
477
|
+
@media (max-width: 767px) {
|
|
478
|
+
.home-hero-plate--landscape {
|
|
479
|
+
aspect-ratio: 16 / 9;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.home-hero-plate--landscape img {
|
|
483
|
+
object-fit: contain;
|
|
484
|
+
object-position: center center;
|
|
485
|
+
opacity: 1;
|
|
486
|
+
transition: none;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
472
490
|
.home-hero-plate:hover img {
|
|
473
491
|
transform: scale(1.02);
|
|
474
492
|
}
|
|
@@ -487,6 +505,7 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
487
505
|
vertical tick so each band reads as a ruled plate rather than a soft fade. */
|
|
488
506
|
.home-band:not(.home-band--hero) .ledger-section-banner {
|
|
489
507
|
padding-top: var(--space-lg);
|
|
508
|
+
padding-bottom: var(--space-sm);
|
|
490
509
|
border-top: 1px solid var(--border-gentle);
|
|
491
510
|
box-shadow: 0 4px 0 -3px var(--border-gentle-faint);
|
|
492
511
|
position: relative;
|
|
@@ -1674,20 +1693,28 @@ button.home-project-inventory-row {
|
|
|
1674
1693
|
margin-inline: var(--space-lg);
|
|
1675
1694
|
}
|
|
1676
1695
|
|
|
1696
|
+
/* Card-scale title, not `.section-title`: that face is for band banners and
|
|
1697
|
+
was oversized once the three index panels sat in one row. Locked here so
|
|
1698
|
+
Sources / Network / Bibliography share one title register. */
|
|
1677
1699
|
.home-index-title {
|
|
1678
1700
|
margin: 0;
|
|
1701
|
+
font-family: var(--font-title);
|
|
1679
1702
|
font-size: var(--type-card);
|
|
1703
|
+
font-weight: var(--weight-display);
|
|
1704
|
+
line-height: 1.2;
|
|
1705
|
+
color: var(--color-on-surface);
|
|
1706
|
+
font-synthesis: none;
|
|
1707
|
+
font-optical-sizing: auto;
|
|
1680
1708
|
}
|
|
1681
1709
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
/* ── The plate ── */
|
|
1710
|
+
/* ── The plate ──
|
|
1711
|
+
Every panel grows its plate so the title + Open row lands on one baseline
|
|
1712
|
+
across Sources, Network, and Bibliography — the bib plate already flexed;
|
|
1713
|
+
the other two used to sit shorter and leave their titles higher. */
|
|
1689
1714
|
.home-index-plate {
|
|
1690
1715
|
align-self: stretch;
|
|
1716
|
+
flex: 1 1 auto;
|
|
1717
|
+
min-height: 0;
|
|
1691
1718
|
margin: 0;
|
|
1692
1719
|
background: var(--color-background);
|
|
1693
1720
|
border-bottom: 1px solid var(--border-gentle-subtle);
|
|
@@ -1924,8 +1951,9 @@ html[data-theme="light"] .home-featured .ledger-poster {
|
|
|
1924
1951
|
ends flush with the viewport and adds no scrollable width; this must stay
|
|
1925
1952
|
below 768px, which is where .home-band-inner swaps that token for --gutter.
|
|
1926
1953
|
The side hairlines go with it: a border pinned to the screen edge reads as a
|
|
1927
|
-
frame cut off rather than as the plate's edge.
|
|
1928
|
-
|
|
1954
|
+
frame cut off rather than as the plate's edge. The plate stays 3:4, the
|
|
1955
|
+
same frame as the portrait file, so cover does not crop it again. A
|
|
1956
|
+
configured hero_landscape switches this query to 16:9 and contain. */
|
|
1929
1957
|
@media (max-width: 767px) {
|
|
1930
1958
|
.home-hero-portrait {
|
|
1931
1959
|
max-width: none;
|
|
@@ -1952,8 +1980,6 @@ html[data-theme="light"] .home-featured .ledger-poster {
|
|
|
1952
1980
|
#home-bib-panel .home-index-plate {
|
|
1953
1981
|
display: flex;
|
|
1954
1982
|
flex-direction: column;
|
|
1955
|
-
flex: 1 1 auto;
|
|
1956
|
-
min-height: 0;
|
|
1957
1983
|
}
|
|
1958
1984
|
|
|
1959
1985
|
#home-bib-panel .home-index-plate-head {
|
|
@@ -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.
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.9.0"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
data/lib/pandorga/fonts.rb
CHANGED
|
@@ -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
|
-
|
|
82
|
-
|
|
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)
|
|
@@ -49,6 +49,9 @@ home:
|
|
|
49
49
|
hero_art:
|
|
50
50
|
type: string
|
|
51
51
|
notes: Include path for hero decoration (default `page/hero-default.html`)
|
|
52
|
+
hero_landscape:
|
|
53
|
+
type: string
|
|
54
|
+
notes: Optional URL of a 16:9 image used at or below 767px. Home front matter `hero_landscape` overrides this key. Omitted, the phone plate stays 3:4
|
|
52
55
|
|
|
53
56
|
fonts:
|
|
54
57
|
title: "`pandorga.fonts`"
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -3,17 +3,38 @@
|
|
|
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.
|
|
7
|
+
# Variants should be 3:4 portrait fills (not landscape width-only resizes):
|
|
8
|
+
# the plate is object-fit cover at 3:4, including on a phone, so a 480×270
|
|
9
|
+
# landscape candidate is too short and looks soft. A separate 16:9 file for
|
|
10
|
+
# narrow viewports is pandorga.home.hero_landscape, not a pool variant.
|
|
6
11
|
|
|
7
12
|
require "pathname"
|
|
8
13
|
|
|
9
14
|
module HeroPortraits
|
|
10
15
|
IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .gif].freeze
|
|
16
|
+
WIDTHS = [480, 960, 1440].freeze
|
|
17
|
+
# Slightly above the plate CSS width (14/16rem at html 110%) so 1.75–2×
|
|
18
|
+
# DPR picks the next 3:4 variant instead of a soft 480w. Variants must be
|
|
19
|
+
# portrait fills; landscape width-only resizes are too short under cover.
|
|
20
|
+
SIZES = "(max-width: 767px) 100vw, (max-width: 1023px) 18rem, 20rem"
|
|
11
21
|
|
|
12
22
|
module_function
|
|
13
23
|
|
|
14
24
|
def entries(content_root)
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
root = Pathname.new(content_root)
|
|
26
|
+
image_files(root).map do |file|
|
|
27
|
+
entry = { "src" => "/media/images/hero/#{file.basename}" }
|
|
28
|
+
stem = file.basename.to_s.sub(/\.[^.]+\z/, "")
|
|
29
|
+
parts = WIDTHS.filter_map do |width|
|
|
30
|
+
variant = root.join("media/images/hero/variants/#{stem}-#{width}.jpg")
|
|
31
|
+
next unless variant.file?
|
|
32
|
+
|
|
33
|
+
"/media/images/hero/variants/#{variant.basename} #{width}w"
|
|
34
|
+
end
|
|
35
|
+
next entry if parts.empty?
|
|
36
|
+
|
|
37
|
+
entry.merge("srcset" => parts.join(", "), "sizes" => SIZES)
|
|
17
38
|
end
|
|
18
39
|
end
|
|
19
40
|
|
|
@@ -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
|
data/scripts/test/test-fonts.rb
CHANGED
|
@@ -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") ==
|
|
32
|
-
|
|
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)
|
|
@@ -23,6 +23,7 @@ require_relative "../content/hero_portraits"
|
|
|
23
23
|
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
24
24
|
EXPORT = ROOT.join("scripts/content/export-content-json.rb")
|
|
25
25
|
LAYOUT = ROOT.join("_layouts/home.html")
|
|
26
|
+
SHELL = ROOT.join("_layouts/shell.html")
|
|
26
27
|
HOME_CSS = ROOT.join("assets/css/ledger/home.css")
|
|
27
28
|
RUNTIME = ROOT.join("_includes/content-runtime/60-fragments.html")
|
|
28
29
|
SCHEMA = ROOT.join("studio/schema.yml")
|
|
@@ -96,9 +97,28 @@ check!(!layout.include?("profile-monologue"),
|
|
|
96
97
|
"the deleted shell photograph must not be referenced")
|
|
97
98
|
check!(!ROOT.join("assets/images/profile-monologue.JPG").exist?,
|
|
98
99
|
"profile-monologue.JPG must not exist")
|
|
99
|
-
|
|
100
|
-
check!(
|
|
101
|
-
|
|
100
|
+
hero_imgs = layout.scan(/<img[^>]*data-content-hero-portrait[^>]*>/m)
|
|
101
|
+
check!(hero_imgs.any?, "the plate image is missing")
|
|
102
|
+
hero_imgs.each do |hero_img|
|
|
103
|
+
check!(!hero_img.include?("src=") && hero_img.include?('alt=""'),
|
|
104
|
+
"the plate starts without a photographic src and keeps an empty alt, got #{hero_img.inspect}")
|
|
105
|
+
check!(hero_img.include?('fetchpriority="high"'),
|
|
106
|
+
"the hero image must request fetchpriority high, got #{hero_img.inspect}")
|
|
107
|
+
end
|
|
108
|
+
check!(layout.include?("page.hero_landscape") && layout.include?("site.pandorga.home.hero_landscape"),
|
|
109
|
+
"narrow hero reads front matter and pandorga.home.hero_landscape")
|
|
110
|
+
check!(layout.include?('media="(max-width: 767px)"') && layout.include?("<picture>"),
|
|
111
|
+
"a configured landscape file is a picture source for narrow viewports")
|
|
112
|
+
check!(layout.include?("data-hero-landscape"),
|
|
113
|
+
"the image carries the landscape URL for the runtime")
|
|
114
|
+
shell = SHELL.read
|
|
115
|
+
check!(shell.include?('rel="preload" as="image"') && shell.include?('fetchpriority="high"') &&
|
|
116
|
+
shell.include?('media="(max-width: 767px)"') && shell.include?("hero_landscape"),
|
|
117
|
+
"the shell preloads a configured landscape image on narrow viewports")
|
|
118
|
+
tailwind_at = shell.index('href="/assets/css/tailwind.css"')
|
|
119
|
+
icons_at = shell.index("window.__pandorgaIconsHref")
|
|
120
|
+
check!(tailwind_at && icons_at && tailwind_at < icons_at,
|
|
121
|
+
"tailwind must be linked before the icon scripts")
|
|
102
122
|
|
|
103
123
|
home_css = HOME_CSS.read
|
|
104
124
|
check!(home_css.include?("background: black"),
|
|
@@ -109,6 +129,14 @@ check!(plate_img&.match?(/opacity:\s*0/) && plate_img.match?(/transition:[^;]*op
|
|
|
109
129
|
shown = home_css[/^\.home-hero-plate img\.is-shown \{.*?\n\}/m]
|
|
110
130
|
check!(shown&.match?(/opacity:\s*1/),
|
|
111
131
|
"a ready photograph fades to full opacity, got #{shown.inspect}")
|
|
132
|
+
check!(!home_css.include?("aspect-ratio: 4 / 3"),
|
|
133
|
+
"the phone plate must not cover-crop the portrait into 4:3")
|
|
134
|
+
landscape_plate = home_css[/@media \(max-width: 767px\) \{\s*\.home-hero-plate--landscape \{[^}]*\}/m]
|
|
135
|
+
check!(landscape_plate&.include?("aspect-ratio: 16 / 9"),
|
|
136
|
+
"a landscape plate is 16:9 on a phone, got #{landscape_plate.inspect}")
|
|
137
|
+
landscape_img = home_css[/\.home-hero-plate--landscape img \{[^}]*\}/m]
|
|
138
|
+
check!(landscape_img&.include?("object-fit: contain") && landscape_img.include?("opacity: 1"),
|
|
139
|
+
"the 16:9 file is shown whole and can paint without the portrait fade, got #{landscape_img.inspect}")
|
|
112
140
|
|
|
113
141
|
runtime = RUNTIME.read
|
|
114
142
|
start = runtime.index("function heroPortraitEntries")
|
|
@@ -125,6 +153,20 @@ onload = body[/probe\.onload = function \(\) \{.*?\n \};/m]
|
|
|
125
153
|
check!(onload&.include?("img.src = nextSrc") && onload.include?("img.alt = nextAlt") &&
|
|
126
154
|
onload.include?("img.classList.add('is-shown')"),
|
|
127
155
|
"src, alt, and the fade-in must be applied together after load, got #{onload.inspect}")
|
|
156
|
+
check!(onload && onload.index("applyHeroPortraitSources") && onload.index("img.src = nextSrc") &&
|
|
157
|
+
onload.index("applyHeroPortraitSources") < onload.index("img.src = nextSrc"),
|
|
158
|
+
"srcset must be applied before src so the full file is not the first request")
|
|
159
|
+
check!(body.include?("probe.fetchPriority = 'high'"),
|
|
160
|
+
"the portrait probe must use fetchPriority high")
|
|
161
|
+
check!(body.include?("heroLandscapeActive") && body.include?("data-hero-landscape"),
|
|
162
|
+
"a narrow viewport with hero_landscape must not swap in a portrait")
|
|
163
|
+
check!(runtime.include?("hydrateHeroPortrait();"),
|
|
164
|
+
"hydratePageFragments must draw the plate before the later fragment chain")
|
|
165
|
+
plate_call = runtime.index("function hydratePageFragments")
|
|
166
|
+
later = runtime.index("hydrateHomeSections();", plate_call || 0)
|
|
167
|
+
check!(plate_call && runtime.index("hydrateHeroPortrait();", plate_call) &&
|
|
168
|
+
runtime.index("hydrateHeroPortrait();", plate_call) < later,
|
|
169
|
+
"the plate draw must start at the beginning of hydratePageFragments")
|
|
128
170
|
onerror = body[/probe\.onerror = function \(\) \{.*?\n \};/m]
|
|
129
171
|
check!(onerror && !onerror.include?("img.src") && !onerror.include?("is-shown"),
|
|
130
172
|
"a failed image load must leave the plate black, got #{onerror.inspect}")
|
data/scripts/validate.sh
CHANGED