@janga/norna 0.7.26 → 0.7.28

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 (110) hide show
  1. package/README.md +51 -35
  2. package/astro.config.mjs +19 -1
  3. package/bin/norna-cli.mjs +6 -4
  4. package/package.json +9 -5
  5. package/schemas/card-list.schema.json +1 -1
  6. package/schemas/config.schema.json +66 -66
  7. package/schemas/content-frontmatter.schema.json +17 -8
  8. package/schemas/manifest.json +49 -3
  9. package/schemas/page-theme.schema.json +1585 -41
  10. package/schemas/root-theme.schema.json +1767 -0
  11. package/schemas/sitewide-content.schema.json +18 -18
  12. package/schemas/theme.schema.json +243 -243
  13. package/scripts/add-site-node.mjs +13 -161
  14. package/scripts/build-site.mjs +11 -6
  15. package/scripts/check-config.mjs +7 -23
  16. package/scripts/dev-local.mjs +17 -10
  17. package/scripts/export-theme-preset.mjs +2 -2
  18. package/scripts/generate-images.mjs +7 -6
  19. package/scripts/generate-schemas.mjs +9 -6
  20. package/scripts/lib/category-destinations.mjs +13 -12
  21. package/scripts/lib/documentation-links.mjs +33 -2
  22. package/scripts/lib/documentation-routes.json +229 -0
  23. package/scripts/lib/docusaurus-migration-audit.mjs +9 -6
  24. package/scripts/lib/editor-file-policy.mjs +18 -0
  25. package/scripts/lib/editor-language-service.mjs +53 -55
  26. package/scripts/lib/editor-page-addresses.mjs +55 -0
  27. package/scripts/lib/editor-page-placement.mjs +172 -0
  28. package/scripts/lib/editor-site-editing-tree.mjs +146 -0
  29. package/scripts/lib/editor-site-files.mjs +153 -0
  30. package/scripts/lib/editor-site-links.mjs +79 -0
  31. package/scripts/lib/editor-site-tree.mjs +151 -0
  32. package/scripts/lib/editor-source-files.mjs +87 -0
  33. package/scripts/lib/editor-theme-help.mjs +43 -0
  34. package/scripts/lib/locale-registry.mjs +1 -1
  35. package/scripts/lib/locales/en.mjs +1 -1
  36. package/scripts/lib/navigation-model.mjs +2 -44
  37. package/scripts/lib/navigation-review.mjs +14 -50
  38. package/scripts/lib/norna-markdown-blocks.mjs +1 -1
  39. package/scripts/lib/page-aliases.mjs +3 -1
  40. package/scripts/lib/page-model.mjs +39 -20
  41. package/scripts/lib/page-move-apply.mjs +117 -0
  42. package/scripts/lib/page-move-plan.mjs +27 -59
  43. package/scripts/lib/presentation.mjs +3 -3
  44. package/scripts/lib/project-config.mjs +7 -8
  45. package/scripts/lib/schema-definitions.mjs +12 -39
  46. package/scripts/lib/schema-editor-metadata.mjs +12 -59
  47. package/scripts/lib/schema-value-definitions.mjs +1 -1
  48. package/scripts/lib/site-content.mjs +7 -24
  49. package/scripts/lib/site-conventions.mjs +19 -1
  50. package/scripts/lib/site-navigation-tree.mjs +2 -7
  51. package/scripts/lib/site-node-create.mjs +169 -0
  52. package/scripts/lib/site-paths.mjs +22 -10
  53. package/scripts/lib/site-structure.mjs +72 -82
  54. package/scripts/lib/site-upgrade.mjs +30 -0
  55. package/scripts/lib/source-files.mjs +34 -0
  56. package/scripts/lib/theme-config.mjs +26 -68
  57. package/scripts/lib/theme-packages.mjs +47 -0
  58. package/scripts/lib/theme-presets.mjs +20 -53
  59. package/scripts/lib/typography.mjs +2 -2
  60. package/scripts/lib/yaml-config.mjs +1 -1
  61. package/scripts/lib/yaml-source-edit.mjs +94 -0
  62. package/scripts/move-site-page.mjs +3 -124
  63. package/scripts/show-typography.mjs +32 -17
  64. package/scripts/sync-content-sections.mjs +9 -7
  65. package/scripts/sync-site-public.mjs +1 -1
  66. package/scripts/upgrade-site.mjs +17 -0
  67. package/src/components/AreaNavigationMenu.astro +24 -0
  68. package/src/components/AreaNavigationPanel.astro +55 -0
  69. package/src/components/AreaNavigationSwitcher.astro +30 -0
  70. package/src/components/ImageStackEnhancement.astro +0 -7
  71. package/src/components/NavigationContinuity.astro +26 -0
  72. package/src/components/NavigationPageTree.astro +13 -4
  73. package/src/components/NavigationState.astro +11 -0
  74. package/src/components/NavigationTreeControls.astro +3 -2
  75. package/src/components/PageAliasRedirect.astro +44 -3
  76. package/src/components/SearchPage.astro +9 -1
  77. package/src/components/SectionNavigationScript.astro +106 -26
  78. package/src/components/SiteNavigation.astro +45 -20
  79. package/src/components/SitePage.astro +37 -61
  80. package/src/components/SiteTreeNavigation.astro +38 -18
  81. package/src/components/TopPageMenu.astro +4 -3
  82. package/src/components/TreeNavigationScript.astro +50 -57
  83. package/src/content.config.ts +15 -16
  84. package/src/layouts/BaseLayout.astro +15 -39
  85. package/src/lib/areaNavigation.ts +42 -0
  86. package/src/lib/navigationContinuity.ts +212 -0
  87. package/src/lib/navigationFollowing.ts +28 -18
  88. package/src/lib/navigationState.mjs +88 -0
  89. package/src/lib/navigationStateScript.mjs +76 -0
  90. package/src/lib/navigationVisibility.ts +9 -0
  91. package/src/lib/pageThemes.ts +5 -42
  92. package/src/lib/sectionContent.ts +3 -2
  93. package/src/lib/siteNavigation.ts +6 -0
  94. package/src/lib/sitePages.ts +12 -23
  95. package/src/pages/404.astro +1 -1
  96. package/src/pages/[...slug].astro +5 -15
  97. package/src/pages/index.astro +1 -3
  98. package/src/styles/area-navigation.css +399 -0
  99. package/src/styles/content.css +5 -31
  100. package/src/styles/navigation-continuity.css +163 -0
  101. package/src/styles/page-layout.css +57 -68
  102. package/src/styles/responsive.css +0 -4
  103. package/starters/basic/README.md +10 -9
  104. package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
  105. package/schemas/category.schema.json +0 -20
  106. package/src/components/PageContentsNavigation.astro +0 -20
  107. /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
  108. /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
  109. /package/starters/basic/site/{config.yaml → site-config/settings.yaml} +0 -0
  110. /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
