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
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>',
@@ -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),
@@ -397,6 +400,8 @@ html[data-theme="light"] {
397
400
  --mark-quiet-b: 9%;
398
401
  --mark-quiet-c: 3%;
399
402
  --facet-ink-mix: 82%;
403
+ /* Detail actions: green section primary on the gray hover mark was 4.48:1 at 82%. */
404
+ --action-ink-mix: 74%;
400
405
  --mk-angle: 104deg;
401
406
  --mk-s0: 0.9%;
402
407
  --mk-s1: 2.6%;
@@ -424,6 +429,7 @@ html[data-theme="dark"] {
424
429
  --mark-quiet-c: 3%;
425
430
  /* Not 100%: the YouTube red facet was 4.24:1 on its own dark mark. */
426
431
  --facet-ink-mix: 85%;
432
+ --action-ink-mix: 85%;
427
433
  }
428
434
 
429
435
  /* Shared mark foundation */
@@ -434,6 +440,7 @@ html[data-theme="dark"] {
434
440
  .source-facet,
435
441
  .bib-facet,
436
442
  :is(.ledger-cta--emphasis, .ledger-cta--emphasis-alt),
443
+ .ledger-cta:where(:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)),
437
444
  :is(.detail-action, .detail-type-size-trigger, .article-related-section-pill, .listing-filter-trigger) {
438
445
  --mark-ink: var(--color-primary);
439
446
  --mark-1: var(--mark-a);
@@ -534,6 +541,19 @@ html[data-theme="dark"] {
534
541
  font-optical-sizing: auto;
535
542
  }
536
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
+
537
557
  /* Label-face meta on CV identity lines, etc. */
538
558
  .card-meta {
539
559
  font-family: var(--font-label);
@@ -568,8 +588,7 @@ html[data-theme="dark"] {
568
588
  letter-spacing: 0.04em;
569
589
  }
570
590
 
571
- /* Dates, kickers, section notes — body face at meta size. */
572
- .ledger-meta,
591
+ /* Kickers, section notes — body face at meta size. */
573
592
  .ledger-kicker,
574
593
  .ledger-section-note {
575
594
  font-family: var(--font-body);
@@ -1035,14 +1054,19 @@ a.ledger-chip:hover {
1035
1054
 
1036
1055
  /* Dates live here. The chips and language pills inside the strip set their
1037
1056
  own uppercase, so the strip itself leaves case alone — running small caps
1038
- over a date is heavier than the date deserves. */
1057
+ over a date is heavier than the date deserves. Meta uses mono face. */
1039
1058
  .ledger-meta {
1040
1059
  display: flex;
1041
1060
  flex-wrap: wrap;
1042
1061
  align-items: center;
1043
1062
  gap: var(--space-sm);
1044
1063
  color: var(--color-on-surface-variant);
1045
- 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;
1046
1070
  }
1047
1071
 
1048
1072
  /* A separator is ink, not a border. --color-outline-variant is tuned for
@@ -1071,17 +1095,6 @@ a.ledger-chip:hover {
1071
1095
  line-height: 1;
1072
1096
  }
1073
1097
 
1074
- /* Text CTAs carry a gentle accent underline so the clickable cue is not the
1075
- same gesture as the marker wash on titles. Emphasis buttons carry a
1076
- strong mark instead. */
1077
- .ledger-cta:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
1078
- text-decoration: underline;
1079
- text-decoration-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
1080
- text-decoration-thickness: 1.5px;
1081
- text-underline-offset: 0.48em;
1082
- text-underline-position: under;
1083
- }
1084
-
1085
1098
  .ledger-cta-icon,
1086
1099
  .ledger-cta-arrow {
1087
1100
  display: inline-flex;
@@ -1092,12 +1105,21 @@ a.ledger-chip:hover {
1092
1105
  text-decoration: none;
1093
1106
  }
1094
1107
 
1095
- .ledger-cta:hover {
1096
- 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));
1097
1116
  }
1098
1117
 
1099
- .ledger-cta:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt):hover>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
1100
- 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));
1101
1123
  }
1102
1124
 
