pandorga 1.14.0 → 2.0.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/DESIGN.md +15 -30
  4. data/_includes/content-runtime/43-blocks.html +0 -3
  5. data/_includes/content-runtime/60-fragments.html +83 -149
  6. data/_includes/content-runtime/90-bootstrap.html +3 -0
  7. data/_includes/cv/cv-box.html +10 -4
  8. data/_includes/home/band-index.html +22 -7
  9. data/_includes/home/band-listing.html +7 -3
  10. data/_includes/page/timeline-section.html +8 -3
  11. data/_includes/theme/theme-vars-block.liquid +3 -0
  12. data/_layouts/articles.html +0 -2
  13. data/_layouts/bibliography.html +0 -2
  14. data/_layouts/blog.html +0 -5
  15. data/_layouts/cv.html +27 -8
  16. data/_layouts/home.html +2 -13
  17. data/_layouts/projects.html +17 -9
  18. data/_layouts/resources.html +0 -9
  19. data/_layouts/shell.html +1 -7
  20. data/assets/css/article.css +10 -2
  21. data/assets/css/base.css +68 -44
  22. data/assets/css/ledger/bibliography.css +0 -31
  23. data/assets/css/ledger/chrome.css +51 -2
  24. data/assets/css/ledger/cv.css +0 -40
  25. data/assets/css/ledger/detail.css +20 -6
  26. data/assets/css/ledger/home.css +261 -402
  27. data/assets/css/ledger/projects.css +17 -19
  28. data/assets/css/ledger/rascunhos.css +0 -27
  29. data/assets/css/ledger/sources.css +0 -57
  30. data/assets/css/ledger/writing.css +0 -62
  31. data/lib/pandorga/commands/new.rb +2 -2
  32. data/lib/pandorga/commands/thumbnails.rb +3 -2
  33. data/lib/pandorga/registry/schema.yml +0 -3
  34. data/lib/pandorga/thumbnail_variants.rb +29 -3
  35. data/lib/pandorga/version.rb +1 -1
  36. data/scripts/check_home_lists.mjs +33 -5
  37. data/scripts/content/hero_portraits.rb +59 -11
  38. data/scripts/test/test-css-structure.rb +0 -1
  39. data/scripts/test/test-hero-16x9.rb +379 -0
  40. data/scripts/test/test-hero-portraits.mjs +10 -20
  41. data/scripts/test/test-hero-portraits.rb +31 -25
  42. data/scripts/test/test-highlighter-pills.rb +12 -2
  43. data/scripts/test/test-home-lists.rb +5 -0
  44. data/scripts/test/test-new.rb +6 -6
  45. data/scripts/test/test-shell-a11y.rb +2 -1
  46. data/scripts/test/test-studio-section-texts.rb +625 -0
  47. data/scripts/test/test-theme-contrast.rb +13 -2
  48. data/scripts/test/test-v3-layout.rb +559 -0
  49. data/scripts/validate.sh +3 -0
  50. data/studio/assets/{index-BJyN-kB3.js → index-B9h1WUYY.js} +1 -1
  51. data/studio/index.html +1 -1
  52. data/studio/schema.yml +126 -2
  53. data/studio-app/src/lib/yaml-doc.js +6 -0
  54. data/templates/network/studio.yml +10 -0
  55. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f6585d9644f2c5715b1c18088443f0a17d57f65783b0805899ad2882cc57643
4
- data.tar.gz: f749368069fe9166d3f483292abed8f8da5b32b0c5f7c1da0b2e798241f859ed
3
+ metadata.gz: 96a7bd991685c2ae92de18f572112e692d02b211e134be12f0838247da013ade
4
+ data.tar.gz: 3f23ff94186ed6931a4f947efd18858cb16908a7db3c294a0eb9d92e244fc6f6
5
5
  SHA512:
6
- metadata.gz: 81fff53869d080e555aa8cb77e19c8d8882da7855d23786ec01d255ae0e0d47c9b357f26fba11d8ca663dd697778fe7d8f3315bbff59ecdbd64820e90b4cc385
7
- data.tar.gz: dba781ffd7ee1879bdd6676d45117a72b3dbdc8582b9c76cf45e04b9757889d36a0345282226602a7d2350398e7eb2b4089b0d7468d0c81185e92d3983cfec3c
6
+ metadata.gz: 8e924c057f9e70fdbea5e2b5ca40c3499ee18d482ce580ab091ff72e368bd4fc931c5ef7fb4a099155c04328a2ac2d98c93a36d5c3f1c0961e998c3e1a9edeb5
7
+ data.tar.gz: e8c7c19029b08a5cd20c6f5c109872b40c2d26877ffa8946ae01b6ab5c2d0c53dc6aad0eae0adf65ba23003fca69f18942dc3171e82ce44f7f7513d97c286ec4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.0.0] — 2026-10-24
6
+
7
+ ### Added
8
+
9
+ - Section texts are editable in Studio from `content/pages/headers.yml`: a projects page form (title, kicker, note, home link label and the two projects page banners), a `cta` link label for every page on the home, a `note` for Sources and Network, and the CV section titles, timeline title and Contact label. The Sources, Network and Bibliography home panels show each page's headers title and, under it, its `note`. A field left out keeps the theme default; a field saved empty renders nothing and a hidden heading keeps its default as the region's accessible name. Studio saves an emptied field as `""` (`keep_empty`), and the runtime fills these texts from `pages/headers.json` for content-agnostic shells (PLT-AC-50).
10
+
11
+ ### Changed
12
+
13
+ - `pandorga new`, getting started and the deploy and upgrade guides pin `gem "pandorga", "~> 2.0"`; `~> 1.3` does not resolve 2.0.0.
14
+ - **Breaking.** The home hero plate is 16:9 at every width. From 1024px the name sets on two lines in a 46% copy column and the photograph takes the rest of the row, as tall as the copy and at least 15rem, with `object-fit: cover`; below 1024px the plate spans the column. The export writes 16:9 `variants/<stem>-{480,960,1440}.jpg` for each hero photograph with the project thumbnail generator and lists only 16:9 variants in `portraits.json`. See docs/upgrading.md (PLT-AC-51).
15
+ - Every page sits on one canvas (`--color-canvas`, from `neutrals.canvas`, else `panel_contrast`) with square 1px hairline cards on the card surface and a 2px accent rule on featured cards. Home bands set the title and "View all" on one row with the note below; band titles, card titles, excerpts and metadata step down below 768px, metadata uses the mono face, and excerpts clamp to 3 lines on phones and 4 from 768px (PLT-AC-49).
16
+ - On phones the featured article opens with its image edge to edge, Rascunhos show three untilted cards (two side by side, the third a landscape card across both columns; the tilt stays from 768px), project posters run edge to edge, and the reading column and its sidebar have no box (PLT-AC-49).
17
+ - The Sources, Network and Bibliography panels open with the title and Open link above the illustration. The top bar sits on the translucent canvas with a hairline, and the theme and menu buttons are bordered squares (PLT-AC-49).
18
+ - Text calls to action ("View all", "Open", "Read", "Repository", "Article") carry the strong highlighter mark instead of an underline (PLT-AC-49).
19
+
20
+ ### Removed
21
+
22
+ - **Breaking.** `pandorga.home.hero_landscape`, home front matter `hero_landscape`, the `landscape` field of a `portraits.json` entry, the narrow-viewport hero draw and its preload, and the 3:4 hero variants (PLT-AC-51).
23
+ - The radial blur pools, the decorative shards outside the footer, and the frame corners inside cards (PLT-AC-49).
24
+
25
+ ### Fixed
26
+
27
+ - From 1024px the hero layout rules apply: the base plate rules (`max-width`, `position`, `aspect-ratio`) used to follow them in `home.css` and win on source order (PLT-AC-51).
28
+ - From 1024px to 1279px the top bar no longer runs the section links under the wordmark and the theme and menu buttons. The links stay in the menu until 1280px, and the rail gap is tighter up to 1439px (PLT-AC-49).
29
+
30
+ ## [1.14.1] — 2026-10-20
31
+
32
+ ### Fixed
33
+
34
+ - Hovered Back, Share, Cite, and reading-size buttons on article pages meet 4.5:1 in the light theme; the green section text on the gray hover mark was 4.48:1 (PLT-AC-46).
35
+
5
36
  ## [1.14.0] — 2026-10-20
6
37
 
7
38
  ### Changed
data/DESIGN.md CHANGED
@@ -824,7 +824,7 @@ the sources host line, the network hint and the detail back link all follow.
824
824
  | Token | Base | Larger | Line height |
825
825
  | :--- | :--- | :--- | :--- |
826
826
  | `pageTitle` | 3.625rem | 5.875rem (≥768px) | 0.95 |
827
- | `homeHeroTitle` | `clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg))` | floor `--type-section-lg` (≥640px) | 1.05 |
827
+ | `homeHeroTitle` | `clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg))` | floor `--type-section-lg` (≥640px); `min(calc(100cqi / var(--hero-name-fit, 8.4)), var(--hero-name-max, 4.5rem))` on two lines (≥1024px) | 1.05 |
828
828
  | `pageHeaderTitle` | 2.5rem | 3.5rem (≥768px) | 1.15 → 1.1 |
829
829
  | `textTitle` | 2.15rem | 2.65rem (≥768px) | 1.12 |
830
830
  | `textTitle` in the dossier | 1.95rem | 2.25rem (≥1024px) | 1.15 |
@@ -836,9 +836,11 @@ The home hero sizes to its copy column through a container query on
836
836
  `--type-page-lg`. The floor is the band-title step (`--type-section`, then
837
837
  `--type-section-lg` from 640px) so the name meets Articles, Rascunhos, and
838
838
  Projects on a phone instead of falling to the card size. Below 768px the
