diff --git a/astro.config.mjs b/astro.config.mjs index 97d9800..b54e3ed 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,6 +6,11 @@ import topics from "starlight-sidebar-topics"; import starlightMarkdown from "starlight-markdown"; import mermaid from "astro-mermaid"; import { fileURLToPath } from "node:url"; +import { ecThemes } from "./src/ec-themes.mjs"; + +/** Pick a value per code theme; keeps styleOverrides below readable. */ +const byTheme = (dark, light) => ({ theme }) => + theme.type === "dark" ? dark : light; const docsRoot = fileURLToPath(new URL(".", import.meta.url)); @@ -37,13 +42,58 @@ export default defineConfig({ ], components: { Head: "./src/starlightOverrides/Head.astro", + Header: "./src/starlightOverrides/Header.astro", + SiteTitle: "./src/starlightOverrides/SiteTitle.astro", + Footer: "./src/starlightOverrides/Footer.astro", + MarkdownContent: "./src/starlightOverrides/MarkdownContent.astro", + MobileMenuFooter: "./src/starlightOverrides/MobileMenuFooter.astro", }, expressiveCode: { - themes: ['github-dark-high-contrast', 'github-light-high-contrast'], + themes: ecThemes, styleOverrides: { + codeFontFamily: '"dm-mono", ui-monospace, monospace', + uiFontFamily: '"dm-mono", ui-monospace, monospace', + borderRadius: "4px", + borderWidth: "1px", + borderColor: byTheme("#2b2e38", "#c4c7cf"), + codeBackground: byTheme("#000000", "#ffffff"), + frames: { + shadowColor: "transparent", + frameBoxShadowCssValue: "none", + terminalBackground: byTheme("#000000", "#ffffff"), + terminalTitlebarBackground: "transparent", + terminalTitlebarBorderBottomColor: "transparent", + terminalTitlebarForeground: byTheme("#8a8e9b", "#6c6e79"), + terminalTitlebarDotsForeground: byTheme("#484a54", "#c4c7cf"), + editorTabBarBackground: "transparent", + editorTabBarBorderColor: "transparent", + editorTabBarBorderBottomColor: "transparent", + editorActiveTabBackground: "transparent", + editorActiveTabForeground: byTheme("#f4f5f9", "#0a0a0d"), + editorActiveTabBorderColor: "transparent", + editorActiveTabIndicatorTopColor: byTheme("#ff00d2", "#b8009c"), + editorActiveTabIndicatorBottomColor: "transparent", + }, twoSlash: { - cursorColor: 'rgba(255 0 210)' - } + cursorColor: "#ff00d2", + borderColor: byTheme("#2b2e38", "#c4c7cf"), + background: byTheme("#191b23", "#ffffff"), + textColor: byTheme("#f4f5f9", "#0a0a0d"), + hoverUnderlineColor: byTheme("#8a8e9b", "#a9abb6"), + tagColor: byTheme("#00e5ff", "#00798a"), + linkColor: byTheme("#00e5ff", "#00798a"), + linkColorHover: byTheme("#ffffff", "#0a0a0d"), + errorColor: byTheme("#ff3e47", "#d21f28"), + warnColor: byTheme("#e8cf27", "#a37e00"), + suggestionColor: byTheme("#07f53f", "#00842e"), + messageColor: byTheme("#c4c7cf", "#484a54"), + completionBoxBackground: byTheme("#191b23", "#ffffff"), + completionBoxBorder: byTheme("#2b2e38", "#c4c7cf"), + completionBoxColor: byTheme("#f4f5f9", "#0a0a0d"), + completionBoxMatchedColor: byTheme("#00e5ff", "#00798a"), + completionBoxHoverBackground: byTheme("#2b2e38", "#f4f5f9"), + highlightHue: "311", + }, }, plugins: [ecTwoSlash({ twoslashOptions: { @@ -66,7 +116,7 @@ export default defineConfig({ tag: "link", attrs: { rel: "stylesheet", - href: "https://use.typekit.net/bst3mzh.css?v=5", + href: "https://use.typekit.net/bst3mzh.css?v=7", }, }, { diff --git a/package.json b/package.json index a075fc4..e3c0421 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@bomb.sh/tty": "^0.8.0", "@clack/core": "^1.4.3", "@clack/prompts": "^1.7.0", + "@fontsource-variable/inter": "^5.3.0", "@types/node": "^22.19.3", "@webcontainer/api": "^1.6.1", "@webcontainer/snapshot": "^0.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c08d42..21304b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@clack/prompts': specifier: ^1.7.0 version: 1.7.0 + '@fontsource-variable/inter': + specifier: ^5.3.0 + version: 5.3.0 '@types/node': specifier: ^22.19.3 version: 22.19.3 @@ -597,6 +600,9 @@ packages: '@expressive-code/plugin-text-markers@0.41.5': resolution: {integrity: sha512-0DSiTsjWFEz6/iuLOGNNy2GaeCW41OwnVJMKx1tS+XKeQxAL89UkZP3egWNzxjWNHNMzEv3ZWWWYqbonEQlv/Q==} + '@fontsource-variable/inter@5.3.0': + resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==} + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -3349,6 +3355,8 @@ snapshots: dependencies: '@expressive-code/core': 0.41.5 + '@fontsource-variable/inter@5.3.0': {} + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.4': diff --git a/src/assets/dark.svg b/src/assets/dark.svg index dfcd04d..6cb1d64 100644 --- a/src/assets/dark.svg +++ b/src/assets/dark.svg @@ -1,21 +1,4 @@ - diff --git a/src/assets/light.svg b/src/assets/light.svg index 9f8dd80..aa4c914 100644 --- a/src/assets/light.svg +++ b/src/assets/light.svg @@ -1,21 +1,4 @@ - diff --git a/src/ec-themes.mjs b/src/ec-themes.mjs new file mode 100644 index 0000000..147560c --- /dev/null +++ b/src/ec-themes.mjs @@ -0,0 +1,136 @@ +// Monochrome Expressive Code themes: pure black & white, with hierarchy +// carried by grayscale dimming and font style instead of hue — code blocks +// read like the bomb.sh hero (white on black in a hairline frame). Terminal +// ANSI colors keep the brand palette so rendered terminal output still pops. +import { ExpressiveCodeTheme } from "@astrojs/starlight/expressive-code"; + +const palette = { + dark: { + bg: "#000000", + fg: "#f4f5f9", // --color-gray-20 + bright: "#ffffff", + string: "#a9abb6", // --color-gray-40 + punctuation: "#6c6e79", // --color-gray-60 + comment: "#484a54", // --color-gray-70 + selection: "#97979b33", + }, + light: { + bg: "#ffffff", + fg: "#191b23", // --color-gray-90 + bright: "#000000", + string: "#484a54", // --color-gray-70 + punctuation: "#8a8e9b", // --color-gray-50 + comment: "#a9abb6", // --color-gray-40 + selection: "#97979b33", + }, +}; + +function makeTheme(name, type, c) { + return new ExpressiveCodeTheme({ + name, + type, + colors: { + "editor.background": c.bg, + "editor.foreground": c.fg, + "editor.selectionBackground": c.selection, + "editorLineNumber.foreground": c.comment, + "terminal.ansiBlack": "#0a0a0d", + "terminal.ansiRed": "#ff3e47", + "terminal.ansiGreen": "#07f53f", + "terminal.ansiYellow": "#e8cf27", + "terminal.ansiBlue": "#054bff", + "terminal.ansiMagenta": "#ff00d2", + "terminal.ansiCyan": "#00e5ff", + "terminal.ansiWhite": "#ffffff", + }, + tokenColors: [ + { + scope: ["comment", "punctuation.definition.comment"], + settings: { foreground: c.comment, fontStyle: "italic" }, + }, + { + scope: [ + "punctuation", + "meta.brace", + "punctuation.definition.tag", + "keyword.operator", + ], + settings: { foreground: c.punctuation }, + }, + { + scope: [ + "keyword", + "storage.type", + "storage.modifier", + "keyword.control", + "entity.name.tag", + "punctuation.definition.keyword", + ], + settings: { foreground: c.bright, fontStyle: "bold" }, + }, + { + scope: [ + "string", + "string.template", + "punctuation.definition.string", + "markup.inline.raw", + ], + settings: { foreground: c.string }, + }, + { + scope: [ + "entity.name.function", + "support.function", + "meta.function-call entity.name.function", + "variable.function", + "entity.name.type", + "entity.name.class", + "support.type", + "support.class", + "entity.other.inherited-class", + "constant.numeric", + "constant.language", + "constant.character", + "constant.other", + "variable.other.constant", + "entity.other.attribute-name", + "support.type.property-name.json", + ], + settings: { foreground: c.bright }, + }, + { + scope: ["variable", "variable.parameter", "meta.object-literal.key"], + settings: { foreground: c.fg }, + }, + { + scope: [ + "string.regexp", + "constant.character.escape", + "punctuation.definition.template-expression", + ], + settings: { foreground: c.fg }, + }, + { + scope: ["invalid", "markup.deleted"], + settings: { foreground: c.punctuation }, + }, + { scope: ["markup.inserted"], settings: { foreground: c.bright } }, + { scope: ["markup.changed"], settings: { foreground: c.string } }, + { + scope: ["markup.heading", "entity.name.section"], + settings: { foreground: c.bright, fontStyle: "bold" }, + }, + { scope: ["markup.bold"], settings: { fontStyle: "bold" } }, + { scope: ["markup.italic"], settings: { fontStyle: "italic" } }, + { + scope: ["markup.underline.link", "string.other.link"], + settings: { foreground: c.fg, fontStyle: "underline" }, + }, + ], + }); +} + +export const ecThemes = [ + makeTheme("bombshell-dark", "dark", palette.dark), + makeTheme("bombshell-light", "light", palette.light), +]; diff --git a/src/starlightOverrides/Footer.astro b/src/starlightOverrides/Footer.astro new file mode 100644 index 0000000..55c8ed7 --- /dev/null +++ b/src/starlightOverrides/Footer.astro @@ -0,0 +1,100 @@ +--- +import Default from '@astrojs/starlight/components/Footer.astro'; +import { Icon } from '@astrojs/starlight/components'; +--- + + + + + diff --git a/src/starlightOverrides/Head.astro b/src/starlightOverrides/Head.astro index 127bb5b..f28424b 100644 --- a/src/starlightOverrides/Head.astro +++ b/src/starlightOverrides/Head.astro @@ -1,6 +1,7 @@ --- import Default from "@astrojs/starlight/components/Head.astro"; import VtbotStarlight from "astro-vtbot/components/starlight/Base.astro"; +import "@fontsource-variable/inter"; import "/src/styles/starlight.css"; // Per-topic accent colors, matching the bomb.sh project cards @@ -36,9 +37,116 @@ const accent = ACCENTS[Astro.locals.starlightRoute.id.split("/")[0]]; h4, h5, h6, - .site-title, .sl-doc-title, .sl-nav-link { font-family: var(--sl-font-heading); } + +{/* The site is dark-only, like bomb.sh: there is no theme switcher, and the + stored preference is pinned to dark (covering visitors who picked light + before the switcher was removed). This must run before Starlight's + ThemeProvider script (rendered later in ). */} + + + diff --git a/src/starlightOverrides/Header.astro b/src/starlightOverrides/Header.astro new file mode 100644 index 0000000..8238e26 --- /dev/null +++ b/src/starlightOverrides/Header.astro @@ -0,0 +1,115 @@ +--- +import Search from '@astrojs/starlight/components/Search.astro'; +import SocialIcons from '@astrojs/starlight/components/SocialIcons.astro'; +import SiteTitle from './SiteTitle.astro'; +--- + +
+
+ +
+
+ +
+
+ + +
+
+ + diff --git a/src/starlightOverrides/MarkdownContent.astro b/src/starlightOverrides/MarkdownContent.astro new file mode 100644 index 0000000..8fdb56c --- /dev/null +++ b/src/starlightOverrides/MarkdownContent.astro @@ -0,0 +1,10 @@ +--- +import "@astrojs/starlight/style/markdown.css"; +--- + +{/* Identical to Starlight's default MarkdownContent, plus the `content` class: + Starlight's TOC scrollspy observes `main .content > *`, but this site's + heading-wrapper markup leaves headings with no observable siblings, so + without this class `aria-current` only updates when a heading itself + crosses the intersection band and goes stale on jump-scrolls. */} +
diff --git a/src/starlightOverrides/MobileMenuFooter.astro b/src/starlightOverrides/MobileMenuFooter.astro new file mode 100644 index 0000000..1befb64 --- /dev/null +++ b/src/starlightOverrides/MobileMenuFooter.astro @@ -0,0 +1,30 @@ +--- +import SocialIcons from "@astrojs/starlight/components/SocialIcons.astro"; +--- + +{/* Like Starlight's default, minus the theme select — the site is dark-only. */} +
+
+ +
+
+ + diff --git a/src/starlightOverrides/SiteTitle.astro b/src/starlightOverrides/SiteTitle.astro new file mode 100644 index 0000000..cce6fa3 --- /dev/null +++ b/src/starlightOverrides/SiteTitle.astro @@ -0,0 +1,58 @@ +--- +--- + + + + + + + + + diff --git a/src/styles/starlight.css b/src/styles/starlight.css index c62b8f3..3f818de 100644 --- a/src/styles/starlight.css +++ b/src/styles/starlight.css @@ -2,19 +2,29 @@ @layer user.overrides { :root { - --sl-font: system-ui, sans-serif; + --font-fallback: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", + sans-serif; + --sl-font: "Inter Variable", var(--font-fallback); --sl-font-heading: "polymath-display", sans-serif; + --sl-font-mono: "dm-mono", ui-monospace, monospace; --sl-color-text-heading: var(--heading-fill); - --sl-text-h1: var(--sl-text-6xl); + + /* Heading baseline: one notch smaller than Starlight's wide-viewport + scale; the full-size scale only applies at desktop widths (below) */ + --sl-text-h1: var(--sl-text-5xl); + --sl-text-h2: var(--sl-text-2xl); + --sl-text-h3: var(--sl-text-xl); + --sl-text-h4: var(--sl-text-lg); --sl-color-accent-hsl: 311 100% 50%; - --sl-color-accent-low: hsl(var(--sl-color-accent-hsl) / 0.5); + --sl-color-accent-low: hsl(var(--sl-color-accent-hsl) / 0.1); --sl-color-accent: hsl(var(--sl-color-accent-hsl) / 0.9); --sl-color-accent-high: hsl(var(--sl-color-accent-hsl) / 1); - --sl-color-bg: var(--container-fill); - --sl-color-bg-nav: var(--container-fill); - --sl-color-bg-sidebar: var(--container-fill); + --sl-color-bg: var(--background); + --sl-color-bg-nav: var(--background); + --sl-color-bg-sidebar: var(--background); --sl-color-bg-inline-code: var(--surface-inner-fill); --sl-color-bg-accent: var(--sl-color-accent-high); --sl-color-hairline-light: var(--surface-border-fill); @@ -22,6 +32,92 @@ --sl-color-hairline-shade: var(--surface-border-fill); --sl-icon-color: var(--heading-fill); + + /* extra room for the hatched separator strip under the navbar */ + --hatch-height: 0.75rem; + --sl-nav-height: 4.25rem; + + scrollbar-width: thin; + scrollbar-gutter: stable; + scrollbar-color: hsl(var(--sl-color-accent-hsl)) var(--surface-fill); + } + + ::selection { + background-color: var(--color-pink-100); + color: var(--color-gray-100); + } + + /* bomb.sh viewport frame: 8px background strips + 1px grid border, drawn + over everything (pointer-events: none) so scrolled content passes + "behind" the frame exactly like on the marketing site */ + body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 101; + pointer-events: none; + background-image: /* fill strips */ + linear-gradient(var(--background), var(--background)), + linear-gradient(var(--background), var(--background)), + linear-gradient(var(--background), var(--background)), + linear-gradient(var(--background), var(--background)), + /* border lines */ linear-gradient(var(--c-grid), var(--c-grid)), + linear-gradient(var(--c-grid), var(--c-grid)), + linear-gradient(var(--c-grid), var(--c-grid)), + linear-gradient(var(--c-grid), var(--c-grid)); + background-size: + /* fill: top, bottom, left, right */ + 100% var(--s-grid-padding), + 100% var(--s-grid-padding), + var(--s-grid-padding) 100%, + var(--s-grid-padding) 100%, + /* border: top, bottom, left, right */ 100% var(--s-grid-width), + 100% var(--s-grid-width), + var(--s-grid-width) 100%, + var(--s-grid-width) 100%; + background-position: + top left, + bottom left, + top left, + top right, + 0 var(--s-grid-padding), + 0 calc(100% - var(--s-grid-padding)), + var(--s-grid-padding) 0, + calc(100% - var(--s-grid-padding)) 0; + background-repeat: no-repeat; + } + + /* bomb.sh hatched separator strip along the bottom of the navbar. + Nav content stays centered across the full nav height; the strip + overlays the bottom edge like the homepage separator. */ + header.header::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: var(--hatch-height); + border-top: var(--s-grid-width) solid var(--c-grid); + --_size: var(--s-grid-width); + --_space: 6px; + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent var(--_space), + var(--c-grid-muted) var(--_space), + var(--c-grid-muted) calc(var(--_space) + var(--_size)) + ); + pointer-events: none; + } + + /* Full-size heading scale on desktop viewports only */ + @media (min-width: 72rem) { + :root { + --sl-text-h1: var(--sl-text-6xl); + --sl-text-h2: var(--sl-text-4xl); + --sl-text-h3: var(--sl-text-3xl); + --sl-text-h4: var(--sl-text-2xl); + } } :is(h1, h2, h3, h4, h5, h6) { @@ -55,16 +151,17 @@ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-image: linear-gradient(to right, var(--c-start), var(--c-end)); - letter-spacing: 1px; + letter-spacing: 0.1ch; } :is(h2, h3, h4, h5, h6) { --c-start: var(--heading-fill); - --c-end: var(--heading-muted); + --c-end: color-mix(in srgb, var(--heading-fill) 35%, transparent); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-image: linear-gradient(to right, var(--c-start), var(--c-end)); + letter-spacing: 0.1ch; } :is(h1, h2, h3, h4, h5, h6) code { @@ -86,9 +183,16 @@ color: var(--heading-fill); } + /* Right sidebar (TOC) background gradient */ + .right-sidebar { + background: linear-gradient( + to bottom, + var(--panel-grad-start) 0%, + var(--background) 100% + ); + } + starlight-toc nav > ul { - --c-start: var(--heading-muted); - --c-end: var(--separator-fill); border-left: 1px solid transparent; position: relative; @@ -99,7 +203,11 @@ bottom: 0; left: 0; width: var(--border-050); - background: linear-gradient(to bottom, var(--c-start), var(--c-end)); + background: linear-gradient( + to bottom, + var(--surface-border-highlight), + var(--surface-border-fill) + ); } a { @@ -108,7 +216,32 @@ } [aria-current] { - border-left-color: var(--sl-color-accent); + border-left-color: hsl(var(--sl-color-accent-hsl) / 0.9); + font-weight: 600; + color: var(--separator-fill); + } + + /* When the animated marker is active, the static per-link border is + replaced by a single sliding indicator (injected by Head.astro). */ + &.has-marker [aria-current] { + border-left-color: transparent; + } + + .toc-marker { + position: absolute; + left: 0; + width: 2px; + background: hsl(var(--sl-color-accent-hsl) / 0.9); + transition: + top 250ms cubic-bezier(0.23, 1, 0.32, 1), + height 250ms cubic-bezier(0.23, 1, 0.32, 1); + pointer-events: none; + } + + @media (prefers-reduced-motion: reduce) { + .toc-marker { + transition: none; + } } } @@ -121,33 +254,173 @@ sans-serif; } + /* Buttons — match the bomb.sh homepage hero CTAs */ + .sl-link-button { + border-radius: 0.33em; + corner-shape: bevel; + font-family: var(--sl-font); + font-weight: 500; + transition: + color 120ms cubic-bezier(0.23, 1, 0.32, 1), + background-color 120ms cubic-bezier(0.23, 1, 0.32, 1), + border-color 120ms cubic-bezier(0.23, 1, 0.32, 1); + } + .sl-link-button.primary { - background: linear-gradient( - to right in oklab, - var(--color-pink-90), - var(--color-pink-100) - ); + background: var(--color-white-100); color: var(--color-gray-100); - border-color: var(--color-pink-100); - border-radius: 0px; + border: var(--s-grid-width) solid var(--c-grid); + + &:is(:hover, :focus-visible) { + background: color-mix(in srgb, var(--color-pink-100) 30%, transparent); + color: var(--color-pink-100); + border-color: var(--color-pink-100); + } + } + + .sl-link-button:not(.primary) { + background: transparent; + color: var(--color-gray-40); + border: var(--s-grid-width) solid var(--color-gray-40); + + &:is(:hover, :focus-visible) { + background: var(--color-gray-80); + color: var(--color-white-100); + border-color: var(--color-white-100); + } } .sl-link-button > svg { - fill: var(--color-gray-100); + fill: currentColor; + } + + /* Content links — white at rest, topic accent on hover */ + .sl-markdown-content a:not(:where(.not-content *)) { + color: var(--color-white-100); + transition: color 120ms cubic-bezier(0.23, 1, 0.32, 1); + + &:is(:hover, :focus-visible) { + color: hsl(var(--sl-color-accent-hsl)); + } + + /* Links wrapping code chips: accent at rest (so the underline matches + the chip), flipping to white on hover — the inverse of plain links */ + &:has(> code) { + color: hsl(var(--sl-color-accent-hsl)); + + &:is(:hover, :focus-visible) { + color: var(--color-white-100); + + & > code { + --color: var(--color-white-100); + } + } + } + } + + /* Inline code — bomb.sh gradient chip, tinted by the topic accent */ + .sl-markdown-content code:not(:where(.not-content *)):not(pre *) { + --color: hsl(var(--sl-color-accent-hsl)); + font-family: var(--sl-font-mono); + color: var(--color); + transition: + color 120ms cubic-bezier(0.23, 1, 0.32, 1), + background 120ms cubic-bezier(0.23, 1, 0.32, 1); + font-size-adjust: 0.5; + background: linear-gradient( + -45deg, + color-mix(in srgb, var(--color) 25%, transparent) 0%, + color-mix(in srgb, var(--color) 12%, transparent) 100% + ); + padding-inline: 0.25em; + padding-block: 0.1em; + border-radius: 4px; + } + + /* Search — a full-height section of the nav, delimited by vertical + hairline bars, aligned with the main content column */ + site-search { + width: 100%; + height: 100%; + display: flex; + align-items: center; + } + + /* Desktop with a TOC column: cap the search at the main content area so + its right bar aligns with the content/TOC boundary (measured value from + Head.astro; the calc is a close fallback that ignores scrollbar width) */ + @media (min-width: 72rem) { + :root[data-has-toc] site-search { + max-width: var( + --search-clamp, + calc((100vw - 2 * var(--sl-sidebar-width) + var(--sl-content-width)) / 2) + ); + } + } + + @media (min-width: 50rem) { + site-search button[data-open-modal] { + border-radius: 0; + max-width: 100%; + height: 100%; + border-block: none; + border-inline: 1px solid var(--surface-border-fill); + /* No fill — the nav's hatch strip stays visible through the section */ + background-color: transparent; + /* Stack above the nav's hatch strip so the bars stay unbroken, and + nudge 1px left so the left bar overlaps the sidebar border exactly */ + position: relative; + z-index: 1; + translate: -1px 0; + } } .cli-preview { padding: 1em; - border: 1px solid #292929; + border: 1px solid var(--surface-border-fill); border-radius: 0.5em; white-space: pre; line-height: 1.2em; - font-family: monospace; - background-color: #181818; + font-family: var(--sl-font-mono); + background-color: var(--container-fill); color: var(--color-white-100); } + /* Code frames — subtle gradient fade on terminal/editor title bars, + matching the hatch-and-hairline chrome of the rest of the site */ + .expressive-code .frame.is-terminal .header, + .expressive-code .frame.has-title:not(.is-terminal) .header { + background: linear-gradient( + to bottom, + transparent, + color-mix(in srgb, var(--heading-fill) 7%, transparent) + ); + border-bottom: 1px solid var(--surface-border-fill); + } + + /* Twoslash hovercards — same panel language as the rest of the chrome */ + .expressive-code .twoslash-popup-container { + border-radius: var(--radius-100); + box-shadow: 0 8px 24px color-mix(in srgb, #000 35%, transparent); + } + + .expressive-code .twoslash-popup-docs, + .expressive-code .twoslash-popup-docs-tags { + font-family: var(--sl-font); + font-size: var(--sl-text-xs); + } + + .expressive-code .twoslash-popup-code { + font-family: var(--sl-font-mono); + } + .sidebar-pane { + background: linear-gradient( + to bottom, + var(--panel-grad-start) 0%, + var(--background) 100% + ); + a { border: 1px solid transparent; } @@ -208,14 +481,28 @@ --sl-icon-color: var(--color-gray-100); - .sl-link-button { - background-color: var(--color-gray-100); + .sl-link-button.primary { + background: var(--color-gray-100); color: var(--color-white-100); border-color: var(--color-gray-100); + + &:is(:hover, :focus-visible) { + background: color-mix(in srgb, var(--color-pink-100) 12%, transparent); + color: var(--color-gray-100); + border-color: var(--color-pink-100); + } } - .sl-link-button > svg { - fill: var(--color-white-100); + .sl-link-button:not(.primary) { + background: transparent; + color: var(--color-gray-70); + border-color: var(--color-gray-40); + + &:is(:hover, :focus-visible) { + background: var(--color-gray-20); + color: var(--color-gray-100); + border-color: var(--color-gray-100); + } } } @@ -344,3 +631,52 @@ ul.starlight-sidebar-topics .starlight-sidebar-topics li + li { margin-top: 0; } + +/* Run the topics separator on the same horizontal line as the divider under + the page title, so it reads as one continuous rule across sidebar and + content. The min-height mirrors the first content panel's box: mobile-toc + bar + panel padding (2 × 1.5rem) + h1 margin (1rem) + one h1 line + (--sl-text-6xl × 1.2 line-height), minus the sidebar's own 1rem top + padding. Applies wherever the sidebar is visible. */ +@media (min-width: 50rem) { + .starlight-sidebar-topics { + display: flex; + flex-direction: column; + /* Measured offset set by Head.astro (no static fallback — it would act + as a floor and defeat the natural-height measurement; without JS the + two rules are simply compact and unaligned). */ + min-height: var(--topics-rule-offset, auto); + } + + .starlight-sidebar-topics::after { + margin-top: auto; + } + + /* When the title panel grows to meet a taller topics column, anchor the + title to the bottom of the available space, just above the shared rule */ + main .content-panel:first-of-type { + display: flex; + flex-direction: column; + justify-content: flex-end; + + /* As a flex item the inner container would otherwise shrink to fit and + its auto margins would center it */ + & > .sl-container { + width: 100%; + } + } +} + +/* Tablet only: drop the title panel's top padding and the h1's default top + margin — they only add dead space above the title and force the topics + list to stretch further to keep the shared rule aligned. Desktop keeps + its larger title spacing. */ +@media (min-width: 50rem) and (max-width: 71.9375rem) { + main .content-panel:first-of-type { + padding-block-start: 0; + } + + main h1#_top { + margin-top: 0; + } +} diff --git a/src/styles/tint.css b/src/styles/tint.css index 11c32f2..199296a 100644 --- a/src/styles/tint.css +++ b/src/styles/tint.css @@ -130,6 +130,14 @@ --space-card-gap: var(--space-400); --space-card-padding: var(--space-400); --space-cell-padding: var(--space-150); + + /* bomb.sh page chrome: page background + viewport frame grid */ + --background: var(--color-white-100); + --c-grid: var(--color-gray-30); + --c-grid-muted: var(--color-gray-20); + --s-grid-width: 1px; + --s-grid-padding: 8px; + --panel-grad-start: var(--color-gray-20); } :root[data-theme=dark] { @@ -148,6 +156,11 @@ --accent-fill: var(--color-pink-100); --accent-muted: var(--color-pink-90); --accent-highlight: var(--color-pink-100); + + --background: #000; + --c-grid: var(--color-gray-80); + --c-grid-muted: var(--color-gray-90); + --panel-grad-start: var(--color-gray-90); } :root[data-theme=light] {