1103
1125
  /* Pill read-more only. The box is the affordance, so the label line would
@@ -1248,21 +1270,11 @@ a.ledger-chip:hover {
1248
1270
  display: flex;
1249
1271
  flex-direction: column;
1250
1272
  background: var(--color-surface-card);
1251
- border: 1.5px solid var(--border-ink);
1273
+ border: 1px solid var(--border-ink);
1252
1274
  border-radius: var(--radius-card);
1253
1275
  transition: background-color var(--motion-duration-fast) var(--motion-ease-default), border-color var(--motion-duration-fast) var(--motion-ease-default);
1254
1276
  }
1255
1277
 
1256
- /* Light paper: match the CV disclaimer edge — 1px outline hairline, not the
1257
- 1.5px ink frame dark mode needs for faint canvas borders. */
1258
- :root:not([data-theme="dark"]) .ledger-card,
1259
- :root:not([data-theme="dark"]) .ledger-system-card,
1260
- html[data-theme="light"] .ledger-card,
1261
- html[data-theme="light"] .ledger-system-card {
1262
- border-width: 1px;
1263
- border-color: var(--border-ink);
1264
- }
1265
-
1266
1278
  /* Drafting corner brackets — four L-corners, no labels. Pure decoration.
1267
1279
  Outline ink (not primary): the corners are drafting marks on the plate,
1268
1280
  same register as `.ledger-frame-crosshairs`, not a brand accent. */
@@ -1320,19 +1332,6 @@ html[data-theme="light"] .ledger-system-card {
1320
1332
  border-color: color-mix(in srgb, var(--color-primary) 10%, var(--border-ink));
1321
1333
  }
1322
1334
 
1323
- /* Cards and rows sitting on contrast bands keep the same transparent rest
1324
- as the canvas cards; hover steps to surface-high-contrast. */
1325
- .home-band--contrast .ledger-card,
1326
- .home-band--contrast .ledger-row,
1327
- .home-band--contrast .home-index-panel {
1328
- background: var(--color-surface-card-contrast);
1329
- }
1330
-
1331
- .home-band--contrast .ledger-card:hover,
1332
- .home-band--contrast .ledger-row:hover,
1333
- .home-band--contrast .home-index-panel:hover {
1334
- background: var(--color-surface-high-contrast);
1335
- }
1336
1335
 
1337
1336
  /* A project card used to end in a "Details" link pointing at the same place
1338
1337
  its own title points at, which is a second copy of the destination the
@@ -1368,8 +1367,33 @@ html[data-theme="light"] .ledger-system-card {
1368
1367
  flex: 1;
1369
1368
  }
1370
1369
 
1370
+ @media (max-width: 767px) {
1371
+ .ledger-card-body,
1372
+ .ledger-system-card {
1373
+ padding: var(--space-md);
1374
+ }
1375
+ }
1376
+
1371
1377
  .ledger-card-copy {
1372
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
+ }
1373
1397
  }
1374
1398
 
1375
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,
@@ -62,46 +62,6 @@ html[data-theme="light"] .cv-product {
62
62
  }
63
63
  }
64
64
 
65
- .cv-dossier-shard {
66
- top: -4rem;
67
- right: -3rem;
68
- width: 16rem;
69
- height: 16rem;
70
- background: var(--color-secondary);
71
- }
72
-
73
- .cv-dossier-shard--beta {
74
- top: auto;
75
- right: auto;
76
- left: -3rem;
77
- bottom: -4rem;
78
- width: 12rem;
79
- height: 12rem;
80
- background: var(--color-primary);
81
- }
82
-
83
- /* A shard is a corner accent. Between the 640px suppression breakpoint and the
84
- desktop layout the panel is narrow enough that a 16rem shard stops being a
85
- corner and starts crossing the title — "Curriculum Vitæ" was reading over a
86
- tonal step at 768px. Same correction on the writing lead and the rascunhos
87
- masthead, which had the same collision. */
88
- @media (max-width: 1023px) {
89
- .cv-dossier-shard {
90
- top: -2.5rem;
91
- right: -2rem;
92
- width: 9rem;
93
- height: 9rem;
94
- }
95
-
96
- .cv-dossier-shard--beta {
97
- top: auto;
98
- left: -2rem;
99
- bottom: -2.5rem;
100
- width: 7rem;
101
- height: 7rem;
102
- }
103
- }
104
-
105
65
  .cv-dossier-inner {
106
66
  position: relative;
107
67
  z-index: 1;