jekyll-theme-zer0 1.28.0 → 1.30.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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1143 -13
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +507 -2
  7. data/_data/consumers.yml +157 -9
  8. data/_data/features.yml +303 -20
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/i18n/fr.yml +12 -7
  11. data/_data/i18n/manifest.yml +39 -12
  12. data/_data/ingredient_densities.yml +122 -0
  13. data/_data/landing.yml +5 -2
  14. data/_data/navigation/main.yml +16 -0
  15. data/_data/navigation/quickstart.yml +4 -0
  16. data/_data/recipe_courses.yml +64 -0
  17. data/_data/site_builder.yml +874 -0
  18. data/_data/theme-manifest.yml +160 -124
  19. data/_data/ui-text.yml +26 -0
  20. data/_includes/README.md +26 -2
  21. data/_includes/analytics/posthog.html +2 -2
  22. data/_includes/components/admin-links.html +2 -2
  23. data/_includes/components/admin-tabs.html +2 -2
  24. data/_includes/components/ai-chat.html +14 -11
  25. data/_includes/components/analytics-dashboard.html +8 -8
  26. data/_includes/components/author-bio.html +1 -1
  27. data/_includes/components/author-card.html +10 -2
  28. data/_includes/components/author-eeat.html +4 -4
  29. data/_includes/components/background-customizer.html +10 -10
  30. data/_includes/components/background-image.html +114 -0
  31. data/_includes/components/background-settings.html +28 -15
  32. data/_includes/components/collection-manager.html +5 -5
  33. data/_includes/components/component-showcase.html +13 -13
  34. data/_includes/components/config-editor.html +12 -12
  35. data/_includes/components/config-viewer.html +8 -8
  36. data/_includes/components/cookie-consent.html +15 -15
  37. data/_includes/components/cta-button.html +7 -2
  38. data/_includes/components/dev-shortcuts.html +7 -7
  39. data/_includes/components/env-dashboard.html +8 -8
  40. data/_includes/components/env-switcher.html +9 -9
  41. data/_includes/components/feature-card.html +2 -2
  42. data/_includes/components/halfmoon.html +2 -2
  43. data/_includes/components/info-section.html +42 -37
  44. data/_includes/components/js-cdn.html +15 -15
  45. data/_includes/components/language-toggle.html +168 -21
  46. data/_includes/components/mermaid.html +72 -435
  47. data/_includes/components/nanobar.html +5 -5
  48. data/_includes/components/nav-editor.html +2 -2
  49. data/_includes/components/nav-export.html +2 -2
  50. data/_includes/components/nav-overview.html +2 -2
  51. data/_includes/components/page-feedback.html +45 -30
  52. data/_includes/components/page-views-init.html +55 -0
  53. data/_includes/components/page-views.html +33 -0
  54. data/_includes/components/post-card.html +22 -22
  55. data/_includes/components/post-type-badge.html +2 -2
  56. data/_includes/components/powered-by.html +2 -2
  57. data/_includes/components/preview-image.html +6 -0
  58. data/_includes/components/quick-index.html +2 -2
  59. data/_includes/components/recipe-card.html +67 -0
  60. data/_includes/components/recipe-duration.html +50 -0
  61. data/_includes/components/recipe-grams.html +58 -0
  62. data/_includes/components/recipe-index.html +96 -0
  63. data/_includes/components/recipe-ingredients.html +90 -0
  64. data/_includes/components/recipe-meta.html +96 -0
  65. data/_includes/components/recipe-nutrition.html +57 -0
  66. data/_includes/components/recipe-qty.html +73 -0
  67. data/_includes/components/recipe-ratio.html +151 -0
  68. data/_includes/components/recipe-scaler.html +73 -0
  69. data/_includes/components/recipe-steps.html +86 -0
  70. data/_includes/components/recipe-temp.html +45 -0
  71. data/_includes/components/search-modal.html +29 -4
  72. data/_includes/components/searchbar.html +2 -2
  73. data/_includes/components/shortcuts-modal.html +3 -0
  74. data/_includes/components/svg-background.html +2 -2
  75. data/_includes/components/theme-customizer.html +2 -2
  76. data/_includes/components/theme-info.html +14 -7
  77. data/_includes/components/theme-preview-gallery.html +22 -22
  78. data/_includes/content/giscus.html +2 -2
  79. data/_includes/content/intro.html +8 -8
  80. data/_includes/content/jsonld-faq.html +2 -2
  81. data/_includes/content/jsonld-software.html +24 -5
  82. data/_includes/content/seo.html +4 -4
  83. data/_includes/content/sitemap.html +27 -27
  84. data/_includes/content/toc.html +183 -183
  85. data/_includes/core/branding.html +6 -6
  86. data/_includes/core/console-capture.html +32 -74
  87. data/_includes/core/favicon.html +49 -7
  88. data/_includes/core/footer-fabs.html +17 -3
  89. data/_includes/core/footer.html +49 -34
  90. data/_includes/core/head.html +110 -86
  91. data/_includes/core/header.html +76 -54
  92. data/_includes/docs/bootstrap-docs.html +8 -8
  93. data/_includes/landing/landing-install-cards.html +2 -2
  94. data/_includes/landing/landing-quick-links.html +1 -1
  95. data/_includes/navigation/admin-nav.html +2 -2
  96. data/_includes/navigation/nav-tree.html +8 -8
  97. data/_includes/navigation/navbar.html +12 -12
  98. data/_includes/navigation/section-sidebar.html +109 -27
  99. data/_includes/navigation/sidebar-config.html +36 -2
  100. data/_includes/navigation/sidebar-left.html +17 -16
  101. data/_includes/navigation/sidebar-right.html +8 -7
  102. data/_includes/obsidian/full-graph.html +2 -2
  103. data/_includes/setup/claude-session.html +72 -0
  104. data/_includes/setup/prereq-checklist.html +90 -0
  105. data/_includes/setup/wizard.html +924 -222
  106. data/_includes/stats/stats-categories.html +8 -8
  107. data/_includes/stats/stats-header.html +14 -14
  108. data/_includes/stats/stats-metrics.html +14 -14
  109. data/_includes/stats/stats-no-data.html +12 -12
  110. data/_includes/stats/stats-overview.html +6 -6
  111. data/_includes/stats/stats-tags.html +8 -8
  112. data/_layouts/404.html +38 -24
  113. data/_layouts/README.md +2 -0
  114. data/_layouts/admin.html +24 -24
  115. data/_layouts/article.html +43 -33
  116. data/_layouts/author.html +20 -20
  117. data/_layouts/authors.html +2 -2
  118. data/_layouts/book-abc.html +12 -12
  119. data/_layouts/book-story.html +15 -15
  120. data/_layouts/book.html +12 -12
  121. data/_layouts/collection.html +33 -33
  122. data/_layouts/cookbook.html +88 -0
  123. data/_layouts/default.html +31 -28
  124. data/_layouts/home.html +23 -23
  125. data/_layouts/index.html +10 -10
  126. data/_layouts/landing.html +17 -17
  127. data/_layouts/news.html +44 -44
  128. data/_layouts/note.html +38 -38
  129. data/_layouts/notebook.html +34 -34
  130. data/_layouts/recipe.html +274 -0
  131. data/_layouts/root.html +92 -55
  132. data/_layouts/section.html +62 -33
  133. data/_layouts/setup.html +3 -3
  134. data/_layouts/sitemap-collection.html +49 -49
  135. data/_layouts/stats.html +40 -40
  136. data/_layouts/tag.html +12 -12
  137. data/_layouts/welcome.html +21 -21
  138. data/_sass/components/_callout.scss +1 -1
  139. data/_sass/components/_footer.scss +37 -1
  140. data/_sass/components/_mermaid.scss +375 -0
  141. data/_sass/components/_page-views.scss +36 -0
  142. data/_sass/components/_recipe.scss +506 -0
  143. data/_sass/components/_setup-wizard.scss +764 -0
  144. data/_sass/components/_ui-enhancements.scss +6 -6
  145. data/_sass/core/_navbar.scss +261 -46
  146. data/_sass/layouts/_landing.scss +2 -2
  147. data/_sass/layouts/_navbar-extras.scss +14 -4
  148. data/_sass/tokens/_color.scss +6 -0
  149. data/_sass/tokens/_index.scss +2 -0
  150. data/_sass/tokens/_radius.scss +21 -0
  151. data/_sass/tokens/_typography.scss +4 -0
  152. data/_sass/utilities/_focus.scss +14 -0
  153. data/assets/css/main.scss +4 -0
  154. data/assets/js/ai-chat.js +47 -5
  155. data/assets/js/fleet-feedback-capture.js +124 -0
  156. data/assets/js/fleet-feedback.js +853 -0
  157. data/assets/js/mermaid-diagrams.js +1267 -0
  158. data/assets/js/modules/navigation/config.js +9 -6
  159. data/assets/js/modules/navigation/navbar.js +55 -0
  160. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  161. data/assets/js/modules/theme/appearance.js +8 -2
  162. data/assets/js/obsidian-wiki-links.js +8 -3
  163. data/assets/js/page-feedback.js +125 -192
  164. data/assets/js/page-views.js +372 -0
  165. data/assets/js/recipe-scaler.js +501 -0
  166. data/assets/js/search-modal.js +36 -0
  167. data/assets/js/setup-wizard.js +2279 -226
  168. data/assets/js/site-builder.js +1834 -0
  169. data/assets/js/ui-enhancements.js +11 -3
  170. data/scripts/README.md +44 -0
  171. data/scripts/ai/README.md +38 -0
  172. data/scripts/ai/api_call.rb +124 -0
  173. data/scripts/ai/usage.rb +314 -0
  174. data/scripts/ai/usage_report.rb +225 -0
  175. data/scripts/bin/audit-consumer +39 -7
  176. data/scripts/bin/giscus-discussions +213 -14
  177. data/scripts/bin/manifest +35 -12
  178. data/scripts/ci/agent_review_result.py +164 -0
  179. data/scripts/ci/test_agent_review_result.py +172 -0
  180. data/scripts/ci/test_visual_evidence_autogen.py +341 -0
  181. data/scripts/ci/visual_evidence_autogen.py +1060 -0
  182. data/scripts/content-review.rb +20 -1
  183. data/scripts/design-system-check.rb +170 -0
  184. data/scripts/lib/audit.sh +42 -2
  185. data/scripts/lint-liquid-raw.rb +137 -0
  186. data/scripts/test/integration/mermaid +22 -8
  187. data/scripts/test/lib/run_tests.sh +3 -1
  188. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  189. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  190. data/scripts/translate.rb +94 -16
  191. metadata +48 -2
