pandorga 1.14.1 → 2.0.1

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 +32 -0
  3. data/DESIGN.md +15 -30
  4. data/_includes/content-runtime/43-blocks.html +5 -4
  5. data/_includes/content-runtime/60-fragments.html +90 -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 +44 -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 +3 -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 +65 -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 +18 -4
  26. data/assets/css/ledger/home.css +269 -413
  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-home-article-cards.rb +56 -0
  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 +4 -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 +6 -2
@@ -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>
data/_layouts/cv.html CHANGED
@@ -1,12 +1,24 @@
1
1
  ---
2
2
  layout: shell
3
3
  ---
4
+ {%- comment -%} Section titles from the page headers: absent keeps the default, empty hides it (PLT-AC-50). {%- endcomment -%}
5
+ {%- assign _cv_head = site.data.page_headers[page.key] -%}
6
+ {%- assign _summary_title = "Summary" -%}
7
+ {%- if _cv_head.summary_title != nil -%}{%- assign _summary_title = _cv_head.summary_title | strip -%}{%- endif -%}
8
+ {%- assign _experience_title = "Experience" -%}
9
+ {%- if _cv_head.experience_title != nil -%}{%- assign _experience_title = _cv_head.experience_title | strip -%}{%- endif -%}
10
+ {%- assign _products_title = "Products" -%}
11
+ {%- if _cv_head.products_title != nil -%}{%- assign _products_title = _cv_head.products_title | strip -%}{%- endif -%}
12
+ {%- assign _education_title = "Education" -%}
13
+ {%- if _cv_head.education_title != nil -%}{%- assign _education_title = _cv_head.education_title | strip -%}{%- endif -%}
14
+ {%- assign _languages_title = "Languages" -%}
15
+ {%- if _cv_head.languages_title != nil -%}{%- assign _languages_title = _cv_head.languages_title | strip -%}{%- endif -%}
16
+ {%- assign _contact_label = "Contact" -%}
17
+ {%- if _cv_head.contact_label != nil -%}{%- assign _contact_label = _cv_head.contact_label | strip -%}{%- endif -%}
4
18
 
5
19
  <main class="cv-page{% if page.accent and page.accent != 'blue' %} page-accent--{{ page.accent }}{% endif %} max-w-7xl mx-auto px-4 sm:px-8 md:px-16 pt-10 pb-16 min-h-screen">
6
20
  <div id="cv-page-header">
7
21
  <div class="cv-dossier">
8
- <span class="shard-accent cv-dossier-shard" aria-hidden="true"></span>
9
- <span class="shard-accent shard-accent-beta cv-dossier-shard cv-dossier-shard--beta" aria-hidden="true"></span>
10
22
  <div class="cv-dossier-inner">
11
23
  {% include page/page-header.html
12
24
  key=page.key
@@ -15,7 +27,9 @@ layout: shell
15
27
  standfirst_class="cv-dossier-standfirst font-body hidden" %}
16
28
  <div class="cv-dossier-aside">
17
29
  <div class="cv-contact-cta" id="contact">
18
- <span class="cv-contact-cta-label">Contact</span>
30
+ {%- if _contact_label != "" -%}
31
+ <span class="cv-contact-cta-label" data-content-header-text="{{ page.key }}.contact_label">{{ _contact_label }}</span>
32
+ {%- endif -%}
19
33
  {% include page/professional-connections.html wrapper_class="cv-contacts" %}
20
34
  </div>
21
35
  </div>
@@ -32,7 +46,8 @@ layout: shell
32
46
  {% capture cv_summary %}
33
47
  <div class="cv-summary font-body" data-content-fragment="cv/summary" data-content-quiet></div>
34
48
  {% endcapture %}
35
- {% include cv/cv-box.html id="summary" title="Summary" content=cv_summary %}
49
+ {%- capture _summary_title_key -%}{{ page.key }}.summary_title{%- endcapture -%}
50
+ {% include cv/cv-box.html id="summary" title=_summary_title text_key=_summary_title_key label="Summary" content=cv_summary %}
36
51
 
37
52
  {% capture cv_experience %}
38
53
  <div id="cv-experience-list" class="cv-entry-stack" data-content-reserve aria-busy="true">
@@ -41,7 +56,8 @@ layout: shell
41
56
  <span class="cv-reserve-card" aria-hidden="true"></span>
42
57
  </div>
43
58
  {% endcapture %}
