ultimate-jekyll-manager 1.4.3 → 1.6.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 (66) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CLAUDE-ATTRIBUTION.md +215 -0
  3. package/CLAUDE.md +7 -6
  4. package/README.md +1 -0
  5. package/dist/assets/css/pages/test/libraries/layers/index.scss +28 -0
  6. package/dist/assets/js/modules/redirect.js +5 -4
  7. package/dist/assets/js/pages/download/index.js +1 -1
  8. package/dist/assets/js/pages/feedback/index.js +1 -1
  9. package/dist/assets/js/pages/test/libraries/layers/index.js +11 -0
  10. package/dist/assets/themes/_template/README.md +50 -0
  11. package/dist/assets/themes/_template/_config.scss +60 -0
  12. package/dist/assets/themes/_template/_theme.js +13 -4
  13. package/dist/assets/themes/_template/_theme.scss +16 -4
  14. package/dist/assets/themes/_template/css/base/_root.scss +19 -0
  15. package/dist/assets/themes/_template/css/components/_components.scss +23 -0
  16. package/dist/assets/themes/classy/README.md +18 -6
  17. package/dist/assets/themes/neobrutalism/README.md +98 -0
  18. package/dist/assets/themes/neobrutalism/_config.scss +139 -0
  19. package/dist/assets/themes/neobrutalism/_theme.js +27 -0
  20. package/dist/assets/themes/neobrutalism/_theme.scss +33 -0
  21. package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
  22. package/dist/assets/themes/neobrutalism/css/base/_root.scss +80 -0
  23. package/dist/assets/themes/neobrutalism/css/base/_typography.scss +77 -0
  24. package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +25 -0
  25. package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
  26. package/dist/assets/themes/neobrutalism/css/components/_cards.scss +69 -0
  27. package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
  28. package/dist/assets/themes/neobrutalism/css/components/_infinite-scroll.scss +94 -0
  29. package/dist/assets/themes/neobrutalism/css/layout/_general.scss +200 -0
  30. package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +153 -0
  31. package/dist/assets/themes/neobrutalism/js/initialize-tooltips.js +20 -0
  32. package/dist/assets/themes/neobrutalism/js/navbar-scroll.js +29 -0
  33. package/dist/assets/themes/neobrutalism/pages/index.scss +227 -0
  34. package/dist/assets/themes/neobrutalism/pages/pricing/index.scss +267 -0
  35. package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.js +9 -0
  36. package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.scss +7 -0
  37. package/dist/build.js +2 -5
  38. package/dist/commands/install.js +1 -1
  39. package/dist/commands/setup.js +41 -0
  40. package/dist/defaults/CLAUDE.md +5 -1
  41. package/dist/defaults/dist/_includes/core/head.html +17 -0
  42. package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +4 -4
  43. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +2 -0
  44. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +7 -3
  45. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/core/base.html +31 -0
  46. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/index.html +345 -0
  47. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +483 -0
  48. package/dist/defaults/dist/pages/test/libraries/layers.html +57 -0
  49. package/dist/defaults/src/_config.yml +2 -0
  50. package/dist/defaults/test/_init.js +10 -0
  51. package/dist/gulp/tasks/defaults.js +8 -0
  52. package/dist/gulp/tasks/sass.js +43 -2
  53. package/dist/gulp/tasks/translation.js +11 -0
  54. package/dist/gulp/tasks/utils/manage-test-layers.js +97 -0
  55. package/dist/index.js +30 -4
  56. package/dist/test/runner.js +62 -0
  57. package/dist/test/suites/build/manager.test.js +11 -4
  58. package/dist/test/suites/build/mode-helpers.test.js +54 -2
  59. package/dist/utils/mode-helpers.js +65 -40
  60. package/docs/assets.md +6 -1
  61. package/docs/environment-detection.md +85 -0
  62. package/docs/test-framework.md +48 -3
  63. package/docs/themes.md +451 -0
  64. package/package.json +2 -1
  65. package/docs/cross-context-helpers.md +0 -75
  66. package/package copy.json +0 -75
