jekyll-theme-zer0 1.19.1 → 1.20.2

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +395 -0
  3. data/README.md +27 -19
  4. data/_data/authors.yml +154 -5
  5. data/_data/backlog.yml +5 -5
  6. data/_data/content_statistics.yml +273 -297
  7. data/_data/features.yml +4 -25
  8. data/_data/navigation/README.md +24 -0
  9. data/_data/navigation/about.yml +2 -0
  10. data/_data/navigation/main.yml +2 -7
  11. data/_data/roadmap.yml +86 -12
  12. data/_includes/components/author-avatar-url.html +28 -0
  13. data/_includes/components/author-bio.html +86 -0
  14. data/_includes/components/author-card.html +184 -121
  15. data/_includes/components/author-eeat.html +10 -4
  16. data/_includes/components/info-section.html +1 -1
  17. data/_includes/components/mermaid.html +0 -3
  18. data/_includes/components/post-card.html +19 -9
  19. data/_includes/content/giscus.html +3 -2
  20. data/_includes/core/footer-fabs.html +28 -0
  21. data/_includes/core/footer.html +7 -17
  22. data/_includes/core/head.html +2 -2
  23. data/_includes/navigation/breadcrumbs.html +20 -2
  24. data/_includes/navigation/local-graph.html +18 -2
  25. data/_includes/obsidian/full-graph.html +4 -6
  26. data/_layouts/article.html +44 -74
  27. data/_layouts/author.html +274 -0
  28. data/_layouts/authors.html +55 -0
  29. data/_layouts/news.html +3 -3
  30. data/_layouts/note.html +21 -6
  31. data/_layouts/notebook.html +21 -6
  32. data/_layouts/root.html +31 -17
  33. data/_layouts/section.html +3 -3
  34. data/_plugins/author_pages_generator.rb +121 -0
  35. data/_sass/components/_author.scss +219 -0
  36. data/_sass/components/_content-tables.scss +16 -1
  37. data/_sass/components/_notes-index.scss +102 -0
  38. data/_sass/components/_search-modal.scss +40 -0
  39. data/_sass/components/_ui-enhancements.scss +570 -0
  40. data/_sass/core/_docs-code-examples.scss +463 -0
  41. data/_sass/core/_docs-layout.scss +0 -453
  42. data/_sass/core/_navbar.scss +253 -0
  43. data/_sass/core/_sidebar-extras.scss +79 -0
  44. data/_sass/core/_toc.scss +87 -0
  45. data/_sass/core/_variables.scss +7 -142
  46. data/_sass/custom.scss +24 -1122
  47. data/_sass/layouts/_global-chrome.scss +59 -0
  48. data/assets/css/main.scss +19 -2
  49. data/assets/js/author-profile.js +190 -0
  50. data/assets/js/modules/navigation/navbar.js +104 -0
  51. data/assets/js/obsidian-graph.js +2 -2
  52. data/assets/js/obsidian-local-graph.js +11 -5
  53. data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
  54. data/scripts/README.md +39 -0
  55. data/scripts/bin/validate +11 -1
  56. data/scripts/dev/css-diff.sh +49 -0
  57. data/scripts/dev/shot.js +37 -0
  58. data/scripts/features/generate-preview-images +110 -6
  59. data/scripts/features/pixelate-preview-images +126 -0
  60. data/scripts/features/pixelate_images.py +662 -0
  61. data/scripts/github-setup.sh +0 -0
  62. data/scripts/lib/preview_generator.py +47 -3
  63. data/scripts/pixelate-preview-images.sh +12 -0
  64. data/scripts/test/integration/auto-version +10 -8
  65. data/scripts/test/lib/run_tests.sh +2 -0
  66. data/scripts/test/lib/test_content_review.sh +205 -0
  67. data/scripts/test/lib/test_pixelate_images.sh +108 -0
  68. metadata +25 -20
  69. data/_data/hub.yml +0 -68
  70. data/_data/hub_index.yml +0 -203
  71. data/_data/navigation/hub.yml +0 -110
  72. data/assets/vendor/font-awesome/css/all.min.css +0 -9
  73. data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  74. data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  75. data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  76. data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  77. data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  78. data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  79. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
  80. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
  81. data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
  82. data/scripts/lib/hub.rb +0 -208
  83. data/scripts/provision-org-sites.rb +0 -252
  84. data/scripts/provision-org-sites.sh +0 -23
  85. data/scripts/sync-hub-metadata.rb +0 -184
  86. data/scripts/sync-hub-metadata.sh +0 -22
