@master/internal-site 0.2.6 → 0.2.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 (112) hide show
  1. package/README.md +1 -1
  2. package/dist/astro.mjs +9 -1
  3. package/package.json +3 -2
  4. package/src/benchmarks/BenchmarkChart.astro +8 -8
  5. package/src/benchmarks/BenchmarkDelta.astro +2 -3
  6. package/src/benchmarks/BenchmarkFigure.astro +1 -1
  7. package/src/benchmarks/BenchmarkMetricTable.astro +10 -10
  8. package/src/benchmarks/BenchmarkScrollRegion.svelte +2 -2
  9. package/src/benchmarks/BenchmarkStatus.astro +2 -3
  10. package/src/benchmarks/utils.ts +1 -9
  11. package/src/css/NamespaceUtilityTable.astro +11 -11
  12. package/src/css/ThemeNumberVariableTable.astro +14 -14
  13. package/src/demo/Demo.svelte +2 -2
  14. package/src/demo/DemoAsset.astro +3 -3
  15. package/src/demo/DemoBadge.svelte +3 -3
  16. package/src/demo/DemoContainer.svelte +2 -2
  17. package/src/demo/DemoControls.svelte +1 -1
  18. package/src/demo/DemoCopyButton.svelte +1 -1
  19. package/src/demo/DemoFeatureSupport.svelte +3 -4
  20. package/src/demo/DemoIndex.astro +1 -1
  21. package/src/demo/DemoMotion.svelte +2 -2
  22. package/src/demo/DemoSurface.svelte +2 -2
  23. package/src/demo/DemoTokenTable.astro +6 -6
  24. package/src/demo/DemoViewport.svelte +11 -11
  25. package/src/demo/foundations/FoundationDialog.astro +1 -1
  26. package/src/demo/foundations/FoundationMedia.astro +2 -2
  27. package/src/demo/foundations/FoundationShrink.astro +2 -2
  28. package/src/demo/foundations/Palette.svelte +1 -1
  29. package/src/demo/reference/document.ts +3 -2
  30. package/src/docs-shell/common/locales.js +1 -1
  31. package/src/docs-shell/components/CodeCopyButton.astro +1 -1
  32. package/src/docs-shell/components/DocHeader.svelte +3 -3
  33. package/src/docs-shell/components/DocMenuButton.svelte +6 -5
  34. package/src/docs-shell/components/DocVersionSelect.svelte +2 -2
  35. package/src/docs-shell/components/DocumentCodeExampleView.astro +2 -2
  36. package/src/docs-shell/components/DocumentCodeTable.astro +5 -5
  37. package/src/docs-shell/components/DocumentCopyButton.svelte +1 -1
  38. package/src/docs-shell/components/DocumentFileTree.astro +2 -2
  39. package/src/docs-shell/components/DocumentFlow.astro +2 -2
  40. package/src/docs-shell/components/DocumentIllustration.astro +2 -2
  41. package/src/docs-shell/components/DocumentNamespaceTable.astro +6 -6
  42. package/src/docs-shell/components/DocumentParameters.astro +2 -2
  43. package/src/docs-shell/components/ExpandContent.svelte +1 -1
  44. package/src/docs-shell/components/LanguageButton.svelte +2 -2
  45. package/src/docs-shell/components/LanguageSelect.svelte +2 -2
  46. package/src/docs-shell/components/NotFound.astro +1 -1
  47. package/src/docs-shell/components/PageNavs.astro +4 -4
  48. package/src/docs-shell/components/SearchButton.svelte +7 -4
  49. package/src/docs-shell/components/SearchDialog.svelte +4 -1
  50. package/src/docs-shell/components/SearchDialogPanel.svelte +8 -8
  51. package/src/docs-shell/components/Segments.svelte +2 -1
  52. package/src/docs-shell/components/ThemeSelect.svelte +2 -2
  53. package/src/docs-shell/components/markdown/Table.astro +3 -3
  54. package/src/docs-shell/components/markdown/TableCell.astro +4 -0
  55. package/src/docs-shell/components/markdown/TableHead.astro +4 -0
  56. package/src/docs-shell/components/markdown/TableRow.astro +4 -0
  57. package/src/docs-shell/components/markdown-components.ts +6 -0
  58. package/src/docs-shell/contexts/search.ts +11 -1
  59. package/src/docs-shell/contexts/theme-mode.svelte.ts +1 -0
  60. package/src/docs-shell/data/legacy-design-routes.json +176 -0
  61. package/src/docs-shell/remark/markdown-components.js +2 -2
  62. package/src/docs-shell/utils/snackbar.ts +1 -1
  63. package/src/examples/article-transitions/page.astro +0 -1
  64. package/src/examples/components/ArticleTransitionScene.svelte +4 -4
  65. package/src/examples/components/ViewTransitionScene.svelte +1 -1
  66. package/src/examples/view-transitions/page.astro +0 -1
  67. package/src/layouts/Document.astro +1 -0
  68. package/src/scripts/legacy-design-routes.ts +76 -0
  69. package/src/scripts/style-delivery.mjs +22 -2
  70. package/src/sections/document.astro +1 -1
  71. package/src/styles/docs-shell/documentation-search.css +6 -5
  72. package/src/styles/docs-shell/materials.css +10 -22
  73. package/src/styles/docs-shell/menu.css +8 -8
  74. package/src/styles/docs-shell/segments.css +2 -2
  75. package/src/styles/docs-shell/theme.css +2 -17
  76. package/src/styles/foundation.css +1 -0
  77. package/src/styles/site-contract.md +31 -0
  78. package/src/styles/system/badge-tones.css +4 -0
  79. package/src/styles/system/badge.ts +2 -0
  80. package/src/styles/system/browser.css +3 -6
  81. package/src/styles/system/button-feedback.css +6 -9
  82. package/src/styles/system/buttons.css +1 -51
  83. package/src/styles/system/checks.css +4 -18
  84. package/src/styles/system/controls.css +0 -1
  85. package/src/styles/system/dialog.css +3 -3
  86. package/src/styles/system/expand.css +3 -3
  87. package/src/styles/system/fields.css +7 -23
  88. package/src/styles/system/header-core.css +10 -10
  89. package/src/styles/system/kbd.css +1 -4
  90. package/src/styles/system/mixins.css +2 -2
  91. package/src/styles/system/pager.css +2 -13
  92. package/src/styles/system/range.css +1 -12
  93. package/src/styles/system/search-trigger.css +2 -2
  94. package/src/styles/system/segmented.css +1 -16
  95. package/src/styles/system/sheet.css +1 -8
  96. package/src/styles/system/sheet.ts +2 -0
  97. package/src/styles/system/sidebar.css +1 -1
  98. package/src/styles/system/specimens.css +5 -17
  99. package/src/styles/system/surfaces.css +0 -1
  100. package/src/styles/system/table-layout.css +12 -0
  101. package/src/styles/system/table.css +3 -23
  102. package/src/styles/system/table.ts +4 -0
  103. package/src/styles/system/tokens.css +48 -63
  104. package/src/styles/system/tooltip.css +2 -4
  105. package/src/styles/system/tray-layout.css +20 -0
  106. package/src/styles/system/tray.css +2 -20
  107. package/src/styles/system/tray.ts +3 -0
  108. package/src/demo/DemoScrollArea.svelte +0 -17
  109. package/src/examples/scene.css +0 -3
  110. package/src/styles/system/DESIGN.md +0 -128
  111. package/src/styles/system/menu.css +0 -14
  112. package/src/styles/system/scroll.css +0 -6