839
- name may wrap (`white-space: normal`); from 768px it stays one line. The
840
- given name is `onSurface` and the family name is `primary` on the same
841
- line. An earlier container-query fit —
839
+ name may wrap (`white-space: normal`); from 768px to 1023px it stays one
840
+ line. From 1024px the copy column is 46% of the row and the name sets on
841
+ two lines, given name over family name, sized so the longer part
842
+ (`--hero-name-fit` em wide) fills the column. The given name is
843
+ `onSurface` and the family name is `primary`. An earlier container-query fit —
842
844
  `min(4.875rem, calc(100cqw * 0.96 / 4.5))`, whose `4.5` was the advance of
843
845
  `"Hello, I am"` in Lora — remains in `base.css` but reaches no markup.
844
846
 
@@ -950,9 +952,13 @@ Letter-spacing in use, by frequency: `0.1em` (12), `0.08em` (10), `0.12em` (7),
950
952
  in place; specimen click opens the project. Below 900px the rack is an
951
953
  icon strip above the specimen, one equal column per project (up to six)
952
954
  so it spans the band, with 1.375rem glyphs. Contract: `PLT-AC-13`–`17` in
953
- `templates/portfolio/README.md`. Below 768px the hero portrait runs to the screen edge, pulled
955
+ `templates/portfolio/README.md`. The hero photograph is one 16:9 plate at
956
+ every width (`PLT-AC-51`). Below 768px it runs to the screen edge, pulled
954
957
  out of the band's gutter by exactly `--gutter-mobile` on each side and losing
955
- its side hairlines, which at the edge would read as a frame cut off.
958
+ its side hairlines, which at the edge would read as a frame cut off. From
959
+ 768px to 1023px it spans the column under the copy. From 1024px it takes
960
+ the row beside the two-line name, as tall as the copy and at least 15rem,
961
+ and covers that box like the featured article poster.
956
962
  * **Writing index.** Masthead over a hairline, then a `panelContrast` filter
957
963
  deck (tag tabs, sort, keyword), then a `panelContrast` lead panel for the
958
964
  newest entry, then an archive of `.ledger-row`s under a section banner.
@@ -1091,27 +1097,13 @@ everything else, so the query catches them too.
1091
1097
  Placement, size and hue are set per use by the page stylesheet, never by the
1092
1098
  utility.
1093
1099
 
1094
- Fifteen uses site-wide, all `aria-hidden`. No panel carries two shards in
1095
- the same corner or the same hue, and no two panels carry the same pair of
1096
- corners, which is the whole rule the placement follows:
1100
+ Since `PLT-AC-49` the shards live only in the site footer: the page and
1101
+ card shards competed with the content on the new canvas and were removed.
1102
+ The footer pair is `aria-hidden`; the hero kite is listed for reference:
1097
1103
 
1098
1104
  | Where | Class | Corner | Hue |
1099
1105
  | :--- | :--- | :--- | :--- |
1100
1106
  | Home hero band | `.home-hero-pandorga` (geometric kite & ribbon) | background | `primary` / `secondary` / `tertiary` |
1101
- | Home hero portrait | `.shard-accent.home-hero-shard` | top-right | `primary` |
1102
- | Home featured card | `.shard-accent.home-featured-shard` | top-left | `tertiary` |
1103
- | Home featured card | `.shard-accent-beta.home-featured-shard--beta` | bottom-right | `secondary` |
1104
- | Home index panel | `.shard-accent.home-index-shard` | bottom-right | `secondary` |
1105
- | Home index panel | `.shard-accent-beta.home-index-shard--beta` | bottom-left | `primary` |
1106
- | Writing index lead panel | `.shard-accent.writing-lead-shard` | bottom-left | `primary` |
1107
- | Writing index lead panel | `.shard-accent-beta.writing-lead-shard--beta` | top-right | `tertiary` |
1108
- | CV dossier | `.shard-accent.cv-dossier-shard` | top-right | `secondary` |
1109
- | CV dossier | `.shard-accent-beta.cv-dossier-shard--beta` | bottom-left | `primary` |
1110
- | Projects lead | `.shard-accent.projects-lead-shard` | top-right | `primary` |
1111
- | Projects hero | `.shard-accent.projects-hero-shard` | bottom-right | `secondary` |
1112
- | Rascunhos masthead | `.shard-accent.rascunhos-shard` | top-right | `secondary` |
1113
- | Sources masthead | `.shard-accent.sources-masthead-shard` | top-right | `secondary` |
1114
- | Sources foot | `.shard-accent-beta.sources-foot-shard` | bottom-left | `tertiary` |
1115
1107
  | Site footer | `.shard-accent.site-footer-shard` | bottom-right | `secondary` |
1116
1108
  | Site footer | `.shard-accent-beta.site-footer-shard--beta` | bottom-left | `primary` |
1117
1109
 
@@ -1119,13 +1111,6 @@ everything else, so the query catches them too.
1119
1111
  carries both class names, so any media query that repositions the pair has to
1120
1112
  restate the base rule's `auto` offsets on the beta: leave all four set and
1121
1113
  the browser resolves to top and left, and the pair collapses into one corner.