@@ -48,8 +48,7 @@ layout: default
48
48
  <!-- Author -->
49
49
  {% if page.author %}
50
50
  <span class="notebook-author" itemprop="author" itemscope itemtype="https://schema.org/Person">
51
- <i class="bi bi-person me-1"></i>
52
- <span itemprop="name">{{ page.author }}</span>
51
+ {% include components/author-card.html author=page.author style="inline" name_itemprop="name" %}
53
52
  </span>
54
53
  <span class="mx-2">•</span>
55
54
  {% endif %}
@@ -92,12 +91,21 @@ layout: default
92
91
  </div>
93
92
 
94
93
  <!-- Tags -->
94
+ {% comment %} Link tag badges only when a tags index exists (configurable via
95
+ `tags_page`); otherwise render plain badges so remote-theme consumers
96
+ without a /tags/ page don't get 404 links. See issue #204. {% endcomment %}
95
97
  {% if page.tags and page.tags.size > 0 %}
98
+ {% assign _tags_base = site.tags_page | default: '/tags/' %}
99
+ {% assign _tags_page = site.html_pages | where: "url", _tags_base | first %}
96
100
  <div class="notebook-tags mb-3">
97
101
  {% for tag in page.tags %}
98
- <a href="{{ '/tags/' | relative_url }}#{{ tag | slugify }}" class="badge bg-secondary text-decoration-none me-1">
99
- {{ tag }}
100
- </a>
102
+ {% if _tags_page %}
103
+ <a href="{{ _tags_base | relative_url }}#{{ tag | slugify }}" class="badge bg-secondary text-decoration-none me-1">
104
+ {{ tag }}
105
+ </a>
106
+ {% else %}
107
+ <span class="badge bg-secondary me-1">{{ tag }}</span>
108
+ {% endif %}
101
109
  {% endfor %}
102
110
  </div>
103
111
  {% endif %}
@@ -129,7 +137,14 @@ layout: default
129
137
  <div class="notebook-content e-content" itemprop="articleBody">
130
138
  {{ content }}
131
139
  </div>
132
-
140
+
141
+ <!-- ================================ -->
142
+ <!-- AUTHOR BIO SECTION -->
143
+ <!-- ================================ -->
144
+ {% if page.author and page.author_profile != false %}
145
+ {% include components/author-bio.html author=page.author %}
146
+ {% endif %}
147
+
133
148
  <!-- ================================ -->
134
149
  <!-- NOTEBOOK FOOTER -->
135
150
  <!-- ================================ -->
data/_layouts/root.html CHANGED
@@ -54,7 +54,8 @@
54
54
  <!-- ============================================== -->
55
55
 
56
56
  <!-- Inline SVG symbols for consistent iconography throughout the site -->
57
- {% include components/svg.html %}
57
+ <!-- Page-invariant: cached once per build via jekyll-include-cache. -->
58
+ {% include_cached components/svg.html %}
58
59
 
59
60
  <!-- Google Tag Manager body code for analytics (if configured) -->
60
61
  <!-- Note: Currently commented out - uncomment when GTM is needed -->
@@ -66,35 +67,45 @@
66
67
  Enable with `navigation.unified_mobile_drawer: true` in _config.yml. -->
67
68
  {% include navigation/unified-drawer.html %}
68
69
 