44
- {% include cv/cv-box.html id="experience" title="Experience" content=cv_experience %}
59
+ {%- capture _experience_title_key -%}{{ page.key }}.experience_title{%- endcapture -%}
60
+ {% include cv/cv-box.html id="experience" title=_experience_title text_key=_experience_title_key label="Experience" content=cv_experience %}
45
61
 
46
62
  {% capture cv_products %}
47
63
  <div id="cv-products-grid" class="cv-product-grid" data-content-reserve aria-busy="true">
@@ -50,7 +66,8 @@ layout: shell
50
66
  <span class="cv-reserve-card" aria-hidden="true"></span>
51
67
  </div>
52
68
  {% endcapture %}
53
- {% include cv/cv-box.html id="products" title="Products" content=cv_products %}
69
+ {%- capture _products_title_key -%}{{ page.key }}.products_title{%- endcapture -%}
70
+ {% include cv/cv-box.html id="products" title=_products_title text_key=_products_title_key label="Products" content=cv_products %}
54
71
 
55
72
  {% capture cv_education %}
56
73
  <div id="cv-education-list" class="cv-entry-stack" data-content-reserve aria-busy="true">
@@ -58,7 +75,8 @@ layout: shell
58
75
  <span class="cv-reserve-card" aria-hidden="true"></span>
59
76
  </div>
60
77
  {% endcapture %}
61
- {% include cv/cv-box.html id="education" title="Education" content=cv_education %}
78
+ {%- capture _education_title_key -%}{{ page.key }}.education_title{%- endcapture -%}
79
+ {% include cv/cv-box.html id="education" title=_education_title text_key=_education_title_key label="Education" content=cv_education %}
62
80
 
63
81
  {% capture cv_languages %}
64
82
  <div id="cv-languages-list" class="cv-language-list" data-content-reserve aria-busy="true">
@@ -66,7 +84,8 @@ layout: shell
66
84
  <span class="cv-reserve-card" aria-hidden="true"></span>
67
85
  </div>
68
86
  {% endcapture %}
69
- {% include cv/cv-box.html id="languages" title="Languages" content=cv_languages %}
87
+ {%- capture _languages_title_key -%}{{ page.key }}.languages_title{%- endcapture -%}
88
+ {% include cv/cv-box.html id="languages" title=_languages_title text_key=_languages_title_key label="Languages" content=cv_languages %}
70
89
 
71
90
  {% include page/timeline-section.html %}
72
91
  </div>
data/_layouts/home.html CHANGED
@@ -17,7 +17,6 @@ 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 -%}
21
20
  {%- assign _home_bands = site.data.pandorga_home_bands -%}
22
21
 
23
22
  <main class="home-ledger">
@@ -51,18 +50,10 @@ layout: shell
51
50
  </div>
52
51
  </div>
53
52
  <div class="home-hero-portrait">
54
- <span class="shard-accent home-hero-shard" aria-hidden="true"></span>
55
53
  <div class="home-hero-plate-wrap">
56
54
  <span class="ledger-frame-crosshairs" aria-hidden="true"></span>
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 -%}
55
+ <figure class="home-hero-plate">
56
+ <img alt="" width="1600" height="900" fetchpriority="high" decoding="sync" data-content-hero-portrait />
66
57
  <span class="ledger-frame-corners" aria-hidden="true"></span>
67
58
  </figure>
68
59
  </div>
@@ -173,8 +164,6 @@ layout: shell
173
164
  var chips = runtime.renderTagChips(entry.tags, { hrefFor: tagHref });
174
165
  var cta = runtime.renderCtaLink(href, 'Read', { className: 'home-featured-cta' });
175
166
  wrap.innerHTML = [
176
- '<span class="shard-accent home-featured-shard" aria-hidden="true"></span>',
177
- '<span class="shard-accent shard-accent-beta home-featured-shard home-featured-shard--beta" aria-hidden="true"></span>',
178
167
  '<div class="home-featured-lead">',
179
168
  '<div class="home-featured-rail">',
180
169
  '<span class="ledger-chip home-featured-badge">Featured</span>',
@@ -494,6 +483,7 @@ layout: shell
494
483
  locale: locale,
495
484
  ctaLabel: 'Read',
496
485
  tagHref: articleTagHref,
486
+ tagsInFoot: true,
497
487
  metaParts: [runtime.formatDate(entry.date, locale)]
498
488
  });