1122
- And a shard behind an opaque plate is a shard nobody sees, which is why the
1123
- home featured pair goes around its poster rather than across the card.
1124
-
1125
- On the writing index the shard was for a long time written to stand down
1126
- whenever the featured entry carried a thumbnail — and that panel selects the
1127
- first entry that *has* one, so the condition was always true and the shard
1128
- never rendered on a built page.
1129
1114
 
1130
1115
  A further shard sits on the detail sidebar heading. It is a `::after`
1131
1116
  pseudo-element, so it cannot take the utility class and re-implements it by
@@ -431,9 +431,6 @@
431
431
  if (options.className) card.className += ' ' + options.className;
432
432
 
433
433
  var html = '';
434
- if (options.frameCorners) {
435
- html += '<span class="ledger-frame-corners" aria-hidden="true"></span>';
436
- }
437
434
  var poster = renderPosterMedia(fm.thumbnail, name, {
438
435
  srcset: fm.thumbnail_srcset,
439
436
  sizes: options.sizes || PROJECT_CARD_SIZES
@@ -255,6 +255,85 @@
255
255
  });
256
256
  }
257
257
 
258
+ function headerText(headers, key, field, fallback) {
259
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return fallback;
260
+ var section = headers[key];
261
+ if (!section || typeof section !== 'object' || Array.isArray(section)) return fallback;
262
+ var val = section[field];
263
+ if (typeof val !== 'string') return fallback;
264
+ return val.trim();
265
+ }
266
+
267
+ /* An absent field keeps the built default; an empty one removes the element
268
+ * and its wrapper, keeping the region's accessible name.
269
+ */
270
+ function hydrateHeaderTexts(root) {
271
+ root = root || document;
272
+ var elements = Array.prototype.slice.call(
273
+ root.querySelectorAll('[data-content-header-text]')
274
+ );
275
+ if (!elements.length) return Promise.resolve();
276
+ return loadPageHeaders().then(function (headers) {
277
+ elements.forEach(function (el) {
278
+ var attr = el.getAttribute('data-content-header-text') || '';
279
+ var dot = attr.indexOf('.');
280
+ var key = dot !== -1 ? attr.substring(0, dot) : attr;
281
+ var field = dot !== -1 ? attr.substring(dot + 1) : '';
282
+ var text = headerText(headers, key, field, null);
283
+ if (text === null) return;
284
+ if (text !== '') {
285
+ var next = stripSingleParagraph(parseInlineMarkdown(text));
286
+ if (el.innerHTML.trim() !== next.trim()) el.innerHTML = next;
287
+ } else {
288
+ var tag = String(el.tagName || '').toUpperCase();
289
+ if (tag >= 'H1' && tag <= 'H6' && tag.length === 2) {
290
+ var nameEl = el.closest('[data-content-header-name]');
291
+ if (nameEl && !nameEl.hasAttribute('aria-label') && !nameEl.hasAttribute('aria-labelledby')) {
292
+ nameEl.setAttribute('aria-label', el.textContent.trim());
293
+ }
294
+ }
295
+ var target = el.closest('[data-content-header-wrap]') || el;
296
+ var parent = target.parentElement;
297
+ target.remove();
298
+ if (parent && parent.hasAttribute('data-content-header-group') && parent.childElementCount === 0) {
299
+ parent.remove();
300
+ }
301
+ }
302
+ });
303
+ }).catch(function (error) {
304
+ console.error(error);
305
+ });
306
+ }
307
+
308
+ function hydratePanelNotes(root) {
309
+ root = root || document;
310
+ var elements = Array.prototype.slice.call(
311
+ root.querySelectorAll('[data-content-panel-note]')
312
+ );
313
+ if (!elements.length) return Promise.resolve();
314
+ return loadPageHeaders().then(function (headers) {
315
+ elements.forEach(function (el) {
316
+ var key = el.getAttribute('data-content-panel-note') || '';
317
+ var text = headerText(headers, key, 'note', '');
318
+ if (text) {
319
+ var next = stripSingleParagraph(parseInlineMarkdown(text));
320
+ if (el.innerHTML.trim() !== next.trim()) el.innerHTML = next;
321
+ el.hidden = false;
322
+ el.classList.remove('hidden');
323
+ } else {
324
+ el.remove();
325
+ }
326
+ });
327
+ }).catch(function (error) {
328
+ console.error(error);
329
+ elements.forEach(function (el) {
330
+ if (!el.textContent.trim()) {
331
+ el.remove();
332
+ }
333
+ });
334
+ });
335
+ }
336
+
258
337
  function hydratePageHeader(root) {
259
338
  if (!root) return Promise.resolve();
260
339
  var pageKey = root.getAttribute('data-content-page-header');
@@ -519,9 +598,8 @@
519
598
  // Home hero plate. pages/home/portraits.json lists every image in
520
599
  // content/media/images/hero/. One entry is chosen per document load.
521
600
  // A file that fails to load is skipped. The plate stays black until one
522
- // pool file has loaded, and stays black when every entry fails.
523
- // At or below 767px the draw uses entries that carry `landscape` (a 16:9
524
- // URL). The single data-hero-landscape URL is the fallback when none do.
601
+ // pool file has loaded, and stays black when every entry fails. The
602
+ // plate is 16:9 at every width, so one draw serves every viewport.
525
603
  function heroPortraitEntries(data) {
526
604
  var list = Array.isArray(data) ? data : [];
527
605
  var entries = [];
@@ -539,10 +617,6 @@
539
617
  var sizes = typeof entry.sizes === 'string' ? entry.sizes.trim() : '';
540
618
  if (srcset) record.srcset = srcset;
541
619
  if (sizes) record.sizes = sizes;
542
- // Optional 16:9 file for the phone plate. Absent, the entry stays a
543
- // portrait and the narrow draw skips it.
544
- var landscape = typeof entry.landscape === 'string' ? entry.landscape.trim() : '';
545
- if (landscape) record.landscape = landscape;
546
620
  entries.push(record);
547
621
  }
548
622
  return entries;
@@ -602,16 +676,6 @@
602
676
  });