69
- <!-- Search modal (site-wide) -->
70
- {% include components/search-modal.html %}
70
+ <!-- Search modal (site-wide) — page-invariant, cached once per build. -->
71
+ {% include_cached components/search-modal.html %}
71
72
 
72
- <!-- Keyboard shortcuts help modal (triggered by `?`) -->
73
- {% include components/shortcuts-modal.html %}
73
+ <!-- Keyboard shortcuts help modal (triggered by `?`) — page-invariant. -->
74
+ {% include_cached components/shortcuts-modal.html %}
74
75
 
75
76
  <!-- Site-wide information banner or announcement section -->
76
77
  {% include components/info-section.html %}
77
78
 
78
- <!-- Setup banner for unconfigured sites -->
79
- {% include components/setup-banner.html %}
79
+ <!-- Setup banner for unconfigured sites — page-invariant. -->
80
+ {% include_cached components/setup-banner.html %}
80
81
 
81
82
  <!-- ======================== -->
82
83
  <!-- MAIN CONTENT AREA -->
83
84
  <!-- ======================== -->
84
- <!-- Skip-link target: keep a single, consistent #main-content anchor site-wide -->
85
- <div id="main-content">
85
+ <!-- Skip-link target: keep a single, consistent #main-content anchor site-wide.
86
+ Semantic <main> landmark (consistent with the default/section/news layouts)
87
+ so assistive tech, search engines, and AI content extractors can isolate the
88
+ primary content from the surrounding header/nav/footer chrome. -->
89
+ <main id="main-content">
86
90
  {{ content }}
87
- </div>
91
+ </main>
88
92
 
89
93
  <!-- ======================== -->
90
94
  <!-- FOOTER AND SCRIPTS -->
91
95
  <!-- ======================== -->
92
96
 
93
- <!-- Site footer with links, copyright, and additional navigation -->
94
- {%- include core/footer.html -%}
95
-
96
- <!-- Privacy-compliant cookie consent banner -->
97
- {%- include components/cookie-consent.html -%}
97
+ <!-- Site footer with links, copyright, and additional navigation.
98
+ Page-invariant chrome is cached once per build; the page-dependent
99
+ FAB/local-graph tail renders fresh per page (see core/footer-fabs.html).
100
+ Build-time values inside (e.g. the copyright year from site.time) stay
101
+ fresh: jekyll-include-cache stores in memory only (Jekyll 3.x has no
102
+ Jekyll::Cache) and clears on every build's :pre_render hook — it never
103
+ persists to .jekyll-cache. -->
104
+ {%- include_cached core/footer.html -%}
105
+ {%- include core/footer-fabs.html -%}
106
+
107
+ <!-- Privacy-compliant cookie consent banner — page-invariant. -->
108
+ {%- include_cached components/cookie-consent.html -%}
98
109
 
99
110
  <!-- AI Chat Assistant -->
100
111
  {%- include components/ai-chat.html -%}
@@ -104,8 +115,11 @@
104
115
  <!-- Search functionality -->
105
116
  <!-- Note: Currently commented out - uncomment when search is implemented -->
106
117
 
107
- <!-- External JavaScript libraries and custom scripts -->
108
- {% include components/js-cdn.html %}
118
+ <!-- External JavaScript libraries and custom scripts — page-invariant.
119
+ The "?v=" cache-buster (site.time) in js-cdn.html stays correct:
120
+ the include cache is in-memory and reset every build (see footer
121
+ note above), so v= advances on each build, never frozen across deploys. -->
122
+ {% include_cached components/js-cdn.html %}
109
123
  </div>
110
124
 
111
125
  <!-- Analytics Integration -->
@@ -216,7 +216,7 @@ layout: root
216
216
  </small>
217
217
  {% if featured_in_section.author %}
218
218
  <small class="text-muted">
219
- <i class="bi bi-person me-1"></i>{{ featured_in_section.author }}
219
+ {% include components/author-card.html author=featured_in_section.author style="inline" show_avatar=false %}
220
220
  </small>