499
489
  }, 'Articles could not be loaded right now.');
@@ -96,6 +96,12 @@ layout: shell
96
96
  runtime.slugFromDetailPattern(window.location.pathname, detailPattern);
97
97
  var selectedTag = (params.get('tag') || '').trim();
98
98
 
99
+ /* Section banner texts come from the page headers; a failed fetch keeps
100
+ the defaults (PLT-AC-50). */
101
+ var PAGE_KEY = {{ page.key | default: "projects" | jsonify }};
102
+ var headers = {};
103
+ var headersReady = runtime.loadPageHeaders().then(function (h) { headers = h || {}; }).catch(function () {});
104
+
99
105
  function entryUrl(slug) {
100
106
  return runtime.detailUrl(detailPattern, slug);
101
107
  }
@@ -235,7 +241,6 @@ layout: shell
235
241
  var line = ledgerLine(fm, 'ledger-system-icon');
236
242
 
237
243
  var html = '<article class="projects-lead">';
238
- html += '<span class="shard-accent projects-lead-shard" aria-hidden="true"></span>';
239
244
  /* The plate links to the same place as the title beside it. It is out of
240
245
  the tab order so keyboard users are not made to pass the same
241
246
  destination twice, but it keeps its accessible name — the poster's alt
@@ -312,8 +317,11 @@ layout: shell
312
317
  reposRoot.innerHTML = '';
313
318
  return;
314
319
  }
315
- var html = '<section class="projects-repos">';
316
- html += runtime.renderSectionBanner({ kicker: 'Open source', title: 'Repositories' });
320
+ var reposKicker = runtime.headerText(headers, PAGE_KEY, 'repositories_kicker', 'Open source');
321
+ var reposTitle = runtime.headerText(headers, PAGE_KEY, 'repositories_title', 'Repositories');
322
+ /* Without its banner the list keeps its name for assistive tech. */
323
+ var html = reposTitle ? '<section class="projects-repos">' : '<section class="projects-repos" aria-label="Repositories">';
324
+ html += runtime.renderSectionBanner({ kicker: reposKicker, title: reposTitle });
317
325
  html += '<div class="projects-repo-list">';
