pandorga 1.6.7 → 1.6.9

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +3 -2
  4. data/_includes/content-runtime/40-listing.html +13 -5
  5. data/_includes/content-runtime/60-fragments.html +36 -10
  6. data/_includes/page/content-runtime.html +3 -0
  7. data/_includes/page/network-graph.html +5 -2
  8. data/_includes/page/timeline-section.html +13 -1
  9. data/_includes/theme/favicons.html +3 -1
  10. data/_layouts/articles.html +5 -2
  11. data/_layouts/bibliography.html +5 -2
  12. data/_layouts/blog.html +5 -2
  13. data/_layouts/cv.html +5 -2
  14. data/_layouts/home.html +54 -18
  15. data/_layouts/projects.html +5 -2
  16. data/_layouts/resources.html +5 -2
  17. data/_layouts/shell.html +6 -1
  18. data/assets/css/ledger/home.css +20 -0
  19. data/assets/css/ledger/rascunhos.css +40 -2
  20. data/assets/icons/apple-touch-icon.png +0 -0
  21. data/assets/icons/favicon-32.png +0 -0
  22. data/assets/icons/favicon.ico +0 -0
  23. data/assets/icons/favicon.svg +23 -0
  24. data/assets/icons/icon-192.png +0 -0
  25. data/assets/icons/icon-32.png +0 -0
  26. data/assets/icons/icon-512.png +0 -0
  27. data/assets/icons/icon-maskable-192.png +0 -0
  28. data/assets/icons/icon-maskable-512.png +0 -0
  29. data/assets/icons/studio-apple-touch-icon.png +0 -0
  30. data/assets/icons/studio-icon-192.png +0 -0
  31. data/assets/icons/studio-icon-512.png +0 -0
  32. data/assets/icons/studio-logo-32.png +0 -0
  33. data/assets/icons/studio-logo.ico +0 -0
  34. data/assets/icons/studio-logo.svg +25 -5
  35. data/assets/icons/studio-maskable-192.png +0 -0
  36. data/assets/icons/studio-maskable-512.png +0 -0
  37. data/lib/pandorga/jekyll/plugins/web_manifest.rb +81 -0
  38. data/lib/pandorga/version.rb +1 -1
  39. data/scripts/check_home_lists.mjs +237 -0
  40. data/scripts/lighthouse-home.sh +6 -0
  41. data/scripts/render-pwa-icons.py +141 -0
  42. data/scripts/test/test-home-articles.rb +30 -0
  43. data/scripts/test/test-home-lists.rb +83 -0
  44. data/scripts/test/test-pwa-icons.rb +280 -0
  45. data/scripts/test/test-rascunhos-licence.rb +43 -0
  46. data/scripts/validate.sh +4 -0
  47. data/studio/index.html +4 -1
  48. data/studio-app/index.html +4 -1
  49. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28483b04968177f92f86ee26292b1bca873db8515dcb0d1735b72be27a564fb8
4
- data.tar.gz: d2d20eb68479f49655c122a3948398e8e2719a27be458e27bc6f60aa002729da
3
+ metadata.gz: 110f9fdf3636db9ef496093f969e15ef4cd3322ee143c5efadb210fb5e4b98b9
4
+ data.tar.gz: 6c5625b1cbdac652c0120d2b25e49a722c59941862cf91da7f44eff6f1ada004
5
5
  SHA512:
6
- metadata.gz: 89dd79f11d075413daac4ebcda740dc4f36a426a8b11aa608570551a446c79b09fc2223a74d703a36c2cae2fdaf0a14f41b6e3cf0048261c336a3a2a082e0100
7
- data.tar.gz: efeaa1c9b49c5884e19b0e778e1a7205eec1dd000dc3c8875fa62747cf3a5df813202310f2cd2092d4d71847d629e6e8d1fedbf63e6bff8c99245f568f1a73f2
6
+ metadata.gz: 22f67174268b9edd80046d916e6d8394d4ce1df62e8802be7dc4eab8771479825c1ba8b3aa2f668ebef53bc95f116f840ddd300428362b938a19b9955d0914d8
7
+ data.tar.gz: 85ce7605aae925b73d384a9ddbc0269b5b31bb58652b05b60617c5b784f8cfc2c68dc92cc3824ed07bf016f305aee36c5fba17a51c891ecce06cd58a0c3f94d0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.9] — 2026-10-10
4
+
5
+ ### Fixed
6
+
7
+ - Home list hydration no longer fills every band in one turn. The project
8
+ specimen measurement yields between projects, each band starts on its
9
+ own turn, and each home section introduction yields before it is
10
+ parsed. Dates reuse one formatter. The lists still render.
11
+
12
+ No `pandorga:` config keys were added or renamed.
13
+
14
+ ## [1.6.8] — 2026-10-10
15
+
16
+ ### Fixed
17
+
18
+ - Home bands and the other listing pages fill again. Their scripts sit in
19
+ the page and used to run before the runtime existed, then return. The
20
+ titles stayed in the HTML and the lists stayed empty, on the phone and
21
+ on the desktop. Lighthouse scored that empty document. Those scripts
22
+ now start when the runtime is assigned. The runtime stays after the
23
+ page. The static hero summary, the deferred text faces, the deferred
24
+ markdown library, and the named icon subset stay as they are.
25
+ - CI loads the built example home and fails when the projects, articles,
26
+ or posts band has no items after load, on a phone viewport and on a
27
+ desktop viewport.
28
+ - On a phone, the featured card in the home Articles band has more space
29
+ above its text. The other article cards keep their padding. Article
30
+ titles in that band are 2pt larger.
31
+ - On the Rascunhos page, the licence sits under the entries again below
32
+ 1024px. The per-page stylesheet split had stopped loading the sheet
33
+ that ordered it, and the page's own `display: block` put it back on
34
+ top. The order now lives on the sheet the blog layout always loads.
35
+
36
+ ### Changed
37
+
38
+ - The site and Studio launcher icons are the same stamp, on the same
39
+ plate, in the same pastel triad. The stamp is the same size on both,
40
+ with the same padding, at 512, 192, 180, and 32. Studio adds a small
41
+ badge inside that stamp. The build writes `/site.webmanifest` and
42
+ `/studio.webmanifest` with maskable icons, so Android masks both to
43
+ one shape and one visual size. The shell links the site manifest.
44
+ Studio links the studio manifest.
45
+
46
+ No `pandorga:` config keys were added or renamed.
47
+
3
48
  ## [1.6.7] — 2026-10-10
4
49
 
5
50
  ### Fixed
data/README.md CHANGED
@@ -85,8 +85,9 @@ and an urgent performance campaign must then bring it back to 90 or above.
85
85
  Below 75 is never acceptable for a release. A pull request or a merge may
86
86
  score lower when that is unavoidable to keep the work modular. The release
87
87
  build publishes at 90 or above. Notes that include `LIGHTHOUSE_BUGFIX=1`
88
- may publish a bug fix from 75 to 89. Pull requests and merges only warn.
89
- The checklist is in [CONTRIBUTING.md](CONTRIBUTING.md).
88
+ may publish a bug fix from 75 to 89. Pull requests and merges only warn
89
+ on the score. The same run fails when the loaded example home has no
90
+ project or article items. The checklist is in [CONTRIBUTING.md](CONTRIBUTING.md).
90
91
 
91
92
  ## License
92
93
 
@@ -113,15 +113,23 @@
113
113
  return textExcerpt((item && item.body_markdown) || '');
114
114
  }
115
115
 
