coursecode 0.1.6 → 0.1.8

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 (37) hide show
  1. package/README.md +17 -11
  2. package/bin/cli.js +1 -0
  3. package/framework/css/02-layout.css +8 -1
  4. package/framework/css/components/audio-player.css +4 -3
  5. package/framework/css/components/buttons.css +7 -6
  6. package/framework/css/components/footer.css +5 -2
  7. package/framework/css/components/hero.css +31 -4
  8. package/framework/css/framework.css +2 -1
  9. package/framework/css/interactions/interactive-image.css +2 -2
  10. package/framework/css/layouts/article.css +1 -1
  11. package/framework/css/responsive-structure.css +210 -0
  12. package/framework/css/responsive.css +15 -170
  13. package/framework/docs/COURSE_AUTHORING_GUIDE.md +12 -15
  14. package/framework/docs/DATA_MODEL.md +1 -1
  15. package/framework/docs/FRAMEWORK_GUIDE.md +31 -10
  16. package/framework/docs/USER_GUIDE.md +29 -1
  17. package/framework/docs/examples/cloudflare-channel-relay.js +3 -0
  18. package/framework/docs/examples/cloudflare-data-worker.js +3 -0
  19. package/framework/docs/examples/cloudflare-error-worker.js +3 -0
  20. package/framework/js/utilities/icons.js +4 -2
  21. package/lib/cloud.js +5 -1
  22. package/lib/headless-browser.js +1 -1
  23. package/lib/mcp-prompts.js +22 -22
  24. package/lib/mcp-server.js +1 -1
  25. package/lib/preview-routes-api.js +59 -1
  26. package/lib/stub-player/outline-mode.js +187 -13
  27. package/lib/stub-player/styles/_base.css +1 -1
  28. package/lib/stub-player/styles/_header-bar.css +18 -0
  29. package/lib/stub-player/styles/_outline-mode.css +257 -20
  30. package/package.json +10 -1
  31. package/template/course/course-config.js +2 -0
  32. package/template/course/slides/example-course-structure.js +1 -1
  33. package/template/course/slides/example-finishing.js +4 -4
  34. package/template/course/slides/example-preview-tour.js +1 -1
  35. package/template/course/slides/example-ui-showcase.js +1 -1
  36. package/template/course/slides/example-welcome.js +4 -4
  37. package/template/course/slides/example-workflow.js +1 -1
package/README.md CHANGED
@@ -1,21 +1,23 @@
1
1
  # CourseCode
2
2
 
3
- **AI-first course authoring: create interactive e-learning without writing code, then deploy to any LMS format.**
3
+ **Open-source, local-first course authoring with AI help no coding required to start, full code control when you need it.**
4
4
 
5
- Drop in your existing PDFs, Word docs, or PowerPoints AI converts them into complete, interactive courses. No vendor lock-in, no subscriptions. Your content, your code, deployed to any LMS.
5
+ CourseCode creates real project files you can inspect, version, and edit directly with a predictable, file-based workflow instead of a black-box GUI.
6
+
7
+ Bring your own PDFs, Word docs, or PowerPoints, use AI to accelerate authoring, and deploy to any LMS format without vendor lock-in or subscriptions.
6
8
 
7
9
  ## Start Here
8
10
 
9
- If you're creating courses (non-technical or technical), use the **[User Guide](framework/docs/USER_GUIDE.md)** as your primary documentation.
11
+ Start with the workflow that fits you:
10
12
 