318
326
  rows.forEach(function (item) {
319
327
  var fm = item.front_matter || {};
@@ -393,10 +401,9 @@ layout: shell
393
401
  var featured = runtime.pickFeatured(ordered, FEATURED_PICK) || visible[0];
394
402
  renderLead(featured);
395
403
  rest = visible.filter(function (item) { return item.slug !== featured.slug; });
396
- bannerRoot.innerHTML = runtime.renderSectionBanner({
397
- kicker: 'Index',
398
- title: 'Other projects'
399
- });
404
+ var othersKicker = runtime.headerText(headers, PAGE_KEY, 'others_kicker', 'Index');
405
+ var othersTitle = runtime.headerText(headers, PAGE_KEY, 'others_title', 'Other projects');
406
+ bannerRoot.innerHTML = runtime.renderSectionBanner({ kicker: othersKicker, title: othersTitle });
400
407
  }
401
408
 
402
409
  rest.forEach(function (item) { grid.appendChild(renderCard(item)); });
@@ -416,7 +423,6 @@ layout: shell
416
423
  var line = ledgerLine(fm, 'projects-hero-icon');
417
424
 
418
425
  var html = '<header class="projects-hero">';
419
- html += '<span class="shard-accent projects-hero-shard" aria-hidden="true"></span>';
420
426
  html += '<div class="projects-hero-body">';
421
427
  if (line) html += line;
422
428
  var chips = runtime.renderTagChips(fm.tags, { hrefFor: tagHref });
@@ -522,7 +528,9 @@ layout: shell
522
528
 
523
529
  function loadListing() {
524
530
  showListing();
525
- runtime.loadWritingIndex(collection).then(renderListing).catch(function (error) {
531
+ Promise.all([runtime.loadWritingIndex(collection), headersReady]).then(function (results) {
532
+ renderListing(results[0]);
533
+ }).catch(function (error) {
526
534
  console.error(error);
527
535
  state.textContent = 'Projects could not be loaded right now.';
528
536
  state.classList.remove('hidden');
@@ -6,15 +6,7 @@ layout: shell
6
6
  {% include page/listing-runtime-attrs.html
7
7
  default_collection="resources"
8
8
  default_path="/pages/sources/" %}>
9
- {%- comment -%}
10
- The shards live here rather than inside the shared page header include,
11
- which every listing uses and which no single page may restyle — the same
12
- arrangement /pages/blog/ uses for its masthead. They bracket the grid
13
- rather than sitting on it: a page of many small cards has no one panel to
14
- accent, so the accent goes on the space above and below the run.
15
- {%- endcomment -%}
16
9
  <div class="sources-masthead">
17
- <span class="shard-accent sources-masthead-shard" aria-hidden="true"></span>
18
10
  {% include page/page-header.html key=page.key listing_filter=true spacer_class="mb-8"
19
11
  header_class="sources-header mb-10 md:mb-12"
20
12
  filter_label="Filter"
@@ -29,7 +21,6 @@ layout: shell
29
21
  </section>
30
22
  </div>
31
23
  <div id="resource-pagination" class="source-pagination"></div>
32
- <span class="shard-accent shard-accent-beta sources-foot-shard" aria-hidden="true"></span>
33
24
  </section>
34
25
  </main>
35
26
 
data/_layouts/shell.html CHANGED
@@ -66,10 +66,6 @@
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 -%}
73
69
  {% include theme/critical-preload.html %}
74
70
  <link rel="stylesheet" href="/assets/css/base.css" />
75
71
  <link rel="stylesheet" href="/assets/css/global-link.css" />
@@ -220,7 +216,7 @@
220
216
  {% assign _all_pages = site.pages %}
221
217
 
222
218
  <!-- TopNavBar. The bar is the banner landmark. The section links are the only nav inside it. -->
223
- <header class="sticky top-0 z-50 bg-surface">
219
+ <header class="site-topbar sticky top-0 z-50">
224
220
  <div class="flex justify-between items-center w-full px-6 sm:px-8 md:px-10 pt-5 pb-3.5 leading-none max-w-7xl mx-auto">
225
221
  <div class="flex items-center gap-3 sm:gap-4 min-w-0">
226
222
  <a href="/" aria-label="{% if _home_label != blank %}{{ _home_label }} – Home{% else %}Home{% endif %}" class="shrink-0 inline-flex items-center gap-3 site-wordmark-link">
@@ -285,8 +281,6 @@
285
281
  </div>
286
282
  </div>
287
283
  </div>
288
- <!-- Tonal separator — "No-Line" design rule: background-color shift instead of a CSS border -->
289
- <div class="bg-surface-container-low h-[1px]"></div>
290
284
  </header>
291
285
  <script>
292
286
  (function () {
@@ -17,8 +17,8 @@
17
17
  /* Detail pages: title in left third; body starts aligned with the title */
18
18
  .text-page-main {
19
19
  align-self: start;
20
- background-color: var(--color-background);
21
- border: 1.5px solid var(--border-ink);
20
+ background-color: var(--color-surface-card);
21
+ border: 1px solid var(--border-ink);
22
22
  border-radius: var(--radius-card);
23
23
  padding: var(--space-md);
24
24
  width: 100%;
@@ -106,6 +106,14 @@
106
106
  overflow apply. `[jax]` stays ahead of MathJax's own container rule.
107
107
  Desktop stays inline. Do not put this overflow on html or body. */
108
108
  @media (max-width: 767px) {
109
+
110
+ /* Reading view without a box on phones */
111
+ .text-page-main {
112
+ background-color: transparent;
113
+ border: 0;
114
+ padding: 0;
115
+ }
116
+
109
117
  .article-content mjx-container:not([display="true"]),
110
118
  .article-content mjx-container[jax]:not([display="true"]) {
111
119
  display: inline-block;
data/assets/css/base.css CHANGED
@@ -170,12 +170,15 @@ html {
170
170
  --type-page-lg: 2.9015rem;
171
171
  --type-section: 1.9015rem;
172
172
  --type-section-lg: 2.1515rem;
173
+ /* Phone steps for home band titles and card titles (PLT-AC-49). */
174
+ --type-section-sm: 1.375rem;
173
175
 
174
176
  /* Content titles — Newsreader Regular 400 */
175
177
  --type-title: 2.20165rem;
176
178
  --type-title-lg: 2.64165rem;
177
179
  --type-card: 1.4015rem;
178
180
  --type-card-lg: 1.5515rem;
181
+ --type-card-sm: 1.1875rem;
179
182
 
180
183
  /* Reading + UI — Newsreader Light 300.
181
184
  Default reading size is 1.2rem below 1024px and 1.25rem from there.
@@ -231,7 +234,7 @@ html body {
231
234
  font-family: var(--font-body);
232
235
  font-size: var(--font-size-body);
233
236
  font-weight: var(--weight-body);
234
- background-color: var(--color-background);
237
+ background-color: var(--color-canvas);
235
238
  /* Quiet square grid on both palettes; only --color-background changes. */
236
239
  background-image:
237
240
  linear-gradient(color-mix(in srgb, var(--color-outline) 4%, transparent) 1px, transparent 1px),
@@ -437,6 +440,7 @@ html[data-theme="dark"] {
437
440
  .source-facet,
438
441
  .bib-facet,
439
442
  :is(.ledger-cta--emphasis, .ledger-cta--emphasis-alt),
443
+ .ledger-cta:where(:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)),
440
444
  :is(.detail-action, .detail-type-size-trigger, .article-related-section-pill, .listing-filter-trigger) {
441
445
  --mark-ink: var(--color-primary);
442
446
  --mark-1: var(--mark-a);
@@ -537,6 +541,19 @@ html[data-theme="dark"] {
537
541
  font-optical-sizing: auto;
538
542
  }
539
543
 
544
+ .card-title,
545
+ .ledger-card-title {
546
+ font-weight: var(--weight-emphasis);
547
+ }
548
+
549
+ @media (max-width: 767px) {
550
+ .card-title,
551
+ .ledger-card-title {
552
+ font-size: var(--type-card-sm);
553
+ line-height: 1.25;
554
+ }
555
+ }
556
+
540
557
  /* Label-face meta on CV identity lines, etc. */
541
558
  .card-meta {
542
559
  font-family: var(--font-label);
@@ -571,8 +588,7 @@ html[data-theme="dark"] {
571
588
  letter-spacing: 0.04em;
572
589
  }
573
590
 
574
- /* Dates, kickers, section notes — body face at meta size. */
575
- .ledger-meta,
591
+ /* Kickers, section notes — body face at meta size. */
576
592
  .ledger-kicker,
577
593
  .ledger-section-note {
578
594
  font-family: var(--font-body);
@@ -1038,14 +1054,19 @@ a.ledger-chip:hover {
1038
1054
 
1039
1055
  /* Dates live here. The chips and language pills inside the strip set their
1040
1056
  own uppercase, so the strip itself leaves case alone — running small caps
1041
- over a date is heavier than the date deserves. */
1057
+ over a date is heavier than the date deserves. Meta uses mono face. */
1042
1058
  .ledger-meta {
1043
1059
  display: flex;
1044
1060
  flex-wrap: wrap;
1045
1061
  align-items: center;
1046
1062
  gap: var(--space-sm);
1047
1063
  color: var(--color-on-surface-variant);
1048
- letter-spacing: 0.04em;
1064
+ font-family: var(--font-mono);
1065
+ font-size: var(--type-micro);
1066
+ font-weight: var(--weight-body);
1067
+ line-height: 1.45;
1068
+ font-variant-numeric: tabular-nums;
1069
+ letter-spacing: 0;
1049
1070
  }
1050
1071
 
1051
1072
  /* A separator is ink, not a border. --color-outline-variant is tuned for
@@ -1074,17 +1095,6 @@ a.ledger-chip:hover {
1074
1095
  line-height: 1;
1075
1096
  }
1076
1097
 
1077
- /* Text CTAs carry a gentle accent underline so the clickable cue is not the
1078
- same gesture as the marker wash on titles. Emphasis buttons carry a
1079
- strong mark instead. */
1080
- .ledger-cta:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
1081
- text-decoration: underline;
1082
- text-decoration-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
1083
- text-decoration-thickness: 1.5px;
1084
- text-underline-offset: 0.48em;
1085
- text-underline-position: under;
1086
- }
1087
-
1088
1098
  .ledger-cta-icon,
1089
1099
  .ledger-cta-arrow {
1090
1100
  display: inline-flex;
@@ -1095,12 +1105,21 @@ a.ledger-chip:hover {
1095
1105
  text-decoration: none;
1096
1106
  }
1097
1107
 
1098
- .ledger-cta:hover {
1099
- color: var(--color-link-hover);
1108
+ /* Plain text CTAs carry the strong highlighter mark without an underline. */
1109
+ .ledger-cta:where(:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)) {
1110
+ --mark-1: var(--mark-strong-a);
1111
+ --mark-2: var(--mark-strong-b);
1112
+ --mark-3: var(--mark-strong-c);
1113
+ padding: 0.2rem 0.45rem;
1114
+ text-decoration: none;
1115
+ color: color-mix(in srgb, var(--color-primary) var(--action-ink-mix), var(--color-on-surface));
1100
1116
  }
1101
1117
 
1102
- .ledger-cta:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt):hover>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
1103
- text-decoration-color: color-mix(in srgb, var(--color-link-hover) 75%, transparent);
1118
+ .ledger-cta:where(:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)):hover {
1119
+ --mark-1: var(--mark-hover-a);
1120
+ --mark-2: var(--mark-hover-b);
1121
+ --mark-3: var(--mark-hover-c);
1122
+ color: color-mix(in srgb, var(--color-primary) var(--action-ink-mix), var(--color-on-surface));
1104
1123
  }
1105
1124
 
1106
1125
  /* Pill read-more only. The box is the affordance, so the label line would
@@ -1251,21 +1270,11 @@ a.ledger-chip:hover {
1251
1270
  display: flex;
1252
1271
  flex-direction: column;
1253
1272
  background: var(--color-surface-card);
1254
- border: 1.5px solid var(--border-ink);
1273
+ border: 1px solid var(--border-ink);
1255
1274
  border-radius: var(--radius-card);
1256
1275
  transition: background-color var(--motion-duration-fast) var(--motion-ease-default), border-color var(--motion-duration-fast) var(--motion-ease-default);
1257
1276
  }
1258
1277
 
1259
- /* Light paper: match the CV disclaimer edge — 1px outline hairline, not the
1260
- 1.5px ink frame dark mode needs for faint canvas borders. */
1261
- :root:not([data-theme="dark"]) .ledger-card,
1262
- :root:not([data-theme="dark"]) .ledger-system-card,
1263
- html[data-theme="light"] .ledger-card,
1264
- html[data-theme="light"] .ledger-system-card {
1265
- border-width: 1px;
1266
- border-color: var(--border-ink);
1267
- }
1268
-
1269
1278
  /* Drafting corner brackets — four L-corners, no labels. Pure decoration.
1270
1279
  Outline ink (not primary): the corners are drafting marks on the plate,
1271
1280
  same register as `.ledger-frame-crosshairs`, not a brand accent. */
@@ -1323,19 +1332,6 @@ html[data-theme="light"] .ledger-system-card {
1323
1332
  border-color: color-mix(in srgb, var(--color-primary) 10%, var(--border-ink));
1324
1333
  }
1325
1334
 
1326
- /* Cards and rows sitting on contrast bands keep the same transparent rest
1327
- as the canvas cards; hover steps to surface-high-contrast. */
1328
- .home-band--contrast .ledger-card,
1329
- .home-band--contrast .ledger-row,
1330
- .home-band--contrast .home-index-panel {
1331
- background: var(--color-surface-card-contrast);
1332
- }
1333
-
1334
- .home-band--contrast .ledger-card:hover,
1335
- .home-band--contrast .ledger-row:hover,
1336
- .home-band--contrast .home-index-panel:hover {
1337
- background: var(--color-surface-high-contrast);
1338
- }
1339
1335
 
1340
1336
  /* A project card used to end in a "Details" link pointing at the same place
1341
1337
  its own title points at, which is a second copy of the destination the
@@ -1371,8 +1367,33 @@ html[data-theme="light"] .ledger-system-card {
1371
1367
  flex: 1;
1372
1368
  }
1373
1369
 
1370
+ @media (max-width: 767px) {
1371
+ .ledger-card-body,
1372
+ .ledger-system-card {
1373
+ padding: var(--space-md);
1374
+ }
1375
+ }
1376
+
1374
1377
  .ledger-card-copy {
1375
1378
  flex: 1;
1379
+ display: -webkit-box;
1380
+ -webkit-box-orient: vertical;
1381
+ -webkit-line-clamp: 3;
1382
+ line-clamp: 3;
1383
+ overflow: hidden;
1384
+ }
1385
+
1386
+ @media (min-width: 768px) {
1387
+ .ledger-card-copy {
1388
+ -webkit-line-clamp: 4;
1389
+ line-clamp: 4;
1390
+ }
1391
+ }
1392
+
1393
+ @media (max-width: 767px) {
1394
+ .ledger-card-copy {
1395
+ font-size: var(--type-dek);
1396
+ }
1376
1397
  }
1377
1398
 
1378
1399
  .ledger-card-foot {
@@ -16,14 +16,6 @@
16
16
  opacity: 0.85;
17
17
  }
18
18
 
19
- .bib-masthead-shard {
20
- top: calc(-1 * var(--space-2xl));
21
- right: calc(-1 * var(--space-xl));
22
- width: 16rem;
23
- height: 9rem;
24
- background: var(--color-secondary);
25
- }
26
-
27
19
  .bib-body {
28
20
  position: relative;
29
21
  }
@@ -34,29 +26,6 @@
34
26
  z-index: 1;
35
27
  }
36
28
 
37
- .bib-foot-shard {
38
- left: calc(-1 * var(--space-xl));
39
- bottom: calc(-1 * var(--space-lg));
40
- width: 14rem;
41
- height: 8rem;
42
- background: color-mix(in srgb, var(--color-primary) 70%, transparent);
43
- }
44
-
45
- @media (max-width: 1023px) {
46
- .bib-masthead-shard {
47
- top: calc(-1 * var(--space-xl));
48
- right: calc(-1 * var(--space-lg));
49
- width: 12rem;
50
- height: 8rem;
51
- }
52
-
53
- .bib-foot-shard {
54
- left: calc(-1 * var(--space-lg));
55
- width: 10rem;
56
- height: 7rem;
57
- }
58
- }
59
-
60
29
  .bib-hidden {
61
30
  display: none !important;
62
31
  }
@@ -29,6 +29,45 @@
29
29
  outline: none;
30
30
  }
31
31
 
32
+ /* Translucent top bar over canvas ground. */
33
+ .site-topbar {
34
+ background-color: color-mix(in srgb, var(--color-canvas) 92%, transparent);
35
+ -webkit-backdrop-filter: blur(10px);
36
+ backdrop-filter: blur(10px);
37
+ border-bottom: 1px solid var(--border-gentle-divider);
38
+ }
39
+
40
+ /* Boxed controls for theme and mobile menu toggles. */
41
+ #theme-toggle-btn {
42
+ width: 2.25rem;
43
+ height: 2.25rem;
44
+ border: 1px solid var(--color-outline-variant);
45
+ background-color: var(--color-surface-card);
46
+ color: var(--color-on-surface);
47
+ border-radius: var(--radius-card);
48
+ }
49
+
50
+ #mobile-menu-btn {
51
+ width: 2.25rem;
52
+ height: 2.25rem;
53
+ border: 1px solid var(--color-outline-variant);
54
+ background-color: var(--color-surface-card);
55
+ color: var(--color-on-surface);
56
+ border-radius: var(--radius-card);
57
+ }
58
+
59
+ #theme-toggle-btn:hover,
60
+ #theme-toggle-btn:focus-visible {
61
+ color: var(--color-primary);
62
+ border-color: var(--color-outline);
63
+ }
64
+
65
+ #mobile-menu-btn:hover,
66
+ #mobile-menu-btn:focus-visible {
67
+ color: var(--color-primary);
68
+ border-color: var(--color-outline);
69
+ }
70
+
32
71
  .site-wordmark-link {
33
72
  display: inline-flex;
34
73
  align-items: center;
@@ -99,8 +138,10 @@
99
138
  }
100
139
 
101
140
  /* The rail needs room for six labels before it earns its place; below that
102
- the Explore menu carries the same links. */
103
- @media (min-width: 1024px) {
141
+ the Explore menu carries the same links. At 1024px and 1240px the six
142
+ labels ran under the wordmark on the left and the controls on the right,
143
+ so the rail starts at 1280px (PLT-AC-49 item 14). */
144
+ @media (min-width: 1280px) {
104
145
  .site-rail {
105
146
  display: flex;
106
147
  }
@@ -110,6 +151,14 @@
110
151
  }
111
152
  }
112
153
 
154
+ /* Between 1280px and 1439px the bar is only just wide enough; the tighter
155
+ gap keeps clear space beside the wordmark and the controls. */
156
+ @media (min-width: 1280px) and (max-width: 1439px) {
157
+ .site-rail {
158
+ gap: var(--space-sm);
159
+ }
160
+ }
161
+
113
162
  /* Theme (and Explore on narrow viewports) share the rail's label size. */
114
163
  #theme-toggle .marginalia,
115
164
  #mobile-menu-btn .marginalia,