116
+ var dateFormatters = Object.create(null);
117
+
116
118
  function formatDate(value, locale) {
117
119
  if (!value) return '';
118
120
  var parts = String(value).slice(0, 10).split('-').map(Number);
119
121
  if (parts.length !== 3 || parts.some(isNaN)) return value;
120
- return new Intl.DateTimeFormat(locale || 'en', {
121
- day: '2-digit',
122
- month: 'short',
123
- year: 'numeric'
124
- }).format(new Date(parts[0], parts[1] - 1, parts[2]));
122
+ var key = locale || 'en';
123
+ var formatter = dateFormatters[key];
124
+ if (!formatter) {
125
+ formatter = new Intl.DateTimeFormat(key, {
126
+ day: '2-digit',
127
+ month: 'short',
128
+ year: 'numeric'
129
+ });
130
+ dateFormatters[key] = formatter;
131
+ }
132
+ return formatter.format(new Date(parts[0], parts[1] - 1, parts[2]));
125
133
  }
126
134
 
127
135
  function chronologySortKey(item) {
@@ -353,15 +353,31 @@
353
353
  });
354
354
  }
355
355
 
356
+ function yieldToMain() {
357
+ return new Promise(function (resolve) {
358
+ if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
359
+ scheduler.yield().then(resolve);
360
+ return;
361
+ }
362
+ window.setTimeout(resolve, 0);
363
+ });
364
+ }
365
+
356
366
  function hydrateHomeSections() {
357
367
  var roots = Array.prototype.slice.call(
358
368
  document.querySelectorAll('[data-content-home-section]')
359
369
  );
360
370
  if (!roots.length) return Promise.resolve();
361
371
  return loadPageHeaders().then(function (sections) {
372
+ var chain = Promise.resolve();
362
373
  roots.forEach(function (root) {
363
- applyHomeSection(root, sections[root.getAttribute('data-content-home-section')]);
374
+ chain = chain.then(function () {
375
+ return yieldToMain().then(function () {
376
+ applyHomeSection(root, sections[root.getAttribute('data-content-home-section')]);
377
+ });
378
+ });
364
379
  });
380
+ return chain;
365
381
  }).catch(function (error) {
366
382
  console.error(error);
367
383
  roots.forEach(function (root) { applyHomeSection(root, null); });
@@ -605,21 +621,31 @@
605
621
  }
606
622
 
607
623
  function hydratePageFragments() {
608
- document.querySelectorAll('[data-content-page-header]').forEach(function (el) {
609
- hydratePageHeader(el);
624
+ var headers = Array.prototype.slice.call(document.querySelectorAll('[data-content-page-header]'));
625
+ var chain = Promise.resolve();
626
+ headers.forEach(function (el) {
627
+ chain = chain.then(function () {
628
+ return yieldToMain().then(function () { return hydratePageHeader(el); });
629
+ });
610
630
  });
611
631
  document.querySelectorAll('[data-content-fragment]').forEach(function (el) {
612
632
  var path = el.getAttribute('data-content-fragment');
613
633
  if (!path) return;
614
- mountMarkdownFragment(path, el, {
615
- loadingText: el.getAttribute('data-content-loading') || 'Loading...',
616
- errorText: el.getAttribute('data-content-error') || 'Content could not be loaded right now.'
634
+ chain = chain.then(function () {
635
+ return yieldToMain().then(function () {
636
+ return mountMarkdownFragment(path, el, {
637
+ loadingText: el.getAttribute('data-content-loading') || 'Loading...',
638
+ errorText: el.getAttribute('data-content-error') || 'Content could not be loaded right now.'
639
+ });
640
+ });
617
641
  });
618
642
  });
619
- hydrateHomeSections();
620
- hydrateBandArt();
621
- hydrateContacts();
622
- hydrateHeroPortrait();
643
+ chain.then(function () {
644
+ hydrateHomeSections();
645
+ hydrateBandArt();
646
+ hydrateContacts();
647
+ hydrateHeroPortrait();
648
+ });
623
649
  }
624
650
 
625
651
  function schedulePageFragmentHydration() {
@@ -17,4 +17,7 @@
17
17
  {% include content-runtime/43-blocks.html %}
18
18
  {% include content-runtime/90-bootstrap.html %}
19
19
  })();
20
+ /* Band and listing scripts sit in the page, so they run before this
21
+ assignment. They listen for the event and start once the runtime exists. */
22
+ document.dispatchEvent(new CustomEvent('pandorga:runtime'));
20
23
  </script>
@@ -454,11 +454,14 @@
454
454
 
455
455
  <script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
456
456
  <script>
457
- (function () {
457
+ (function bootNetwork() {
458
458
  'use strict';
459
459
 
460
460
  var runtime = window.ContentRuntime;
461
- if (!runtime || !runtime.ListingFilter) return;
461
+ if (!runtime || typeof runtime.loadJson !== 'function') {
462
+ document.addEventListener('pandorga:runtime', bootNetwork, { once: true });
463
+ return;
464
+ }
462
465
 
463
466
  var params = new URLSearchParams(window.location.search);
464
467
  var filterRoot = document.querySelector('[data-listing-filter-root]');
@@ -852,14 +852,26 @@
852
852
  var runtime = window.ContentRuntime;
853
853
  /* The content base is empty when the JSON is served from the site root
854
854
  (local preview), which is a valid source, not a missing one — gate on
855
- the loader instead, and let the catch fall back to the inline stub. */
855
+ the loader instead, and let the catch fall back to the inline stub.
856
+ The runtime script follows the page, so the first call waits for it. */
856
857
  if (runtime && typeof runtime.loadJson === 'function') {
858
+ loadTimeline.started = true;
857
859
  runtime.loadJson('data/timeline.json').then(bootTimeline).catch(function (error) {
858
860
  console.error(error);
859
861
  if (el) bootTimeline(JSON.parse(el.textContent));
860
862
  });
861
863
  return;
862
864
  }
865
+ if (!loadTimeline.armed) {
866
+ loadTimeline.armed = true;
867
+ document.addEventListener('pandorga:runtime', loadTimeline, { once: true });
868
+ window.setTimeout(function () {
869
+ if (loadTimeline.started || !el) return;
870
+ loadTimeline.started = true;
871
+ bootTimeline(JSON.parse(el.textContent));
872
+ }, 0);
873
+ return;
874
+ }
863
875
  if (el) bootTimeline(JSON.parse(el.textContent));
864
876
  }
865
877
 
@@ -1,4 +1,6 @@
1
1
  <link rel="icon" href="/assets/icons/favicon.svg" type="image/svg+xml" sizes="any" />
2
2
  <link rel="icon" href="/assets/icons/favicon-32.png" type="image/png" sizes="32x32" />
3
- <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png" />
3
+ <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png" sizes="180x180" />
4
4
  <link rel="icon" href="/favicon.ico" sizes="16x16 32x32" />
5
+ <link rel="manifest" href="/site.webmanifest" />
6
+ <meta name="theme-color" content="#5e5d59" />
@@ -129,11 +129,14 @@ layout: shell
129
129
  {% include mathjax.html %}
130
130
 
131
131
  <script>
132
- (function () {
132
+ (function bootArticles() {
133
133
  'use strict';
134
134
 
135
135
  var runtime = window.ContentRuntime;
136
- if (!runtime || !runtime.ListingFilter) return;
136
+ if (!runtime || typeof runtime.loadJson !== 'function') {
137
+ document.addEventListener('pandorga:runtime', bootArticles, { once: true });
138
+ return;
139
+ }
137
140
 
138
141
  var configRoot = document.querySelector('[data-listing-collection]');
139
142
  var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'articles';
@@ -24,11 +24,14 @@ layout: shell
24
24
  </main>
25
25
 
26
26
  <script>
27
- (function () {
27
+ (function bootBibliography() {
28
28
  'use strict';
29
29
 
30
30
  var runtime = window.ContentRuntime;
31
- if (!runtime || !runtime.ListingFilter) return;
31
+ if (!runtime || typeof runtime.loadJson !== 'function') {
32
+ document.addEventListener('pandorga:runtime', bootBibliography, { once: true });
33
+ return;
34
+ }
32
35
 
33
36
  var listing = runtime.Listing;
34
37
  var params = new URLSearchParams(window.location.search);
data/_layouts/blog.html CHANGED
@@ -93,11 +93,14 @@ layout: shell
93
93
  {% include mathjax.html %}
94
94
 
95
95
  <script>
96
- (function () {
96
+ (function bootBlog() {
97
97
  'use strict';
98
98
 
99
99
  var runtime = window.ContentRuntime;
100
- if (!runtime || !runtime.ListingFilter) return;
100
+ if (!runtime || typeof runtime.loadJson !== 'function') {
101
+ document.addEventListener('pandorga:runtime', bootBlog, { once: true });
102
+ return;
103
+ }
101
104
 
102
105
  var configRoot = document.querySelector('[data-listing-collection]');
103
106
  var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'posts';
data/_layouts/cv.html CHANGED
@@ -80,11 +80,14 @@ layout: shell
80
80
  </main>
81
81
 
82
82
  <script>
83
- (function () {
83
+ (function bootCv() {
84
84
  'use strict';
85
85
 
86
86
  var runtime = window.ContentRuntime;
87
- if (!runtime) return;
87
+ if (!runtime || typeof runtime.loadJson !== 'function') {
88
+ document.addEventListener('pandorga:runtime', bootCv, { once: true });
89
+ return;
90
+ }
88
91
 
89
92
  var experienceList = document.getElementById('cv-experience-list');
90
93
  var educationList = document.getElementById('cv-education-list');
data/_layouts/home.html CHANGED
@@ -63,8 +63,12 @@ layout: shell
63
63
  </div>
64
64
  </section>
65
65
  <script>
66
- (function () {
66
+ (function bootHomePaint() {
67
67
  var runtime = window.ContentRuntime;
68
+ if (!runtime || typeof runtime.renderMarkdown !== 'function') {
69
+ document.addEventListener('pandorga:runtime', bootHomePaint, { once: true });
70
+ return;
71
+ }
68
72
  var pre = window.__pandorgaPreload || {};
69
73
  function paint(id, path) {
70
74
  var el = document.getElementById(id);
@@ -95,11 +99,14 @@ layout: shell
95
99
 
96
100
  <script>
97
101
  {% raw %}
98
- (function () {
102
+ (function bootHomeBands() {
99
103
  'use strict';
100
104
 
101
105
  var runtime = window.ContentRuntime;
102
- if (!runtime) return;
106
+ if (!runtime || typeof runtime.loadJson !== 'function') {
107
+ document.addEventListener('pandorga:runtime', bootHomeBands, { once: true });
108
+ return;
109
+ }
103
110
 
104
111
  function asItem(entry) {
105
112
  return {
@@ -114,7 +121,21 @@ layout: shell
114
121
  }
115
122
 
116
123
  function byDateDesc(a, b) {
117
- return String(b.date || '').localeCompare(String(a.date || ''));
124
+ var left = String(b.date || '');
125
+ var right = String(a.date || '');
126
+ if (left > right) return 1;
127
+ if (left < right) return -1;
128
+ return 0;
129
+ }
130
+
131
+ function afterPaint() {
132
+ return new Promise(function (resolve) {
133
+ if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
134
+ scheduler.yield().then(resolve);
135
+ return;
136
+ }
137
+ window.setTimeout(resolve, 0);
138
+ });
118
139
  }
119
140
 
120
141
  function tagHrefFrom(element) {
@@ -168,8 +189,13 @@ layout: shell
168
189
  function draftFolios(posts) {
169
190
  var folios = Object.create(null);
170
191
  posts.slice().sort(function (a, b) {
171
- var byDate = String(a.date || '').localeCompare(String(b.date || ''));
172
- return byDate !== 0 ? byDate : String(a.slug || '').localeCompare(String(b.slug || ''));
192
+ var byDate = String(a.date || '') < String(b.date || '') ? -1 : (String(a.date || '') > String(b.date || '') ? 1 : 0);
193
+ if (byDate !== 0) return byDate;
194
+ var left = String(a.slug || '');
195
+ var right = String(b.slug || '');
196
+ if (left < right) return -1;
197
+ if (left > right) return 1;
198
+ return 0;
173
199
  }).forEach(function (entry, index) {
174
200
  folios[entry.slug] = index + 1;
175
201
  });
@@ -214,7 +240,8 @@ layout: shell
214
240
  element.appendChild(fragment);
215
241
  }
216
242
 
217
- function fillBand(section, index) {
243
+ async function fillBand(section, index) {
244
+ await afterPaint();
218
245
  var kind = section.getAttribute('data-home-kind');
219
246
  var collection = section.getAttribute('data-home-collection');
220
247
  var limit = parseInt(section.getAttribute('data-home-limit') || '3', 10) || 3;
@@ -309,7 +336,7 @@ layout: shell
309
336
  fitted(best);
310
337
  }
311
338
 
312
- function paintSpecimen(entry) {
339
+ async function paintSpecimen(entry) {
313
340
  specimenSlot.innerHTML = '';
314
341
  specimenSlot.appendChild(runtime.renderProjectSpecimen(asItem(entry), {
315
342
  href: entry.url || runtime.projectUrl(entry.slug),
@@ -317,10 +344,11 @@ layout: shell
317
344
  folio: positionOf(entry.slug) + 1,
318
345
  openLabel: 'Open project'
319
346
  }));
347
+ await afterPaint();
320
348
  ellipsizeSpecimenCopy(specimenSlot);
321
349
  }
322
350
 
323
- function measureTallestSpecimen() {
351
+ async function measureTallestSpecimen() {
324
352
  var bench = specimenSlot.closest('.home-projects-bench');
325
353
  if (!bench) return 0;
326
354
  var width = specimenSlot.getBoundingClientRect().width;
@@ -341,7 +369,8 @@ layout: shell
341
369
  bench.appendChild(probe);
342
370
 
343
371
  var max = 0;
344
- portfolio.forEach(function (entry, i) {
372
+ for (var i = 0; i < portfolio.length; i += 1) {
373
+ var entry = portfolio[i];
345
374
  probe.innerHTML = '';
346
375
  var card = runtime.renderProjectSpecimen(asItem(entry), {
347
376
  href: entry.url || runtime.projectUrl(entry.slug),
@@ -353,15 +382,16 @@ layout: shell
353
382
  card.style.minHeight = '0';
354
383
  probe.appendChild(card);
355
384
  max = Math.max(max, Math.ceil(card.getBoundingClientRect().height));
356
- });
385
+ await afterPaint();
386
+ }
357
387
  probe.remove();
358
388
  return max;
359
389
  }
360
390
 
361
- function lockSpecimenHeight() {
391
+ async function lockSpecimenHeight() {
362
392
  var bench = specimenSlot.closest('.home-projects-bench');
363
393
  if (!bench) return;
364
- var tallest = measureTallestSpecimen();
394
+ var tallest = await measureTallestSpecimen();
365
395
  /* Floor at the CSS inventory minimum so six rows always clear
366
396
  three stacked resource icons (see --home-inventory-min-height). */
367
397
  var inventoryMin = 0;
@@ -418,8 +448,9 @@ layout: shell
418
448
  });
419
449
  }
420
450
 
421
- lockSpecimenHeight();
422
- paintSpecimen(entryBySlug(selectedSlug));
451
+ await lockSpecimenHeight();
452
+ await paintSpecimen(entryBySlug(selectedSlug));
453
+ await afterPaint();
423
454
  paintInventory();
424
455
 
425
456
  var resizeTimer = null;
@@ -430,8 +461,9 @@ layout: shell
430
461
  var nextWidth = Math.round(specimenSlot.getBoundingClientRect().width);
431
462
  if (nextWidth === lastMeasureWidth) return;
432
463
  lastMeasureWidth = nextWidth;
433
- lockSpecimenHeight();
434
- paintSpecimen(entryBySlug(selectedSlug));
464
+ lockSpecimenHeight().then(function () {
465
+ return paintSpecimen(entryBySlug(selectedSlug));
466
+ });
435
467
  }, 120);
436
468
  });
437
469
  return;
@@ -477,7 +509,11 @@ layout: shell
477
509
 
478
510
  runtime.loadJson('data/writing_index.json').then(function (entries) {
479
511
  var index = Array.isArray(entries) ? entries : [];
480
- sections.forEach(function (section) { fillBand(section, index); });
512
+ var chain = Promise.resolve();
513
+ sections.forEach(function (section) {
514
+ chain = chain.then(function () { return fillBand(section, index); });
515
+ });
516
+ return chain;
481
517
  }).catch(function (error) {
482
518
  console.error(error);
483
519
  sections.forEach(function (section) {
@@ -59,11 +59,14 @@ layout: shell
59
59
  </main>
60
60
 
61
61
  <script>
62
- (function () {
62
+ (function bootProjects() {
63
63
  'use strict';
64
64
 
65
65
  var runtime = window.ContentRuntime;
66
- if (!runtime) return;
66
+ if (!runtime || typeof runtime.loadJson !== 'function') {
67
+ document.addEventListener('pandorga:runtime', bootProjects, { once: true });
68
+ return;
69
+ }
67
70
 
68
71
  var esc = runtime.escapeHtml;
69
72
  var escAttr = runtime.escapeAttr;
@@ -34,11 +34,14 @@ layout: shell
34
34
  </main>
35
35
 
36
36
  <script>
37
- (function () {
37
+ (function bootResources() {
38
38
  'use strict';
39
39
 
40
40
  var runtime = window.ContentRuntime;
41
- if (!runtime || !runtime.ListingFilter) return;
41
+ if (!runtime || typeof runtime.loadJson !== 'function') {
42
+ document.addEventListener('pandorga:runtime', bootResources, { once: true });
43
+ return;
44
+ }
42
45
 
43
46
  var configRoot = document.querySelector('[data-listing-collection]');
44
47
  var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'resources';
data/_layouts/shell.html CHANGED
@@ -85,7 +85,12 @@
85
85
  {%- assign _ledger = "writing|detail" | split: "|" -%}
86
86
  {%- assign _article_css = true -%}
87
87
  {%- when "blog" -%}
88
- {%- assign _ledger = "rascunhos|detail" | split: "|" -%}
88
+ {%- comment -%}
89
+ writing.css holds the rail, and it has to follow rascunhos.css. The
90
+ body rule in rascunhos.css is `display: block`, which would put the
91
+ licence back above the entries if it won the cascade.
92
+ {%- endcomment -%}
93
+ {%- assign _ledger = "rascunhos|writing|detail" | split: "|" -%}
89
94
  {%- assign _article_css = true -%}
90
95
  {%- when "cv" -%}
91
96
  {%- assign _ledger = "cv|detail" | split: "|" -%}
@@ -740,6 +740,26 @@ html[data-theme="light"] .home-hero-plate {
740
740
  line-height: var(--home-featured-title-line);
741
741
  }
742
742
 
743
+ /* Articles band only. Two points over the size the title already uses,
744
+ the same point step as the phone hero name. The section heading is
745
+ untouched. Listed cards in other bands are untouched. */
746
+ [data-home-kind="articles"] .home-featured-title {
747
+ font-size: calc(var(--home-featured-title-size) + 2pt);
748
+ }
749
+
750
+ [data-home-kind="articles"] .home-card-grid .card-title,
751
+ [data-home-kind="articles"] .home-card-grid .ledger-card-title {
752
+ font-size: calc(var(--type-card) + 2pt);
753
+ }
754
+
755
+ /* Phone only: the featured card's text sat against the top rule. One
756
+ step more than the shared top padding, and only on this card. */
757
+ @media (max-width: 767px) {
758
+ [data-home-kind="articles"] .home-featured {
759
+ padding-top: calc(var(--space-md) + 0.75rem);
760
+ }
761
+ }
762
+
743
763
  /* Home article / rascunho / project cards — content-title voice, not display. */
744
764
  .home-card-grid .ledger-card-title,
745
765
  .home-card-grid .card-title,
@@ -66,8 +66,46 @@
66
66
  display: block;
67
67
  }
68
68
 
69
- /* `.hidden` swaps the listing for the detail view; it has to beat the grid. */
70
- .rascunhos-body.hidden {
69
+ /* Below 1024px the rail is a disclosure above the ledger, and the licence
70
+ is a child of that rail, so source order paints it above the texts.
71
+ `writing.css` already sets this column, but the per-page split dropped
72
+ that sheet from the blog layout, and this `display: block` beats the
73
+ flex rule whenever rascunhos.css is the later file. The selector here is
74
+ more specific than that block display, so the licence stays under the
75
+ entries even if the sheet list changes again. */
76
+ @media (max-width: 1023px) {
77
+ .rascunhos-main .rascunhos-body {
78
+ display: flex;
79
+ flex-direction: column;
80
+ }
81
+
82
+ .rascunhos-main .rascunhos-body>.writing-rail:not([hidden]) {
83
+ display: contents;
84
+ }
85
+
86
+ .rascunhos-main .rascunhos-body>.rascunhos-ledger {
87
+ order: 1;
88
+ }
89
+
90
+ .rascunhos-main .writing-rail-licence {
91
+ order: 2;
92
+ margin-top: var(--space-xl);
93
+ padding-top: var(--space-lg);
94
+ border-top: 1px solid var(--border-gentle-subtle);
95
+ }
96
+
97
+ .rascunhos-main .writing-rail-panel {
98
+ margin-bottom: var(--space-xl);
99
+ }
100
+
101
+ .rascunhos-main .writing-rail:has(> .writing-rail-panel[hidden]) .writing-rail-trigger {
102
+ margin-bottom: var(--space-xl);
103
+ }
104
+ }
105
+
106
+ /* `.hidden` swaps the listing for the detail view; it has to beat the
107
+ mobile flex column above, which is why the page class is on the selector. */
108
+ .rascunhos-main .rascunhos-body.hidden {
71
109
  display: none;
72
110
  }
73
111
 
Binary file
Binary file
Binary file
@@ -0,0 +1,23 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Site">
2
+ <title>Site</title>
3
+ <style>
4
+ /* Light plate: pastel triad on the dark frame. Dark plate: production triad
5
+ on the light frame. The mark stays inside the maskable safe zone. */
6
+ .primary { fill: #a4c8da; }
7
+ .secondary { fill: #95c8a6; }
8
+ .tertiary { fill: #ddd8a8; }
9
+ .plate { fill: #5e5d59; }
10
+ @media (prefers-color-scheme: dark) {
11
+ .primary { fill: #1c5a7a; }
12
+ .secondary { fill: #287445; }
13
+ .tertiary { fill: #746f30; }
14
+ .plate { fill: #b8b7ae; }
15
+ }
16
+ </style>
17
+ <rect class="plate" width="32" height="32"/>
18
+ <g transform="translate(7.68 7.68) scale(0.52)">
19
+ <polygon class="primary" points="0,0 23,0 15,17 0,20"/>
20
+ <polygon class="secondary" points="23,0 32,0 32,32 17,21 15,17"/>
21
+ <polygon class="tertiary" points="0,20 15,17 17,21 32,32 0,32"/>
22
+ </g>
23
+ </svg>
Binary file
Binary file
Binary file
Binary file
Binary file