221
221
  {% endif %}
222
222
  </div>
@@ -425,7 +425,7 @@ layout: root
425
425
  </span>
426
426
  {% if gpost.author %}
427
427
  <span>
428
- <i class="bi bi-person me-1"></i>{{ gpost.author | truncate: 15 }}
428
+ {% include components/author-card.html author=gpost.author style="inline" show_avatar=false %}
429
429
  </span>
430
430
  {% endif %}
431
431
  </div>
@@ -476,7 +476,7 @@ layout: root
476
476
  <div class="d-flex gap-3 text-muted small">
477
477
  <span><i class="bi bi-calendar me-1"></i>{{ lpost.date | date: "%B %d, %Y" }}</span>
478
478
  {% if lpost.author %}
479
- <span><i class="bi bi-person me-1"></i>{{ lpost.author }}</span>
479
+ <span>{% include components/author-card.html author=lpost.author style="inline" show_avatar=false %}</span>
480
480
  {% endif %}
481
481
  </div>
482
482
  </div>
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # File: author_pages_generator.rb
5
+ # Path: _plugins/author_pages_generator.rb
6
+ # Purpose: Auto-generate an author profile page (/authors/:key/) for every
7
+ # entry in _data/authors.yml, plus an /authors/ index, so author
8
+ # profiles work out of the box without authors hand-creating pages.
9
+ #
10
+ # Mirrors the pattern in search_and_sitemap_generator.rb (PageWithoutFile,
11
+ # safe true, low priority, idempotent, opt-out via _config.yml).
12
+ #
13
+ # Behaviour:
14
+ # - One /authors/:key/ page per author (layout: author).
15
+ # - One /authors/ index page (layout: authors).
16
+ # - If a page OR collection document already exists at the target permalink it
17
+ # is left untouched. This site's committed author pages live in
18
+ # pages/_about/authors/ (the `about` collection) with explicit
19
+ # /authors/:key/ permalinks, so they build under GitHub Pages safe mode.
20
+ # - An author entry with `profile: false` in _data/authors.yml is skipped.
21
+ # - Generation can be disabled via _config.yml:
22
+ # authors:
23
+ # generate_pages: false
24
+ #
25
+ # Note: like the other generators in this directory, this runs during a normal
26
+ # `jekyll build`. Sites consuming the theme remotely on GitHub Pages (which does
27
+ # not load custom plugins) can still create author pages by hand using the
28
+ # `author` / `authors` layouts shipped with the theme.
29
+ #
30
+
31
+ module Jekyll
32
+ class AuthorPagesGenerator < Generator
33
+ safe true
34
+ priority :low
35
+
36
+ def generate(site)
37
+ return if generation_disabled?(site)
38
+
39
+ authors = site.data["authors"]
40
+ return unless authors.is_a?(Hash)
41
+
42
+ generate_author_profiles(site, authors)
43
+ generate_authors_index(site)
44
+ end
45
+
46
+ private
47
+
48
+ def generate_author_profiles(site, authors)
49
+ authors.each do |key, data|
50
+ data ||= {}
51
+ next if data["profile"] == false
52
+
53
+ permalink = "/authors/#{Jekyll::Utils.slugify(key)}/"
54
+ next if page_exists?(site, permalink)
55
+
56
+ name = data["name"] || key
57
+ Jekyll.logger.info "AuthorPagesGenerator:", "Generating profile #{permalink}"
58
+
59
+ page = PageWithoutAFile.new(site, site.source, "", "#{key}.html")
60
+ page.data.merge!(
61
+ "layout" => "author",
62
+ "author_key" => key,
63
+ "title" => name,
64
+ "description" => data["bio"] || "Articles and content by #{name}.",
65
+ "permalink" => permalink,
66
+ "sitemap" => true,
67
+ "sidebar" => false,
68
+ "hide_intro" => true
69
+ )
70
+ page.content = ""
71
+
72
+ site.pages << page
73
+ end
74
+ end
75
+
76
+ def generate_authors_index(site)
77
+ return if page_exists?(site, "/authors/")
78
+
79
+ Jekyll.logger.info "AuthorPagesGenerator:", "Generating /authors/ index"
80
+
81
+ page = PageWithoutAFile.new(site, site.source, "", "authors.html")
82
+ page.data.merge!(
83
+ "layout" => "authors",
84
+ "title" => "Authors",
85
+ "description" => "Meet the people behind the content.",
86
+ "permalink" => "/authors/",
87
+ "sidebar" => false,
88
+ "hide_intro" => true
89
+ )
90
+ page.content = ""
91
+
92
+ site.pages << page
93
+ end
94
+
95
+ # ------------------------------------------------------------------
96
+ # Helpers
97
+ # ------------------------------------------------------------------
98
+
99
+ def page_exists?(site, permalink)
100
+ normalized = permalink.chomp("/")
101
+ page_match = site.pages.any? do |p|
102
+ url = (p.url || "").chomp("/")
103
+ perm = (p.permalink || "").chomp("/")
104
+ url == normalized || url == permalink || perm == normalized || perm == permalink
105
+ end
106
+ return true if page_match
107
+
108
+ # Committed author pages now live in the `about` collection
109
+ # (pages/_about/authors/*.md), so also honour collection documents.
110
+ site.documents.any? do |d|
111
+ url = (d.url || "").chomp("/")
112
+ perm = (d.data["permalink"] || "").to_s.chomp("/")
113
+ url == normalized || url == permalink || perm == normalized || perm == permalink
114
+ end
115
+ end
116
+
117
+ def generation_disabled?(site)
118
+ site.config.dig("authors", "generate_pages") == false
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,219 @@
1
+ // ============================================================================
2
+ // Author — bylines, "About the Author" cards, and profile pages
3
+ // ----------------------------------------------------------------------------
4
+ // Pairs with components/author-card.html, components/author-bio.html, and the
5
+ // author / authors layouts. Token-driven with Bootstrap fallbacks; dark-mode
6
+ // safe (avatars and links inherit surrounding color via `text-reset`).
7
+ // ============================================================================
8
+
9
+ // Inline byline — meta rows and card footers
10
+ .author-inline {
11
+ vertical-align: baseline;
12
+
13
+ &__avatar {
14
+ flex: 0 0 auto;
15
+ }
16
+
17
+ &__link {
18
+ transition: color var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard);
19
+
20
+ &:hover .author-name,
21
+ &:focus-visible .author-name {
22
+ text-decoration: underline;
23
+ text-underline-offset: 2px;
24
+ }
25
+
26
+ &:focus-visible {
27
+ outline: 2px solid var(--zer0-color-primary, var(--bs-primary));
28
+ outline-offset: 2px;
29
+ border-radius: 0.25rem;
30
+ }
31
+ }
32
+ }
33
+
34
+ // Circular fallback used when an author has no avatar image
35
+ .author-avatar-fallback {
36
+ flex: 0 0 auto;
37
+ background: var(--zer0-color-primary, var(--bs-primary));
38
+ }
39
+
40
+ // "About the Author" card + profile hero avatar ring
41
+ .author-card,
42
+ .author-card-compact {
43
+ .author-card__avatar,
44
+ img.rounded-circle {
45
+ border: 2px solid rgba(var(--zer0-color-primary-rgb, var(--bs-primary-rgb)), 0.25);
46
+ }
47
+ }
48
+
49
+ // Expertise chips
50
+ .author-expertise .badge {
51
+ font-weight: 500;
52
+ }
53
+
54
+ // Shared tagline styling (full "About the Author" card + profile hero)
55
+ .author-tagline {
56
+ color: var(--zer0-color-text-muted, var(--bs-secondary-color));
57
+ }
58
+
59
+ // AI author persona markers — a violet accent (the "AI" convention) chosen to
60
+ // stay legible across all skins independent of --zer0-color-primary.
61
+ .author-ai-badge {
62
+ background-color: rgba(111, 66, 193, 0.14);
63
+ color: #6f42c1;
64
+ border: 1px solid rgba(111, 66, 193, 0.4);
65
+ font-size: 0.7em;
66
+ font-weight: 600;
67
+ letter-spacing: 0.04em;
68
+ text-transform: uppercase;
69
+ vertical-align: middle;
70
+
71
+ [data-bs-theme="dark"] & {
72
+ color: #c8a2ff;
73
+ background-color: rgba(111, 66, 193, 0.28);
74
+ border-color: rgba(111, 66, 193, 0.55);
75
+ }
76
+ }
77
+
78
+ .author-ai-disclosure {
79
+ border-left: 3px solid rgba(111, 66, 193, 0.5);
80
+ padding-left: 0.65rem;
81
+ }
82
+
83
+ // ----------------------------------------------------------------------------
84
+ // Author profile page (_layouts/author.html)
85
+ // ----------------------------------------------------------------------------
86
+
87
+ // Profile hero
88
+ .author-hero {
89
+ background: var(--zer0-color-bg-elevated, var(--bs-tertiary-bg));
90
+
91
+ &__avatar {
92
+ border: 3px solid rgba(var(--zer0-color-primary-rgb, var(--bs-primary-rgb)), 0.35);
93
+ box-shadow: var(--zer0-shadow-sm);
94
+ }
95
+
96
+ &__tagline {
97
+ font-size: 1.05rem;
98
+ }
99
+ }
100
+
101
+ // Interactive content hub
102
+ .author-content {
103
+ // Stats dashboard — doubles as the type-filter control group
104
+ .author-stats {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: 0.75rem;
108
+ }
109
+
110
+ .author-stat {
111
+ flex: 1 1 auto;
112
+ min-width: 5.5rem;
113
+ display: flex;
114
+ flex-direction: column;
115
+ align-items: center;
116
+ gap: 0.15rem;
117
+ padding: 0.75rem 1rem;
118
+ background: var(--zer0-color-bg-elevated, var(--bs-tertiary-bg));
119
+ border: 1px solid var(--zer0-color-border, var(--bs-border-color));
120
+ border-radius: 0.625rem;
121
+ color: inherit;
122
+ cursor: pointer;
123
+ transition:
124
+ transform var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard),
125
+ box-shadow var(--zer0-motion-duration-base, 200ms) var(--zer0-motion-ease-standard),
126
+ border-color var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard),
127
+ background-color var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard);
128
+
129
+ &:hover {
130
+ transform: translateY(-2px);
131
+ box-shadow: var(--zer0-shadow-md);
132
+ border-color: rgba(var(--zer0-color-primary-rgb, var(--bs-primary-rgb)), 0.5);
133
+ }
134
+
135
+ &:focus-visible {
136
+ outline: 2px solid var(--zer0-color-primary, var(--bs-primary));
137
+ outline-offset: 2px;
138
+ }
139
+
140
+ &.is-active {
141
+ background: var(--zer0-color-primary, var(--bs-primary));
142
+ border-color: var(--zer0-color-primary, var(--bs-primary));
143
+ color: #fff;
144
+ }
145
+
146
+ &__num {
147
+ font-size: 1.5rem;
148
+ font-weight: 700;
149
+ line-height: 1;
150
+ }
151
+
152
+ &__label {
153
+ font-size: 0.8rem;
154
+ white-space: nowrap;
155
+ }
156
+ }
157
+
158
+ // Search input with a leading icon
159
+ .author-search__icon {
160
+ position: absolute;
161
+ top: 50%;
162
+ left: 0.75rem;
163
+ transform: translateY(-50%);
164
+ color: var(--bs-secondary-color);
165
+ pointer-events: none;
166
+ }
167
+
168
+ .author-search .form-control {
169
+ padding-left: 2.25rem;
170
+ }
171
+
172
+ // Topic / tag cloud
173
+ .author-tagcloud {
174
+ display: flex;
175
+ flex-wrap: wrap;
176
+ align-items: center;
177
+ gap: 0.35rem;
178
+ }
179
+
180
+ .author-tag {
181
+ border: 1px solid var(--zer0-color-border, var(--bs-border-color));
182
+ background: transparent;
183
+ color: var(--bs-body-color);
184
+ cursor: pointer;
185
+ transition:
186
+ background-color var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard),
187
+ color var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard),
188
+ border-color var(--zer0-motion-duration-fast, 120ms) var(--zer0-motion-ease-standard);
189
+
190
+ &:hover { border-color: rgba(var(--zer0-color-primary-rgb, var(--bs-primary-rgb)), 0.6); }
191
+ &:focus-visible { outline: 2px solid var(--zer0-color-primary, var(--bs-primary)); outline-offset: 2px; }
192
+
193
+ &.is-active {
194
+ background: var(--zer0-color-primary, var(--bs-primary));
195
+ border-color: var(--zer0-color-primary, var(--bs-primary));
196
+ color: #fff;
197
+ }
198
+ }
199
+
200
+ // Grid item entrance animation — only when motion is allowed. Re-runs each
201
+ // time an item returns from display:none, giving a gentle filter transition.
202
+ @media (prefers-reduced-motion: no-preference) {
203
+ .author-item:not(.d-none) {
204
+ animation: authorItemIn var(--zer0-motion-duration-base, 220ms) var(--zer0-motion-ease-standard) both;
205
+ }
206
+ }
207
+ }
208
+
209
+ @keyframes authorItemIn {
210
+ from { opacity: 0; transform: translateY(6px); }
211
+ to { opacity: 1; transform: translateY(0); }
212
+ }
213
+
214
+ // Authors index directory cards
215
+ .authors-index {
216
+ .author-card-compact {
217
+ width: 100%;
218
+ }
219
+ }
@@ -149,7 +149,14 @@ $zer0-content-table-scopes: (
149
149
  margin-bottom: var(--zer0-space-4);
150
150
  border: 1px solid var(--zer0-color-border);
151
151
  border-radius: 0.5rem;
152
- overflow: visible;
152
+ // Wide tables must scroll WITHIN this card, not push the page wider. The
153
+ // wrapper sits between the content scope and the table, so the
154
+ // `#{$scope} > table` mobile fallback below no longer matches — without a
155
+ // scroll context here a wide markdown table forces a horizontal page
156
+ // scrollbar, which makes the fixed-top navbar look "cut off" on the right.
157
+ // (Pairs with the `html { overflow-x: clip }` safety net in _global-chrome.)
158
+ overflow-x: auto;
159
+ -webkit-overflow-scrolling: touch;
153
160
  background-color: var(--zer0-color-bg);
154
161
 
155
162
  > .content-table-toolbar {
@@ -256,5 +263,13 @@ $zer0-content-table-scopes: (
256
263
  }
257
264
  }
258
265
  }