@@ -1,89 +1,47 @@
1
1
  {%- comment -%}
2
2
  ===================================================================
3
- CONSOLE CAPTURE — early, tiny console/error ring buffer
3
+ CONSOLE CAPTURE — early console/error ring buffer
4
4
  ===================================================================
5
5
 
6
6
  File: console-capture.html
7
7
  Path: _includes/core/console-capture.html
8
- Purpose: Capture a capped, in-memory buffer of console output and
9
- uncaught errors, installed HIGH in <head> — before the
10
- theme's deferred scripts run — so the page-feedback widget
11
- (assets/js/page-feedback.js) can attach the logs and JS
12
- errors that led up to a report.
8
+ Purpose: Load the fleet feedback kit's capture buffer HIGH in <head> —
9
+ before the theme's deferred scripts run — so the page-feedback
10
+ widget can attach the logs and JS errors that led up to a report.
13
11
 
14
12
  Renders: only when page_feedback.enabled AND page_feedback.capture_logs.
15
13
 
14
+ This used to be ~60 lines of inline ES5 maintained here. It is now the
15
+ fleet kit's capture.js, VENDORED to assets/js/fleet-feedback-capture.js
16
+ (bamr87/bamr87 templates/feedback/capture.js — drift check (i) holds the
17
+ copy to the hub's). Two things came with the move: the buffer shape is
18
+ now the one FleetFeedbackCore reads, so the theme's widget and the fleet's
19
+ web component file byte-identical issues; and every captured line is
20
+ REDACTED before it enters the ring (bearer/API keys, JWTs, GitHub tokens,
21
+ email addresses), so a secret cannot be previewed, copied, or filed.
22
+
23
+ Loaded blocking rather than deferred, and that is the point: a buffer
24
+ installed after parsing misses exactly the load-time failures worth
25
+ reporting. It is ~4 KB, and it REPLACED ~3.7 KB of inline script, so the
26
+ head got smaller — which also buys back room for the charset meta that
27
+ has to land in the first 1024 bytes (issue #372).
28
+
16
29
  Privacy: in-memory only, never persisted, never sent anywhere until the
17
- user opens the feedback modal, reviews the captured logs, and
18
- chooses to submit. Any single entry is truncated; the buffer is
19
- a fixed-size ring (page_feedback.log_limit).
30
+ reader opens the modal, reviews the captured lines, and submits.
31
+ Any single entry is truncated; the buffer is a fixed-size ring
32
+ (page_feedback.log_limit, default 40).
20
33
 
21
- Depends on: nothing — pure inline ES5, GitHub Pages-safe (no CDN).
34
+ Depends on: assets/js/fleet-feedback-capture.js (vendored, no CDN).
22
35
  ===================================================================
23
36
  {%- endcomment -%}
24
37
  {%- if site.page_feedback.enabled and site.page_feedback.capture_logs -%}
25
- <script>
26
- /* page-feedback: early console/error capture — see _includes/core/console-capture.html */
27
- (function () {
28
- "use strict";
29
- if (window.__zer0Feedback && window.__zer0Feedback.installed) { return; }
30
-
31
- var LIMIT = {{ site.page_feedback.log_limit | default: 40 | jsonify }};
32
- var MAX_ENTRY = 600; // truncate any single entry so the buffer stays small
33
- var start = (window.performance && performance.now) ? null : Date.now();
34
-
35
- var buffer = [];
36
- function now() { return start === null ? Math.round(performance.now()) : (Date.now() - start); }
37
-
38
- function stringify(value) {
39
- if (typeof value === "string") { return value; }
40
- if (value instanceof Error) {
41
- return (value.name || "Error") + ": " + (value.message || "") + (value.stack ? "\n" + value.stack : "");
42
- }
43
- try { return JSON.stringify(value); } catch (e) { return String(value); }
44
- }
45
-
46
- function push(level, args) {
47
- try {
48
- var parts = [];
49
- for (var i = 0; i < args.length; i++) { parts.push(stringify(args[i])); }
50
- var text = parts.join(" ");
51
- if (text.length > MAX_ENTRY) { text = text.slice(0, MAX_ENTRY) + "…"; }
52
- buffer.push({ t: now(), level: level, text: text });
53
- while (buffer.length > LIMIT) { buffer.shift(); }
54
- } catch (e) { /* logging must never break the page */ }
55
- }
56
-
57
- // Wrap console methods, preserving passthrough to the real console.
58
- if (window.console) {
59
- ["log", "info", "warn", "error", "debug"].forEach(function (level) {
60
- var original = console[level] ? console[level].bind(console) : null;
61
- console[level] = function () {
62
- push(level, arguments);
63
- if (original) { original.apply(null, arguments); }
64
- };
65
- });
66
- }
67
-
68
- // Uncaught errors and unhandled promise rejections.
69
- window.addEventListener("error", function (e) {
70
- if (!e) { return; }
71
- var where = e.filename ? " (" + e.filename + ":" + (e.lineno || 0) + ":" + (e.colno || 0) + ")" : "";
72
- push("error", [(e.message || "Script error") + where]);
73
- });
74
- window.addEventListener("unhandledrejection", function (e) {
75
- var reason = e && e.reason;
76
- var msg = reason && reason.message ? reason.message : stringify(reason);
77
- push("error", ["Unhandled promise rejection: " + msg]);
78
- });
79
-
80
- window.__zer0Feedback = {
81
- installed: true,
82
- logs: buffer,
83
- /** Snapshot the buffer (newest last) for the widget to read. */
84
- snapshot: function () { return buffer.slice(); },
85
- clear: function () { buffer.length = 0; }
86
- };
87
- })();
88
- </script>
38
+ <script src="{{ '/assets/js/fleet-feedback-capture.js' | relative_url }}"></script>
39
+ {%- comment -%}
40
+ The ring size is site configuration, so it is applied after the buffer
41
+ installs rather than baked into the vendored file (which must stay
42
+ byte-identical to the hub's).
43
+ {%- endcomment -%}
44
+ {%- if site.page_feedback.log_limit -%}
45
+ <script>if(window.__fleetFeedback){window.__fleetFeedback.limit={{ site.page_feedback.log_limit | jsonify }};}</script>
46
+ {%- endif -%}
89
47
  {%- endif -%}
@@ -16,12 +16,17 @@
16
16
  png_size : 32x32 # sizes attr for png (optional)
17
17
  apple_touch : /assets/images/apple-touch.png # iOS home-screen icon (optional)
18
18
  manifest : /site.webmanifest # PWA manifest (optional)
19
- theme_color : "#0d1117" # browser chrome color (optional)
19
+ theme_color : "#0d1117" # pin ONE chrome color (optional)
20
+ theme_color_light : "#ffffff" # light-scheme chrome (optional)
21
+ theme_color_dark : "#212529" # dark-scheme chrome (optional)
20
22
 
21
23
  With NO config at all this still emits the explicit
22
- /favicon.ico link (every consumer site carries one at root),
23
- and theme_color falls back to site.theme_color.main so the
24
- mobile address bar matches the site's design tokens.
24
+ /favicon.ico link (every consumer site carries one at root)
25
+ AND both theme-color tags. That last part is the point of the
26
+ fallbacks below: a remote_theme consumer does not inherit this
27
+ repo's _config.yml (see remote-theme-checklist.md), so a
28
+ config-gated tag emitted nothing at all on exactly the sites
29
+ this include exists to serve (issue #281).
25
30
  Notes: Page-invariant — safe to render via include_cached. All paths
26
31
  run through relative_url so baseurl deployments resolve.
27
32
  {%- endcomment -%}
@@ -40,7 +45,44 @@
40
45
  {%- if _fav.manifest %}
41
46
  <link rel="manifest" href="{{ _fav.manifest | relative_url }}">
42
47
  {%- endif %}
43
- {%- assign _fav_theme_color = _fav.theme_color | default: site.theme_color.main -%}
44
- {%- if _fav_theme_color %}
45
- <meta name="theme-color" content="{{ _fav_theme_color }}">
48
+ {%- comment -%}
49
+ theme-color drives mobile browser chrome (iOS Safari address/status bar,
50
+ the Chrome/Android task-switcher card). It must track the page SURFACE.
51
+
52
+ It used to fall back to site.theme_color.main, which is the brand ACCENT --
53
+ #007bff on this site -- so a dark-first theme advertised a bright blue
54
+ address bar. The defaults below are Bootstrap 5.3.3's own --bs-body-bg for
55
+ each scheme (:root #fff, [data-bs-theme=dark] #212529); the theme adds no
56
+ dark override of that variable, so they are the real computed surface.
57
+
58
+ WHICH SHAPE TO EMIT depends on how the mode is decided, because
59
+ `media="(prefers-color-scheme: ...)"` keys off the OS and nothing else:
60
+
61
+ - mode is pinned (color_mode_lock, or a non-auto color_mode_default)
62
+ -> ONE unconditional tag. A media pair would hand light chrome to an
63
+ OS-light visitor looking at a page this site renders dark.
64
+ - mode is `auto` -> the media pair, which is exactly what `auto` means.
65
+
66
+ A locked `auto` resolves to dark here, matching core/color-mode-init.html.
67
+
68
+ Known limitation, deliberately not solved here: an unlocked site lets a
69
+ visitor override the mode from the Appearance panel via localStorage, which
70
+ no static meta can follow. Keeping chrome in sync with the panel needs a
71
+ runtime hook in the toggle path -- tracked separately.
72
+ {%- endcomment -%}
73
+ {%- assign _tc_light = _fav.theme_color_light | default: '#ffffff' -%}
74
+ {%- assign _tc_dark = _fav.theme_color_dark | default: '#212529' -%}
75
+ {%- assign _tc_mode = site.color_mode_default | default: 'auto' -%}
76
+ {%- if _fav.theme_color %}
77
+ {%- comment -%} Back-compat: a consumer that pinned one colour keeps it. {%- endcomment %}
78
+ <meta name="theme-color" content="{{ _fav.theme_color }}">
79
+ {%- elsif site.color_mode_lock and _tc_mode == 'light' %}
80
+ <meta name="theme-color" content="{{ _tc_light }}">
81
+ {%- elsif site.color_mode_lock or _tc_mode == 'dark' %}
82
+ <meta name="theme-color" content="{{ _tc_dark }}">
83
+ {%- elsif _tc_mode == 'light' %}
84
+ <meta name="theme-color" content="{{ _tc_light }}">
85
+ {%- else %}
86
+ <meta name="theme-color" media="(prefers-color-scheme: light)" content="{{ _tc_light }}">
87
+ <meta name="theme-color" media="(prefers-color-scheme: dark)" content="{{ _tc_dark }}">
46
88
  {%- endif %}
@@ -20,12 +20,26 @@
20
20
  after </footer> is visually identical to the previous nesting.
21
21
  ===================================================================
22
22
  {%- endcomment -%}
23
- {%- assign _root_only_layouts = "landing,welcome,stats,sitemap-collection,setup,section,news,index,home,admin" | split: "," -%}
24
- {%- unless _root_only_layouts contains page.layout -%}
23
+ {%- comment -%}
24
+ The TOC FAB controls `#tocContents`, which navigation/sidebar-right.html
25
+ emits — and only _layouts/default.html includes that. This used to be a
26
+ DENYLIST of layouts to skip, which fails open: any layout not on the list got
27
+ the FAB whether or not it rendered the panel, and the list had already fallen
28
+ behind (404 and the book-* layouts were missing, so those pages shipped a FAB
29
+ pointing at nothing — issue #373).
30
+
31
+ Replaced with the positive gate from navigation/sidebar-config.html, which
32
+ fails safe: a new layout gets no FAB until it is declared as one that renders
33
+ the offcanvas. sidebar-config is included here rather than relied upon from
34
+ header.html because Liquid assigns propagate to the INCLUDING template, and
35
+ these are two sibling includes of root.html.
36
+ {%- endcomment -%}
37
+ {% include navigation/sidebar-config.html %}
38
+ {%- if sidebar_offcanvas_rendered -%}
25
39
  {% include navigation/toc-fab.html %}
26
40
  {%- comment -%} Local-graph FAB/panel are part of the Obsidian feature. {%- endcomment -%}
27
41
  {%- if site.obsidian.enabled != false -%}
28
42
  {% include navigation/local-graph-fab.html %}
29
43
  {% include navigation/local-graph.html %}
30
44
  {%- endif -%}
31
- {%- endunless -%}
45
+ {%- endif -%}
@@ -1,5 +1,5 @@
1
1
  {% comment %} Feature: ZER0-046 {% endcomment %}
2
- <!--
2
+ {% comment %}
3
3
  ===================================================================
4
4
  FOOTER - Comprehensive Site Footer with Branding & Navigation
5
5
  ===================================================================
@@ -33,12 +33,12 @@
33
33
  - Guard optional fields (address/phone/email) to avoid empty output
34
34
  - Use relative_url for internal links to respect baseurl
35
35
  ===================================================================
36
- -->
36
+ {% endcomment %}
37
37
 
38
38
  {%- include core/i18n.html lang=include.lang -%}
39
39
  <footer class="bd-footer border-top" role="contentinfo">
40
40
  {% assign footer_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
41
- <!-- Powered by Row -->
41
+ {% comment %} Powered by Row {% endcomment %}
42
42
  <div class="container-xl footer-powered-by">
43
43
  <p class="footer-powered-by-copyright text-center mb-0">
44
44
  &copy; {{ site.time | date: "%Y" }} {{ site.name | default: site.title }}
@@ -76,15 +76,16 @@
76
76
  </span>
77
77
  {% endif %}
78
78
  {% if forloop.last and powered_by_count > 0 %}
79
- <a href="#"
80
- class="powered-by-link"
81
- data-bs-toggle="offcanvas"
82
- data-bs-target="#info-section"
83
- aria-label="View theme information and system details"
84
- title="Theme Info">
79
+ <button type="button"
80
+ class="powered-by-link"
81
+ data-bs-toggle="offcanvas"
82
+ data-bs-target="#info-section"
83
+ aria-haspopup="dialog"
84
+ aria-label="View theme information and system details"
85
+ title="Theme Info">
85
86
  <i class="{{ site.default_icon }} bi-info-circle" aria-hidden="true"></i>
86
87
  <span class="powered-by-link-text">Info</span>
87
- </a>
88
+ </button>
88
89
  </span>
89
90
  </li>
90
91
  {% else %}
@@ -93,27 +94,28 @@
93
94
  {% endfor %}
94
95
  {% if powered_by_count == 0 %}
95
96
  <li>
96
- <a href="#"
97
- class="powered-by-link"
98
- data-bs-toggle="offcanvas"
99
- data-bs-target="#info-section"
100
- aria-label="View theme information and system details"
101
- title="Theme Info">
97
+ <button type="button"
98
+ class="powered-by-link"
99
+ data-bs-toggle="offcanvas"
100
+ data-bs-target="#info-section"
101
+ aria-haspopup="dialog"
102
+ aria-label="View theme information and system details"
103
+ title="Theme Info">
102
104
  <i class="{{ site.default_icon }} bi-info-circle" aria-hidden="true"></i>
103
105
  <span class="powered-by-link-text">Info</span>
104
- </a>
106
+ </button>
105
107
  </li>
106
108
  {% endif %}
107
109
  </ul>
108
110
  </div>
109
111
  </div>
110
112
 
111
- <!-- Branding and Navigation Block — full-width dark background -->
113
+ {% comment %} Branding and Navigation Block — full-width dark background {% endcomment %}
112
114
  <div class="bg-dark text-light footer-dark-block">
113
115
  <div class="container-xl">
114
- <!-- Top: Site info + Quick Links + Social -->
116
+ {% comment %} Top: Site info + Quick Links + Social {% endcomment %}
115
117
  <div class="row mb-3 mb-lg-4 g-2 g-lg-3 footer-nav-columns justify-content-center">
116
- <!-- Site Info -->
118
+ {% comment %} Site Info {% endcomment %}
117
119
  <div class="col-12 col-md-3 col-lg-3 mb-2 mb-lg-3 order-0 order-md-1 order-lg-1">
118
120
  <h2 class="footer-section-title text-uppercase h6 mb-0">{{ site.title }}</h2>
119
121
  {% if site.address %}<p class="mb-1">{{ site.address }}</p>{% endif %}
@@ -121,12 +123,12 @@
121
123
  {% if site.email %}<p class="mb-0"><a href="mailto:{{ site.email }}" class="text-light text-decoration-none">{{ site.email }}</a></p>{% endif %}
122
124
  </div>
123
125
 
124
- <!-- Quick Links
126
+ {% comment %} Quick Links
125
127
  Resolution order:
126
128
  1. site.footer_quick_links (array of {label, url}) — explicit override
127
129
  2. Auto-detection: only render links whose target pages
128
130
  actually exist in the build. Prevents 404s on bare-minimum
129
- sites that haven't created /about/, /services/, etc. -->
131
+ sites that haven't created /about/, /services/, etc. {% endcomment %}
130
132
  <div class="col-6 col-md-3 col-lg-3 mb-2 mb-lg-3 order-2 order-md-2 order-lg-2">
131
133
  <h2 class="footer-section-title text-uppercase h6 mb-0">Quick Links</h2>
132
134
  <ul class="list-unstyled" aria-label="Footer quick links">
@@ -169,7 +171,7 @@
169
171
  </ul>
170
172
  </div>
171
173
 
172
- <!-- Latest Posts (dynamic) -->
174
+ {% comment %} Latest Posts (dynamic) {% endcomment %}
173
175
  {% assign posts_count = site.posts | size %}
174
176
  {% if posts_count and posts_count > 0 %}
175
177
  <div class="col-12 col-md-3 col-lg-3 mb-2 mb-lg-3 order-4 order-md-3 order-lg-3">
@@ -184,7 +186,7 @@
184
186
  </div>
185
187
  {% endif %}
186
188
 
187
- <!-- Social & RSS -->
189
+ {% comment %} Social & RSS {% endcomment %}
188
190
  <div class="col-6 col-md-3 col-lg-3 mb-2 mb-lg-3 order-3 order-md-4 order-lg-4">
189
191
  <h2 class="footer-section-title text-uppercase h6 mb-0">Follow Us</h2>
190
192
  <ul class="list-inline mb-0" aria-label="Social links">
@@ -192,7 +194,17 @@
192
194
  {% if link.url %}
193
195
  <li class="list-inline-item">
194
196
  {% capture link_icon_classes %}{{ site.default_icon }} {{ link.icon | default: "bi-link-45deg" }}{% endcapture %}
195
- <a href="{{ link.url }}" rel="nofollow" class="text-light text-decoration-none">
197
+ {%- comment -%}
198
+ aria-label is load-bearing below the md breakpoint (issue
199
+ #435): the icon is aria-hidden and the label span is
200
+ d-none there, so without it the link had NO accessible
201
+ name at all on mobile — a screen reader announced bare
202
+ "link". It matches the visible text exactly, so the
203
+ accessible name still contains the visible label at md+
204
+ (WCAG 2.5.3 Label in Name).
205
+ {%- endcomment -%}
206
+ <a href="{{ link.url }}" rel="nofollow" class="text-light text-decoration-none"
207
+ aria-label="{{ link.label }}">
196
208
  <i class="{{ link_icon_classes }}" aria-hidden="true"></i>
197
209
  <span class="d-none d-md-inline">{{ link.label }}</span>
198
210
  </a>
@@ -200,16 +212,19 @@
200
212
  {% endif %}
201
213
  {% endfor %}
202
214
  <li class="list-inline-item">
203
- <a class="text-light text-decoration-none" href="{{ '/feed.xml' | relative_url }}">
215
+ {%- comment -%} Same as the social links above (#435). {%- endcomment -%}
216
+ {%- assign _feed_label = ui.feed_label | default: "Feed" -%}
217
+ <a class="text-light text-decoration-none" href="{{ '/feed.xml' | relative_url }}"
218
+ aria-label="{{ _feed_label }}">
204
219
  <i class="{{ site.default_icon }} bi-rss" aria-hidden="true"></i>
205
- <span class="d-none d-md-inline">{{ ui.feed_label | default: "Feed" }}</span>
220
+ <span class="d-none d-md-inline">{{ _feed_label }}</span>
206
221
  </a>
207
222
  </li>
208
223
  </ul>
209
224
  </div>
210
225
  </div>
211
226
 
212
- <!-- Middle: Subscribe + Policies -->
227
+ {% comment %} Middle: Subscribe + Policies {% endcomment %}
213
228
  <div class="row mb-3 mb-lg-4 align-items-center justify-content-center g-2 footer-subscribe-row">
214
229
  <div class="col-12 col-md-8 col-lg-8">
215
230
  <form class="row g-2 footer-subscribe-form" aria-label="Newsletter signup" onsubmit="return false;">
@@ -239,15 +254,15 @@
239
254
  <li class="list-inline-item"><a href="{{ site.terms_of_service_url | default: '/terms-of-service' | relative_url }}" class="text-light text-decoration-none">Terms of Service</a></li>
240
255
  {% endif %}
241
256
  <li class="list-inline-item">
242
- <a href="#" class="text-light text-decoration-none" data-bs-toggle="modal" data-bs-target="#cookieSettingsModal">
257
+ <button type="button" class="footer-inline-button text-light text-decoration-none" data-bs-toggle="modal" data-bs-target="#cookieSettingsModal">
243
258
  Cookie Preferences
244
- </a>
259
+ </button>
245
260
  </li>
246
261
  </ul>
247
262
  </div>
248
263
  </div>
249
264
 
250
- <!-- Bottom: Copyright -->
265
+ {% comment %} Bottom: Copyright {% endcomment %}
251
266
  <div class="text-center">
252
267
  <p class="mb-0">&copy; {{ site.time | date: "%Y" }} {{ site.title | default: site.name }}. All Rights Reserved.</p>
253
268
  </div>
@@ -263,9 +278,9 @@
263
278
  immediately after </footer> is visually identical to nesting them inside.
264
279
  {%- endcomment -%}
265
280
 
266
- <!-- Back to Top Button (controlled by assets/js/back-to-top.js) -->
267
- <!-- Positioning/stacking handled by _sass/components/_back-to-top.scss
268
- via the FAB tokens (bottom-up: back-to-top → chat toggle → TOC FAB) -->
281
+ {% comment %} Back to Top Button (controlled by assets/js/back-to-top.js) {% endcomment %}
282
+ {% comment %} Positioning/stacking handled by _sass/components/_back-to-top.scss
283
+ via the FAB tokens (bottom-up: back-to-top → chat toggle → TOC FAB) {% endcomment %}
269
284
  <a
270
285
  id="backToTopBtn"
271
286
  href="#"