vowel 0.8.0 → 0.8.1

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.
@@ -1,6 +1,6 @@
1
1
  import path from "node:path"
2
2
  import { h } from "hastscript"
3
- import { isListedPage } from "../../utils.js"
3
+ import { isListedPage, pageClasses } from "../../utils.js"
4
4
  import { isVowelStylesheet } from "../styles/theme.js"
5
5
 
6
6
  /** The one stylesheet name linked without being asked for. */
@@ -96,8 +96,26 @@ function chain(folder) {
96
96
  return ["", ...segments.map((_, i) => segments.slice(0, i + 1).join(path.sep))]
97
97
  }
98
98
 
99
- /** A nav entry: where it goes and what it says. */
100
- const item = (page) => ({ url: page.metadata.prettyURL, label: page.metadata.breadcrumb })
99
+ /**
100
+ * The class a nav's `<li>` carries (per Sam, Nov 7): the page class of
101
+ * the page it links to, the same as that page's `<body>` and cards
102
+ * (`page_blog_hello-world`).
103
+ * @param {any} page
104
+ */
105
+ const pageClass = (page) => pageClasses(page.metadata.prettyURL, page.source).at(-1)
106
+
107
+ /**
108
+ * The class a nav's `<ul>` carries: the folder its pages are in, as
109
+ * their own classes name it (`folder_blog`), or null for the root's.
110
+ * @param {any[]} pages - one folder's
111
+ */
112
+ const folderClass = (pages) => {
113
+ const classes = pages.length ? pageClasses(pages[0].metadata.prettyURL, pages[0].source) : []
114
+ return classes.length > 1 ? classes.at(-2) : null
115
+ }
116
+
117
+ /** A nav entry: where it goes, what it says, and its page's class. */
118
+ const item = (page) => ({ url: page.metadata.prettyURL, label: page.metadata.breadcrumb, page: pageClass(page) })
101
119
 