603
677
  }
604
678
 
605
- function heroLandscapeEntries(entries) {
606
- var list = [];
607
- var i;
608
- var rows = entries || [];
609
- for (i = 0; i < rows.length; i++) {
610
- if (rows[i] && rows[i].landscape) list.push(rows[i]);
611
- }
612
- return list;
613
- }
614
-
615
679
  function showHeroPortrait(img, entries) {
616
680
  var chosen = pickHeroPortrait(entries, Math.random);
617
681
  if (!chosen) return Promise.resolve();
@@ -630,141 +694,9 @@
630
694
  return next();
631
695
  }
632
696
 
633
- function configuredLandscape(img) {
634
- var url = img && img.getAttribute('data-hero-landscape');
635
- url = url ? String(url).trim() : '';
636
- return url;
637
- }
638
-
639
- function heroPlate(img) {
640
- var node = img && img.parentElement;
641
- while (node) {
642
- if (node.classList && node.classList.contains('home-hero-plate')) return node;
643
- node = node.parentElement;
644
- }
645
- return null;
646
- }
647
-
648
- function setNarrowSource(img, url) {
649
- var picture = img.parentElement;
650
- if (!picture || String(picture.tagName).toUpperCase() !== 'PICTURE') return;
651
- var source = picture.querySelector('source');
652
- if (!url) {
653
- if (source) source.removeAttribute('srcset');
654
- return;
655
- }
656
- if (!source) {
657
- source = document.createElement('source');
658
- source.setAttribute('media', '(max-width: 767px)');
659
- picture.insertBefore(source, img);
660
- }
661
- source.setAttribute('srcset', url);
662
- }
663
-
664
- function applyHeroLandscapeFile(img, chosen) {
665
- var nextSrc = resolveAssetUrl(chosen && (chosen.landscape || chosen.src));
666
- var nextAlt = (chosen && chosen.alt) || '';
667
- if (!nextSrc) return Promise.resolve(false);
668
- if (img.getAttribute('src') === nextSrc) {
669
- img.alt = nextAlt;
670
- img.removeAttribute('srcset');
671
- img.removeAttribute('sizes');
672
- setNarrowSource(img, nextSrc);
673
- img.classList.add('is-shown');
674
- return Promise.resolve(true);
675
- }
676
- return new Promise(function (resolve) {
677
- var probe = new Image();
678
- probe.fetchPriority = 'high';
679
- probe.onload = function () {
680
- img.alt = nextAlt;
681
- img.removeAttribute('srcset');
682
- img.removeAttribute('sizes');
683
- setNarrowSource(img, nextSrc);
684
- img.fetchPriority = 'high';
685
- img.src = nextSrc;
686
- img.classList.add('is-shown');
687
- resolve(true);
688
- };
689
- probe.onerror = function () {
690
- resolve(false);
691
- };
692
- probe.src = nextSrc;
693
- });
694
- }
695
-
696
- function showHeroLandscape(img, entries) {
697
- var chosen = pickHeroPortrait(entries, Math.random);
698
- if (!chosen) return Promise.resolve(false);
699
- var start = entries.indexOf(chosen);
700
- var ordered = entries.slice(start).concat(entries.slice(0, start));
701
- var index = 0;
702
- function next() {
703
- if (index >= ordered.length) return Promise.resolve(false);
704
- var entry = ordered[index];
705
- index += 1;
706
- return applyHeroLandscapeFile(img, entry).then(function (ok) {
707
- if (ok) return true;
708
- return next();
709
- });
710
- }
711
- return next();
712
- }
713
-
714
- function showConfiguredLandscape(img) {
715
- var url = configuredLandscape(img);
716
- if (!url) return Promise.resolve(false);
717
- var plate = heroPlate(img);
718
- if (plate) plate.classList.add('home-hero-plate--landscape');
719
- return applyHeroLandscapeFile(img, { landscape: url, alt: '' });
720
- }
721
-
722
- function paintNarrowHero(img) {
723
- img.classList.remove('is-shown');
724
- return loadJson('pages/home/portraits.json').then(function (data) {
725
- var entries = heroPortraitEntries(data);
726
- var landscapes = heroLandscapeEntries(entries);
727
- if (landscapes.length) {
728
- var plate = heroPlate(img);
729
- if (plate) plate.classList.add('home-hero-plate--landscape');
730
- return showHeroLandscape(img, landscapes).then(function (ok) {
731
- if (ok) return;
732
- return showConfiguredLandscape(img);
733
- });
734
- }
735
- if (configuredLandscape(img)) return showConfiguredLandscape(img);
736
- return showHeroPortrait(img, entries);
737
- }).catch(function (error) {
738
- console.error(error);
739
- if (configuredLandscape(img)) return showConfiguredLandscape(img);
740
- });
741
- }
742
-
743
- function watchHeroFrame(img) {
744
- if (!window.matchMedia) return;
745
- var mq = window.matchMedia('(max-width: 767px)');
746
- if (!mq.addEventListener) return;
747
- mq.addEventListener('change', function () {
748
- if (mq.matches) {
749
- paintNarrowHero(img);
750
- return;
751
- }
752
- img.classList.remove('is-shown');
753
- loadJson('pages/home/portraits.json').then(function (data) {
754
- return showHeroPortrait(img, heroPortraitEntries(data));
755
- }).catch(function (error) {
756
- console.error(error);
757
- });
758
- });
759
- }
760
-
761
697
  function hydrateHeroPortrait() {
762
698
  var img = document.querySelector('[data-content-hero-portrait]');
763
699
  if (!img) return Promise.resolve();
764
- watchHeroFrame(img);
765
- if (window.matchMedia && window.matchMedia('(max-width: 767px)').matches) {
766
- return paintNarrowHero(img);
767
- }
768
700
  return loadJson('pages/home/portraits.json').then(function (data) {
769
701
  return showHeroPortrait(img, heroPortraitEntries(data));
770
702
  }).catch(function (error) {
@@ -795,6 +727,8 @@
795
727
  });
796
728
  chain.then(function () {
797
729
  hydrateHomeSections();
730
+ hydrateHeaderTexts(document);
731
+ hydratePanelNotes(document);
798
732
  hydrateBandArt();
799
733
  hydrateContacts();
800
734
  });
@@ -26,6 +26,9 @@
26
26
  adoptCanonicalDetailUrl: adoptCanonicalDetailUrl,
27
27
  productUrl: productUrl,
28
28
  projectUrl: projectUrl,
29
+ loadPageHeaders: loadPageHeaders,
30
+ headerText: headerText,
31
+ hydrateHeaderTexts: hydrateHeaderTexts,
29
32
  hydratePageHeader: hydratePageHeader,
30
33
  hydratePageFragments: hydratePageFragments,
31
34
  hydrateContacts: hydrateContacts,
@@ -4,15 +4,21 @@
4
4
  so the shell lives here rather than being retyped five times.
5
5
 
6
6
  Parameters:
7
- title — section heading (required)
7
+ title — section heading; an empty string drops the heading and its rule
8
+ text_key — page headers key.field the runtime fills, e.g. "cv.summary_title"
9
+ label — default heading, the section's accessible name when title is empty
8
10
  content — pre-captured section body (required)
9
11
  id — anchor id, e.g. "products" for the product back-link
10
12
  body_class — extra classes on the entry column
11
13
  {%- endcomment -%}
12
- <section class="cv-register"{% if include.id %} id="{{ include.id }}"{% endif %}>
14
+ <section class="cv-register"{% if include.id %} id="{{ include.id }}"{% endif %} data-content-header-name{% if include.title == "" %} aria-label="{{ include.label }}"{% endif %}>
13
15
  <div class="cv-register-rail">
14
- <h2 class="section-title cv-register-title">{{ include.title }}</h2>
15
- <span class="cv-register-rule" aria-hidden="true"></span>
16
+ {%- if include.title != "" -%}
17
+ <div class="cv-register-head" data-content-header-wrap>
18
+ <h2 class="section-title cv-register-title"{% if include.text_key %} data-content-header-text="{{ include.text_key }}"{% endif %}>{{ include.title }}</h2>
19
+ <span class="cv-register-rule" aria-hidden="true"></span>
20
+ </div>
21
+ {%- endif -%}
16
22
  </div>
17
23
  <div class="cv-register-body{% if include.body_class %} {{ include.body_class }}{% endif %}">{{ include.content }}</div>
18
24
  </section>
@@ -21,21 +21,36 @@
21
21
  {%- for panel in band.pages -%}
22
22
  {%- assign _panel_page = site.pages | where: "key", panel.key | first -%}
23
23
  {%- assign _panel_title = panel.title | default: _panel_page.title | default: panel.key -%}
24
+ {%- comment -%} Headers win when the key is set; an empty value hides the text (PLT-AC-50). {%- endcomment -%}
25
+ {%- assign _panel_head = site.data.page_headers[panel.key] -%}
26
+ {%- assign _panel_default = _panel_title -%}
27
+ {%- if _panel_head.title != nil -%}{%- assign _panel_title = _panel_head.title | strip -%}{%- endif -%}
28
+ {%- assign _panel_cta = "Open" -%}
29
+ {%- if _panel_head.cta != nil -%}{%- assign _panel_cta = _panel_head.cta | strip -%}{%- endif -%}
30
+ {%- assign _panel_note = _panel_head.note | strip -%}
24
31
  {%- assign _panel_path = panel.path | default: _panel_page.url -%}
25
32
  {%- assign _art_key = panel.key -%}
26
33
  {%- if panel.key == "resources" -%}{%- assign _art_key = "sources" -%}{%- endif -%}
27
- <article class="home-index-panel"{% if panel.template == "bibliography" %} id="home-bib-panel"{% endif %}>
28
- <span class="shard-accent home-index-shard" aria-hidden="true"></span>
34
+ <article class="home-index-panel"{% if panel.template == "bibliography" %} id="home-bib-panel"{% endif %} data-content-header-name{% if _panel_title == "" %} aria-label="{{ _panel_default | escape }}"{% endif %}>
35
+ {%- if _panel_title != "" or _panel_cta != "" -%}
36
+ <div class="home-index-panel-head" data-content-header-group>
37
+ {%- if _panel_title != "" -%}
38
+ <h3 class="home-index-title" data-content-header-text="{{ panel.key }}.title">{{ _panel_title }}</h3>
39
+ {%- endif -%}
40
+ {%- if _panel_cta != "" -%}
41
+ <a class="ledger-cta" href="{{ _panel_path }}" data-content-header-wrap>
42
+ <span data-content-header-text="{{ panel.key }}.cta">{{ _panel_cta }}</span>
43
+ <span class="material-symbols-outlined ledger-cta-arrow" aria-hidden="true">chevron_right</span>
44
+ </a>
45
+ {%- endif -%}
46
+ </div>
47
+ {%- endif -%}
48
+ <p class="home-index-note font-body" data-content-panel-note="{{ panel.key }}"{% if _panel_note == "" %} hidden{% endif %}>{{ _panel_note }}</p>
29
49
  {%- if panel.template == "bibliography" -%}
30
50
  <figure class="home-index-plate" id="home-bib-plate" hidden></figure>
31
51
  {%- else -%}
32
52
  <figure class="home-index-plate" data-content-band-art="{{ _art_key }}" hidden></figure>
33
53
  {%- endif -%}
34
- <h3 class="home-index-title">{{ _panel_title }}</h3>
35
- <a class="ledger-cta" href="{{ _panel_path }}">
36
- <span>Open</span>
37
- <span class="material-symbols-outlined ledger-cta-arrow" aria-hidden="true">chevron_right</span>
38
- </a>
39
54
  </article>
40
55
  {%- endfor -%}
41
56
  </div>
@@ -2,6 +2,7 @@
2
2
  One homepage listing band. Expects `band` from site.data.pandorga_home_bands.
3
3
  Kicker and note slots are always emitted, prefilled from headers.yml when it
4
4
  was read at build time and filled by the runtime otherwise (PLT-AC-42).
5
+ Link label comes from headers cta; an empty cta hides the link.
5
6
  Listing slots are filled by the home layout script from writing_index.json.
6
7
  {%- endcomment -%}
7
8
  {%- assign _accent = band.accent -%}
@@ -9,7 +10,9 @@
9
10
  {%- assign _featured = band.home.featured -%}
10
11
  {%- assign _lang = band.language | default: site.pandorga.identity.locale | default: "en" -%}
11
12
  {%- assign _path = band.path -%}
13
+ {%- assign _section = site.data.page_headers[band.section_key] -%}
12
14
  {%- assign _cta = band.home.cta | default: "View all" -%}
15
+ {%- if _section.cta != nil -%}{%- assign _cta = _section.cta | strip -%}{%- endif -%}
13
16
  {%- assign _slot_id = "home-band-" | append: band.key -%}
14
17
  <section
15
18
  id="home-{{ band.key }}"
@@ -25,7 +28,6 @@
25
28
  data-home-language="{{ _lang }}"
26
29
  >
27
30
  <div class="home-band-inner">
28
- {%- assign _section = site.data.page_headers[band.section_key] -%}
29
31
  {%- assign _title = band.title -%}
30
32
  {%- if _section.title != blank -%}{%- assign _title = _section.title -%}{%- endif -%}
31
33
  <div class="ledger-section-banner" data-content-home-section="{{ band.section_key }}">
@@ -34,12 +36,14 @@
34
36
  <h2 class="section-title" data-content-section-title>{{ _title }}</h2>
35
37
  <p class="home-section-note font-body" data-content-section-note>{{ _section.note }}</p>
36
38
  </div>
37
- <div class="home-section-aside">
39
+ {%- if _cta != "" -%}
40
+ <div class="home-section-aside" data-content-header-wrap>
38
41
  <a class="ledger-cta" href="{{ _path }}">
39
- <span>{{ _cta }}</span>
42
+ <span data-content-header-text="{{ band.section_key }}.cta">{{ _cta }}</span>
40
43
  <span class="material-symbols-outlined ledger-cta-arrow" aria-hidden="true">chevron_right</span>
41
44
  </a>
42
45
  </div>
46
+ {%- endif -%}
43
47
  </div>
44
48
  {%- if _featured -%}
45
49
  <div id="{{ _slot_id }}-featured" data-home-featured-slot data-tag-base="{{ _path }}"></div>
@@ -2,10 +2,15 @@
2
2
 
3
3
  <!-- Timeline. The applet owns its own left rail of controls, so it runs as
4
4
  a full-width band rather than inside the register's title rail. -->
5
- <section class="cv-band" id="timeline-section">
6
- <div class="cv-band-head">
7
- <h2 class="section-title">Timeline</h2>
5
+ {%- assign _timeline_head = site.data.page_headers[page.key] -%}
6
+ {%- assign _timeline_title = "Timeline" -%}
7
+ {%- if _timeline_head.timeline_title != nil -%}{%- assign _timeline_title = _timeline_head.timeline_title | strip -%}{%- endif -%}
8
+ <section class="cv-band" id="timeline-section" data-content-header-name{% if _timeline_title == "" %} aria-label="Timeline"{% endif %}>
9
+ {%- if _timeline_title != "" -%}
10
+ <div class="cv-band-head" data-content-header-wrap>
11
+ <h2 class="section-title" data-content-header-text="{{ page.key }}.timeline_title">{{ _timeline_title }}</h2>
8
12
  </div>
13
+ {%- endif -%}
9
14
 
10
15
  <div class="cv-timeline-grid">
11
16
  <div class="cv-timeline-rail">
@@ -87,6 +87,9 @@
87
87
  --color-surface-variant: {{ surface_variant_value }};
88
88
  --color-surface-dim: {{ ts.surface_dim | default: "#d9d9d9" }};
89
89
  --color-panel-contrast: {{ tn.panel_contrast | default: bg_secondary }};
90
+ /* The page ground every card sits on (PLT-AC-49). A palette may name its
91
+ own `canvas`; otherwise it is the panel-contrast step. */
92
+ --color-canvas: {{ tn.canvas | default: tn.panel_contrast | default: bg_secondary }};
90
93
  --color-surface-card: {{ tn.surface_card | default: surface_value }};
91
94
  --color-surface-card-contrast: {{ tn.surface_card_contrast | default: tn.surface_card | default: surface_value }};
92
95
  --color-surface-high: {{ tn.surface_high | default: tn.surface_container_high | default: "#e8e8e8" }};
@@ -344,8 +344,6 @@ layout: shell
344
344
  ]);