@@ -0,0 +1,7 @@
1
+ // /test — Theme (neobrutalism) page CSS — the #theme layer.
2
+ // Loads AFTER the universal layer and turns the "css-theme" dot green.
3
+ // Compiles standalone, so no theme tokens are needed here — just the proof.
4
+
5
+ .layer-dot[data-layer="css-theme"] {
6
+ background: #30a46c; // green
7
+ }
package/dist/build.js CHANGED
@@ -110,11 +110,8 @@ Manager.actLikeProduction = function () {
110
110
  }
111
111
  Manager.prototype.actLikeProduction = Manager.actLikeProduction;
112
112
 
113
- // getEnvironment (calls isServer ? 'production' : 'development')
114
- Manager.getEnvironment = function () {
115
- return Manager.isServer() ? 'production' : 'development';
116
- }
117
- Manager.prototype.getEnvironment = Manager.getEnvironment;
113
+ // getEnvironment() is the SSOT and lives in src/utils/mode-helpers.js (alongside the is*()
114
+ // family). It's mixed onto the Manager via the attachTo() call below, same as in EM/BXM.
118
115
 
119
116
  // getConfig: requires and parses config.yml
120
117
  Manager.getConfig = function (type) {
@@ -17,7 +17,7 @@ module.exports = async function (options) {
17
17
 
18
18
  try {
19
19
  // Install production
20
- if (['prod', 'p', 'production'].includes(type)) {
20
+ if (['live', 'prod', 'p', 'production'].includes(type)) {
21
21
  // Log
22
22
  logger.log('Installing production...');
23
23
 
@@ -39,6 +39,7 @@ module.exports = async function (options) {
39
39
  options.checkLocality = options.checkLocality !== 'false';
40
40
  options.publishGitHubToken = options.publishGitHubToken !== 'false';
41
41
  options.deduplicatePosts = options.deduplicatePosts !== 'false';
42
+ options.removeLegacyTeamMembers = options.removeLegacyTeamMembers !== 'false';
42
43
  options.migrate = options.migrate !== 'false';
43
44
 
44
45
  // Quick mode: skip slow/network operations
@@ -137,6 +138,11 @@ module.exports = async function (options) {
137
138
  if (options.deduplicatePosts) {
138
139
  await deduplicatePosts();
139
140
  }
141
+
142
+ // Remove legacy default team members (first-name-only format, e.g. team/alex)
143
+ if (options.removeLegacyTeamMembers) {
144
+ await removeLegacyTeamMembers();
145
+ }
140
146
  };
141
147
 
142
148
  // --- Version check functions ---
@@ -527,6 +533,41 @@ async function deduplicatePosts() {
527
533
  }
528
534
  }
529
535
 
536
+ async function removeLegacyTeamMembers() {
537
+ // Legacy default team members that used the first-name-only format.
538
+ // These shipped before team members were renamed to first-last (e.g. team/alex -> team/alex-raeburn).
539
+ const legacyTeamMembers = ['alex'];
540
+
541
+ logger.log('Checking for legacy default team members to remove...');
542
+
543
+ let removedCount = 0;
544
+
545
+ legacyTeamMembers.forEach((slug) => {
546
+ // The _team collection file (any markdown/html extension)
547
+ const memberFiles = glob(`src/_team/${slug}.{md,markdown,html}`, { nodir: true });
548
+
549
+ memberFiles.forEach((filePath) => {
550
+ jetpack.remove(filePath);
551
+ logger.log(` ✓ Removed legacy team member: ${filePath}`);
552
+ removedCount++;
553
+ });
554
+
555
+ // The associated image directory (src/assets/images/team/<slug>)
556
+ const imageDir = path.join(process.cwd(), 'src', 'assets', 'images', 'team', slug);
557
+
558
+ if (jetpack.exists(imageDir)) {
559
+ jetpack.remove(imageDir);
560
+ logger.log(` ✓ Removed legacy team member image directory: team/${slug}`);
561
+ }
562
+ });
563
+
564
+ if (removedCount > 0) {
565
+ logger.log(logger.format.green(`✓ Removed ${removedCount} legacy team member(s)`));
566
+ } else {
567
+ logger.log('No legacy team members found');
568
+ }
569
+ }
570
+
530
571
  // --- Migration functions ---
531
572
 
532
573
  async function migrate() {
@@ -29,8 +29,12 @@ npm run build # production build (UJ_BUILD_MODE=true): clean → setup →
29
29
  npm run deploy # build → `npu sync --message='Deploy'` (publishes _site/)
30
30
  npx mgr test # run framework + project test suites (build / page / boot layers)
31
31
  npx mgr audit # HTML validation + spellcheck + optional Lighthouse
32
+ npx mgr install dev # use LOCAL ultimate-jekyll-manager source (to test framework edits)
33
+ npx mgr install live # restore the published ultimate-jekyll-manager from npm
32
34
  ```
33
35
 
36
+ > Editing the UJM framework source while working here? Run `npx mgr install dev` so this project picks up your uncommitted framework changes (it otherwise uses its installed `node_modules/ultimate-jekyll-manager`). Run `npx mgr install live` to switch back.
37
+
34
38
  ## Where things live
35
39
 
36
40
  - `src/_config.yml` — Jekyll config: brand, theme, meta, web_manager (Firebase). `Manager.getConfig('project')` reads this. **`brand.id` + `theme.id` are required.**
@@ -72,7 +76,7 @@ At build time, `require('ultimate-jekyll-manager/build')` exposes:
72
76
  - `Manager.getConfig(type)` — read `_config.yml` (`'project'` or `'main'`)
73
77
  - `Manager.getPackage(type)` — read `package.json` (`'project'` or `'main'`)
74
78
  - `Manager.getUJMConfig()` — read `config/ultimate-jekyll-manager.json`
75
- - `Manager.getEnvironment()` — `'development'` or `'production'`
79
+ - `Manager.getEnvironment()` — `'development' | 'testing' | 'production'` (mutually exclusive; testing wins). Gate side effects on the intentional check (`isProduction()` for prod-only; `isDevelopment() || isTesting()` for local-or-test) — never `!isDevelopment()`.
76
80
  - `Manager.isBuildMode()` / `isQuickMode()` / `isServer()` / `actLikeProduction()` — env-gated flags
77
81
  - `Manager.logger(name)` — timestamped logger instance
78
82
  - `Manager.require(path)` — escape hatch for UJM transitive deps (use sparingly)
@@ -26,12 +26,15 @@
26
26
 
27
27
  {% iftruthy page.resolved.asset_path %}
28
28
  {% capture page-css-path %}/assets/css/pages/{{ page.resolved.asset_path }}.bundle.css{% endcapture %}
29
+ {% capture page-css-theme-path %}/assets/css/pages/{{ page.resolved.asset_path }}.{{ page.resolved.theme.id }}.bundle.css{% endcapture %}
29
30
  {% endiftruthy %}
30
31
  {% iffalsy page.resolved.asset_path %}
31
32
  {% if page.canonical.path == "/" %}
32
33
  {% capture page-css-path %}/assets/css/pages/index.bundle.css{% endcapture %}
34
+ {% capture page-css-theme-path %}/assets/css/pages/index.{{ page.resolved.theme.id }}.bundle.css{% endcapture %}
33
35
  {% else %}
34
36
  {% capture page-css-path %}/assets/css/pages{{ page.canonical.path }}/index.bundle.css{% endcapture %}
37
+ {% capture page-css-theme-path %}/assets/css/pages{{ page.canonical.path }}/index.{{ page.resolved.theme.id }}.bundle.css{% endcapture %}
35
38
  {% endif %}
36
39
  {% endiffalsy %}
37
40
 
@@ -210,6 +213,20 @@
210
213
  {% endif %}
211
214
  {% endiffile %}
212
215
 
216
+ <!-- Then, the active theme's page-specific bundle (loaded AFTER base so it can override).
217
+ Only present when the theme ships page CSS for this path — otherwise nothing loads
218
+ and the theme's component/general styles handle the page. No fallback needed. -->
219
+ {% iffile page-css-theme-path %}
220
+ <link rel="stylesheet" type="text/css" href="{{ site.url }}{{ page-css-theme-path }}?cb={{ site.uj.cache_breaker }}"/>
221
+
222
+ <!-- Dev log -->
223
+ {% if jekyll.environment == "development" %}
224
+ <script>
225
+ console.info("Theme page-specific css loading: #main{{ page-css-theme-path }}");
226
+ </script>
227
+ {% endif %}
228
+ {% endiffile %}
229
+
213
230
  <!-- Style - Scripts are Disabled -->
214
231
  <noscript>
215
232
  {{ page-ie-script }}
@@ -70,10 +70,10 @@
70
70
  </div>
71
71
 
72
72
  <!-- Bottom Section -->
73
- <div class="row mt-4 pt-3 border-top">
73
+ <div class="row mt-4 pt-3 border-top align-items-center">
74
74
  <div class="col-md-6 d-flex align-items-center">
75
75
  <!-- Language Dropdown -->
76
- <div class="d-inline-block me-3">
76
+ <div class="d-flex align-items-center me-3">
77
77
  <div class="dropup uj-language-dropdown">
78
78
  <button class="btn btn-sm btn-outline-adaptive dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
79
79
  <i class="fa fa-sm me-1">
@@ -85,7 +85,7 @@
85
85
  </button>
86
86
  <ul class="dropdown-menu">
87
87
  {% assign default_language = site.translation.default | default: "en" %}
88
- {% if site.translation.languages.size > 0 %}
88
+ {% if site.translation.enabled and site.translation.languages.size > 0 %}
89
89
  {% assign all_languages = site.translation.languages | push: default_language %}
90
90
  {% else %}
91
91
  {% assign all_languages = "" | split: "" | push: default_language %}
@@ -105,7 +105,7 @@
105
105
 
106
106
  <!-- Social Links -->
107
107
  {% if data.socials.enabled %}
108
- <div class="d-inline-block">
108
+ <div class="d-flex align-items-center">
109
109
  {% if data.socials.list and data.socials.list.size > 0 %}
110
110
  {% assign social_list = data.socials.list %}
111
111
  {% else %}
@@ -330,6 +330,7 @@ cta:
330
330
  <a
331
331
  href="{{ config_url }}"
332
332
  class="btn btn-primary btn-lg d-flex align-items-center justify-content-center"
333
+ data-download="true"
333
334
  >
334
335
  {% uj_icon type.icon, "fa-3xl me-2" %}
335
336
  {{ type.name }}
@@ -339,6 +340,7 @@ cta:
339
340
  <button
340
341
  type="button"
341
342
  class="btn btn-primary btn-lg d-flex align-items-center justify-content-center"
343
+ data-download="true"
342
344
  >
343
345
  {% uj_icon type.icon, "fa-3xl me-2" %}
344
346
  {{ type.name }}
@@ -167,9 +167,13 @@ prerender_icons:
167
167
  <img src="https://cdn.itwcreativeworks.com/assets/general/images/feedback/love.png?cb={{ site.uj.cache_breaker }}" alt="Thank you" width="80" height="80">
168
168
  </div>
169
169
  <h3 class="mb-3" id="review-modal-label">You're awesome!</h3>
170
- <p class="text-muted mb-4">
171
- We're so glad you love {{ site.brand.name }}! Would you mind sharing your experience on a review site? It really helps us grow.
170
+ <p class="text-muted mb-3">
171
+ We're so glad you love {{ site.brand.name }}! Share your experience on a review site to be entered to win a $100 gift card.
172
172
  </p>
173
+ <div class="alert alert-warning text-start small mb-4" role="alert">
174
+ {% uj_icon "circle-exclamation", "me-1" %}
175
+ You must actually post your review to be eligible for the $100 gift card &mdash; closing this without publishing a review won't qualify.
176
+ </div>
173
177
 
174
178
  <!-- Copy-paste feedback -->
175
179
  <div class="text-start mb-4">
@@ -188,7 +192,7 @@ prerender_icons:
188
192
 
189
193
  <a id="review-modal-link" href="#" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-lg mb-3">
190
194
  {% uj_icon "arrow-up-right-from-square", "me-2" %}
191
- Write a review
195
+ Post your review
192
196
  </a>
193
197
  <div>
194
198
  <button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">Maybe later</button>
@@ -0,0 +1,31 @@
1
+ ---
2
+ ### ALL PAGES ###
3
+ layout: core/root
4
+
5
+ ### THEME CONFIG ###
6
+ theme:
7
+ # html:
8
+ # class: ""
9
+ head:
10
+ content: |
11
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;600;800;900&family=Space+Grotesk:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" media="print" onload="this.media='all'">
12
+ body:
13
+ class: "d-flex flex-column min-vh-100"
14
+ # main:
15
+ # class: "flex-fill"
16
+ ---
17
+
18
+ <!-- Nav (controlled by page/layout front matter) -->
19
+ {% unless page.resolved.theme.nav.enabled == false %}
20
+ {%- include /frontend/sections/nav.html -%}
21
+ {% endunless %}
22
+
23
+ <!-- Main Content -->
24
+ <main id="main-content" class="flex-fill {{ page.resolved.theme.main.class | uj_liquify }}" aria-label="Main content">
25
+ {{ content | uj_content_format }}
26
+ </main>
27
+
28
+ <!-- Footer (controlled by page/layout front matter) -->
29
+ {% unless page.resolved.theme.footer.enabled == false %}
30
+ {%- include /frontend/sections/footer.html -%}
31
+ {% endunless %}
@@ -0,0 +1,345 @@
1
+ ---
2
+ ### ALL PAGES ###
3
+ layout: themes/[ site.theme.id ]/frontend/core/base
4
+
5
+ #### THEME CONFIG ####
6
+ theme:
7
+ main:
8
+ class: "pt-0"
9
+
10
+ ### PAGE CONFIG ###
11
+ # Hero Section
12
+ hero:
13
+ tagline: "Introducing {{ site.brand.name }}"
14
+ headline: "The #1 platform for business"
15
+ headline_accent: "success"
16
+ description: "AI automation for modern businesses made simple"
17
+ primary_button:
18
+ text: "Get started free"
19
+ icon: "rocket"
20
+ href: "/dashboard"
21
+ secondary_button:
22
+ text: "See pricing"
23
+ icon: "book-open"
24
+ href: "/pricing"
25
+
26
+ # Trusted By Section
27
+ trusted_by:
28
+ headline: "Trusted by the best"
29
+ logos:
30
+ - name: "amazon"
31
+ - name: "google"
32
+ - name: "hubspot"
33
+ - name: "paypal"
34
+ - name: "slack"
35
+ - name: "shopify"
36
+ - name: "stripe"
37
+
38
+ # Showcase Section
39
+ showcase:
40
+ superheadline:
41
+ icon: "cubes"
42
+ text: "Showcase"
43
+ headline: "What makes {{ site.brand.name }}"
44
+ headline_accent: "different"
45
+ subheadline: "Powerful features designed to accelerate your success"
46
+ items:
47
+ - title: "Seamless integration"
48
+ description: "Connect with your existing tools and platforms effortlessly. Our system automatically syncs data and streamlines your workflow for maximum efficiency."
49
+ - title: "Data-driven insights"
50
+ description: "Make informed decisions with real-time analytics and actionable insights that help you understand what matters most to your business."
51
+ - title: "Smart automation"
52
+ description: "Focus on what matters while our intelligent platform handles routine tasks automatically, working 24/7 to keep your business running smoothly."
53
+
54
+ # Features Section
55
+ features:
56
+ superheadline:
57
+ icon: "sparkles"
58
+ text: "How it works"
59
+ headline: "Design made"
60
+ headline_accent: "simple"
61
+ subheadline: "Get unlimited design requests delivered fast with our streamlined process."
62
+ cta_button:
63
+ text: "Get started"
64
+ icon: "rocket"
65
+ href: "/dashboard"
66
+ items:
67
+ - title: "Subscribe"
68
+ description: "Choose your plan and submit unlimited design requests."
69
+ icon: "rotate"
70
+ - title: "Receive"
71
+ description: "Get your designs delivered in as fast as two business days."
72
+ icon: "bolt"
73
+ - title: "Revise"
74
+ description: "Request unlimited revisions until it's perfect."
75
+ icon: "thumbs-up"
76
+
77
+ # Testimonials Section
78
+ testimonials:
79
+ superheadline:
80
+ icon: "megaphone"
81
+ text: "Testimonials"
82
+ headline: "People {% uj_icon \"heart\", \"text-danger\" %} "
83
+ headline_accent: "{{ site.brand.name }}"
84
+ subheadline: "Hear from real people who have transformed their business with us"
85
+ items:
86
+ - quote: "This platform transformed my business. The support is incredible!"
87
+ author: "Sarah Johnson"
88
+ role: "CEO"
89
+ company: "TechStart Inc"
90
+ initial: "S"
91
+ - quote: "Best investment I've made. Achieved more here than with any consultant."
92
+ author: "Michael Chen"
93
+ role: "Founder"
94
+ company: "DataFlow"
95
+ initial: "M"
96
+ - quote: "The community support made all the difference in our growth journey."
97
+ author: "Emily Davis"
98
+ role: "Director"
99
+ company: "Creative Studio"
100
+ initial: "E"
101
+
102
+ # Stats Section
103
+ stats:
104
+ - number: "50,000+"
105
+ label: "Active users"
106
+ sublabel: "From 120+ countries"
107
+ color: "blue"
108
+ - number: "200+"
109
+ label: "Solutions"
110
+ sublabel: "Industry-leading"
111
+ color: "pink"
112
+ - number: "4.9"
113
+ label: "Average rating"
114
+ color: "yellow"
115
+ - number: "98%"
116
+ label: "Success rate"
117
+ sublabel: "Business growth"
118
+ color: "green"
119
+
120
+ # CTA Section
121
+ cta:
122
+ superheadline:
123
+ icon: "rocket"
124
+ text: "Launch"
125
+ headline: "Ready to transform your"
126
+ headline_accent: "business?"
127
+ description: "Join thousands of successful companies who have already accelerated their growth with {{ site.brand.name }}"
128
+ primary_button:
129
+ text: "Start today"
130
+ icon: "rocket"
131
+ href: "/dashboard"
132
+ secondary_button:
133
+ text: "Talk to an expert"
134
+ icon: "comments"
135
+ href: "/contact"
136
+ ---
137
+
138
+ {% comment %}
139
+ NEOBRUTALISM HOMEPAGE
140
+ Reuses the same page.resolved.* data contract as the classy homepage, but with
141
+ a deliberately different STRUCTURE: an asymmetric split hero, an ink-framed logo
142
+ marquee, alternating offset showcase blocks, big numbered step blocks, oversized
143
+ color-block stats, and a full-bleed CTA. Color blocks rotate through the nb accent
144
+ palette. See docs/themes.md "Per-page HTML layout overrides".
145
+ {% endcomment %}
146
+
147
+ <!-- ============================================ -->
148
+ <!-- HERO — asymmetric split: oversized headline | offset action stack -->
149
+ <!-- ============================================ -->
150
+ <section class="section-hero text-bg-warning">
151
+ <div class="container">
152
+ <div class="row align-items-center g-5">
153
+ <!-- Left: kicker + giant headline + description -->
154
+ <div class="col-lg-7">
155
+ <p class="text-uppercase fw-semibold mb-4">{{ page.resolved.hero.tagline }}</p>
156
+ <h1 class="hero-title mb-4">
157
+ {{ page.resolved.hero.headline }}
158
+ <span class="text-accent">{{ page.resolved.hero.headline_accent }}</span>
159
+ </h1>
160
+ <p class="fs-3 fw-medium mb-0" style="max-width: 36ch;">
161
+ {{ page.resolved.hero.description }}
162
+ </p>
163
+ </div>
164
+
165
+ <!-- Right: stacked, full-width CTA buttons (d-grid = full-width children) -->
166
+ <div class="col-lg-5">
167
+ <div class="d-grid gap-3 hero-actions">
168
+ <a href="{{ page.resolved.hero.primary_button.href }}" class="btn btn-dark btn-lg">
169
+ {% uj_icon page.resolved.hero.primary_button.icon, "me-2" %}{{ page.resolved.hero.primary_button.text }}
170
+ </a>
171
+ <a href="{{ page.resolved.hero.secondary_button.href }}" class="btn btn-light btn-lg">
172
+ {% uj_icon page.resolved.hero.secondary_button.icon, "me-2" %}{{ page.resolved.hero.secondary_button.text }}
173
+ </a>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </section>
179
+
180
+ <!-- ============================================ -->
181
+ <!-- TRUSTED BY — ink-framed marquee strip -->
182
+ <!-- ============================================ -->
183
+ <section class="logo-strip">
184
+ <div class="container">
185
+ <div class="logo-strip-box">
186
+ <span class="logo-strip-label">{{ page.resolved.trusted_by.headline }}</span>
187
+ <div class="infinite-scroll-wrapper">
188
+ <div class="infinite-scroll-track">
189
+ {% for logo in page.resolved.trusted_by.logos %}
190
+ <div class="infinite-scroll-item infinite-scroll-item--logo filter-adaptive">
191
+ {% uj_logo logo.name, "combomarks", "original" %}
192
+ </div>
193
+ {% endfor %}
194
+ {% comment %} duplicate for seamless loop {% endcomment %}
195
+ {% for logo in page.resolved.trusted_by.logos %}
196
+ <div class="infinite-scroll-item infinite-scroll-item--logo filter-adaptive" aria-hidden="true">
197
+ {% uj_logo logo.name, "combomarks", "original" %}
198
+ </div>
199
+ {% endfor %}
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </section>
205
+
206
+ <!-- ============================================ -->
207
+ <!-- SHOWCASE — alternating offset blocks that break the grid -->
208
+ <!-- ============================================ -->
209
+ <section class="showcase">
210
+ <div class="container">
211
+ <header class="section-head mb-5">
212
+ {% iftruthy page.resolved.showcase.superheadline.text %}
213
+ <span class="kicker mb-3">
214
+ {% uj_icon page.resolved.showcase.superheadline.icon, "me-1" %}{{ page.resolved.showcase.superheadline.text }}
215
+ </span>
216
+ {% endiftruthy %}
217
+ <h2 class="section-title">
218
+ {{ page.resolved.showcase.headline }}
219
+ <span class="text-accent">{{ page.resolved.showcase.headline_accent }}</span>
220
+ </h2>
221
+ </header>
222
+
223
+ {% assign showcase_bgs = "text-bg-primary,text-bg-secondary,text-bg-success,text-bg-info,text-bg-warning" | split: "," %}
224
+ <div class="showcase-list">
225
+ {% for item in page.resolved.showcase.items %}
226
+ {% assign bg = showcase_bgs[forloop.index0] | default: "text-bg-primary" %}
227
+ {% comment %} Liquid can't use a filter inside `if`, so compute the parity first {% endcomment %}
228
+ {% assign row_parity = forloop.index0 | modulo: 2 %}
229
+ <article class="showcase-row {% if row_parity == 1 %}showcase-row--flip{% endif %}">
230
+ <div class="showcase-num {{ bg }}">
231
+ {{ forloop.index | prepend: '0' | slice: -2, 2 }}
232
+ </div>
233
+ <div class="showcase-body">
234
+ <h3 class="showcase-title">{{ item.title }}</h3>
235
+ <p class="showcase-desc">{{ item.description }}</p>
236
+ </div>
237
+ </article>
238
+ {% endfor %}
239
+ </div>
240
+ </div>
241
+ </section>
242
+
243
+ <!-- ============================================ -->
244
+ <!-- FEATURES — big numbered step cards -->
245
+ <!-- ============================================ -->
246
+ <section class="steps bg-body-tertiary">
247
+ <div class="container">
248
+ <header class="section-head mb-5">
249
+ {% iftruthy page.resolved.features.superheadline.text %}
250
+ <span class="kicker mb-3">
251
+ {% uj_icon page.resolved.features.superheadline.icon, "me-1" %}{{ page.resolved.features.superheadline.text }}
252
+ </span>
253
+ {% endiftruthy %}
254
+ <h2 class="section-title">
255
+ {{ page.resolved.features.headline }}
256
+ <span class="text-accent">{{ page.resolved.features.headline_accent }}</span>
257
+ </h2>
258
+ </header>
259
+
260
+ <div class="row g-4">
261
+ {% for feature in page.resolved.features.items %}
262
+ <div class="col-md-4">
263
+ <div class="card step-card h-100">
264
+ <div class="card-body">
265
+ <div class="step-card-top">
266
+ <span class="step-card-num">{{ forloop.index }}</span>
267
+ <span class="step-card-icon">{% uj_icon feature.icon %}</span>
268
+ </div>
269
+ <h3 class="step-card-title">{{ feature.title }}</h3>
270
+ <p class="step-card-desc">{{ feature.description }}</p>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ {% endfor %}
275
+ </div>
276
+
277
+ {% iftruthy page.resolved.features.cta_button %}
278
+ <div class="text-center mt-5">
279
+ <a href="{{ page.resolved.features.cta_button.href }}" class="btn btn-warning btn-lg">
280
+ {% uj_icon page.resolved.features.cta_button.icon, "me-2" %}{{ page.resolved.features.cta_button.text }}
281
+ </a>
282
+ </div>
283
+ {% endiftruthy %}
284
+ </div>
285
+ </section>
286
+
287
+ <!-- ============================================ -->
288
+ <!-- STATS — oversized color-block cells -->
289
+ <!-- ============================================ -->
290
+ <section class="stats">
291
+ <div class="container">
292
+ {% assign stat_bgs = "blue:text-bg-primary,pink:text-bg-secondary,green:text-bg-success,yellow:text-bg-warning,info:text-bg-info" | split: "," %}
293
+ <div class="stats-grid">
294
+ {% for stat in page.resolved.stats %}
295
+ {% assign stat_bg = "text-bg-primary" %}
296
+ {% case stat.color %}
297
+ {% when "pink" %}{% assign stat_bg = "text-bg-secondary" %}
298
+ {% when "green" %}{% assign stat_bg = "text-bg-success" %}
299
+ {% when "yellow" %}{% assign stat_bg = "text-bg-warning" %}
300
+ {% when "info" %}{% assign stat_bg = "text-bg-info" %}
301
+ {% else %}{% assign stat_bg = "text-bg-primary" %}
302
+ {% endcase %}
303
+ <div class="stat-block {{ stat_bg }}">
304
+ <span class="stat-block-num">{{ stat.number }}</span>
305
+ <span class="stat-block-label">{{ stat.label }}</span>
306
+ {% iftruthy stat.sublabel %}<span class="stat-block-sub">{{ stat.sublabel }}</span>{% endiftruthy %}
307
+ </div>
308
+ {% endfor %}
309
+ </div>
310
+ </div>
311
+ </section>
312
+
313
+ {% include themes/neobrutalism/frontend/components/testimonial-scroll.html testimonials=page.resolved.testimonials %}
314
+
315
+ <!-- ============================================ -->
316
+ <!-- CTA — full-bleed ink block -->
317
+ <!-- ============================================ -->
318
+ <section class="cta">
319
+ <div class="container">
320
+ <div class="cta-panel">
321
+ {% iftruthy page.resolved.cta.superheadline.text %}
322
+ <span class="kicker kicker--invert mb-4">
323
+ {% uj_icon page.resolved.cta.superheadline.icon, "me-1" %}{{ page.resolved.cta.superheadline.text }}
324
+ </span>
325
+ {% endiftruthy %}
326
+ <h2 class="cta-title">
327
+ {{ page.resolved.cta.headline }}
328
+ <span class="text-accent">{{ page.resolved.cta.headline_accent }}</span>
329
+ </h2>
330
+ <p class="cta-desc">{{ page.resolved.cta.description }}</p>
331
+ <div class="d-flex flex-wrap gap-3 justify-content-center">
332
+ <a href="{{ page.resolved.cta.primary_button.href }}" class="btn btn-warning btn-lg">
333
+ {% uj_icon page.resolved.cta.primary_button.icon, "me-2" %}{{ page.resolved.cta.primary_button.text }}
334
+ </a>
335
+ {% iftruthy page.resolved.cta.secondary_button %}
336
+ <a href="{{ page.resolved.cta.secondary_button.href }}" class="btn btn-outline-light btn-lg">
337
+ {% uj_icon page.resolved.cta.secondary_button.icon, "me-2" %}{{ page.resolved.cta.secondary_button.text }}
338
+ </a>
339
+ {% endiftruthy %}
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </section>
344
+
345
+ {{ content | uj_content_format }}