102
120
  /**
103
121
  * The aside tree: top-level pages with no date, each with the pages of
@@ -118,16 +136,14 @@ function asideTree(pages) {
118
136
  byFolder.get(url).push(page)
119
137
  }
120
138
  // A folder with too many pages for a menu shows its own entry only (asMenu).
121
- const children = (parent) => {
122
- const pages = byFolder.get(parent.metadata.prettyURL) ?? []
123
- return asMenu(pages).map(node)
124
- }
139
+ const children = (parent) => asMenu(byFolder.get(parent.metadata.prettyURL) ?? [])
125
140
 
126
141
  function node(page) {
127
142
  const below = children(page)
128
143
  // The aside links the file, not the pretty url - kept as it was
129
144
  // (see the note in the commit).
130
- return { url: "/" + page.path, label: page.metadata.breadcrumb, ...(below.length ? { children: below } : {}) }
145
+ const entry = { url: "/" + page.path, label: page.metadata.breadcrumb, page: pageClass(page) }
146
+ return below.length ? { ...entry, children: below.map(node), folder: folderClass(below) } : entry
131
147
  }
132
148
 
133
149
  const top = undated
@@ -241,7 +257,8 @@ function createPartialStubs(api) {
241
257
  // it, a folder with too many pages shows none of them (asMenu).
242
258
  levels: chain(folder).map(level => {
243
259
  const list = lists.get(level) ?? []
244
- return (level ? asMenu(list) : list).map(item)
260
+ const shown = level ? asMenu(list) : list
261
+ return { folder: folderClass(shown), items: shown.map(item) }
245
262
  }),
246
263
  sheets: chain(folder).flatMap(level => sheets.get(level) ?? [])
247
264
  }
@@ -255,20 +272,30 @@ function expandPartial({ params }) {
255
272
  return { text: JSON.stringify(params) }
256
273
  }
257
274
 
258
- /** The header nav: one <ul> per level that has entries, root first. */
275
+ /**
276
+ * The header nav: one <ul> per level that has entries, root first.
277
+ * Labelled "Local" and the aside "Global", after the settings that
278
+ * leave a page out of each (`local_nav_item`, `global_nav_item`), so a
279
+ * screen reader can tell the page's navs apart. The labels are CSS
280
+ * hooks too, like `nav[aria-label=Breadcrumbs]`: renaming one breaks
281
+ * an author's stylesheet.
282
+ */
259
283
  function renderNav({ levels }) {
260
- return h("nav", levels
261
- .filter(level => level.length)
262
- .map(level => h("ul", level.map(({ url, label }) => h("li", h("a", { href: url }, label)))))
284
+ return h("nav", { "aria-label": "Local" }, levels
285
+ .filter(level => level.items.length)
286
+ .map(level => h("ul", classed(level.folder), level.items.map(({ url, label, page }) => h("li", classed(page), h("a", { href: url }, label)))))
263
287
  )
264
288
  }
265
289
 
290
+ /** One class, or none. @param {string | null | undefined} name */
291
+ const classed = (name) => name ? { className: [name] } : {}
292
+
266
293
  /** The aside tree. */
267
294
  function renderAside({ items }) {
268
- const li = ({ url, label, children }) => children
269
- ? h("li", [h("a", { href: url }, label), h("ul", children.map(li))])
270
- : h("li", h("a", { href: url }, label))
271
- return h("nav", h("ul", items.map(li)))
295
+ const li = ({ url, label, page, children, folder }) => children
296
+ ? h("li", classed(page), [h("a", { href: url }, label), h("ul", classed(folder), children.map(li))])
297
+ : h("li", classed(page), h("a", { href: url }, label))
298
+ return h("nav", { "aria-label": "Global" }, h("ul", items.map(li)))
272
299
  }
273
300
 
274
301
  /** "Linked from", outside section#content: it is generated, and the editor must not see it. */
@@ -294,16 +321,39 @@ function readPartial(source) {
294
321
  }
295
322
 
296
323
  /**
297
- * The page's copy of a nav, with the current page marked. A copy: the
298
- * tracked read hands back a fresh parse, but a rule that relies on that
299
- * would be fragile, and the walk mutates.
324
+ * A nav link's href as the page url it names: the aside links files
325
+ * (`/blog.html`, and `/index.html` for the home page), the header nav
326
+ * pretty urls.
327
+ * @param {string} href
328
+ */
329
+ const linkedURL = (href) => href === "/index.html" ? "/" : href.replace(/\.html$/, "")
330
+
331
+ /**
332
+ * The urls of the folders above a page, not the root: `/blog` and
333
+ * `/blog/2020` for `/blog/2020/post`. Each is its folder's index page.
334
+ * @param {string} url
335
+ */
336
+ function ancestorURLs(url) {
337
+ const segments = url.split("/").filter(Boolean)
338
+ return segments.slice(0, -1).map((segment, index) => "/" + segments.slice(0, index + 1).join("/"))
339
+ }
340
+
341
+ /**
342
+ * The page's copy of a nav, with the current page marked
343
+ * `aria-current=page` and the folders above it `aria-current=true`
344
+ * (per Sam, Nov 7), the home page aside, since every page is below it.
345
+ * A copy: the tracked read hands back a fresh parse, but a rule that
346
+ * relies on that would be fragile, and the walk mutates.
300
347
  * @param {any} hast
301
348
  * @param {string} url - the page's prettyURL
302
349
  */
303
350
  function withCurrent(hast, url) {
304
351
  const copy = structuredClone(hast)
352
+ const ancestors = new Set(ancestorURLs(url))
305
353
  const walk = (node) => {
306
- if (node.type === "element" && node.tagName === "a" && node.properties?.href === url) node.properties.ariaCurrent = "page"
354
+ const href = node.type === "element" && node.tagName === "a" && typeof node.properties?.href === "string" ? linkedURL(node.properties.href) : null
355
+ if (href === url) node.properties.ariaCurrent = "page"
356
+ else if (href !== null && ancestors.has(href)) node.properties.ariaCurrent = "true"
307
357
  node.children?.forEach(walk)
308
358
  }
309
359
  walk(copy)
@@ -1,6 +1,7 @@
1
1
  import { h } from "hastscript"
2
2
  import { imageSizes } from "../../utils.js"
3
3
  import { imageURL } from "./image.js"
4
+ import { isExternalURL, externalProperties } from "./externalLinks.js"
4
5
 
5
6
  /**
6
7
  * Social links, from the `social_links` setting:
@@ -52,6 +53,7 @@ function socialLinks(settings) {
52
53
  function socialLinksNav(settings, api) {
53
54
  const links = socialLinks(settings)
54
55
  if (!links.length) return null
56
+ const domain = settings?.lastNonNull?.("fm_domain")
55
57
 
56
58
  return h("nav.social-links", { "aria-label": "Social links" },
57
59
  h("ul", links.map(({ label, url, icon }) => {
@@ -61,7 +63,8 @@ function socialLinksNav(settings, api) {
61
63
  // published (image.js).
62
64
  const iconURL = icon ? imageURL(String(icon), api, imageSizes[0]) : null
63
65
  const image = iconURL && h("img", { src: iconURL, alt: text, style: `--url-icon: url("${iconURL}")` })
64
- return h("li", h("a", { href: url, rel: "me" }, image || text))
66
+ const properties = { href: url, rel: "me" }
67
+ return h("li", h("a", isExternalURL(url, domain) ? externalProperties(properties) : properties, image || text))
65
68
  }))
66
69
  )
67
70
  }
@@ -8,6 +8,8 @@ import { globClasses, alertLabel } from "./editor/directives.js"
8
8
  import createDynamicImage, { derivativeURL } from "./image.js"
9
9
  import { highlightCode } from "./highlighting.js"
10
10
  import { displayPath } from "../../secretPaths.js"
11
+ import { isExternalURL, externalProperties } from "./externalLinks.js"
12
+ import { HEADING_ID_PREFIX, headingAnchor, withHeadingAnchor } from "./headingIds.js"
11
13
 
12
14
  /**
13
15
  * The page's source, at the node a warning is about (utils.js). Nodes
@@ -148,8 +150,39 @@ const internalLink = {
148
150
  return
149
151
  }
150
152
  // A link to a png or jpeg opens its largest derivative: the
151
- // original is not published (image.js).
152
- node.properties.href = (derivativeURL(found, imageSizes.at(-1)) ?? urlOf(found)) + suffix
153
+ // original is not published (image.js). A page's anchor is a
154
+ // heading's (headingIds.js); a file's, like a pdf's `#page=2`, isn't.
155
+ const anchored = found.extension === ".html" ? withHeadingAnchor(suffix) : suffix
156
+ node.properties.href = (derivativeURL(found, imageSizes.at(-1)) ?? urlOf(found)) + anchored
157
+ }
158
+ }
159
+
160
+ /**
161
+ * `[x](#kerning)`, an anchor on this page: a heading's, so `#heading_kerning`
162
+ * (headingIds.js). In place, so the walk goes on into its text.
163
+ * @type {import("../markdown/visitor.js").BlockRule}
164
+ */
165
+ const anchorLink = {
166
+ name: "anchorLink",
167
+ test: (node) => isElement(node) && node.tagName === "a" && typeof node.properties?.href === "string" && node.properties.href.startsWith("#"),
168
+ transform: (node) => {
169
+ node.properties.href = "#" + headingAnchor(node.properties.href.slice(1))
170
+ }
171
+ }
172
+
173
+ /**
174
+ * A link to another site opens in a new tab (externalLinks.js). Any
175
+ * absolute link matches; one to the site's own domain is left as it
176
+ * is, which a test can't tell, since only the transform has the
177
+ * settings. In place, so the walk goes on into its text.
178
+ * @type {import("../markdown/visitor.js").BlockRule}
179
+ */
180
+ const externalLink = {
181
+ name: "externalLink",
182
+ test: (node) => isElement(node) && node.tagName === "a" && /^(https?:)?\/\//i.test(node.properties?.href ?? ""),
183
+ transform: (node, index, parent, { settings }) => {
184
+ if (!isExternalURL(node.properties.href, settings?.lastNonNull?.("fm_domain"))) return
185
+ node.properties = externalProperties(node.properties)
153
186
  }
154
187
  }
155
188
 
@@ -247,7 +280,7 @@ const wikilink = {
247
280
  return SKIP
248
281
  }
249
282
  const url = found.metadata?.prettyURL ?? `/${found.path}`
250
- const anchor = section ? `#${new GithubSlugger().slug(section)}` : ""
283
+ const anchor = section ? `#${HEADING_ID_PREFIX}${new GithubSlugger().slug(section)}` : ""
251
284
  node.properties.href = url + anchor
252
285
  return SKIP
253
286
  }
@@ -323,6 +356,7 @@ const captionedImage = {
323
356
  transform: (node) => {
324
357
  const [img, ...caption] = node.children
325
358
  node.tagName = "figure"
359
+ node.properties = { ...node.properties, className: [...(node.properties?.className ?? []), "image"] }
326
360
  node.children = [img, h("figcaption", caption)]
327
361
  }
328
362
  }
@@ -419,7 +453,7 @@ const directive = {
419
453
  // Lowercased: every vowel target path is (config.js's router).
420
454
  const found = api.target(path.relative("/", path.format(info)).toLowerCase())
421
455
  if (!found) return SKIP
422
- return makeHeader(found.metadata, cardLink(found), api, config, found.source)
456
+ return asReference(makeHeader(found.metadata, cardLink(found), api, config, found.source))
423
457
  }
424
458
 
425
459
  const folder = path.relative("/", dir)
@@ -455,6 +489,19 @@ const directive = {
455
489
  */
456
490
  const cardLink = (found) => isPageWithoutFile(found) ? null : found.metadata.prettyURL
457
491
 
492
+ /**
493
+ * A reference card's <article> marked `reference`, which a listing's
494
+ * cards aren't: `<a><article class=reference>`, or the article alone for
495
+ * a page with no file. The editor's ingest reads it to put `/blog/post`
496
+ * back.
497
+ * @param {any} card - makeHeader's result
498
+ */
499
+ function asReference(card) {
500
+ const article = card.tagName === "article" ? card : card.children[0]
501
+ article.properties.className = [...(article.properties.className ?? []), "reference"]
502
+ return card
503
+ }
504
+
458
505
  /**
459
506
  * A relative directive path as the absolute published path it names.
460
507
  * The glob's last segment (`*`, `**`) rides along untouched: the
@@ -569,7 +616,7 @@ function trimText(nodes) {
569
616
  const skipped = new Set(["pre", "code", "script", "style"])
570
617
 
571
618
  const writeWalk = createVisitor({
572
- block: [internalLink, wikilink, icon, highlight, captionedImage, image, directive, preview, alert],
619
+ block: [internalLink, anchorLink, externalLink, wikilink, icon, highlight, captionedImage, image, directive, preview, alert],
573
620
  skip: (node) => isElement(node) && skipped.has(node.tagName)
574
621
  })
575
622
 
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A fenced code block's info string: its language, then its meta, which
3
+ * can carry a title (per Sam, Nov 6):
4
+ *
5
+ * ```md "Hello World.md"
6
+ * ```js title="app.js"
7
+ *
8
+ * The title is `title="..."` when the meta has one, else the first
9
+ * quoted string standing on its own, in double or single quotes. The
10
+ * rest of the meta is the author's, as frontmatter keys vowel doesn't
11
+ * read are. GitHub ignores the meta, so a titled block still renders
12
+ * there, without its title. `title="..."` is Docusaurus', Astro's and
13
+ * rehype-pretty-code's spelling; the bare string is Vowel's short form,
14
+ * which rules out Expressive Code's meaning for one (mark this text).
15
+ *
16
+ * Free of node builtins: the editor may need it.
17
+ */
18
+
19
+ const KEYED = /(?:^|\s)title=(?:"([^"]*)"|'([^']*)')(?=\s|$)/
20
+ const BARE = /(?:^|\s)(?:"([^"]*)"|'([^']*)')(?=\s|$)/
21
+
22
+ /**
23
+ * The fence as micromark split it, at the first whitespace, read again.
24
+ * A quoted first word is not a language: in ```` ```"notes.txt" ````
25
+ * micromark's language is `"notes.txt"`, and the block has none.
26
+ * @param {string | null | undefined} lang - mdast's `code.lang`
27
+ * @param {string | null | undefined} meta - mdast's `code.meta`
28
+ * @returns {{ language: string | null, meta: string | null, title: string | undefined }}
29
+ */
30
+ export function parseFence(lang, meta) {
31
+ const quoted = /^["']/.test(lang ?? "")
32
+ const language = lang && !quoted ? lang : null
33
+ const rest = (quoted ? [lang, meta].filter(Boolean).join(" ") : meta) || null
34
+ const match = rest ? KEYED.exec(rest) ?? BARE.exec(rest) : null
35
+ const title = (match?.[1] ?? match?.[2])?.trim() || undefined
36
+ return { language, meta: rest, title }
37
+ }
38
+
39
+ /**
40
+ * A title written back into a fence, as mdast's `lang` and `meta`:
41
+ * `title="..."` after a language, the spelling other tools read, and
42
+ * the bare string without one, since a `title=` would be read as the
43
+ * language. Split at the first space, as micromark splits it: a `lang`
44
+ * with a space in it is written with the space escaped. A title with a
45
+ * double quote in it is in single quotes.
46
+ * @param {string | null} language
47
+ * @param {string} title
48
+ * @returns {{ lang: string | null, meta: string | null }}
49
+ */
50
+ export function fenceWithTitle(language, title) {
51
+ const quote = title.includes('"') ? "'" : '"'
52
+ const quotedTitle = `${quote}${title}${quote}`
53
+ if (language) return { lang: language, meta: `title=${quotedTitle}` }
54
+ const [first, ...rest] = quotedTitle.split(/\s+/)
55
+ return { lang: first, meta: rest.join(" ") || null }
56
+ }
@@ -20,6 +20,7 @@ import { toString as hastToString } from 'hast-util-to-string'
20
20
  import getMetadata, { imageFromPage, reservedProperties } from "./metadata.js"
21
21
  import { isSecretPath, secretRouter, displayPath } from "./../../secretPaths.js"
22
22
  import { collectLinks } from "./links.js"
23
+ import { parseFence } from "./codeFence.js"
23
24
  import { readWalk } from "./readRules.js"
24
25
  import { h } from "hastscript"
25
26
  import { mirrorStubs, mirrorWriter, llmsStubs } from "./mirror.js"
@@ -146,8 +147,18 @@ function readFile(source, { api, config }) {
146
147
  // is too wide, so the table itself can be a real table at full width
147
148
  // (a table that scrolls has to be display: block, which sizes its
148
149
  // columns to their content), and a caption has somewhere to go.
150
+ // A code block with a title (codeFence.js) is in one too, the title
151
+ // as its caption. One without stays a bare <pre>.
149
152
  handlers: {
150
- table: (state, node) => h("figure", [defaultHandlers.table(state, node)])
153
+ table: (state, node) => h("figure.table", [defaultHandlers.table(state, node)]),
154
+ code: (state, node) => {
155
+ const fence = parseFence(node.lang, node.meta)
156
+ const pre = defaultHandlers.code(state, { ...node, lang: fence.language, meta: fence.meta })
157
+ if (!fence.title) return pre
158
+ const figure = h("figure.code", [h("figcaption", fence.title), pre])
159
+ state.patch(node, figure)
160
+ return figure
161
+ }
151
162
  },
152
163
  // `state.all(node)` converts the children to hast; handing over
153
164
  // `node.children` gives the HTML compiler raw *mdast* instead. Plain
@@ -20,7 +20,7 @@
20
20
  * @property {string} variable - the custom property in TypographyStyles.css
21
21
  * @property {string} label
22
22
  * @property {string} [hint]
23
- * @property {"x" | "ch" | ""} unit - "x" is a multiple of --fontsize-root
23
+ * @property {"ch" | ""} unit
24
24
  * @property {number} min
25
25
  * @property {number} max
26
26
  * @property {number} step
@@ -29,7 +29,7 @@
29
29
 
30
30
  /** @type {TypographySetting[]} */
31
31
  export const rampSettings = [
32
- { key: "heading-size", variable: "--fontsize-delta", label: "Heading size", hint: "How much larger h1 is than body text", unit: "x", min: 0.25, max: 4, step: 0.05, default: 2 },
32
+ { key: "heading-size", variable: "--fontsize-scale", label: "Heading size", hint: "How much larger h1 is than body text, as a multiple of it", unit: "", min: 0.25, max: 4, step: 0.05, default: 1.5 },
33
33
  { key: "heading-size-decay", variable: "--fontsize-decay", label: "Size decay", hint: "Higher makes subheadings smaller", unit: "", min: 0.5, max: 6, step: 0.1, default: 3 },
34
34
  { key: "h1-letter-spacing", variable: "--letterspacing-h1", label: "h1 letter spacing", unit: "ch", min: -0.1, max: 0.1, step: 0.005, default: -0.01 },
35
35
  { key: "h6-letter-spacing", variable: "--letterspacing-h6", label: "h6 letter spacing", unit: "ch", min: -0.1, max: 0.1, step: 0.005, default: 0 },
@@ -49,15 +49,15 @@ function themeSettings(theme) {
49
49
  }
50
50
 
51
51
  /**
52
- * A ramp setting's value as CSS, clamped to its range. "x" is a multiple
53
- * of the root size, so headings follow the root's clamp() up and down.
52
+ * A ramp setting's value as CSS, clamped to its range. `heading-size` is
53
+ * a plain number, `--fontsize-scale`, which the sheet multiplies by the
54
+ * root size, so headings follow the root's clamp() up and down.
54
55
  * @param {TypographySetting} setting
55
56
  * @param {number} value
56
57
  */
57
58
  function rampValue(setting, value) {
58
59
  const clamped = Math.min(setting.max, Math.max(setting.min, value))
59
60
  const number = String(Number(clamped.toFixed(4)))
60
- if (setting.unit === "x") return `calc(${number} * var(--fontsize-root))`
61
61
  return `${number}${setting.unit}`
62
62
  }
63
63
 
@@ -66,10 +66,29 @@
66
66
  --color-text-hover-panel: light-dark(var(--color-primary-02), var(--color-primary-08));
67
67
  --color-border-hover-panel: light-dark(var(--color-accent-08), var(--color-accent-05));
68
68
 
69
- /* pre colors */
70
- --color-background-pre: light-dark(var(--color-tertiary-09), var(--color-tertiary-02));
71
- --color-border-pre: light-dark(var(--color-tertiary-08), var(--color-tertiary-01));
72
- --color-text-pre: light-dark(var(--color-tertiary-02), var(--color-tertiary-02));
69
+ /*
70
+ pre colors, and the syntax-highlighting sheet's from the
71
+ palette: the text at the far end from the background, every
72
+ token one shade in, so each stays above 4.5:1 on the
73
+ background for any ordinary palette (tests/syntaxColors.js).
74
+ Comments too, told apart by italics: a shade lighter fell to
75
+ 4.2:1 on a palette from bright colours (#ffcc00, #66ff00).
76
+ */
77
+ --color-background-pre: light-dark(var(--color-tertiary-10), var(--color-tertiary-02));
78
+ --color-border-pre: light-dark(var(--color-tertiary-08), var(--color-tertiary-02));
79
+ --color-text-pre: light-dark(var(--color-tertiary-04), var(--color-tertiary-08));
80
+ --color-comment-pre: light-dark(var(--color-accent-04), var(--color-accent-09));
81
+ --color-keyword-pre: light-dark(var(--color-primary-04), var(--color-primary-07));
82
+ --color-function-pre: light-dark(var(--color-secondary-04), var(--color-secondary-07));
83
+ --color-constant-pre: light-dark(var(--color-info-04), var(--color-info-07));
84
+ --color-string-pre: light-dark(var(--color-tertiary-04), var(--color-tertiary-08));
85
+ --color-builtin-pre: light-dark(var(--color-warning-04), var(--color-warning-07));
86
+ --color-tag-pre: light-dark(var(--color-success-05), var(--color-success-07));
87
+ --color-list-pre: light-dark(var(--color-warning-05), var(--color-warning-07));
88
+ --color-inserted-pre: light-dark(var(--color-success-05), var(--color-success-07));
89
+ --color-background-inserted-pre: light-dark(var(--color-success-07), var(--color-success-05));
90
+ --color-deleted-pre: light-dark(var(--color-danger-05), var(--color-danger-07));
91
+ --color-background-deleted-pre: light-dark(var(--color-danger-07), var(--color-danger-05));
73
92
 
74
93
  /*
75
94
  alert colors: --color-{element}-alert-{type} for the types the
@@ -484,6 +503,11 @@
484
503
  background: var(--color-background-pre);
485
504
  }
486
505
 
506
+ /* a comment is the text's colour, a shade lighter: italics tell it apart */
507
+ .token.comment {
508
+ font-style: italic;
509
+ }
510
+
487
511
  blockquote {
488
512
  border-left: 0.5ch solid var(--color-border-body);
489
513
  }