345
345
 
346
346
  var html = '<article class="writing-lead' + (thumbnail ? ' writing-lead--illustrated' : '') + '">';
347
- html += '<span class="shard-accent writing-lead-shard" aria-hidden="true"></span>';
348
- html += '<span class="shard-accent shard-accent-beta writing-lead-shard writing-lead-shard--beta" aria-hidden="true"></span>';
349
347
  html += '<div class="writing-lead-inner">';
350
348
  html += '<div class="writing-lead-body">';
351
349
  html += '<div class="writing-lead-rail">';
@@ -4,7 +4,6 @@ layout: shell
4
4
 
5
5
  <main class="relative max-w-7xl mx-auto px-4 sm:px-8 md:px-16 pt-10 pb-16">
6
6
  <div class="bib-masthead">
7
- <span class="shard-accent bib-masthead-shard" aria-hidden="true"></span>
8
7
  {% include page/page-header.html key=page.key listing_filter=true spacer_class="mb-8"
9
8
  header_class="bib-header mb-10 md:mb-12"
10
9
  filter_label="Filter"
@@ -19,7 +18,6 @@ layout: shell
19
18
  </section>
20
19
  </div>
21
20
  <div id="bib-pagination" class="bib-pagination"></div>
22
- <span class="shard-accent shard-accent-beta bib-foot-shard" aria-hidden="true"></span>
23
21
  </section>
24
22
  </main>
25
23
 
data/_layouts/blog.html CHANGED
@@ -16,12 +16,7 @@ layout: shell
16
16
  default_path="/pages/blog/"
17
17
  default_detail="/posts/:slug/" %}>
18
18
 
19
- {%- comment -%}
20
- Masthead. The shard lives here rather than inside the shared page header
21
- include, which every listing uses and which no single page may restyle.
22
- {%- endcomment -%}
23
19
  <div class="rascunhos-masthead" data-rascunhos-masthead>
24
- <span class="shard-accent rascunhos-shard" aria-hidden="true"></span>
25
20
  {% include page/page-header.html key=page.key
26
21
  header_class="rascunhos-header" %}
27
22
  </div>