@abreen/tada 1.18.2 → 1.18.4

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.
package/README.md CHANGED
@@ -109,7 +109,7 @@ Create a new Tada site in a new directory. Prompts for:
109
109
  - **Site title**: displayed in the header and `<title>` tag
110
110
  - **Symbol**: short text (1-5 uppercase characters) shown in the logo and favicon
111
111
  - **Theme color**: HSL color, e.g. `hsl(195 70% 40%)`
112
- - **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `20`)
112
+ - **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `33`)
113
113
  - **Background tint amount**: percentage (0-100) of tint to apply (defaults to `100`)
114
114
  - **Default time zone**: for `<time>` elements (defaults to your system zone)
115
115
  - **Production base URL**: e.g. `https://example.edu`
@@ -272,7 +272,7 @@ vars:
272
272
  | `logo` | *Optional*, image path relative to `public/`, replacing the header symbol |
273
273
  | `favicon` | *Optional*, `.ico` path relative to `public/`, replacing generated favicons and the web app manifest |
274
274
  | `themeColor` | Theme color for the site (e.g., `"tomato"`, `"#c04040"`, `"hsl(195 70% 40%)"`) |
275
- | `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `20`) |
275
+ | `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `33`) |
276
276
  | `tintAmount` | *Optional*, percentage (0-100) of tint to apply to the neutral palette (default `100`); link saturation stays fixed |
277
277
  | `defaultFont` | *Optional*, initial font pairing: `sans` or `serif` (default `sans`) |
278
278
  | `defaultContrast` | *Optional*, initial neutral-palette contrast: `standard` or `high` (default `standard`) |
package/bin/tada.ts CHANGED
@@ -90,7 +90,7 @@ const INIT_QUESTIONS: Record<string, InitQuestion> = {
90
90
  },
91
91
  tintHue: {
92
92
  prompt: 'Background tint hue (0-360)',
93
- defaultValue: '20',
93
+ defaultValue: '33',
94
94
  validate: validateHue,
95
95
  },