@@ -1,12 +1,12 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
2
  import path from 'node:path';
3
- import { decodePageDirectoryPath, parsePageDirectoryPath } from '../../scripts/lib/page-model.mjs';
3
+ import { decodePageEntryId, parsePageDirectoryPath } from '../../scripts/lib/page-model.mjs';
4
4
  import { parsePageMarkdown } from '../../scripts/lib/page-markdown.mjs';
5
5
  import { readSiteFile } from '../../scripts/lib/site-content.mjs';
6
- import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
6
+ import { homePageDirectory, getPageSourceDirectory } from '../../scripts/lib/site-conventions.mjs';
7
7
  import { assertCategoryDestinationModel, createCategoryDestinationModel } from '../../scripts/lib/category-destinations.mjs';
8
8
  import { getSiteStructure } from '../../scripts/lib/site-structure.mjs';
9
- import { sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
9
+ import { siteDir, siteDirLabel } from '../../scripts/lib/site-paths.mjs';
10
10
 
11
11
  type SiteEntry = CollectionEntry<'site'>;
12
12
 
@@ -44,28 +44,15 @@ export type SitePage = SiteNodeBase & {
44
44
  export type SiteCategory = SiteNodeBase & {
45
45
  kind: 'category';
46
46
  categorySourceLabel: string;
47
+ description?: string;
47
48
  };
48
49
 
49
50
  export type SiteNode = SitePage | SiteCategory;
50
51
 
51
- const pageIdMarker = '-page-';
52
-
53
- const stripPageIdPrefix = (id: string) => {
54
- const markerIndex = id.lastIndexOf(pageIdMarker);
55
- if (markerIndex === -1) return null;
56
-
57
- const candidate = id.slice(markerIndex + pageIdMarker.length);
58
- try {
59
- return parsePageDirectoryPath(decodePageDirectoryPath(candidate)).pageDirectory;
60
- } catch {
61
- return null;
62
- }
63
- };
64
-
65
- export const isHomePageEntry = (entry: SiteEntry) => stripPageIdPrefix(entry.id) === homePageDirectory;
52
+ export const isHomePageEntry = (entry: SiteEntry) => decodePageEntryId(siteDirLabel, entry.id) === homePageDirectory;
66
53
 
67
54
  export const getPageDirectory = (entry: SiteEntry) => {
68
- const pageDirectory = stripPageIdPrefix(entry.id);
55
+ const pageDirectory = decodePageEntryId(siteDirLabel, entry.id);
69
56
  if (!pageDirectory) throw new Error(`Page entry "${entry.id}" has no valid page directory.`);
70
57
  return pageDirectory;
71
58
  };
@@ -89,8 +76,9 @@ const compareNumberPaths = (left: number[], right: number[]) => {
89
76
 
90
77
  const readPageMarkdownDocument = async (entry: SiteEntry) => {
91
78
  const pageDirectory = getPageDirectory(entry);
92
- const contentLabel = `${sitePagesLabel}/${pageDirectory}/content.md`;
93
- const contentPath = path.join(sitePagesDir, pageDirectory, 'content.md');
79
+ const contentRelativePath = [getPageSourceDirectory(pageDirectory), 'content.md'].filter(Boolean).join('/');
80
+ const contentLabel = `${siteDirLabel}/${contentRelativePath}`;
81
+ const contentPath = path.join(siteDir, contentRelativePath);
94
82
  const { body, frontmatter } = await readSiteFile(contentPath, contentLabel);
95
83
  const markdownDocument = await parsePageMarkdown(body, {
96
84
  label: contentLabel,
@@ -155,6 +143,7 @@ const createSiteCategory = (category: Awaited<ReturnType<typeof getSiteStructure
155
143
  pathSegment: category.pagePath,
156
144
  pathname: getPagePathname(category.pagePath),
157
145
  categorySourceLabel: category.categorySourceLabel,
146
+ description: category.description,
158
147
  pageDirectory: category.pageDirectory,
159
148
  pageDirectories: category.pageDirectories,
160
149
  pageId: category.pageId,
@@ -175,7 +164,7 @@ const createSitePages = async (entries: SiteEntry[]) => {
175
164
 
176
165
  for (const page of pages) {
177
166
  if (page.isHome && page.navigation.listed === false) {
178
- throw new Error(`${homePageDirectory} is the required homepage and cannot set navigation.listed to false.`);
167
+ throw new Error(`The homepage at ${siteDirLabel}/root/content.md cannot set navigation.listed to false.`);
179
168
  }
180
169
  const existing = pathnames.get(page.pathname);
181
170
  if (existing) {
@@ -204,7 +193,7 @@ const createSitePages = async (entries: SiteEntry[]) => {
204
193
  siblingPageOrders.set(pageOrderKey, page);
205
194
  }
206
195
  if (!pages.some(({ isHome }) => isHome)) {
207
- throw new Error(`Homepage page entry ${homePageDirectory} is missing.`);
196
+ throw new Error(`Homepage content ${siteDirLabel}/root/content.md is missing.`);
208
197
  }
209
198
 
210
199
  return pages.sort((left, right) => (
@@ -20,7 +20,7 @@ const homePage = sitePages.find((page) => page.isHome);
20
20
  if (!homePage) throw new Error('The required homepage is missing from the site page model.');
21
21
 
22
22
  const [siteTheme] = await getCollection('theme');
23
- if (!siteTheme) throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
23
+ if (!siteTheme) throw new Error('site/root/tree-theme.yaml is required. Add the shared site theme before building the site.');
24
24
  const [sitewide] = await getCollection('sitewide');
25
25
  const sitewideData = sitewide?.data ?? {};
26
26
  const siteThemeData = siteTheme.data;
@@ -45,28 +45,18 @@ export async function getStaticPaths() {
45
45
  ? [{ params: { slug: 'search' }, props: { searchPage: true } }]
46
46
  : [];
47
47
 
48
- const categoryPaths = siteModel.categoryDestinations.destinations.map((destination) => ({
49
- params: { slug: destination.category.pathSegment },
50
- props: destination.kind === 'redirect' ? {
51
- alias: {
52
- targetPathname: destination.target.pathname,
53
- targetTitle: destination.target.title,
54
- categoryTitle: destination.category.title,
55
- },
56
- } : { categoryPath: destination.pathname },
57
- }));
58
- return [...pagePaths, ...aliasPaths, ...searchPath, ...categoryPaths];
48
+ return [...pagePaths, ...aliasPaths, ...searchPath];
59
49
  }
60
50
 
61
- const { alias, entry, searchPage, categoryPath } = Astro.props;
51
+ const { alias, entry, searchPage } = Astro.props;
62
52
 
63
- if (!searchPage && !alias && !categoryPath && (!entry || isHomePageEntry(entry))) {
53
+ if (!searchPage && !alias && (!entry || isHomePageEntry(entry))) {
64
54
  throw new Error('Page entry is missing.');
65
55
  }
66
56
 
67
57
  const currentPage = entry ? await getSitePage(entry) : null;
68
58
 
69
- if (!searchPage && !alias && !categoryPath && !currentPage?.pathSegment) {
59
+ if (!searchPage && !alias && !currentPage?.pathSegment) {
70
60
  throw new Error(`Page "${entry.id}" needs a page id.`);
71
61
  }
72
62
  ---
@@ -75,4 +65,4 @@ if (!searchPage && !alias && !categoryPath && !currentPage?.pathSegment) {
75
65
  ? <SearchPage />
76
66
  : alias
77
67
  ? <PageAliasRedirect targetPathname={alias.targetPathname} targetTitle={alias.targetTitle} categoryTitle={alias.categoryTitle} />
78
- : <SitePage entry={entry} categoryPath={categoryPath} />}
68
+ : <SitePage entry={entry} />}
@@ -2,10 +2,8 @@
2
2
  import { getCollection } from 'astro:content';
3
3
  import SitePage from '../components/SitePage.astro';
4
4
  import { isHomePageEntry } from '../lib/sitePages';
5
- import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
5
+ import { siteContentLabel } from '../../scripts/lib/site-paths.mjs';
6
6
 
7
- const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
8
- const siteContentLabel = `${siteDirectory || 'site'}/pages/${homePageDirectory}/content.md`;
9
7
  const entries = await getCollection('site');
10
8
  const site = entries.find(isHomePageEntry);
11
9
 
@@ -0,0 +1,399 @@
1
+ /* Area menus, integrated H2 navigation and the tree-mode reading frame. */
2
+ .site-reading-location,
3
+ .area-navigation-switcher {
4
+ display: none;
5
+ }
6
+
7
+ /* An elevated dark panel needs a distinct surface and edge: a black shadow
8
+ cannot separate it from the page. Keep its link feedback distinct as well. */
9
+ :root[data-area-navigation][data-appearance='dark'] {
10
+ --area-menu-background: var(--color-surface-soft-background);
11
+ --area-menu-border: color-mix(in srgb, var(--color-primary-text) 40%, var(--color-surface-soft-background));
12
+ --area-menu-hover-background: var(--color-surface-emphasis-background);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root[data-area-navigation][data-appearance='system'] {
17
+ --area-menu-background: var(--color-surface-soft-background);
18
+ --area-menu-border: color-mix(in srgb, var(--color-primary-text) 40%, var(--color-surface-soft-background));
19
+ --area-menu-hover-background: var(--color-surface-emphasis-background);
20
+ }
21
+ }
22
+
23
+ :root[data-area-navigation] .site-nav-row {
24
+ position: relative;
25
+ }
26
+
27
+ :root[data-area-navigation] .site-nav-item:has(.area-navigation-menu) {
28
+ position: static;
29
+ }
30
+
31
+ :root[data-area-navigation] .site-nav > ul > .site-nav-item > a,
32
+ :root[data-area-navigation] .area-navigation-menu > summary {
33
+ display: flex;
34
+ align-items: center;
35
+ min-height: var(--target-size-minimum);
36
+ padding-block: 0;
37
+ width: auto;
38
+ color: var(--color-secondary-text);
39
+ font-size: 0.78rem;
40
+ font-weight: 500;
41
+ letter-spacing: 0.08em;
42
+ line-height: 1.2;
43
+ text-transform: uppercase;
44
+ }
45
+
46
+ :root[data-area-navigation] .area-navigation-menu > summary {
47
+ position: relative;
48
+ isolation: isolate;
49
+ text-decoration: none;
50
+ }
51
+
52
+ /* Paint around the trigger without moving its label or neighboring links. */
53
+ :root[data-area-navigation] .area-navigation-menu > summary::before {
54
+ position: absolute;
55
+ inset: -0.25rem -0.6rem;
56
+ z-index: -1;
57
+ border: 1px solid transparent;
58
+ border-radius: var(--corner-radius-small);
59
+ content: '';
60
+ pointer-events: none;
61
+ }
62
+
63
+ :root[data-area-navigation] .area-navigation-menu > summary:is(:hover, :focus-visible),
64
+ :root[data-area-navigation] .area-navigation-menu[open] > summary,
65
+ :root[data-area-navigation] .site-nav-item-current-branch .area-navigation-menu > summary {
66
+ color: var(--site-top-text-color, var(--color-primary-text));
67
+ }
68
+
69
+ :root[data-area-navigation] .area-navigation-menu > summary:is(:hover, :focus-visible)::before,
70
+ :root[data-area-navigation] .area-navigation-menu[open] > summary::before {
71
+ border-color: color-mix(in srgb, currentColor 22%, transparent);
72
+ background: color-mix(in srgb, currentColor 8%, transparent);
73
+ }
74
+
75
+ :root[data-area-navigation] .area-navigation-panel {
76
+ --area-menu-width: min(calc(var(--area-menu-columns) * 16rem), calc(100vw - 2rem));
77
+ /* Anchor to the entire sticky row, including wrapped global links. This
78
+ also fits without the optional JavaScript panel-position adjustment. */
79
+ inset-inline-end: 0;
80
+ width: var(--area-menu-width);
81
+ max-width: calc(100vw - 2rem);
82
+ border-radius: var(--corner-radius-medium);
83
+ border-color: var(--area-menu-border, var(--color-border));
84
+ background: var(--area-menu-background, var(--site-top-background-color, var(--color-page)));
85
+ padding: 1rem;
86
+ display: grid;
87
+ grid-template-columns: repeat(var(--area-menu-columns), minmax(0, 1fr));
88
+ gap: 1rem;
89
+ }
90
+
91
+ :root[data-area-navigation] .area-navigation-choices {
92
+ margin: 0;
93
+ padding: 0;
94
+ list-style: none;
95
+ display: grid;
96
+ grid-column: span var(--area-group-columns);
97
+ grid-template-rows: repeat(var(--area-group-rows), auto);
98
+ grid-template-columns: repeat(var(--area-group-columns), minmax(0, 1fr));
99
+ grid-auto-flow: column;
100
+ grid-auto-columns: minmax(0, 1fr);
101
+ gap: 0.75rem 1rem;
102
+ align-content: start;
103
+ }
104
+
105
+ :root[data-area-navigation] [data-area-choice-group='branches'] + [data-area-choice-group='pages'] {
106
+ border-inline-start: 1px solid var(--color-border);
107
+ padding-inline-start: 1rem;
108
+ }
109
+
110
+ :root[data-area-navigation] [data-area-choice-group='pages'] {
111
+ row-gap: 0.15rem;
112
+ }
113
+
114
+ :root[data-area-navigation] [data-area-choice-group='pages'] .area-navigation-title {
115
+ font-weight: 400;
116
+ }
117
+
118
+ :root[data-area-navigation] .area-navigation-choices a {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 0.45rem;
122
+ height: 100%;
123
+ padding: 0.65rem 0.75rem;
124
+ border-radius: var(--corner-radius-small);
125
+ color: var(--color-primary-text);
126
+ font-size: 0.875rem;
127
+ font-weight: 400;
128
+ letter-spacing: 0;
129
+ line-height: 1.4;
130
+ text-decoration: none;
131
+ text-transform: none;
132
+ overflow-wrap: break-word;
133
+ }
134
+
135
+ :root[data-area-navigation] .area-navigation-choices a:is(:hover, :focus-visible) {
136
+ background: var(--area-menu-hover-background, var(--color-surface-soft-background));
137
+ }
138
+
139
+ :root[data-area-navigation] .area-navigation-title {
140
+ display: flex;
141
+ justify-content: space-between;
142
+ align-items: baseline;
143
+ gap: 0.4rem;
144
+ font-weight: 500;
145
+ }
146
+
147
+ :root[data-area-navigation] .area-navigation-title svg {
148
+ flex: 0 0 auto;
149
+ width: 0.85rem;
150
+ height: 0.85rem;
151
+ }
152
+
153
+ :root[data-area-navigation] .area-navigation-description {
154
+ color: var(--color-secondary-text);
155
+ font-size: 0.8rem;
156
+ line-height: 1.55;
157
+ }
158
+
159
+ :root[data-area-navigation] .area-navigation-other-roots {
160
+ grid-column: 1 / -1;
161
+ display: flex;
162
+ flex-wrap: wrap;
163
+ gap: 0.25rem 0.75rem;
164
+ margin: 0;
165
+ border-block-start: 1px solid var(--color-border);
166
+ padding: 0.75rem 0 0;
167
+ list-style: none;
168
+ }
169
+
170
+ :root[data-area-navigation] .area-navigation-other-roots a {
171
+ display: block;
172
+ padding: 0.45rem 0.75rem;
173
+ border-radius: var(--corner-radius-small);
174
+ color: var(--color-secondary-text);
175
+ font-size: 0.8rem;
176
+ font-weight: 400;
177
+ line-height: 1.4;
178
+ text-decoration: none;
179
+ }
180
+
181
+ :root[data-area-navigation] .area-navigation-other-roots a:is(:hover, :focus-visible) {
182
+ background: var(--area-menu-hover-background, var(--color-surface-soft-background));
183
+ color: var(--color-primary-text);
184
+ }
185
+
186
+ :root[data-area-navigation] .navigation-area-title {
187
+ display: block;
188
+ margin: 0 0 1rem;
189
+ color: var(--color-primary-text);
190
+ font-size: 0.95rem;
191
+ font-weight: 600;
192
+ text-decoration: none;
193
+ }
194
+
195
+ :root[data-area-navigation] a.navigation-area-title:is(:hover, :focus-visible) {
196
+ text-decoration: underline;
197
+ text-underline-offset: 0.25em;
198
+ }
199
+
200
+ /* The sticky controls mask 1.25rem above their box. Keep that background
201
+ below the area's linked title, including its underline. */
202
+ :root[data-area-navigation] .navigation-area-title:has(+ .navigation-tree-controls) {
203
+ margin-block-end: 1.5rem;
204
+ }
205
+
206
+ /* Scoped pages are now at the tree's first level. Override the ordinary
207
+ first-level emphasis as well as current-page emphasis at every depth. */
208
+ :root[data-navigation-continuity][data-area-navigation]
209
+ .tree-local-navigation .navigation-page-tree
210
+ :is(.navigation-page-link, .navigation-page-open-link, .navigation-page-summary) {
211
+ font-size: 0.84rem;
212
+ font-weight: 400;
213
+ }
214
+
215
+ @media (min-width: 961px) {
216
+ :root[data-area-reading-frame] {
217
+ --area-reading-rail-width: 16rem;
218
+ --area-reading-header-height: 4rem;
219
+ --site-top-anchor-offset: calc(var(--area-reading-header-height) + 1px + var(--nav-top-safety-inset));
220
+ --area-reading-outer-margin: max(var(--content-gutter), calc((100vw - max(var(--page-width), 86rem)) / 2));
221
+ --area-reading-content-start: calc(var(--area-reading-outer-margin) + var(--area-reading-rail-width) + var(--local-navigation-gap, clamp(1.5rem, 3vw, 3rem)));
222
+ }
223
+
224
+ :root[data-area-reading-frame] .site-page-layout-tree > .site-content {
225
+ grid-column: 2;
226
+ }
227
+
228
+ /* One sticky row shares the article/tree columns. The ordinary header
229
+ measurement supplies anchor offsets and the fixed tree's upper edge. */
230
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .site-top {
231
+ background: var(--color-page);
232
+ color: var(--color-primary-text);
233
+ }
234
+
235
+ /* Keep header geometry stable when Focus reading switches to compact controls. */
236
+ :root[data-area-reading-frame] .site-nav-row {
237
+ min-height: var(--area-reading-header-height);
238
+ padding: 0.625rem 0;
239
+ }
240
+
241
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .site-nav-row {
242
+ display: grid;
243
+ grid-template-columns: var(--area-reading-rail-width) minmax(0, 1fr);
244
+ column-gap: var(--local-navigation-gap, clamp(1.5rem, 3vw, 3rem));
245
+ width: min(calc(100% - (var(--content-gutter) * 2)), max(var(--page-width), 86rem));
246
+ }
247
+
248
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .site-nav-leading {
249
+ grid-column: 1;
250
+ grid-row: 1;
251
+ max-width: calc(100% - 6rem);
252
+ padding-inline-start: 0.35rem;
253
+ }
254
+
255
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .site-brand {
256
+ min-width: 0;
257
+ max-width: 100%;
258
+ min-height: 2.25rem;
259
+ overflow: hidden;
260
+ color: inherit;
261
+ text-decoration: none;
262
+ }
263
+
264
+ :root[data-area-reading-frame] .site-brand-logo {
265
+ max-width: 100%;
266
+ max-height: 2.25rem;
267
+ object-fit: contain;
268
+ }
269
+
270
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .site-nav-actions {
271
+ grid-column: 1;
272
+ grid-row: 1;
273
+ justify-self: end;
274
+ gap: 0.35rem;
275
+ padding-inline-end: 0.75rem;
276
+ }
277
+
278
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .site-reading-location {
279
+ display: flex;
280
+ grid-column: 2;
281
+ grid-row: 1;
282
+ align-items: center;
283
+ gap: 0.65rem;
284
+ min-width: 0;
285
+ }
286
+
287
+ :root[data-area-reading-frame] .site-reading-location .site-breadcrumbs {
288
+ flex: 1 1 auto;
289
+ min-width: 0;
290
+ min-height: 0;
291
+ width: auto;
292
+ margin: 0;
293
+ padding: 0;
294
+ }
295
+
296
+ :root[data-area-reading-frame] .site-reading-location .site-breadcrumbs ol {
297
+ flex-wrap: nowrap;
298
+ }
299
+
300
+ :root[data-area-reading-frame] .site-reading-location .site-breadcrumbs li > :is(a, span) {
301
+ overflow: hidden;
302
+ text-overflow: ellipsis;
303
+ white-space: nowrap;
304
+ }
305
+
306
+ :root[data-area-reading-frame] .site-reading-location-label {
307
+ font-size: 0.78rem;
308
+ font-weight: 650;
309
+ }
310
+
311
+ /* The in-article breadcrumb remains available in the compact layout. */
312
+ :root[data-area-reading-frame] .site-content > .site-breadcrumbs {
313
+ display: none;
314
+ }
315
+
316
+ :root[data-area-reading-frame] .site-content-has-breadcrumbs > .site-section:first-of-type {
317
+ padding-top: var(--space-nav-to-first-section);
318
+ }
319
+
320
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .display-settings-panel {
321
+ left: 0;
322
+ right: auto;
323
+ max-height: calc(100svh - 4.5rem);
324
+ }
325
+
326
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .tree-local-navigation {
327
+ position: fixed;
328
+ top: var(--site-top-anchor-offset);
329
+ left: var(--area-reading-outer-margin);
330
+ bottom: 0;
331
+ width: var(--area-reading-rail-width);
332
+ max-height: calc(100svh - var(--site-top-anchor-offset));
333
+ }
334
+
335
+ :root[data-area-reading-frame] .site-banners {
336
+ width: calc(100% - var(--area-reading-content-start) - var(--area-reading-outer-margin));
337
+ margin-inline: var(--area-reading-content-start) var(--area-reading-outer-margin);
338
+ }
339
+
340
+ :root[data-area-reading-frame]:not([data-focus-reading='on']) .area-navigation-switcher {
341
+ position: static;
342
+ display: block;
343
+ flex: 0 0 auto;
344
+ min-width: 0;
345
+ }
346
+
347
+ :root[data-area-reading-frame] .area-navigation-switcher > summary {
348
+ width: 2.25rem;
349
+ height: 2.25rem;
350
+ border-radius: var(--corner-radius-small);
351
+ }
352
+
353
+ :root[data-area-reading-frame] .area-navigation-switcher > summary:is(:hover, :focus-visible),
354
+ :root[data-area-reading-frame] .area-navigation-switcher[open] > summary {
355
+ background: var(--color-surface-soft-background);
356
+ }
357
+
358
+ :root[data-area-reading-frame] .area-navigation-switcher > summary svg {
359
+ flex: 0 0 auto;
360
+ transform: none;
361
+ }
362
+
363
+ :root[data-area-reading-frame] .area-navigation-switcher .area-navigation-panel {
364
+ /* Fit beneath the shared row before the disclosure's toggle event.
365
+ This also keeps the panel visible without JavaScript. */
366
+ inset-inline-start: min(
367
+ calc(var(--area-reading-rail-width) + var(--local-navigation-gap, clamp(1.5rem, 3vw, 3rem))),
368
+ calc(100% - var(--area-menu-width))
369
+ );
370
+ inset-inline-end: auto;
371
+ inset-block-start: 100%;
372
+ max-height: calc(100svh - var(--site-top-anchor-offset) - 1rem);
373
+ background: var(--area-menu-background, var(--color-page));
374
+ color: var(--color-primary-text);
375
+ overscroll-behavior: contain;
376
+ }
377
+
378
+ :root[data-area-navigation] .site-page-layout-tree {
379
+ --tree-navigation-width: 16rem;
380
+ grid-template-columns: var(--tree-navigation-width) minmax(0, 1fr);
381
+ }
382
+
383
+ /* The recovered right column belongs to the content canvas. Do not count
384
+ the old outline allowance a second time for notes or wide blocks. */
385
+ :root[data-area-navigation] .site-page-layout-tree .section-markdown > :is(.norna-table-frame, .code-block) {
386
+ --wide-block-end-width: 100cqi;
387
+ --wide-block-canvas-width: var(--wide-block-end-width);
388
+ }
389
+
390
+ :root[data-area-navigation][data-reader-preferences-ready='true'][data-focus-reading='on'] .site-page-layout-tree {
391
+ grid-template-columns: var(--tree-navigation-width) minmax(0, 1fr);
392
+ }
393
+
394
+ :root[data-area-navigation][data-reader-preferences-ready='true'][data-focus-reading='on']
395
+ .site-page-layout-tree .section-markdown > :is(.norna-table-frame, .code-block) {
396
+ --wide-block-end-width: 100cqi;
397
+ --wide-block-canvas-width: calc(100cqi + var(--tree-navigation-width) + var(--tree-layout-gap));
398
+ }
399
+ }
@@ -466,7 +466,8 @@ ul {
466
466
  }
467
467
 
468
468
  .section-markdown > .norna-table-frame {
469
- width: var(--wide-block-end-width);
469
+ /* Start in the same lane that the layout enhancement tries first. */
470
+ width: var(--wide-block-prose-width);
470
471
  }
471
472
 
472
473
  .section-markdown > :is([data-table-layout='prose'], [data-code-layout='prose']) {
@@ -784,32 +785,10 @@ ul {
784
785
  }
785
786
  }
786
787
 
787
- @media (min-width: 1101px) {
788
- :root[data-reader-preferences-ready='true'][data-focus-reading='on']
789
- .site-page-layout-tree .section-markdown > :is(.norna-table-frame, .code-block) {
790
- --wide-block-end-width: calc(100cqi + var(--contents-navigation-width) + var(--tree-layout-gap));
791
- --wide-block-canvas-width: calc(
792
- 100cqi
793
- + var(--tree-navigation-width)
794
- + var(--contents-navigation-width)
795
- + (var(--tree-layout-gap) * 2)
796
- );
797
- --wide-block-canvas-inline-offset: calc(0px - var(--tree-navigation-width) - var(--tree-layout-gap));
798
- }
799
- }
800
-
801
- @media (min-width: 1281px) {
802
- .site-page-layout-tree:not(.site-page-layout-contents) .section-markdown > :is(.norna-table-frame, .code-block) {
803
- --wide-block-end-width: calc(100cqi + var(--contents-navigation-width) + var(--tree-layout-gap));
804
- --wide-block-canvas-width: var(--wide-block-end-width);
805
- }
806
- }
807
-
808
788
  :root[data-reader-preferences-ready='true'][data-focus-reading='on'] :is(
809
789
  .site-nav,
810
790
  .page-nav,
811
791
  .tree-local-navigation,
812
- .page-contents-navigation,
813
792
  .site-breadcrumbs,
814
793
  .site-footer
815
794
  ) {
@@ -835,12 +814,6 @@ ul {
835
814
  }
836
815
  }
837
816
 
838
- @media (min-width: 1101px) {
839
- :root[data-reader-preferences-ready='true'][data-focus-reading='on'] .site-page-layout-tree {
840
- grid-template-columns: var(--tree-navigation-width) minmax(0, 1fr) var(--contents-navigation-width);
841
- }
842
- }
843
-
844
817
  :root[data-reader-preferences-ready='true'][data-focus-reading='on']
845
818
  .site-content-has-breadcrumbs > .site-section:first-of-type {
846
819
  padding-top: var(--space-nav-to-first-section);
@@ -983,8 +956,9 @@ ul {
983
956
  padding: 0.78rem 0.9rem;
984
957
  background: var(--color-code-background) !important;
985
958
  color: var(--color-code-text);
986
- font-size: 0.92em;
987
- line-height: 1.45;
959
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
960
+ font-size: 0.875rem;
961
+ line-height: 1.43;
988
962
  text-align: left;
989
963
  }
990
964