266
+
267
+ // Long unbroken inline-code tokens (URLs, hashes, flags) must wrap rather
268
+ // than force a horizontal page scrollbar on narrow viewports. Scoped to
269
+ // inline code only — block code (`pre code`) keeps its own scroll context.
270
+ :not(pre) > code {
271
+ overflow-wrap: break-word;
272
+ word-break: break-word;
273
+ }
259
274
  }
260
275
  }
@@ -0,0 +1,102 @@
1
+ // ============================================================================
2
+
3
+ // Notes & notebooks index grids + difficulty badges
4
+
5
+ // ----------------------------------------------------------------------------
6
+
7
+ // Extracted from custom.scss (Phase 5).
8
+
9
+ // ============================================================================
10
+
11
+ // ==============================================================================
12
+ // NOTES LAYOUT STYLES
13
+ // ==============================================================================
14
+ // MOVED → _sass/components/_notes.scss (token-aware, dark-mode-safe)
15
+ // The .note-header / .note-content / .related-notes / .note-navigation rules
16
+ // are now defined in the components partial. Difficulty badges remain here
17
+ // because they map to fixed semantic colors per the existing API.
18
+
19
+ // Difficulty badge colors (semantic — kept as fixed values)
20
+ .badge-beginner {
21
+ background-color: var(--zer0-color-success, #198754) !important;
22
+ }
23
+
24
+ .badge-intermediate {
25
+ background-color: var(--zer0-color-warning, #ffc107) !important;
26
+ color: #212529 !important;
27
+ }
28
+
29
+ .badge-advanced {
30
+ background-color: var(--zer0-color-danger, #dc3545) !important;
31
+ }
32
+
33
+ // Share buttons
34
+ .share-buttons {
35
+ margin-top: 2rem;
36
+ padding-top: 1.5rem;
37
+ border-top: 1px solid #dee2e6;
38
+
39
+ .btn-share {
40
+ padding: 0.5rem 1rem;
41
+ border-radius: 0.375rem;
42
+ text-decoration: none;
43
+ display: inline-flex;
44
+ align-items: center;
45
+ gap: 0.5rem;
46
+ transition: transform 0.2s ease;
47
+
48
+ &:hover {
49
+ transform: scale(1.05);
50
+ }
51
+ }
52
+ }
53
+
54
+ // Notes index page styles
55
+ #notes-grid {
56
+ .note-card {
57
+ transition: opacity 0.3s ease, transform 0.3s ease;
58
+
59
+ &[style*="display: none"] {
60
+ opacity: 0;
61
+ transform: scale(0.95);
62
+ }
63
+ }
64
+
65
+ .card {
66
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
67
+
68
+ &:hover {
69
+ transform: translateY(-4px);
70
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
71
+ }
72
+ }
73
+ }
74
+
75
+ // ==============================================================================
76
+ // NOTEBOOKS INDEX PAGE STYLES
77
+ // ==============================================================================
78
+
79
+ #notebooks-grid {
80
+ .notebook-card {
81
+ transition: opacity 0.3s ease, transform 0.3s ease;
82
+
83
+ &[style*="display: none"] {
84
+ opacity: 0;
85
+ transform: scale(0.95);
86
+ }
87
+ }
88
+
89
+ .card {
90
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
91
+ border-left: 4px solid #0d6efd;
92
+
93
+ &:hover {
94
+ transform: translateY(-4px);
95
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
96
+ }
97
+
98
+ .card-header {
99
+ border-bottom: none;
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,40 @@
1
+ // ============================================================================
2
+ // Search modal — quick-search overlay + collapsible folder tree
3
+ // ----------------------------------------------------------------------------
4
+ // Extracted from custom.scss. Styles the search modal (triggered from the
5
+ // navbar Search button) and the nested folder list used in its results.
6
+ // Selectors are unique to this component, so import position is order-neutral.
7
+ // ============================================================================
8
+
9
+ .search-modal .modal-content {
10
+ border-radius: 0.75rem;
11
+ }
12
+
13
+ .search-modal .input-group-text {
14
+ background-color: var(--bs-tertiary-bg);
15
+ border-color: var(--bs-border-color);
16
+ }
17
+
18
+ .search-modal kbd {
19
+ background-color: var(--bs-tertiary-bg);
20
+ color: var(--bs-body-color);
21
+ }
22
+
23
+ .search-modal .search-results {
24
+ max-height: 50vh;
25
+ overflow-y: auto;
26
+ }
27
+
28
+ .search-modal .list-group-item {
29
+ border-color: var(--bs-border-color-translucent);
30
+ }
31
+
32
+ .nested-list-group {
33
+ display: none;
34
+ }
35
+ .nested-list-group.show {
36
+ display: block;
37
+ }
38
+ .folder {
39
+ cursor: pointer;
40
+ }