@@ -0,0 +1,176 @@
1
+ {
2
+ "version": 1,
3
+ "locales": [
4
+ "en",
5
+ "tw",
6
+ "ja",
7
+ "ko",
8
+ "cn"
9
+ ],
10
+ "routes": [
11
+ {
12
+ "source": "/design-system/authoring",
13
+ "status": 410
14
+ },
15
+ {
16
+ "source": "/design-system",
17
+ "status": 301,
18
+ "destination": "https://design.master.co/principles",
19
+ "markdownDestination": "https://design.master.co/guidelines/principles.md"
20
+ },
21
+ {
22
+ "source": "/design-system/review/agent",
23
+ "status": 410
24
+ },
25
+ {
26
+ "source": "/design-system/review/asset",
27
+ "status": 410
28
+ },
29
+ {
30
+ "source": "/design-system/review/axes",
31
+ "status": 410
32
+ },
33
+ {
34
+ "source": "/design-system/review/badge",
35
+ "status": 410
36
+ },
37
+ {
38
+ "source": "/design-system/review/catalog",
39
+ "status": 410
40
+ },
41
+ {
42
+ "source": "/design-system/review/chart-group",
43
+ "status": 410
44
+ },
45
+ {
46
+ "source": "/design-system/review/code",
47
+ "status": 410
48
+ },
49
+ {
50
+ "source": "/design-system/review/comparison",
51
+ "status": 410
52
+ },
53
+ {
54
+ "source": "/design-system/review/container",
55
+ "status": 410
56
+ },
57
+ {
58
+ "source": "/design-system/review/controls",
59
+ "status": 410
60
+ },
61
+ {
62
+ "source": "/design-system/review/copy",
63
+ "status": 410
64
+ },
65
+ {
66
+ "source": "/design-system/review/data-table",
67
+ "status": 410
68
+ },
69
+ {
70
+ "source": "/design-system/review/demo",
71
+ "status": 410
72
+ },
73
+ {
74
+ "source": "/design-system/review/documentation-index",
75
+ "status": 410
76
+ },
77
+ {
78
+ "source": "/design-system/review/documents",
79
+ "status": 410
80
+ },
81
+ {
82
+ "source": "/design-system/review/feature-support",
83
+ "status": 410
84
+ },
85
+ {
86
+ "source": "/design-system/review/home-icons",
87
+ "status": 410
88
+ },
89
+ {
90
+ "source": "/design-system/review/item",
91
+ "status": 410
92
+ },
93
+ {
94
+ "source": "/design-system/review/label",
95
+ "status": 410
96
+ },
97
+ {
98
+ "source": "/design-system/review/legend",
99
+ "status": 410
100
+ },
101
+ {
102
+ "source": "/design-system/review/measure",
103
+ "status": 410
104
+ },
105
+ {
106
+ "source": "/design-system/review/media",
107
+ "status": 410
108
+ },
109
+ {
110
+ "source": "/design-system/review/metrics",
111
+ "status": 410
112
+ },
113
+ {
114
+ "source": "/design-system/review/motion",
115
+ "status": 410
116
+ },
117
+ {
118
+ "source": "/design-system/review/numeric-table",
119
+ "status": 410
120
+ },
121
+ {
122
+ "source": "/design-system/review/package-api",
123
+ "status": 410
124
+ },
125
+ {
126
+ "source": "/design-system/review/palette",
127
+ "status": 410
128
+ },
129
+ {
130
+ "source": "/design-system/review/parameters",
131
+ "status": 410
132
+ },
133
+ {
134
+ "source": "/design-system/review/scroll",
135
+ "status": 410
136
+ },
137
+ {
138
+ "source": "/design-system/review/source",
139
+ "status": 410
140
+ },
141
+ {
142
+ "source": "/design-system/review/steps",
143
+ "status": 410
144
+ },
145
+ {
146
+ "source": "/design-system/review/stylesheet",
147
+ "status": 410
148
+ },
149
+ {
150
+ "source": "/design-system/review/surface",
151
+ "status": 301,
152
+ "destination": "https://design.master.co/foundations/elevation",
153
+ "markdownDestination": "https://design.master.co/guidelines/foundations/elevation.md"
154
+ },
155
+ {
156
+ "source": "/design-system/review/swatch",
157
+ "status": 410
158
+ },
159
+ {
160
+ "source": "/design-system/review/tooling",
161
+ "status": 410
162
+ },
163
+ {
164
+ "source": "/design-system/review/utility-table",
165
+ "status": 410
166
+ },
167
+ {
168
+ "source": "/design-system/review/viewport",
169
+ "status": 410
170
+ },
171
+ {
172
+ "source": "/design-system/review/waterfall",
173
+ "status": 410
174
+ }
175
+ ]
176
+ }
@@ -57,7 +57,7 @@ function fenceProps(node) {
57
57
  // of every component also propagates every component's styles to every MDX page.
58
58
  const componentPaths = {
59
59
  a: 'markdown/Anchor.astro', h2: 'markdown/Heading2.astro', h3: 'markdown/Heading3.astro',
60
- img: 'Image.svelte', Image: 'Image.svelte', table: 'markdown/Table.astro',
60
+ img: 'Image.svelte', Image: 'Image.svelte', table: 'markdown/Table.astro', th: 'markdown/TableHead.astro', td: 'markdown/TableCell.astro', tr: 'markdown/TableRow.astro',
61
61
  Code: 'Code.astro', InlineCode: 'InlineCode.astro', PMAction: 'PMAction.astro',
62
62
  Do: 'Do.astro', DoNot: 'DoNot.astro', Info: 'Info.astro', Warn: 'Warn.astro',
63
63
  InteractingIndicator: 'InteractingIndicator.astro'
@@ -68,7 +68,7 @@ function documentComponents(tree) {
68
68
  visit(tree, node => {
69
69
  if (node.type === 'link' || node.type === 'linkReference') used.add('a')
70
70
  if (node.type === 'image' || node.type === 'imageReference') used.add('img')
71
- if (node.type === 'table') used.add('table')
71
+ if (node.type === 'table') for (const part of ['table', 'th', 'td', 'tr']) used.add(part)
72
72
  if (node.type === 'heading' && [2, 3].includes(node.depth)) used.add(`h${node.depth}`)
73
73
  if (['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type) && node.name in componentPaths) used.add(node.name)
74
74
  })
@@ -7,7 +7,7 @@ export async function snackbar(text: string, { placement }: any = {}) {
7
7
  }
8
8
  // A floating `ui-sheet`; placement is inline so a message never needs a utility class the page has not generated.
9
9
  const element = document.createElement('div')
10
- element.className = 'ui-sheet'
10
+ element.className = 'sheet'
11
11
  element.dataset.elevation = '3'
12
12
  element.setAttribute('role', 'status')
13
13
  Object.assign(element.style, {
@@ -1,5 +1,4 @@
1
1
  ---
2
- import '@master/internal-site/examples/scene.css'
3
2
  import '@master/internal-site/examples/article-transitions/transitions.css'
4
3
  import ArticleTransitionScene from '@master/internal-site/examples/components/ArticleTransitionScene.svelte'
5
4
  ---
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import '@master/internal-site/styles/system/button-feedback.css'
3
3
  import '@master/internal-site/styles/system/buttons.css'
4
- import '@master/internal-site/styles/system/sheet.css'
4
+ import '@master/internal-site/styles/system/sheet.ts'
5
5
 
6
6
  import Image from '@master/internal-site/components/Image.svelte'
7
7
  import IconArrowLeft from '@tabler/icons-svelte-runes/icons/arrow-left'
@@ -54,14 +54,14 @@
54
54
  <DemoLabel>Field notes / Collection 01</DemoLabel>
55
55
  <span class="text-xs fg-text-muted" role="status">{selected ? 'Article detail' : '2 articles'} · {transition.mode}</span>
56
56
  </div>
57
- <article data-article-detail hidden={!selected} class="overflow-hidden ui-sheet" data-elevation="1">
57
+ <article data-article-detail hidden={!selected} class="overflow-hidden sheet sheet-raised ui-sheet">
58
58
  <Image alt={detail.imageAlt} src={detail.image} loading="eager" sizes="(max-width: 600px) 100vw, 600px"
59
59
  style={`view-transition-name:article-${detail.id}-image`} class="block h-auto w-full (aspect-ratio:16/9) object-cover article-capture" />
60
60
  <div class="p-lg">
61
61
  <time datetime={detail.id === 'aurora' ? '2026-05-12' : '2026-05-09'} style={`view-transition-name:article-${detail.id}-date`} class="text-xs fg-text-muted article-capture">{detail.date}</time>
62
62
  <h1 bind:this={heading} tabindex="-1" style={`view-transition-name:article-${detail.id}-title`} class="mb(0) mt-sm text-2xl font-regular outline-solid:focus-visible outline(2):focus-visible outline-text-link:focus-visible outline-offset-4xs:focus-visible article-capture">{detail.title}</h1>
63
63
  <p class="mb(0) mt-md text-sm fg-text-muted">{detail.description} The image, title and date keep their names in both views. Each snapshot can move to its new bounds while surrounding content fades.</p>
64
- <button type="button" class="mt-lg ui-button" onclick={() => { const id = detail.id; transition.run(() => { selectedId = undefined }, () => buttons.get(id) ?? null) }}>
64
+ <button type="button" class="mt-lg btn" onclick={() => { const id = detail.id; transition.run(() => { selectedId = undefined }, () => buttons.get(id) ?? null) }}>
65
65
  <IconArrowLeft aria-hidden="true" />Back to collection
66
66
  </button>
67
67
  </div>
@@ -77,7 +77,7 @@
77
77
  <h2 style={`view-transition-name:article-${article.id}-title`} class="mb(0) mt-xs text-lg font-regular article-capture">{article.title}</h2>
78
78
  <p class="mb-md mt-sm text-sm fg-text-muted">{article.description}</p>
79
79
  <button {@attach cardButton(article.id)}
80
- type="button" class="self-start mt-auto ui-button" aria-label={`Read ${article.title}`}
80
+ type="button" class="self-start mt-auto btn" aria-label={`Read ${article.title}`}
81
81
  onclick={() => transition.run(() => { selectedId = article.id }, () => heading ?? null)}>
82
82
  Read article<IconArrowUpRight aria-hidden="true" />
83
83
  </button>
@@ -18,7 +18,7 @@
18
18
  <main class="wrap-anywhere p-md bg-surface-base fg-text-body">
19
19
  <DemoControls label="Choose a view" variant="segmented">
20
20
  {#each views as view (view.id)}
21
- <button type="button" aria-pressed={activeId === view.id}
21
+ <button class="segmented-item" type="button" aria-pressed={activeId === view.id}
22
22
  onclick={() => { if (view.id !== activeId) transition.run(() => { activeId = view.id }) }}>{view.title}</button>
23
23
  {/each}
24
24
  </DemoControls>
@@ -1,5 +1,4 @@
1
1
  ---
2
- import '@master/internal-site/examples/scene.css'
3
2
  import '@master/internal-site/examples/view-transitions/transitions.css'
4
3
  import ViewTransitionScene from '@master/internal-site/examples/components/ViewTransitionScene.svelte'
5
4
  ---
@@ -46,6 +46,7 @@ const applyTheme = () => {
46
46
  const value = preference === 'system' ? darkScheme.matches ? 'dark' : 'light' : preference;
47
47
  root.classList.remove('dark', 'light');
48
48
  root.classList.add(value);
49
+ root.dataset.theme = value;
49
50
  root.style.colorScheme = ['dark', 'light'].includes(value) ? value : '';
50
51
  };
51
52
  applyTheme();
@@ -0,0 +1,76 @@
1
+ /** Shared, exact legacy URLs. Consumers enable emission only after destination deployment. */
2
+ export interface LegacyDesignManifest {
3
+ version: number
4
+ locales: readonly string[]
5
+ routes: readonly { source: string; status: number; destination?: string; markdownDestination?: string; localizedMarkdownDestinations?: Readonly<Record<string, string>>; localizedDestinations?: Readonly<Record<string, string>> }[]
6
+ }
7
+
8
+ function entries(manifest: LegacyDesignManifest) {
9
+ if (manifest.version !== 1) throw new Error('Unsupported legacy design route manifest version')
10
+ const prefixes = ['', ...manifest.locales.map(locale => `/${locale}`)]
11
+ const result: { source: string; status: 301 | 410; destination?: string; markdownDestination?: string; localizedMarkdownDestinations?: Readonly<Record<string, string>>; localizedDestinations?: Readonly<Record<string, string>> }[] = []
12
+ const seen = new Set<string>()
13
+ for (const route of manifest.routes) {
14
+ if (!/^\/design-system(?:\/[a-z0-9-]+)*$/.test(route.source)) throw new Error(`Invalid exact legacy route: ${route.source}`)
15
+ if (route.status !== 301 && route.status !== 410) throw new Error(`Invalid legacy status: ${route.status}`)
16
+ if (route.status === 301 && (!route.destination || new URL(route.destination).protocol !== 'https:')) throw new Error(`Missing HTTPS destination: ${route.source}`)
17
+ if (route.markdownDestination && new URL(route.markdownDestination).protocol !== 'https:') throw new Error('Invalid Markdown destination')
18
+ for (const [locale, destination] of Object.entries({ ...route.localizedDestinations, ...route.localizedMarkdownDestinations })) {
19
+ if (!manifest.locales.includes(locale) || new URL(destination).protocol !== 'https:') throw new Error(`Invalid localized destination: ${locale}`)
20
+ }
21
+ for (const prefix of prefixes) for (const suffix of ['', '/', '.html', '.md']) {
22
+ const source = prefix + route.source + suffix
23
+ if (seen.has(source)) throw new Error(`Duplicate legacy route: ${source}`)
24
+ seen.add(source)
25
+ let destination: string | undefined
26
+ if (route.status === 301) {
27
+ // Preserve a language only when the manifest names an existing translated page.
28
+ const locale = prefix.slice(1)
29
+ const markdown = suffix === '.md' ? route.localizedMarkdownDestinations?.[locale] ?? route.markdownDestination : undefined
30
+ const url = new URL(markdown ?? route.localizedDestinations?.[locale] ?? route.destination!)
31
+ url.pathname = url.pathname.replace(/\/$/, '') + (suffix === '.md' && !markdown ? '.md' : '')
32
+ destination = url.href
33
+ }
34
+ result.push({ source, status: route.status, ...(destination ? { destination } : {}) })
35
+ }
36
+ }
37
+ return result
38
+ }
39
+
40
+ /** Cloudflare Pages redirects support moved URLs; 410 responses belong to the worker. */
41
+ export function legacyDesignRedirects(manifest: LegacyDesignManifest): string[] {
42
+ return entries(manifest).filter(route => route.status === 301).map(route => `${route.source} ${route.destination} 301`)
43
+ }
44
+
45
+ /** Requests within these compact patterns still use exact route matching in the worker. */
46
+ export function legacyDesignWorkerRoutes(manifest: LegacyDesignManifest) {
47
+ entries(manifest)
48
+ return {
49
+ version: 1,
50
+ include: ['', ...manifest.locales.map(locale => `/${locale}`)].flatMap(prefix => ['', '/', '.html', '.md', '/*'].map(suffix => `${prefix}/design-system${suffix}`)),
51
+ exclude: [],
52
+ }
53
+ }
54
+
55
+ /** Pages Functions bypass _redirects, so the worker handles both moved and retired URLs. */
56
+ export function legacyDesignWorker(manifest: LegacyDesignManifest): string {
57
+ const routes = Object.fromEntries(entries(manifest).map(({ source, ...route }) => [source, route]))
58
+ return `const routes = ${JSON.stringify(routes)};
59
+ export default {
60
+ async fetch(request, env) {
61
+ const url = new URL(request.url);
62
+ const route = Object.hasOwn(routes, url.pathname) ? routes[url.pathname] : undefined;
63
+ if (!route) return env.ASSETS.fetch(request);
64
+ if (route.status === 301) {
65
+ const destination = new URL(route.destination);
66
+ destination.search = url.search;
67
+ return Response.redirect(destination.href, 301);
68
+ }
69
+ return new Response(request.method === 'HEAD' ? null : 'This review page has been retired.\\n', {
70
+ status: 410,
71
+ headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Robots-Tag': 'noindex', 'Cache-Control': 'public, max-age=3600' }
72
+ });
73
+ }
74
+ };
75
+ `
76
+ }
@@ -19,7 +19,27 @@ const hash = text => createHash('sha256').update(text).digest('hex')
19
19
  const sizes = text => ({ bytes: Buffer.byteLength(text), gzip: gzipSync(text).length, brotli: brotliCompressSync(text).length })
20
20
  const attributes = text => Object.fromEntries([...text.matchAll(/([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)].map(m => [m[1], m[2] ?? m[3]]))
21
21
  export const importsOf = css => [...css.replace(/\/\*[\s\S]*?\*\//g, '').matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)].map(m => m[1])
22
- const families = ['sidebar', 'toc', 'pager', 'tree', 'tray', 'switch', 'range', 'chart', 'code', 'table']
22
+ // Keep the report vocabulary stable while ownership moves from site controls to UI.
23
+ const families = {
24
+ sidebar: ['ui-sidebar'], toc: ['ui-toc'], pager: ['ui-pager', 'pager'],
25
+ tree: ['ui-tree', 'tree'], tray: ['ui-tray', 'tray'], switch: ['ui-switch', 'switch'],
26
+ range: ['ui-range', 'slider'], chart: ['ui-chart'], code: ['ui-code', 'code', 'window'],
27
+ table: ['ui-table', 'table'], button: ['ui-button', 'btn'], input: ['ui-input', 'input'],
28
+ select: ['ui-select', 'select'], window: ['window'],
29
+ }
30
+
31
+ function deliveredFamilies(css) {
32
+ // Ignore comments, declaration strings and quoted attribute values. Inspect rule
33
+ // preludes so nested/compound selectors count, without matching class prefixes.
34
+ const unquoted = css.replace(/\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '')
35
+ // Shared navigation names optional transition participants on every route;
36
+ // naming a sidebar or contents rail does not deliver its component styles.
37
+ .replace(/[^{};]+\{(?:\s*view-transition-(?:name|class)\s*:[^;{}]+;?\s*)+\}/g, '')
38
+ const selectors = [...unquoted.matchAll(/([^{};]+)\{/g)].map(match => match[1].trim()).filter(prelude => !prelude.startsWith('@')).join('\n')
39
+ return Object.fromEntries(Object.entries(families).map(([name, classes]) => [name,
40
+ new RegExp(`(?<!\\\\)\\.(?:${classes.join('|')})(?![\\w\\\\-]|[^\\x00-\\x7f])`).test(selectors)
41
+ ]))
42
+ }
23
43
 
24
44
  export async function auditStyles(directory) {
25
45
  const root = resolve(directory), assets = {}, routes = {}
@@ -59,7 +79,7 @@ export async function auditStyles(directory) {
59
79
  ...Object.fromEntries(['bytes', 'gzip', 'brotli'].map(k => [k, totals.reduce((n, x) => n + x[k], 0)])),
60
80
  inlineSHA256: hash(inline.join('\n')),
61
81
  inlineBytes: inline.reduce((n, s) => n + Buffer.byteLength(s), 0),
62
- families: Object.fromEntries(families.map(name => [name, new RegExp(`(?:^|[;{}])\\s*\\.ui-${name}\\s*\\{`).test(delivered)])),
82
+ families: deliveredFamilies(delivered),
63
83
  foundationCount: [...delivered.matchAll(/body,\s*:host\s*\{\s*font-family:\s*var\(--font-family-sans\)/g)].length,
64
84
  uncompiled: /(?:^|[;{}])\s*@(apply|compose|reference|prune|light|dark|variant|theme|mixin|utility|master|source|safelist|blocklist)\b/.test(delivered),
65
85
  layerOrder: [...new Set([...delivered.matchAll(/@layer\s+([\w-]+(?:\s*,\s*[\w-]+)*)\s*[;{]/g)].flatMap(m => m[1].split(/\s*,\s*/)))],
@@ -5,4 +5,4 @@ import SearchDialog from '@master/internal-site/components/SearchDialog.svelte'
5
5
  ---
6
6
 
7
7
  <slot />
8
- <SearchDialog client:idle={{ timeout: 500 }} />
8
+ <SearchDialog client:load />
@@ -1,3 +1,4 @@
1
+ @import '../system/dialog.css';
1
2
  /*
2
3
  * The documentation search (`ui-dialog` with `ui-dialog-search`, `ui-results` and `ui-dialog-foot`): a floating sheet
3
4
  * near the top of the viewport whose results scroll between the field and the key hints; a full screen on phones.
@@ -12,13 +13,13 @@
12
13
  .ui-dialog-search input::placeholder { color: var(--ui-text-muted); }
13
14
  .ui-dialog-search input::-webkit-search-cancel-button { display: none; }
14
15
  .ui-dialog-close > span:last-child { display: none; }
15
- .ui-dialog-close > .ui-kbd { box-shadow: none; background: none; }
16
+ .ui-dialog-close > .kbd { box-shadow: none; background: none; }
16
17
  .ui-dialog[data-variant="search"] > .ui-dialog-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
17
18
  .ui-dialog-status { margin: 0; padding: var(--ui-space-sm) var(--ui-space-md) 0; color: var(--ui-text-muted); @apply --ui-label-type; }
18
19
  .ui-dialog-note { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-xs); padding: var(--ui-space-sm) var(--ui-space-md) var(--ui-space-lg); }
19
20
  .ui-dialog-note > p { width: 100%; margin: 0; color: var(--ui-text-body); font-size: .8125rem; line-height: 1.6; }
20
- .ui-dialog-note > .ui-button code { font: var(--ui-weight-regular) .75rem/1 var(--font-family-mono); }
21
- .ui-dialog-note > .ui-button svg { width: 14px; height: 14px; color: var(--ui-text-muted); }
21
+ .ui-dialog-note > .btn code { font: var(--ui-weight-regular) .75rem/1 var(--font-family-mono); }
22
+ .ui-dialog-note > .btn svg { width: 14px; height: 14px; color: var(--ui-text-muted); }
22
23
  .ui-dialog-note[data-align="center"] { flex-direction: column; padding-block: var(--ui-space-xl); color: var(--ui-text-muted); text-align: center; }
23
24
  .ui-dialog-note[data-align="center"] > b { color: var(--ui-text-strong); font-size: var(--ui-text-small); font-weight: var(--ui-weight-strong); }
24
25
  .ui-results:empty { display: none; }
@@ -30,10 +31,10 @@
30
31
  .ui-dialog-foot { flex: none; }
31
32
  .ui-dialog-foot > span:last-child { margin-left: auto; }
32
33
  .ui-search-shortcut { white-space: nowrap; }
33
- .ui-dialog-foot .ui-kbd svg { width: 12px; height: 12px; }
34
+ .ui-dialog-foot .kbd svg { width: 12px; height: 12px; }
34
35
  @media (width < 48rem) {
35
36
  .ui-dialog[data-variant="search"] { width: 100%; max-width: 100%; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }
36
- .ui-dialog-close > .ui-kbd, .ui-dialog-foot { display: none; }
37
+ .ui-dialog-close > .kbd, .ui-dialog-foot { display: none; }
37
38
  .ui-dialog-close > span:last-child { display: inline; }
38
39
  }
39
40
  @media (forced-colors: active) {
@@ -1,25 +1,13 @@
1
- /*
2
- * Site utility tokens. The design system lives in styles/system (--ui-* roles, ui-* components);
3
- * these @theme names only expose that ladder to Master CSS utilities used in content and on the homepage
4
- * (bg-surface-canvas, b-line-contour, shadow-raised …). Values always resolve to the system roles.
5
- */
1
+ /* Site vocabulary adapts the public UI theme without defining another material palette. */
6
2
  @theme {
7
- --color-surface-canvas: var(--ui-surface-canvas);
8
- --color-surface-shell: var(--ui-surface-tray);
9
- --color-surface-ink: var(--ui-surface-ink);
10
- --color-text-subtle: var(--ui-text-muted);
11
- --color-line-contour: var(--ui-line-contour);
12
- /* Media uses a quiet 10% black/white perimeter in the active theme. */
3
+ --color-surface-canvas: var(--color-surface-base);
4
+ --color-surface-shell: var(--color-surface-tray);
5
+ --color-surface-ink: var(--color-surface-inverse);
6
+ --color-text-subtle: var(--color-text-muted);
13
7
  --opacity-media-ring: 0.1;
14
- --color-line-media: light-dark(color(srgb 0 0 0 / var(--opacity-media-ring)), color(srgb 1 1 1 / var(--opacity-media-ring)));
15
- }
16
-
17
- @theme {
18
- --shadow-edge: inset 0 1px 0 var(--ui-highlight);
19
- --shadow-contact: 0 1px 1px -.5px var(--ui-shade-1), 0 2px 2px -1px var(--ui-shade-2), 0 4px 4px -2px var(--ui-shade-3);
20
- --shadow-raised: var(--ui-elevation-rise-1);
21
- --shadow-floating-contact: 0 1px 1px -.5px var(--ui-shade-1), 0 3px 3px -1.5px var(--ui-shade-2), 0 6px 6px -3px var(--ui-shade-3), 0 12px 12px -6px var(--ui-shade-4), 0 24px 24px -12px var(--ui-shade-4), 0 48px 48px -24px var(--ui-shade-5);
22
- --shadow-floating: var(--ui-elevation-rise-3);
23
- --shadow-pressed-contact: inset 0 1px 2px light-dark(oklch(18% 0 none / .07), oklch(0% 0 none / .5));
24
- --shadow-pressed: var(--ui-elevation-sink);
8
+ --color-line-media: var(--color-line-image-strong);
9
+ --shadow-edge: inset 0 1px 0 var(--color-highlight);
10
+ --shadow-contact: var(--shadow-raised);
11
+ --shadow-floating-contact: var(--shadow-floating);
12
+ --shadow-pressed-contact: var(--shadow-pressed);
25
13
  }
@@ -6,10 +6,10 @@
6
6
  .ui-menu-glyph[data-open="true"] > path { transform: translateY(4px) rotate(45deg) scaleX(1.06066); }
7
7
  .ui-menu-glyph[data-open="true"] > path + path { transform: translateY(-4px) rotate(-45deg) scaleX(1.06066); }
8
8
 
9
- .ui-dialog.ui-mobile-menu { position: fixed; inset: 0; width: 100%; max-width: 100%; height: 100dvh; max-height: 100dvh; margin: 0; padding: 0; overflow-y: auto; overscroll-behavior: contain; border-radius: 0; background: var(--ui-surface-canvas); box-shadow: none; opacity: 0; transition: opacity 180ms ease-out; }
10
- .ui-dialog.ui-mobile-menu[open] { display: flex; flex-direction: column; }
11
- .ui-dialog.ui-mobile-menu[data-open="true"] { opacity: 1; transition-duration: 320ms; }
12
- .ui-dialog.ui-mobile-menu::backdrop { background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; }
9
+ .ui-mobile-menu { position: fixed; inset: 0; width: 100%; max-width: 100%; height: 100dvh; max-height: 100dvh; margin: 0; padding: 0; overflow-y: auto; overscroll-behavior: contain; border: 0; border-radius: 0; color: var(--ui-text-body); background: var(--ui-surface-canvas); box-shadow: none; opacity: 0; transition: opacity 180ms ease-out; }
10
+ .ui-mobile-menu[open] { display: flex; flex-direction: column; }
11
+ .ui-mobile-menu[data-open="true"] { opacity: 1; transition-duration: 320ms; }
12
+ .ui-mobile-menu::backdrop { background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; }
13
13
  .ui-mobile-menu-bar { position: sticky; top: 0; z-index: 1; flex: none; display: grid; grid-template-columns: var(--ui-header-target) minmax(0, 1fr) var(--ui-header-target); align-items: center; height: calc(3.75rem + env(safe-area-inset-top)); padding: env(safe-area-inset-top) max(var(--ui-space-xs), env(safe-area-inset-right)) 0 max(var(--ui-space-xs), env(safe-area-inset-left)); background: var(--ui-surface-canvas); }
14
14
  .ui-mobile-menu-bar > .ui-header-brand { justify-self: center; margin: 0; }
15
15
  .ui-mobile-menu-body { flex: 1; display: flex; flex-direction: column; gap: 4rem; width: 100%; max-width: 48rem; margin-inline: auto; padding: clamp(3rem, 10dvh, 6rem) max(clamp(1.5rem, 6vw, 3rem), env(safe-area-inset-right)) max(1.5rem, env(safe-area-inset-bottom)) max(clamp(1.5rem, 6vw, 3rem), env(safe-area-inset-left)); transform: translateY(-8px); transition: transform 180ms ease-out; }
@@ -21,18 +21,18 @@
21
21
  .ui-mobile-menu[data-open="true"] .ui-mobile-menu-link { opacity: 1; transform: translateY(0); transition: opacity 320ms ease-out, transform 320ms cubic-bezier(.22, 1, .36, 1), color var(--ui-duration-quick); transition-delay: calc(var(--menu-order) * 40ms), calc(var(--menu-order) * 40ms), 0s; }
22
22
  .ui-mobile-menu-link[aria-current] > span { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .2em; text-decoration-color: var(--ui-line-mark); }
23
23
  .ui-mobile-menu-link:focus-visible, .ui-mobile-menu-community > a:focus-visible { @apply --ui-focus-ring; outline-offset: 5px; border-radius: var(--ui-radius-sm); }
24
- .ui-mobile-menu-footer { display: grid; gap: 1rem; margin-top: auto; }
24
+ .ui-mobile-menu-footer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; margin-top: auto; }
25
25
  .ui-mobile-menu-community { display: flex; flex-wrap: wrap; gap: .5rem; margin-left: -.75rem; }
26
26
  .ui-mobile-menu-community > a { position: relative; display: inline-flex; align-items: center; justify-content: center; min-width: 2.75rem; min-height: 2.75rem; padding: .5rem .75rem; color: var(--ui-text-muted); text-decoration: none; transition: color var(--ui-duration-quick); }
27
27
  .ui-mobile-menu-community > a > .ui-menu-external { position: absolute; top: calc(50% - .875rem); right: calc(50% - 1rem); width: .625rem; height: .625rem; pointer-events: none; }
28
28
  .ui-mobile-menu-preferences { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .75rem; padding-top: 1rem; border-top: 1px solid var(--ui-line-hair); }
29
- .ui-mobile-menu-preferences > .ui-select { width: auto; max-width: 100%; min-height: 2.75rem; padding-left: 0; padding-right: 1.5rem; background-color: transparent; background-position: right .125rem center; border-radius: var(--ui-radius-sm); box-shadow: none; color: var(--ui-text-muted); font-size: .8125rem; }
30
- .ui-mobile-menu-preferences > .ui-select:focus-visible { @apply --ui-focus-ring; outline-offset: 3px; }
29
+ .ui-mobile-menu-preferences > .select { width: auto; max-width: 100%; min-height: 2.75rem; padding-left: 0; padding-right: 1.5rem; background-color: transparent; background-position: right .125rem center; border-radius: var(--ui-radius-sm); box-shadow: none; color: var(--ui-text-muted); font-size: .8125rem; }
30
+ .ui-mobile-menu-preferences > .select:focus-visible { @apply --ui-focus-ring; outline-offset: 3px; }
31
31
  @media (hover: hover) {
32
32
  .ui-mobile-menu-link:hover, .ui-mobile-menu-community > a:hover { color: var(--ui-text-body); }
33
33
  }
34
34
  @media (prefers-reduced-motion: reduce) {
35
- .ui-dialog.ui-mobile-menu[data-open], .ui-mobile-menu-body, .ui-mobile-menu-link, .ui-menu-glyph[data-open] > path,
35
+ .ui-mobile-menu[data-open], .ui-mobile-menu-body, .ui-mobile-menu-link, .ui-menu-glyph[data-open] > path,
36
36
  .ui-mobile-menu[data-open="true"] .ui-mobile-menu-body, .ui-mobile-menu[data-open="true"] .ui-mobile-menu-link { transition: none; }
37
37
  .ui-mobile-menu-body, .ui-mobile-menu-link { transform: none; }
38
38
  }
@@ -1,11 +1,11 @@
1
1
  @layer components {
2
2
  /* Related views switch with the system's segmented control; the panels keep their own frames. */
3
3
  .segment-views, .segment-panel { min-width: 0; }
4
- .segment-views > .ui-segmented { margin-bottom: var(--ui-space-sm); }
4
+ .segment-views > .segmented { margin-bottom: var(--ui-space-sm); }
5
5
  .segment-panel-label { display: none; }
6
6
  .segment-panel:focus-visible { @apply --ui-focus-ring; outline-offset: 3px; border-radius: var(--ui-radius-sm); }
7
7
  @media (scripting: none), print {
8
- .segment-views > .ui-segmented { display: none; }
8
+ .segment-views > .segmented { display: none; }
9
9
  .segment-panel[hidden] { display: block !important; }
10
10
  .segment-panel + .segment-panel { margin-top: var(--ui-space-xl); }
11
11
  .segment-panel-label { display: block; margin-bottom: var(--ui-space-md); color: var(--ui-text-strong); font-size: var(--ui-text-small); font-weight: var(--ui-weight-medium); }
@@ -12,8 +12,8 @@
12
12
  --font-mono: "IBM Plex Mono";
13
13
  --font-feature-sans: "ss02", "ss03", "ss04", "ss06", "ss07", "ss08";
14
14
  --font-feature-mono: "cv01", "cv02", "cv29";
15
- --radius-card: 0.75rem;
16
- --radius-panel: 1rem;
15
+ --radius-card: var(--radius-control-lg);
16
+ --radius-panel: var(--radius-surface);
17
17
  }
18
18
 
19
19
  @keyframes slide-to-right {
@@ -32,11 +32,6 @@
32
32
  --tiny-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cg fill='black' fill-opacity='0.1'%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/%3E%3C/g%3E%3C/svg%3E");
33
33
  --color-primary: light-dark(var(--color-yellow-40), var(--color-yellow-50));
34
34
  --color-accent: light-dark(var(--color-zinc-90), var(--color-zinc-5));
35
- --color-focus: light-dark(var(--color-zinc-90), var(--color-zinc-5));
36
- --color-text-success: light-dark(var(--color-green-70), var(--color-green-40));
37
- --color-text-danger: light-dark(var(--color-red-60), var(--color-red-30));
38
- --color-info: light-dark(var(--color-cyan-40), var(--color-cyan-50));
39
- --color-text-info: light-dark(var(--color-cyan-70), var(--color-cyan-40));
40
35
  }
41
36
 
42
37
  @layer defaults {
@@ -45,11 +40,6 @@
45
40
  --tiny-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cg fill='black' fill-opacity='0.1'%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/%3E%3C/g%3E%3C/svg%3E");
46
41
  --color-primary: var(--color-yellow-40);
47
42
  --color-accent: var(--color-zinc-90);
48
- --color-focus: var(--color-zinc-90);
49
- --color-text-success: var(--color-green-70);
50
- --color-text-danger: var(--color-red-60);
51
- --color-info: var(--color-cyan-40);
52
- --color-text-info: var(--color-cyan-70);
53
43
  }
54
44
  }
55
45
 
@@ -59,10 +49,5 @@
59
49
  --tiny-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cg fill='white' fill-opacity='0.1'%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/%3E%3C/g%3E%3C/svg%3E");
60
50
  --color-primary: var(--color-yellow-50);
61
51
  --color-accent: var(--color-zinc-5);
62
- --color-focus: var(--color-zinc-5);
63
- --color-text-success: var(--color-green-40);
64
- --color-text-danger: var(--color-red-30);
65
- --color-info: var(--color-cyan-50);
66
- --color-text-info: var(--color-cyan-40);
67
52
  }
68
53
  }
@@ -1,3 +1,4 @@
1
+ @import '@master/ui/theme.css';
1
2
  @import './docs-shell/theme.css';
2
3
  @import './system/mixins.css';
3
4
  @import './system/tokens.css';
@@ -0,0 +1,31 @@
1
+ # Master site composition
2
+
3
+ Master UI owns reusable components, their theme, materials, states and contracts. Master Design owns design principles and usage guidance at https://design.master.co. This package owns the website shell, reading layout, preview orchestration and content rendering.
4
+
5
+ Import the UI theme through foundation and component styles through their narrow owning entry. Do not import the complete UI library into every page. The `--ui-*` variables retained by the site are composition aliases to UI tokens or site-only reading, teaching and brand values, not an independent theme. Generic controls use the UI contract classes directly.
6
+
7
+ Keep header, sidebar, reading measure, contents rail, breakpoints and homepage composition stable. UI controls retain their public internal geometry. Site adapters retain link navigation semantics, overlay native preferences, table scrolling, preview isolation and code-copy orchestration. Never attach a second behavior controller to a node already owned by a site island.
8
+
9
+ Import `styles/system/table.ts`, `tray.ts` and `sheet.ts` from site components. These entries share canonical UI styles as module dependencies and add only their site composition CSS. The matching `.css` entrypoints remain for compatibility; nesting them inside new CSS collections can compile the same UI surface more than once. Bare examples import the styles of their actual components rather than the full controls or surfaces collections.
10
+
11
+ Foundation, interface, documents and component styles retain separate route boundaries. Every class needed after hydration must be visible to prerender. Build tools stay server-only. Request context and initial HTML must remain isolated and deterministic.
12
+
13
+ ## Shell
14
+
15
+ The shell follows the original documentation layout, refined:
16
+
17
+ - **Header** 60px, 32px gutters. Community and language actions share square 36px desktop / 44px compact targets, centred 18px Tabler icons at 1.5 stroke, and an 8px gap. Desktop search retains its 30px hairline pill, 16px glass and visible ⌘K; compact search uses the same 44px target and 18px icon as its neighbours; the homepage's wide header leaves it out. Section links are plain words (body ink, current one in strong ink at medium weight), no pill. The version is plain text with a chevron. Version chevrons retain their original muted 12px size; external-link indicators remain small raised 10px marks. Text links/version controls share the target height with content-sized widths. Controls only change ink on hover. The separator has 4px margins plus the 8px control gap on each side.
18
+ - **Sidebar** 276px with a 32px start gutter aligned with the logo; it starts with its first group, whose caption stands level with the page kicker and the contents caption (y = header + 56px). Group captions are muted eyebrows; entries are 14px muted on one hairline guide, 32px rows; the current entry inks a short 2px bar on the guide and takes medium weight.
19
+ - **Contents rail** (252px) is a ruler: h2 entries have 24px ticks and h3 entries have 12px ticks with their right ends aligned before the text; titles share a second edge without indentation. The current entry takes strong ink and medium weight without changing its tick length. 13px entries in 34px rows (44px touch targets); "On this page" is a muted eyebrow on the title edge, level with the page kicker. Long lists fold inactive subsections without adding icons; following the section opens them.
20
+ - **Page head** a muted eyebrow kicker, a regular-weight title (fluid 30–38px), an 18px lead (a second lead line, such as a layout's subtitle, at reading size), then 48px of space — no rule — before the reading starts.
21
+ - **Page tabs** between sibling documents (`Tabs`, `ui-segmented[data-variant="underline"]`) are words on one hairline, the current one underlined in ink — the original's tabs, not a raised segmented thumb.
22
+ - **Compact menu** below 64rem is a full-height modal: Tabler’s two-line menu at 18px / 1.5 stroke becomes a small close glyph in a 44px target, brand stays centred, search sits opposite. Primary links use fluid 36–56px medium-weight type and generous space; chapters belong to the section indexes. Community icons and native preferences sit beneath the navigation, at the bottom when space permits. External community icons carry the same raised 10px marker as header actions, anchored 11px right / 9px above the icon centre. The surface enters over 320ms, links follow at 40ms intervals, and closing takes 180ms; reduced motion removes transitions.
23
+ - **Search trigger** (`ui-search-trigger`, on the 404) is a quiet row over one hairline with a flat `⌘K` hint; results in the dialog take a chip wash, never a raised frame. The desktop dialog footer teaches “Search anytime” with ⌘ + K keycaps on Apple devices or Ctrl + K elsewhere.
24
+ - **Pager** is one hairline over two links, 80px below the article: an eyebrow (Previous / Next) over a 16px title with a muted arrow; ink on hover, no boxes.
25
+ - **Steps** hang on one hairline rail from flat 28px numbered rings (canvas face, strong contour, sans tabular figures) and take the wide column. A step title is 16px medium; its text is 14/1.8. A row step puts its text on two fifths and its code on three once the list is 46rem wide (narrower, the code stacks under its text). A preview after the steps hangs like any surface and the rail runs into it.
26
+ - **Footer** in the documentation takes the article's wide column from the text's edge; elsewhere it is 72rem. Groups keep 11rem columns from the start (`auto-fill`), so a wide footer leaves the rest to the canvas. Link groups (Docs, Resources, Community, Legal — the license and the trademark policy) under strong-ink eyebrows, 14px links on a 32px pitch. One quiet base row under a hairline, every word 12px tracked .04em: the notice at the start, the theme and language preferences at the end, each an icon and its value with no "Theme:" or "Language:" label (the selects keep those names for assistive technology). No search field — the header carries search.
27
+
28
+
29
+ ## Verification
30
+
31
+ Use packed consumers, real SSR and hydration, light/dark desktop and compact widths, keyboard and touch, reduced motion and forced colors. Compare shell geometry against the recorded baseline. Keep preview documents isolated from site CSS.
@@ -0,0 +1,4 @@
1
+ /* A comparison annotation keeps its teaching hue; Master UI owns the badge recipe. */
2
+ @layer components {
3
+ .demo-badge-violet { --badge-tone: var(--ui-tone-violet); }
4
+ }
@@ -0,0 +1,2 @@
1
+ import '@master/ui/components/badge.css'
2
+ import './badge-tones.css'