96
96
  tintAmount: {
package/build/bundle.ts CHANGED
@@ -52,12 +52,12 @@ function renderThemeScss(siteVariables: SiteVariables): string {
52
52
  const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
53
53
  const template = fs.readFileSync(templatePath, 'utf-8');
54
54
  const theme = deriveTheme(siteVariables.themeColor);
55
- const tintHue = siteVariables.tintHue ?? 20;
55
+ const tintHue = siteVariables.tintHue ?? 33;
56
56
  const tintAmount = siteVariables.tintAmount ?? 100;
57
57
 
58
58
  const linkHue = formatCssNumber(deriveLinkHue(tintHue));
59
- const linkColor = `hsl(${linkHue}deg 44.4% 49.4%)`;
60
- const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
59
+ const linkColor = `hsl(${linkHue}deg 66% 38%)`;
60
+ const linkColorHover = `hsl(${linkHue}deg 56% 49%)`;
61
61
  const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
62
62
  const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
63
63
  const traceLineActiveHue = formatCssNumber(deriveTraceLineActiveHue(tintHue));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.18.2",
3
+ "version": "1.18.4",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "type": "integer",
39
39
  "minimum": 0,
40
40
  "maximum": 360,
41
- "default": 20
41
+ "default": 33
42
42
  },
43
43
  "tintAmount": {
44
44
  "type": "integer",
package/src/_content.scss CHANGED
@@ -56,14 +56,16 @@ a.external:not(:has(.external-link-tail))::after {
56
56
  padding-top: 32px;
57
57
  margin-top: calc(var(--thematic-break-margin) / 2);
58
58
  font-size: 0.9em;
59
- border-top: 3px solid var(--bg2-color);
60
59
  }
61
60
 
62
61
  .footnotes .title {
63
62
  @include user-select(none);
64
63
 
65
- font-size: larger;
66
- font-weight: 700;
64
+ padding: var(--gap) 0;
65
+ font-style: italic;
66
+ color: var(--fg2-color);
67
+ text-align: center;
68
+ border-bottom: 1px solid var(--fg2-color);
67
69
  }
68
70
 
69
71
  .footnotes p {
@@ -387,7 +389,7 @@ dfn {
387
389
  }
388
390
 
389
391
  [aria-current='page'] {
390
- font-style: italic;
392
+ font-weight: 700;
391
393
  }
392
394
 
393
395
  .material-symbol-icon {
package/src/globals.ts CHANGED
@@ -11,6 +11,10 @@ export interface Globals {
11
11
  createResizeObserver: (
12
12
  callback: ResizeObserverCallback,
13
13
  ) => Pick<ResizeObserver, 'disconnect' | 'observe'>;
14
+ createIntersectionObserver: (
15
+ callback: IntersectionObserverCallback,
16
+ options?: IntersectionObserverInit,
17
+ ) => Pick<IntersectionObserver, 'disconnect' | 'observe'>;
14
18
  fetch: FetchFunction;
15
19
  importModule: ImportModuleFunction;
16
20
  isDocumentHidden: (document: Document) => boolean;
@@ -26,6 +30,9 @@ export const globals: Globals = {
26
30
  createResizeObserver(callback) {
27
31
  return new ResizeObserver(callback);
28
32
  },
33
+ createIntersectionObserver(callback, options) {
34
+ return new IntersectionObserver(callback, options);
35
+ },
29
36
  fetch(input: RequestInfo | URL, init?: RequestInit) {
30
37
  return fetch(input, init);
31
38
  },
@@ -5,6 +5,12 @@ and, where supported, animates its `<details>` content. The component's
5
5
  JavaScript adds dismissal behavior for outside clicks, focus leaving the menu,
6
6
  and the Escape key.
7
7
 
8
- This component doesn't provide any other functionality related to the header.
8
+ The component also rolls the header title between the site title and the page
9
+ title as the page heading scrolls under the header (see
10
+ [Header Title](../../spec/header-title.md)). The navigator calls
11
+ `swapHeaderTitle()` after each content swap so the header adopts the new page
12
+ title and shows the title for the new scroll position without rolling.
13
+
9
14
  If JavaScript is turned off in the web browser, the `<details>` element is still
10
- functional and its disclosure content opens immediately.
15
+ functional and its disclosure content opens immediately, and the header shows
16
+ only the site title.
@@ -5,32 +5,19 @@
5
5
  --header-menu-icon-size: 24px;
6
6
  }
7
7
 
8
+ // The summary and the controls share one grid row, so the site title gets
9
+ // exactly the width the currently rendered controls leave behind.
8
10
  header {
9
- --header-search-reservation: 0px;
10
- --header-to-top-reservation: 0px;
11
-
12
11
  position: fixed;
13
12
  top: 0;
14
13
  z-index: 1000;
15
14
  display: grid;
16
- grid-template-columns: 1fr;
15
+ grid-template-columns: minmax(0, 1fr) max-content;
17
16
  width: 100%;
17
+ container-type: inline-size;
18
18
  background: var(--bg-color-translucent);
19
- border-bottom: 1px solid var(--bg2-color);
20
19
  backdrop-filter: var(--backdrop-filter);
21
20
  view-transition-name: site-header;
22
-
23
- &:has(.search-controls) {
24
- --header-search-reservation: 200px;
25
-
26
- @media (width <= 400px) {
27
- --header-search-reservation: 0px;
28
- }
29
- }
30
-
31
- &:has(.to-top-container .is-visible) {
32
- --header-to-top-reservation: 120px;
33
- }
34
21
  }
35
22
 
36
23
  :root[data-contrast-preference='high'] header {
@@ -57,6 +44,7 @@ body:has(header details[open]) > .container::before {
57
44
  }
58
45
 
59
46
  header > details {
47
+ grid-area: 1 / 1;
60
48
  min-width: 0;
61
49
  }
62
50
 
@@ -65,9 +53,7 @@ header details summary {
65
53
  display: flex;
66
54
  align-items: center;
67
55
  height: var(--header-collapsed-height);
68
- padding: 0
69
- calc(var(--header-search-reservation) + var(--header-to-top-reservation)) 0
70
- 0;
56
+ padding: 0;
71
57
  cursor: pointer;
72
58
  list-style-type: none;
73
59
 
@@ -78,7 +64,8 @@ header details summary {
78
64
  background: var(--bg2-color);
79
65
  }
80
66
 
81
- &:hover .site-title {
67
+ &:hover .site-title,
68
+ &:hover .header-page-title {
82
69
  @include link-underline;
83
70
  }
84
71
  }
@@ -160,7 +147,9 @@ header details[open] summary .menu-icon-line-bottom {
160
147
 
161
148
  :root.js header > details::details-content {
162
149
  min-block-size: 0;
163
- overflow: clip;
150
+
151
+ // Clip only the height animation; the navigation spans the full header
152
+ overflow: visible clip;
164
153
  opacity: 0;
165
154
  transform: translateY(-4px);
166
155
  transition:
@@ -176,8 +165,12 @@ header details[open] summary .menu-icon-line-bottom {
176
165
  }
177
166
  }
178
167
 
168
+ // The details element sits in the first column; its navigation extends under
169
+ // the controls column to fill the header.
179
170
  header .content {
180
171
  position: relative;
172
+ box-sizing: border-box;
173
+ width: 100cqw;
181
174
  }
182
175
 
183
176
  header summary .logo {
@@ -201,36 +194,89 @@ header summary .logo-image {
201
194
  object-fit: contain;
202
195
  }
203
196
 
204
- header summary .site-title {
197
+ // The site title and page title sit on a reel, one header row each, behind a
198
+ // window one row tall. Scrolling past the page heading rolls the reel up to the
199
+ // page title; without JavaScript, only the site title is shown.
200
+ header summary .header-title {
205
201
  flex: 1 1 0;
202
+ align-self: stretch;
206
203
  min-width: 0;
207
- margin: 0 8px 0 0;
204
+ margin: 0 var(--gap) 0 0;
208
205
  overflow: hidden;
209
- text-overflow: ellipsis;
210
206
 
211
207
  // Values above 500 resolve to the supplied bold face when a custom family
212
208
  // provides only fixed 400 and 700 weights.
213
209
  font-weight: 600;
214
- white-space: nowrap;
215
210
  pointer-events: none;
216
211
  }
217
212
 
213
+ // Feather the window's top and bottom edges so a title fades as it rolls out
214
+ :root.js header summary .header-title {
215
+ -webkit-mask-image: linear-gradient(
216
+ transparent,
217
+ #000 25%,
218
+ #000 75%,
219
+ transparent
220
+ );
221
+ mask-image: linear-gradient(transparent, #000 25%, #000 75%, transparent);
222
+ }
223
+
224
+ header summary .header-title-reel {
225
+ display: block;
226
+ }
227
+
228
+ header summary .header-title.is-page-title .header-title-reel {
229
+ transform: translateY(calc(-1 * var(--header-collapsed-height)));
230
+ }
231
+
232
+ @media (prefers-reduced-motion: no-preference) {
233
+ header summary .header-title-reel {
234
+ transition: transform 300ms cubic-bezier(0.2, 0, 0, 1);
235
+ }
236
+ }
237
+
238
+ header summary .site-title,
239
+ header summary .header-page-title {
240
+ display: block;
241
+ height: var(--header-collapsed-height);
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ line-height: var(--header-collapsed-height);
245
+ white-space: nowrap;
246
+ }
247
+
248
+ // Code page titles keep their monospace font, without the inline code chip,
249
+ // like the file title heading
250
+ header summary .header-page-title :is(tt, code) {
251
+ padding: 0;
252
+ background: none;
253
+ }
254
+
255
+ :root:not(.js) header summary .header-page-title {
256
+ display: none;
257
+ }
258
+
218
259
  header > .header-controls {
219
- position: absolute;
220
- top: 0;
221
- right: 0;
222
- z-index: 1;
223
260
  display: flex;
261
+ grid-area: 1 / 2;
262
+ gap: var(--gap);
224
263
  align-items: center;
264
+ align-self: start;
225
265
  height: var(--header-collapsed-height);
226
266
  padding: 0 var(--gap) 0 0;
227
267
  }
228
268
 
269
+ // Continue the summary's hover background behind the controls
270
+ @media (hover: hover) {
271
+ header:has(> details > summary:hover) > .header-controls {
272
+ background: var(--bg2-color);
273
+ }
274
+ }
275
+
229
276
  header .to-top-container {
230
277
  display: none;
231
278
  align-items: center;
232
279
  height: var(--header-collapsed-height);
233
- padding: 0 var(--gap) 0 0;
234
280
  white-space: nowrap;
235
281
  pointer-events: none;
236
282
 
@@ -1,10 +1,150 @@
1
1
  import { getElement } from '../util';
2
+ import { globals } from '../globals';
3
+
4
+ export type HeaderTitleState = 'site' | 'page';
5
+
6
+ export interface HeadingGeometry {
7
+ /** Bottom edge of the page heading block (breadcrumbs, h1, and info) */
8
+ blockBottom: number;
9
+ /** Top edge of the page's h1 */
10
+ titleTop: number;
11
+ /** Bottom edge of the collapsed header */
12
+ headerBottom: number;
13
+ }
14
+
15
+ const HEADING_BLOCK_SELECTOR = '.title-and-info, .file-header';
16
+ const PAGE_TITLE_CLASS = 'is-page-title';
17
+
18
+ /**
19
+ * Show the page title once the whole heading block has scrolled under the
20
+ * header, and the site title once the h1 is entirely below the header again.
21
+ * In between, keep the current title so it does not flicker.
22
+ */
23
+ export function nextHeaderTitleState(
24
+ current: HeaderTitleState,
25
+ geometry: HeadingGeometry | null,
26
+ ): HeaderTitleState {
27
+ if (!geometry) {
28
+ return 'site';
29
+ }
30
+ if (geometry.blockBottom <= geometry.headerBottom) {
31
+ return 'page';
32
+ }
33
+ if (geometry.titleTop >= geometry.headerBottom) {
34
+ return 'site';
35
+ }
36
+ return current;
37
+ }
38
+
39
+ function getHeadingElements(document: Document) {
40
+ const block = document.querySelector(HEADING_BLOCK_SELECTOR);
41
+ const title = block?.querySelector('h1');
42
+ return block && title ? { block, title } : null;
43
+ }
44
+
45
+ interface TitleRoll {
46
+ swap(newDoc: Document): void;
47
+ disconnect(): void;
48
+ }
49
+
50
+ let titleRoll: TitleRoll | null = null;
51
+
52
+ function getTitleElements(summary: HTMLElement) {
53
+ const container = summary.querySelector<HTMLElement>('.header-title');
54
+ const reel = container?.querySelector<HTMLElement>('.header-title-reel');
55
+ const pageTitle = reel?.querySelector<HTMLElement>('.header-page-title');
56
+ return container && reel && pageTitle ? { container, reel, pageTitle } : null;
57
+ }
58
+
59
+ function mountTitleRoll(
60
+ window: Window,
61
+ summary: HTMLElement,
62
+ ): TitleRoll | null {
63
+ const elements = getTitleElements(summary);
64
+ if (!elements) {
65
+ return null;
66
+ }
67
+ const { container, reel, pageTitle } = elements;
68
+
69
+ let state: HeaderTitleState = 'site';
70
+
71
+ function measure(): HeadingGeometry | null {
72
+ const heading = getHeadingElements(window.document);
73
+ // A hidden heading (for example, while presenting slides) has no boxes
74
+ if (!heading || heading.block.getClientRects().length === 0) {
75
+ return null;
76
+ }
77
+ return {
78
+ blockBottom: heading.block.getBoundingClientRect().bottom,
79
+ titleTop: heading.title.getBoundingClientRect().top,
80
+ headerBottom: summary.getBoundingClientRect().bottom,
81
+ };
82
+ }
83
+
84
+ // Scrolling rolls the reel; other changes, like navigation, switch at once
85
+ function update(roll: boolean) {
86
+ const next = nextHeaderTitleState(state, measure());
87
+ if (next !== state) {
88
+ state = next;
89
+ container.classList.toggle(PAGE_TITLE_CLASS, state === 'page');
90
+ }
91
+ if (!roll && typeof reel.getAnimations === 'function') {
92
+ for (const animation of reel.getAnimations()) {
93
+ animation.finish();
94
+ }
95
+ }
96
+ }
97
+
98
+ const observer = globals.createIntersectionObserver(() => update(true), {
99
+ rootMargin: `${-summary.getBoundingClientRect().bottom}px 0px 0px 0px`,
100
+ threshold: [0, 1],
101
+ });
102
+
103
+ function observeHeading() {
104
+ observer.disconnect();
105
+ const heading = getHeadingElements(window.document);
106
+ if (heading) {
107
+ observer.observe(heading.block);
108
+ observer.observe(heading.title);
109
+ }
110
+ }
111
+
112
+ update(false);
113
+ observeHeading();
114
+
115
+ return {
116
+ swap(newDoc) {
117
+ const newPageTitle = newDoc.querySelector('header .header-page-title');
118
+ if (newPageTitle) {
119
+ pageTitle.replaceChildren(
120
+ ...window.document.importNode(newPageTitle, true).childNodes,
121
+ );
122
+ }
123
+ observeHeading();
124
+ update(false);
125
+ },
126
+ disconnect() {
127
+ observer.disconnect();
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Called by the navigator once a new page's content and scroll position are in
134
+ * place: adopt the new page title and show the title for the new position
135
+ * without rolling, so the page transition crossfades it.
136
+ */
137
+ export function swapHeaderTitle(newDoc: Document): void {
138
+ titleRoll?.swap(newDoc);
139
+ }
2
140
 
3
141
  export default (window: Window) => {
4
142
  const header: HTMLElement = getElement(window.document, 'header');
5
143
  const details = getElement(header, 'details') as HTMLDetailsElement;
6
144
  const summary = getElement(details, 'summary') as HTMLElement;
7
145
 
146
+ titleRoll = mountTitleRoll(window, summary);
147
+
8
148
  function close() {
9
149
  details.open = false;
10
150
  }
@@ -47,6 +187,8 @@ export default (window: Window) => {
47
187
  details.addEventListener('focusout', handleDetailsFocusOut);
48
188
 
49
189
  return () => {
190
+ titleRoll?.disconnect();
191
+ titleRoll = null;
50
192
  window.removeEventListener('keydown', handleWindowKeyDown);
51
193
  window.removeEventListener('click', handleWindowClick);
52
194
  details.removeEventListener('click', handleDetailsClick);
@@ -75,6 +75,14 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
75
75
  inline-size: auto;
76
76
  }
77
77
 
78
+ // The header stays in place. Parts that change between pages, such as the
79
+ // title shown in it, crossfade in step with the page heading.
80
+ ::view-transition-group(site-header),
81
+ ::view-transition-old(site-header),
82
+ ::view-transition-new(site-header) {
83
+ animation-duration: $duration;
84
+ }
85
+
78
86
  // The site-wide banner does not change between pages. Keep it out of the root
79
87
  // slide and pin its named transition layer in place without a crossfade.
80
88
  .site-banner {
@@ -5,6 +5,7 @@ import {
5
5
  teardownPerPageComponents,
6
6
  } from './lifecycle';
7
7
  import { globals } from '../globals';
8
+ import { swapHeaderTitle } from '../header';
8
9
 
9
10
  export const NAVIGATION_EVENT = 'tada:navigation';
10
11
 
@@ -327,6 +328,9 @@ export async function navigateToUrl(
327
328
 
328
329
  scrollByIndex.set(historyIndex, window.scrollY);
329
330
  scrollByLocation.set(getLocationKey(window), window.scrollY);
331
+
332
+ // Measured at the destination scroll position; the transition crossfades it
333
+ swapHeaderTitle(newDoc);
330
334
  return true;
331
335
  };
332
336
 
@@ -21,28 +21,28 @@
21
21
  --note-bg-color: hsl(209deg 95% 75%);
22
22
 
23
23
  // Main foreground color (the color of most text)
24
- --fg-color: hsl(var(--tint-hue) calc(8% * var(--tint-amount)) 8%);
24
+ --fg-color: hsl(var(--tint-hue) calc(30% * var(--tint-amount)) 8%);
25
25
  --fg-color-translucent: hsl(
26
26
  var(--tint-hue) calc(70% * var(--tint-amount)) 8% / 33%
27
27
  );
28
28
 
29
29
  // Secondary foreground color (lighter)
30
- --fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 45%);
30
+ --fg2-color: hsl(var(--tint-hue) calc(8% * var(--tint-amount)) 45%);
31
31
  --link-color: <%= linkColor %>;
32
32
  --link-color-hover: <%= linkColorHover %>;
33
33
 
34
34
  // Main background color (the color of the page background)
35
- --bg-color: hsl(var(--tint-hue) calc(5% * var(--tint-amount)) 97%);
35
+ --bg-color: hsl(var(--tint-hue) calc(65% * var(--tint-amount)) 97%);
36
36
 
37
37
  // Same saturation as --bg-color, so frosting doesn't add color to the page
38
38
  --bg-color-translucent: hsl(
39
- var(--tint-hue) calc(5% * var(--tint-amount)) 95% / 66%
39
+ var(--tint-hue) calc(65% * var(--tint-amount)) 95% / 66%
40
40
  );
41
41
 
42
42
  // Secondary background color (lighter)
43
- --bg2-color: hsl(var(--tint-hue) calc(12% * var(--tint-amount)) 92%);
43
+ --bg2-color: hsl(var(--tint-hue) calc(55% * var(--tint-amount)) 92%);
44
44
  --bg2-color-translucent: hsl(
45
- var(--tint-hue) calc(12% * var(--tint-amount)) 92% / 66%
45
+ var(--tint-hue) calc(55% * var(--tint-amount)) 92% / 66%
46
46
  );
47
47
  --bg-trace-line-active: <%= bgTraceLineActive %>;
48
48
  --box-shadow: 0 2px 9px
@@ -110,20 +110,20 @@
110
110
  --theme-color: <%= themeColorDark %>;
111
111
  --theme-color-text: <%= themeColorTextDark %>;
112
112
  --text-on-theme: <%= textOnThemeDark %>;
113
- --fg-color: hsl(var(--tint-hue) calc(10% * var(--tint-amount)) 92%);
113
+ --fg-color: hsl(var(--tint-hue) calc(40% * var(--tint-amount)) 92%);
114
114
  --fg-color-translucent: hsl(
115
115
  var(--tint-hue) calc(85% * var(--tint-amount)) 90% / 33%
116
116
  );
117
- --fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 60%);
117
+ --fg2-color: hsl(var(--tint-hue) calc(10% * var(--tint-amount)) 60%);
118
118
  --link-color: <%= linkColorDark %>;
119
119
  --link-color-hover: <%= linkColorHoverDark %>;
120
- --bg-color: hsl(var(--tint-hue) calc(3% * var(--tint-amount)) 5%);
120
+ --bg-color: hsl(var(--tint-hue) calc(35% * var(--tint-amount)) 5%);
121
121
  --bg-color-translucent: hsl(
122
- var(--tint-hue) calc(3% * var(--tint-amount)) 10% / 66%
122
+ var(--tint-hue) calc(35% * var(--tint-amount)) 10% / 66%
123
123
  );
124
- --bg2-color: hsl(var(--tint-hue) calc(12% * var(--tint-amount)) 12%);
124
+ --bg2-color: hsl(var(--tint-hue) calc(40% * var(--tint-amount)) 12%);
125
125
  --bg2-color-translucent: hsl(
126
- var(--tint-hue) calc(12% * var(--tint-amount)) 12% / 66%
126
+ var(--tint-hue) calc(40% * var(--tint-amount)) 12% / 66%
127
127
  );
128
128
  --bg-trace-line-active: <%= bgTraceLineActiveDark %>;
129
129
  --box-shadow: 0 3px 8px
@@ -220,7 +220,12 @@
220
220
  <path class="menu-icon-line menu-icon-line-bottom" d="M3 18h18" />
221
221
  </svg>
222
222
  <% if (site.logo) { %><img class="logo-image" src="/<%- encodePublicAssetPath(site.logo) %>" alt="" aria-hidden="true" /><% } else if (site.symbol) { %><span class="logo" aria-hidden="true"><%- site.symbol %></span><% } %>
223
- <span class="site-title" aria-hidden="true"><%- site.title %></span>
223
+ <span class="header-title" aria-hidden="true">
224
+ <span class="header-title-reel">
225
+ <span class="site-title"><%- site.title %></span>
226
+ <span class="header-page-title"><%= page.titleHtml %></span>
227
+ </span>
228
+ </span>
224
229
  </summary>
225
230
  <div class="content">
226
231
  <%= render('_nav.html') %>