11
- - **Course authors (non-technical):** Start with the User Guide for step-by-step workflows
12
- - **Course authors (technical):** The User Guide is still the best starting point, then use advanced docs as needed
13
+ - **Course authors (prefer buttons and guided setup):** Start with **[CourseCode Desktop](https://coursecodedesktop.com)** if you want the easiest path and do not want to deal with Node.js or terminal setup
14
+ - **Course authors (prefer editing files and running commands):** Start with the **[User Guide](framework/docs/USER_GUIDE.md)** in this repo for the framework workflow
13
15
  - **AI assistants:** Use the AI-focused docs in `framework/docs/` (for example `COURSE_AUTHORING_GUIDE.md` and `FRAMEWORK_GUIDE.md`)
14
16
 
15
17
  ## Features
16
18
 
17
19
  - **MCP integration**: AI connects directly to your course — previews, screenshots, linting, and testing without manual file sharing
18
- - **No code writing required**: Describe what you want and let AI build slides, interactions, and structure
20
+ - **No coding required to start**: Describe what you want and let AI help build slides, interactions, and structure
19
21
  - **Full LMS integration**: SCORM 1.2, SCORM 2004, cmi5, and LTI with complete tracking records
20
22
  - **AI-assisted authoring workflow**: Structured guides and MCP tools for faster course development
21
23
  - **Rich UI components**: Images, video, accordions, tabs, and custom sandboxed HTML/JS embeds
@@ -26,13 +28,17 @@ If you're creating courses (non-technical or technical), use the **[User Guide](
26
28
  - **Themeable design**: CSS custom properties for easy brand customization
27
29
  - **Custom endpoints**: Optional webhooks for error reporting and learning record storage
28
30
  - **Live preview**: Visual editing, status dashboard, config panels, catalog browser, and full LMS simulation with debug tools
29
- - **[CourseCode Cloud](https://www.coursecodecloud.com)**: Deploy from CLI, share preview links, download any LMS format on demand — no rebuilds
30
- - **CourseCode Desktop**: Native app for Mac and Windows with AI-assisted editing and built-in preview
31
+ - **[CourseCode Cloud](https://coursecodecloud.com)**: Deploy from CLI, share preview links, download any LMS format on demand — no rebuilds
32
+ - **[CourseCode Desktop](https://coursecodedesktop.com)**: Native app for Mac and Windows with AI-assisted editing and built-in preview
31
33
 
32
34
  ---
33
35
 
34
36
  ## Installation
35
37
 
38
+ This section is for the **CourseCode Framework CLI** (file-based workflow using Node.js and terminal commands).
39
+
40
+ If you want the easiest setup with buttons and guided steps, use **[CourseCode Desktop](https://coursecodedesktop.com)** instead.
41
+
36
42
  ### Required
37
43
 
38
44
  Install [Node.js](https://nodejs.org/) (v18 or later), then run:
@@ -65,7 +71,7 @@ Open `http://localhost:4173` to view and edit your course.
65
71
 
66
72
  The example course included with every new project is a complete guide to using CourseCode.
67
73
 
68
- **New to CourseCode?** Start with the [User Guide](framework/docs/USER_GUIDE.md). It is the primary guide for both non-technical and technical course authors.
74
+ **New to CourseCode?** If you want the easiest setup, start with **[CourseCode Desktop](https://coursecodedesktop.com)**. If you're using the framework CLI, start with the [User Guide](framework/docs/USER_GUIDE.md).
69
75
 
70
76
  ---
71
77
 
@@ -179,7 +185,7 @@ The preview server provides:
179
185
 
180
186
  When ready, deploy:
181
187
 
182
- **With [CourseCode Cloud](https://www.coursecodecloud.com)**: Push your course and get a live link. Cloud handles hosting, generates any LMS format on demand, and gives you sharable preview links with optional password protection. No ZIP files, no manual uploads.
188
+ **With [CourseCode Cloud](https://coursecodecloud.com)**: Push your course and get a live link. Cloud handles hosting, generates any LMS format on demand, and gives you sharable preview links with optional password protection. No ZIP files, no manual uploads.
183
189
 
184
190
  ```bash
185
191
  coursecode deploy
@@ -218,7 +224,7 @@ For the full command list and deployment options, see the [User Guide](framework
218
224
 
219
225
  ## UI Components
220
226
 
221
- Build engaging slides with interactive elements. No coding required your AI assistant handles the implementation.
227
+ Build engaging slides with interactive elements. Start with AI assistance, then edit the generated files directly whenever you want more control.
222
228
 
223
229
  ### Media & Widgets
224
230
 
package/bin/cli.js CHANGED
@@ -322,6 +322,7 @@ program
322
322
  .description('Build and deploy course to CourseCode Cloud')
323
323
  .option('--preview', 'Deploy as preview (expires in 7 days)')
324
324
  .option('--password', 'Password-protect preview (interactive prompt)')
325
+ .option('-m, --message <message>', 'Deploy reason (e.g. "Fixed accessibility issues")')
325
326
  .option('--local', 'Use local Cloud instance (http://localhost:3000)')
326
327
  .action(async (options) => {
327
328
  const { deploy, setLocalMode } = await import('../lib/cloud.js');
@@ -298,7 +298,14 @@ main#content {
298
298
  position: relative;
299
299
  flex: 1;
300
300
  overflow-y: auto;
301
- overflow-x: hidden;
301
+ overflow-x: clip;
302
+ }
303
+
304
+ /* Remove top padding when a flush-top hero is present (the hero wants to
305
+ sit directly against the header). Negative-margin tricks can't escape
306
+ the intermediate flex containers, so we remove padding at the source. */
307
+ main#content:has([data-component="hero"].hero-flush-top) {
308
+ padding-top: 0;
302
309
  }
303
310
 
304
311
  /* Content Sections - Clean Card Design with Animation */
@@ -545,9 +545,10 @@ html.bp-max-mobile-landscape[data-layout="article"] #audio-player .audio-player-
545
545
  }
546
546
 
547
547
  html.bp-max-mobile-landscape[data-layout="article"] #audio-player .audio-btn {
548
- width: 36px;
549
- height: 36px;
550
- min-height: 36px;
548
+ width: 44px;
549
+ height: 44px;
550
+ min-height: 44px;
551
+ touch-action: manipulation;
551
552
  }
552
553
 
553
554
  html.bp-max-mobile-landscape[data-layout="article"] #audio-player .audio-progress-container {
@@ -610,19 +610,20 @@ html:not([data-layout="article"]):not([data-layout="focused"]):not([data-layout=
610
610
 
611
611
  html.bp-max-mobile-landscape[data-layout="article"] footer #prevBtn,
612
612
  html.bp-max-mobile-landscape[data-layout="article"] footer #nextBtn {
613
- width: 36px;
614
- max-width: 36px;
615
- height: 36px;
616
- min-height: 36px;
613
+ width: 44px;
614
+ max-width: 44px;
615
+ height: 44px;
616
+ min-height: 44px;
617
617
  padding: 0;
618
618
  overflow: hidden;
619
+ touch-action: manipulation;
619
620
  }
620
621
 
621
622
  html.bp-max-mobile-landscape[data-layout="article"] footer #prevBtn>svg,
622
623
  html.bp-max-mobile-landscape[data-layout="article"] footer #nextBtn>svg {
623
624
  display: block;
624
- width: 18px;
625
- height: 18px;
625
+ width: 20px;
626
+ height: 20px;
626
627
  margin: 0;
627
628
  }
628
629
 
@@ -269,7 +269,10 @@ html.bp-max-mobile-landscape[data-layout="article"] footer {
269
269
  max-width: 100vw;
270
270
  box-sizing: border-box;
271
271
  border-radius: 0;
272
- padding: var(--space-2) var(--space-3);
272
+ padding-top: var(--space-2);
273
+ padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
274
+ padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
275
+ padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
273
276
  display: flex;
274
277
  flex-direction: row;
275
278
  align-items: center;
@@ -289,7 +292,7 @@ html.bp-max-mobile-landscape[data-layout="article"] .nav-controls {
289
292
  html.bp-max-mobile-landscape[data-layout="article"] .nav-nav-buttons {
290
293
  order: 2;
291
294
  margin-left: auto;
292
- margin-right: var(--space-1);
295
+ margin-right: 0;
293
296
  display: flex;
294
297
  flex-direction: row;
295
298
  gap: var(--space-2);
@@ -43,8 +43,10 @@
43
43
 
44
44
  /* Hero content container */
45
45
  [data-component="hero"] .hero-content {
46
+ width: 100%;
46
47
  max-width: 800px;
47
48
  margin: 0 auto;
49
+ box-sizing: border-box;
48
50
  position: relative;
49
51
  z-index: 2;
50
52
  }
@@ -108,6 +110,28 @@
108
110
  border-color: transparent;
109
111
  }
110
112
 
113
+ /* Hero badge color variants - solid backgrounds */
114
+ .hero-badge.hero-badge-primary,
115
+ [data-component="hero"] .hero-badge.hero-badge-primary {
116
+ background: var(--color-primary);
117
+ color: var(--color-white);
118
+ border-color: transparent;
119
+ }
120
+
121
+ .hero-badge.hero-badge-secondary,
122
+ [data-component="hero"] .hero-badge.hero-badge-secondary {
123
+ background: var(--color-secondary);
124
+ color: var(--color-gray-900);
125
+ border-color: transparent;
126
+ }
127
+
128
+ .hero-badge.hero-badge-accent,
129
+ [data-component="hero"] .hero-badge.hero-badge-accent {
130
+ background: var(--color-accent);
131
+ color: var(--color-gray-900);
132
+ border-color: transparent;
133
+ }
134
+
111
135
  [data-component="hero"].hero-overlay .hero-badge,
112
136
  [data-component="hero"].hero-dark .hero-badge {
113
137
  background: var(--color-white-alpha-25);
@@ -139,14 +163,17 @@
139
163
 
140
164
  /* Full bleed - breaks out of container to span full viewport width */
141
165
  [data-component="hero"].hero-full-bleed {
142
- width: 100vw;
166
+ width: auto;
143
167
  margin-left: calc(50% - 50vw);
144
- border-radius: 0; /* Remove rounded corners when full bleed */
168
+ margin-right: calc(50% - 50vw);
169
+ border-radius: 0;
145
170
  }
146
171
 
147
- /* Flush top - removes container top padding (stackable with hero-full-bleed) */
172
+ /* Flush top - hero sits directly against the header.
173
+ Padding removal is handled by main#content:has() in 02-layout.css.
174
+ This class is retained as the semantic marker that triggers the :has() rule. */
148
175
  [data-component="hero"].hero-flush-top {
149
- margin-top: calc(-1 * var(--content-padding, var(--space-10)));
176
+ margin-top: 0;
150
177
  }
151
178
 
152
179
  /* Split hero - Image on one side, content on other */
@@ -83,4 +83,5 @@
83
83
 
84
84
  /* 7. Responsive & Accessibility (load last) */
85
85
  @import './responsive.css';
86
- @import './accessibility.css';
86
+ @import './responsive-structure.css';
87
+ @import './accessibility.css';
@@ -61,7 +61,7 @@
61
61
  .interactive-image-hotspot[data-color="primary"] { --hotspot-color: var(--color-primary, #14213d); }
62
62
  .interactive-image-hotspot[data-color="secondary"] { --hotspot-color: var(--color-secondary, #f18701); }
63
63
  .interactive-image-hotspot[data-color="success"] { --hotspot-color: var(--color-success, #1d7648); }
64
- .interactive-image-hotspot[data-color="danger"] { --hotspot-color: var(--color-danger, #f35b04); }
64
+ .interactive-image-hotspot[data-color="danger"] { --hotspot-color: var(--color-danger, #c7322b); }
65
65
  .interactive-image-hotspot[data-color="warning"] { --hotspot-color: var(--color-warning, #f7b801); }
66
66
  .interactive-image-hotspot[data-color="info"] { --hotspot-color: var(--color-info, #4a6fa5); }
67
67
  .interactive-image-hotspot[data-color="light"] { --hotspot-color: var(--color-gray-50, #fafafa); }
@@ -71,7 +71,7 @@
71
71
  .interactive-image-hotspot[data-viewed-color="primary"] { --viewed-color: var(--color-primary, #14213d); }
72
72
  .interactive-image-hotspot[data-viewed-color="secondary"] { --viewed-color: var(--color-secondary, #f18701); }
73
73
  .interactive-image-hotspot[data-viewed-color="success"] { --viewed-color: var(--color-success, #1d7648); }
74
- .interactive-image-hotspot[data-viewed-color="danger"] { --viewed-color: var(--color-danger, #f35b04); }
74
+ .interactive-image-hotspot[data-viewed-color="danger"] { --viewed-color: var(--color-danger, #c7322b); }
75
75
  .interactive-image-hotspot[data-viewed-color="warning"] { --viewed-color: var(--color-warning, #f7b801); }
76
76
  .interactive-image-hotspot[data-viewed-color="info"] { --viewed-color: var(--color-info, #4a6fa5); }
77
77
  .interactive-image-hotspot[data-viewed-color="light"] { --viewed-color: var(--color-gray-50, #fafafa); }
@@ -254,5 +254,5 @@ html.bp-max-mobile-landscape[data-layout="article"] .sidebar {
254
254
 
255
255
  /* Add bottom padding to content */
256
256
  html.bp-max-mobile-landscape[data-layout="article"] main#content {
257
- padding-bottom: calc(var(--space-8) + 52px);
257
+ padding-bottom: calc(var(--space-8) + 68px + env(safe-area-inset-bottom, 0px));
258
258
  }
@@ -0,0 +1,210 @@
1
+ /* ============================================================================
2
+ RESPONSIVE STRUCTURE - App shell, header chrome, footer/nav/audio chrome
3
+ ============================================================================
4
+
5
+ PURPOSE:
6
+ - Holds responsive behavior for framework chrome and shell structure.
7
+ - Keeps `responsive.css` focused on shared content/component responsiveness.
8
+
9
+ IMPORTANT:
10
+ - If a layout (article/focused/presentation/traditional) owns a responsive
11
+ behavior in layout/component DATA-LAYOUT rules, generic structure selectors
12
+ here must exclude that layout to avoid cascade conflicts.
13
+ ============================================================================ */
14
+
15
+ /* ============================================================================
16
+ LARGE DESKTOP (>= 1440px)
17
+ ============================================================================ */
18
+
19
+ html.bp-min-large-desktop #app {
20
+ max-width: 1440px;
21
+ height: 100%;
22
+ margin: 0 auto;
23
+ }
24
+
25
+ /* ============================================================================
26
+ DESKTOP (<= 1439px)
27
+ ============================================================================ */
28
+
29
+ html.bp-max-desktop #app {
30
+ margin: 0;
31
+ max-width: none;
32
+ height: 100%;
33
+ }
34
+
35
+ /* ============================================================================
36
+ TABLET PORTRAIT (<= 1023px)
37
+ ============================================================================ */
38
+
39
+ html.bp-max-tablet-portrait #app {
40
+ margin: 0;
41
+ border-radius: 0;
42
+ height: 100%;
43
+ }
44
+
45
+ /* Header at tablet portrait: tighter spacing but keep row layout */
46
+ html.bp-max-tablet-portrait header {
47
+ gap: var(--space-2);
48
+ }
49
+
50
+ html.bp-max-tablet-portrait #app > header h1 {
51
+ font-size: var(--font-size-lg);
52
+ }
53
+
54
+ html.bp-max-tablet-portrait #accessibility-menu {
55
+ right: var(--space-4);
56
+ }
57
+
58
+ /* Footer at tablet portrait: tighter gaps but keep row layout
59
+ Exclude article/focused - these layouts own footer behavior. */
60
+ html.bp-max-tablet-portrait:not([data-layout="article"]):not([data-layout="focused"]) .nav-controls {
61
+ gap: var(--space-2);
62
+ }
63
+
64
+ /* ============================================================================
65
+ MOBILE LANDSCAPE (<= 767px)
66
+ ============================================================================ */
67
+
68
+ html.bp-max-mobile-landscape #app {
69
+ margin: 0;
70
+ border-radius: 0;
71
+ height: 100%;
72
+ }
73
+
74
+ /* Mobile header: compact single-row layout */
75
+ html.bp-max-mobile-landscape header {
76
+ flex-wrap: nowrap;
77
+ justify-content: flex-start;
78
+ gap: var(--space-2);
79
+ }
80
+
81
+ html.bp-max-mobile-landscape #brand {
82
+ flex: 1 1 auto;
83
+ justify-content: flex-start;
84
+ min-width: 0;
85
+ }
86
+
87
+ html.bp-max-mobile-landscape #brand img.logo {
88
+ height: 36px;
89
+ padding: var(--space-1);
90
+ padding-left: 0;
91
+ }
92
+
93
+ html.bp-max-mobile-landscape #brand .brand-title {
94
+ font-size: var(--font-size-base);
95
+ white-space: nowrap;
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ }
99
+
100
+ html.bp-max-mobile-landscape #app > header h1 {
101
+ font-size: var(--font-size-base);
102
+ }
103
+
104
+ /* Hide header progress and breadcrumbs on mobile to save space */
105
+ html.bp-max-mobile-landscape .header-progress {
106
+ display: none !important;
107
+ }
108
+
109
+ html.bp-max-mobile-landscape .breadcrumbs {
110
+ display: none !important;
111
+ }
112
+
113
+ /* Generic mobile footer/nav/audio structure (traditional/default-like layouts).
114
+ Exclude article/focused - they define mobile footer/nav/audio in DATA-LAYOUT rules. */
115
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-controls {
116
+ flex-direction: row;
117
+ flex-wrap: nowrap;
118
+ gap: var(--space-2);
119
+ }
120
+
121
+ /* Mobile footer: Previous/Next fill the row, Exit hidden (moved to sidebar) */
122
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-nav-buttons {
123
+ display: flex;
124
+ gap: var(--space-2);
125
+ flex: 1;
126
+ width: auto;
127
+ }
128
+
129
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-nav-buttons button {
130
+ flex: 1;
131
+ width: auto;
132
+ padding: var(--space-2) var(--space-3);
133
+ }
134
+
135
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-exit-button {
136
+ display: none;
137
+ }
138
+
139
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .app-footer button {
140
+ width: auto;
141
+ }
142
+
143
+ /* Mobile footer: stacked layout with audio on top */
144
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .app-footer {
145
+ flex-direction: column;
146
+ gap: var(--space-2);
147
+ }
148
+
149
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-controls {
150
+ width: 100%;
151
+ flex-direction: column;
152
+ gap: var(--space-2);
153
+ }
154
+
155
+ /* Audio player: full width, above nav buttons, compact variant */
156
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) #audio-player {
157
+ width: 100%;
158
+ order: -1;
159
+ margin-left: 0;
160
+ }
161
+
162
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .audio-player-controls {
163
+ width: 100%;
164
+ justify-content: center;
165
+ gap: var(--space-1);
166
+ background: var(--bg-surface);
167
+ border: var(--border-width-md) solid var(--border-default);
168
+ }
169
+
170
+ /* Hide progress bar and time on mobile (compact variant) */
171
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .audio-progress-container,
172
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .audio-time {
173
+ display: none;
174
+ }
175
+
176
+ /* Nav buttons: row layout filling width */
177
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-nav-buttons {
178
+ width: 100%;
179
+ justify-content: center;
180
+ }
181
+
182
+ /* ============================================================================
183
+ MOBILE PORTRAIT (<= 479px)
184
+ ============================================================================ */
185
+
186
+ html.bp-max-mobile-portrait #app {
187
+ margin: 0;
188
+ border-radius: 0;
189
+ height: 100%;
190
+ }
191
+
192
+ /* Mobile portrait: even tighter header */
193
+ html.bp-max-mobile-portrait #brand img.logo {
194
+ height: 32px;
195
+ padding-left: 0;
196
+ }
197
+
198
+ html.bp-max-mobile-portrait #brand .brand-title {
199
+ font-size: var(--font-size-sm);
200
+ }
201
+
202
+ html.bp-max-mobile-portrait #app > header h1 {
203
+ font-size: var(--font-size-sm);
204
+ }
205
+
206
+ /* Exclude article/focused - layout-specific button sizing takes precedence */
207
+ html.bp-max-mobile-portrait:not([data-layout="article"]):not([data-layout="focused"]) .app-footer button {
208
+ padding: var(--space-3) var(--space-4);
209
+ font-size: var(--font-size-sm);
210
+ }