- |
+ |
{form.type}
|
-
+ |
{form.fields
?.filter((field) => field.description || field.default)
@@ -147,16 +153,6 @@ function typeText(row: ConfigRow): string {
diff --git a/site/src/components/SiteTitle.astro b/site/src/components/SiteTitle.astro
index 83b8bb2..852da44 100644
--- a/site/src/components/SiteTitle.astro
+++ b/site/src/components/SiteTitle.astro
@@ -1,13 +1,17 @@
---
-import Default from '@astrojs/starlight/components/SiteTitle.astro';
-// The version is the root package's, read at build time, so the pill always
-// names the release this build documents. It links to the changelog.
-import pkg from '../../../package.json';
+/**
+ * The site title as the noctcore wordmark, then the product: "noctcore /
+ * showcase-kit". base.css draws the crescent rule under the wordmark and
+ * components.css colours the separator and the product.
+ */
+import mark from '../assets/mark.svg';
-const changelog = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/changelog/`;
+const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
---
-
+
+
+ noctcore
+ /
+ {siteTitle}
+
diff --git a/site/src/components/SocialIcons.astro b/site/src/components/SocialIcons.astro
new file mode 100644
index 0000000..0cc8b3d
--- /dev/null
+++ b/site/src/components/SocialIcons.astro
@@ -0,0 +1,15 @@
+---
+/**
+ * The version pill before the header's social icons. The version is the root
+ * package's, read at build time, so the pill always names the release this
+ * build documents. It links to the changelog.
+ */
+import Default from '@astrojs/starlight/components/SocialIcons.astro';
+
+import pkg from '../../../package.json';
+
+const changelog = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/changelog/`;
+---
+
+v{pkg.version}, see the changelog
+
diff --git a/site/src/styles/noctcore/base.css b/site/src/styles/noctcore/base.css
new file mode 100644
index 0000000..e723b46
--- /dev/null
+++ b/site/src/styles/noctcore/base.css
@@ -0,0 +1,140 @@
+/* noctcore docs base.css
+ The token contract. Every preset sets the same --nc-* tokens; this file
+ maps them onto Starlight 0.42's --sl-* variables once, so a preset never
+ names a Starlight variable for colour. Starlight's own CSS sits in cascade
+ layers and customCss is unlayered, so these rules win without !important.
+ The brand constants are decorative only (the crescent rule, the hero
+ button, the wordmark): never body text. Load first in customCss. */
+
+:root,
+::backdrop {
+ /* Brand constants */
+ --nc-violet: #8b7cff;
+ --nc-cyan: #4dd0e1;
+ --nc-night: #0b1020;
+ --nc-gradient: linear-gradient(90deg, var(--nc-violet), var(--nc-cyan));
+
+ /* Type and measure */
+ --sl-font: var(--nc-font-body);
+ --sl-font-mono: var(--nc-font-mono);
+ --sl-content-width: var(--nc-measure);
+
+ /* Starlight's grey ramp is named for dark mode; the --nc names say what each step does */
+ --sl-color-white: var(--nc-ink);
+ --sl-color-gray-1: var(--nc-ink-2);
+ --sl-color-gray-2: var(--nc-text);
+ --sl-color-gray-3: var(--nc-muted);
+ --sl-color-gray-4: var(--nc-faint);
+ --sl-color-gray-5: var(--nc-line);
+ --sl-color-gray-6: var(--nc-surface);
+ --sl-color-gray-7: var(--nc-raised);
+ --sl-color-black: var(--nc-bg);
+
+ /* Accent */
+ --sl-color-accent-low: var(--nc-accent-soft);
+ --sl-color-accent: var(--nc-accent);
+ --sl-color-accent-high: var(--nc-accent-strong);
+
+ /* Aliases Starlight switches per mode. Mapped once here, so the presets hold all the mode logic */
+ --sl-color-text: var(--nc-text);
+ --sl-color-text-accent: var(--nc-link);
+ --sl-color-text-invert: var(--nc-on-link);
+ --sl-color-bg: var(--nc-bg);
+ --sl-color-bg-nav: var(--nc-surface);
+ --sl-color-bg-sidebar: var(--nc-surface);
+ --sl-color-bg-inline-code: var(--nc-code-inline);
+ --sl-color-bg-accent: var(--nc-link);
+ --sl-color-hairline-light: var(--nc-line);
+ --sl-color-hairline: var(--nc-line-soft);
+ --sl-color-hairline-shade: var(--nc-bg);
+
+ /* Signals: Starlight hues by role (blue note, purple tip, orange caution, red danger, green success) */
+ --sl-color-blue-low: var(--nc-note-bg);
+ --sl-color-blue: var(--nc-note-edge);
+ --sl-color-blue-high: var(--nc-note-ink);
+ --sl-color-purple-low: var(--nc-tip-bg);
+ --sl-color-purple: var(--nc-tip-edge);
+ --sl-color-purple-high: var(--nc-tip-ink);
+ --sl-color-orange-low: var(--nc-caution-bg);
+ --sl-color-orange: var(--nc-caution-edge);
+ --sl-color-orange-high: var(--nc-caution-ink);
+ --sl-color-red-low: var(--nc-danger-bg);
+ --sl-color-red: var(--nc-danger-edge);
+ --sl-color-red-high: var(--nc-danger-ink);
+ --sl-color-green-low: var(--nc-success-bg);
+ --sl-color-green: var(--nc-success-edge);
+ --sl-color-green-high: var(--nc-success-ink);
+}
+
+/* Shared structure: the wordmark, the crescent rule, the hero button, radius, focus. */
+.site-title {
+ font-family: var(--sl-font-mono);
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: var(--sl-color-white);
+}
+
+.nc-wordmark {
+ position: relative;
+}
+
+.nc-wordmark::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ bottom: -0.3rem;
+ width: 1.75rem;
+ height: 2px;
+ border-radius: 2px;
+ background: var(--nc-gradient);
+}
+
+h1#_top,
+.hero h1,
+.sl-markdown-content h2,
+.sl-markdown-content h3 {
+ font-family: var(--nc-font-display);
+}
+
+.hero h1::after {
+ content: '';
+ display: block;
+ width: 6rem;
+ height: 0.25rem;
+ margin-top: 1rem;
+ border-radius: 999px;
+ background: var(--nc-gradient);
+}
+
+@media (max-width: 49.99rem) {
+ .hero h1::after {
+ margin-inline: auto;
+ }
+}
+
+.hero .sl-link-button.primary {
+ background: var(--nc-gradient);
+ border-color: transparent;
+ color: var(--nc-night);
+}
+
+.starlight-aside,
+.sl-link-card,
+.card,
+.pagination-links a {
+ border-radius: var(--nc-radius);
+}
+
+.expressive-code {
+ --ec-brdRad: var(--nc-radius);
+}
+
+:focus-visible {
+ outline: 2px solid var(--sl-color-accent);
+ outline-offset: 2px;
+}
+
+.expressive-code pre,
+code {
+ font-variant-ligatures: none;
+}
diff --git a/site/src/styles/noctcore/components.css b/site/src/styles/noctcore/components.css
new file mode 100644
index 0000000..c7cc6d3
--- /dev/null
+++ b/site/src/styles/noctcore/components.css
@@ -0,0 +1,787 @@
+/* noctcore docs components.css
+ The site pieces the noctcore docs sites share. Every colour comes from
+ Starlight's --sl-* variables or a preset's --nc-* tokens, so each piece
+ follows both presets in dark and light. No bare element selectors: it
+ only touches the classes below and Starlight's own component classes.
+
+ Load order in customCss: base.css, components.css, presets/.css,
+ then the site's own CSS. A preset comes after this file so it can tune a
+ component (Observatory draws link cards as ruled rows).
+ Markup for each piece is in COMPONENTS.md. */
+
+/* ---------------------------------------------------------------------------
+ Labels. The small line over facts, stats and stacked table cells. Presets
+ set the look through --nc-label-* (mono capitals for the atlas presets).
+--------------------------------------------------------------------------- */
+.nc-label,
+.nc-facts dt,
+.nc-stats dt,
+.package-card-version {
+ font-family: var(--nc-label-font);
+ font-size: var(--nc-label-size);
+ font-weight: var(--nc-label-weight);
+ letter-spacing: var(--nc-label-tracking);
+ text-transform: var(--nc-label-case);
+ line-height: 1.3;
+ color: var(--sl-color-gray-3);
+}
+
+/* A page's opening paragraph: the showcase-kit lead, and the rule summary that
+ sync.ts writes where the doc has its "> summary" blockquote. */
+.sl-markdown-content .nc-lead,
+.nc-lead {
+ font-size: var(--sl-text-lg);
+ line-height: 1.6;
+ color: var(--sl-color-gray-2);
+ text-wrap: pretty;
+}
+
+/* Starlight styles inline code only inside the Markdown body; a lead can sit above it. */
+.nc-lead code {
+ padding: 0.125rem 0.375rem;
+ background-color: var(--sl-color-bg-inline-code);
+ font-family: var(--__sl-font-mono);
+ font-size: 0.875em;
+}
+
+/* ---------------------------------------------------------------------------
+ Site title: the wordmark (its crescent rule is in base.css), then the
+ product. Markup in COMPONENTS.md (a SiteTitle override).
+--------------------------------------------------------------------------- */
+.site-title .nc-sep {
+ color: var(--sl-color-gray-4);
+ font-weight: 400;
+}
+
+.site-title .nc-product {
+ overflow: hidden;
+ color: var(--sl-color-gray-2);
+ font-weight: 500;
+ text-overflow: ellipsis;
+}
+
+/* A PageTitle override drops Starlight's scoped h1 styles; these are the same
+ values, at zero specificity so a preset can still restyle the title. */
+:where(.sl-container > h1#_top) {
+ margin-top: 1rem;
+ font-size: var(--sl-text-h1);
+ line-height: var(--sl-line-height-headings);
+ font-weight: 600;
+ color: var(--sl-color-white);
+}
+
+/* ---------------------------------------------------------------------------
+ Version pill (showcase-kit header, links to the changelog).
+--------------------------------------------------------------------------- */
+.nc-version {
+ display: inline-flex;
+ align-items: center;
+ height: 1.6rem;
+ padding-inline: 0.55rem;
+ border-radius: calc(var(--nc-radius) * 2 + 0.25rem);
+ font: 600 0.75rem/1 var(--__sl-font-mono);
+ color: var(--sl-color-text-accent);
+ background: var(--sl-color-accent-low);
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.nc-version:hover {
+ color: var(--sl-color-white);
+}
+
+/* ---------------------------------------------------------------------------
+ Window-frame figure: a raw capture in the frame showcase-kit draws, on the
+ preset's --nc-motif backdrop. Sizes are relative to the frame's width, so it
+ scales like the rendered image. Set --nc-win on .nc-window when a window is
+ narrower than its frame (the hero stack uses 74).
+--------------------------------------------------------------------------- */
+.nc-shot {
+ margin: 0;
+}
+
+.nc-shot figcaption {
+ margin-top: 0.75rem;
+ font-size: var(--sl-text-sm);
+ line-height: 1.55;
+ color: var(--sl-color-gray-3);
+}
+
+.nc-frame {
+ container-type: inline-size;
+ padding: 4.5%;
+ border-radius: calc(var(--nc-radius) * 1.25);
+ background: var(--nc-motif);
+}
+
+.nc-window {
+ --nc-u: calc(var(--nc-win, 100) * 0.01cqi);
+ position: relative;
+ overflow: hidden;
+ border-radius: calc(var(--nc-u) * 0.97);
+ background: #0f1017;
+ box-shadow:
+ 0 calc(var(--nc-u) * 2.4) calc(var(--nc-u) * 5) calc(var(--nc-u) * -1) rgba(2, 4, 12, 0.65),
+ 0 0 0 1px rgba(255, 255, 255, 0.07);
+}
+
+.nc-window > img {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+/* The title bar showcase-kit draws with frame.theme 'dark'. */
+.nc-titlebar {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: calc(var(--nc-u) * 2.78);
+ background: #1b1c26;
+ border-bottom: 1px solid #0a0a10;
+}
+
+.nc-lights {
+ position: absolute;
+ top: 50%;
+ left: var(--nc-u);
+ display: flex;
+ gap: calc(var(--nc-u) * 0.55);
+ transform: translateY(-50%);
+}
+
+.nc-lights i {
+ width: calc(var(--nc-u) * 0.85);
+ height: calc(var(--nc-u) * 0.85);
+ border-radius: 50%;
+ background: #ff5f57;
+}
+
+.nc-lights i:nth-child(2) {
+ background: #febc2e;
+}
+
+.nc-lights i:nth-child(3) {
+ background: #28c840;
+}
+
+.nc-wtitle {
+ font: 500 max(calc(var(--nc-u) * 0.95), 5px)/1 var(--sl-font-system, system-ui, sans-serif);
+ color: #a3a6b8;
+}
+
+/* The hero stack: what `showcase hero` composes, minus its text. */
+.nc-banner {
+ position: relative;
+ container-type: inline-size;
+ width: 100%;
+ max-width: 34rem;
+ aspect-ratio: 5 / 4;
+ margin-inline: auto;
+ overflow: hidden;
+ border-radius: calc(var(--nc-radius) * 1.5);
+ background: var(--nc-motif);
+}
+
+.nc-banner .nc-window {
+ --nc-win: 74;
+ position: absolute;
+ width: 74%;
+}
+
+.nc-banner .nc-window:first-child {
+ top: 8%;
+ left: 4%;
+ transform: rotate(-5deg);
+}
+
+.nc-banner .nc-window:last-child {
+ right: 3%;
+ bottom: 9%;
+ transform: rotate(3.5deg);
+}
+
+/* ---------------------------------------------------------------------------
+ Cards. Starlight's LinkCard and Card, and the ESLint package cards, share one
+ surface: --nc-card-bg and --nc-card-border from the preset.
+--------------------------------------------------------------------------- */
+.sl-link-card,
+.card {
+ background-color: var(--nc-card-bg);
+ border-color: var(--nc-card-border);
+}
+
+.package-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
+ gap: 0.75rem;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.package-grid .package-card {
+ margin: 0;
+}
+
+.package-card a {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 0.45rem;
+ height: 100%;
+ padding: 1rem 1.1rem 0.95rem;
+ overflow: hidden;
+ border: 1px solid var(--nc-card-border);
+ border-radius: var(--nc-radius);
+ background: var(--nc-card-bg);
+ color: var(--sl-color-gray-2);
+ text-decoration: none;
+}
+
+.package-card a::before {
+ content: '';
+ position: absolute;
+ inset: 0 0 auto;
+ height: 2px;
+ background: var(--nc-gradient);
+ opacity: 0;
+}
+
+.package-card a:hover,
+.package-card a:focus-visible {
+ border-color: var(--sl-color-accent);
+}
+
+.package-card a:hover::before,
+.package-card a:focus-visible::before {
+ opacity: 1;
+}
+
+.package-card-head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.75rem;
+}
+
+.package-card-short {
+ font-family: var(--nc-font-display);
+ font-size: var(--sl-text-lg);
+ font-weight: 600;
+ line-height: 1.2;
+ color: var(--sl-color-white);
+}
+
+.package-card-name {
+ font: 500 var(--sl-text-2xs)/1.4 var(--__sl-font-mono);
+ color: var(--sl-color-gray-3);
+ overflow-wrap: anywhere;
+}
+
+.package-card-desc {
+ font-size: var(--sl-text-sm);
+ line-height: 1.5;
+}
+
+.package-card-meta {
+ font-size: var(--sl-text-xs);
+ font-variant-numeric: tabular-nums;
+ color: var(--sl-color-gray-3);
+}
+
+/* How a package's rules split across its preset: on, registered off, not listed. */
+.nc-split {
+ display: flex;
+ gap: 2px;
+ height: 0.3125rem;
+ margin-top: auto;
+ overflow: hidden;
+ border-radius: 999px;
+}
+
+.nc-split i {
+ min-width: 3px;
+ background: var(--sl-color-gray-5);
+}
+
+.nc-split .on {
+ background: var(--sl-color-green);
+}
+
+.nc-split .off {
+ background: var(--sl-color-gray-4);
+}
+
+.nc-split .harness {
+ background: var(--sl-color-accent);
+}
+
+/* ---------------------------------------------------------------------------
+ Badges. Rule state, as scannable chips: the preset severity, autofix,
+ suggestions, required options, type information, deprecated. The .pill
+ classes the ESLint site's tables already emit get the same look.
+--------------------------------------------------------------------------- */
+:is(.nc-badge, .pill) {
+ --nc-badge-bg: transparent;
+ --nc-badge-ink: var(--sl-color-gray-2);
+ --nc-badge-edge: var(--sl-color-gray-4);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3em;
+ padding: 0.0625rem 0.45rem;
+ border: 1px solid color-mix(in srgb, var(--nc-badge-edge) 55%, transparent);
+ border-radius: calc(var(--nc-radius) * 0.5 + 0.1875rem);
+ background: var(--nc-badge-bg);
+ color: var(--nc-badge-ink);
+ font: 600 var(--sl-text-2xs)/1.5 var(--__sl-font-mono);
+ white-space: nowrap;
+}
+
+:is(.nc-badge--on, .pill-error) {
+ --nc-badge-bg: var(--sl-color-green-low);
+ --nc-badge-ink: var(--sl-color-green-high);
+ --nc-badge-edge: var(--sl-color-green);
+}
+
+:is(.nc-badge--off, .pill-off) {
+ --nc-badge-ink: var(--sl-color-gray-2);
+ --nc-badge-edge: var(--sl-color-gray-4);
+}
+
+:is(.nc-badge--out, .pill-not-listed) {
+ --nc-badge-ink: var(--sl-color-gray-3);
+ border-style: dashed;
+}
+
+:is(.nc-badge--fix) {
+ --nc-badge-bg: var(--sl-color-purple-low);
+ --nc-badge-ink: var(--sl-color-purple-high);
+ --nc-badge-edge: var(--sl-color-purple);
+}
+
+:is(.nc-badge--suggest) {
+ --nc-badge-bg: var(--sl-color-blue-low);
+ --nc-badge-ink: var(--sl-color-blue-high);
+ --nc-badge-edge: var(--sl-color-blue);
+}
+
+:is(.nc-badge--options) {
+ --nc-badge-bg: var(--sl-color-orange-low);
+ --nc-badge-ink: var(--sl-color-orange-high);
+ --nc-badge-edge: var(--sl-color-orange);
+}
+
+:is(.nc-badge--types) {
+ --nc-badge-bg: var(--sl-color-blue-low);
+ --nc-badge-ink: var(--sl-color-blue-high);
+ --nc-badge-edge: var(--sl-color-blue);
+}
+
+:is(.nc-badge--harness, .pill-harness) {
+ --nc-badge-bg: var(--sl-color-accent-low);
+ --nc-badge-ink: var(--sl-color-text-accent);
+ --nc-badge-edge: var(--sl-color-accent);
+}
+
+:is(.nc-badge--deprecated, .pill-deprecated) {
+ --nc-badge-bg: var(--sl-color-red-low);
+ --nc-badge-ink: var(--sl-color-red-high);
+ --nc-badge-edge: var(--sl-color-red);
+}
+
+/* "opt-in" beside an off or not-listed preset: the rule is yours to turn on. */
+.nc-optin {
+ margin-inline-start: 0.35rem;
+ font-size: var(--sl-text-2xs);
+ color: var(--sl-color-gray-3);
+ white-space: nowrap;
+}
+
+.deprecated-note {
+ display: block;
+ margin-top: 0.25rem;
+ font-size: var(--sl-text-xs);
+}
+
+/* ---------------------------------------------------------------------------
+ Rule facts: the strip under a rule's title. sync.ts writes it in place of
+ the "Recommended preset: ... · Autofix: ..." line. Same six cells on every
+ rule page, so the eye finds a fact in the same place each time.
+--------------------------------------------------------------------------- */
+.sl-markdown-content .nc-facts,
+.nc-facts {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(7.25rem, 1fr));
+ margin-inline: 0;
+ border-block: 1px solid var(--sl-color-hairline-light);
+}
+
+.nc-facts > div {
+ display: grid;
+ align-content: start;
+ gap: 0.3rem;
+ padding: 0.7rem 0.9rem 0.8rem 0;
+}
+
+.nc-facts dt {
+ margin: 0;
+}
+
+.nc-facts dd {
+ margin: 0;
+ padding: 0;
+ font-size: var(--sl-text-sm);
+ line-height: 1.4;
+ color: var(--sl-color-white);
+}
+
+.nc-facts dd.is-no {
+ color: var(--sl-color-gray-3);
+}
+
+.nc-facts dd code {
+ font-size: var(--sl-text-xs);
+}
+
+.nc-facts-note {
+ margin-inline-start: 0.35rem;
+ white-space: nowrap;
+ font: 500 var(--sl-text-2xs) var(--__sl-font-mono);
+ color: var(--sl-color-gray-3);
+}
+
+/* A rule title reads as namespace plus name; the PageTitle override splits it. */
+.nc-title-ns {
+ display: block;
+ margin-bottom: 0.2em;
+ font: 500 max(0.875rem, 0.36em)/1.3 var(--__sl-font-mono);
+ letter-spacing: 0;
+ color: var(--sl-color-gray-3);
+}
+
+/* ---------------------------------------------------------------------------
+ Incorrect and Correct examples. ec.config.mjs puts .verdict-bad or
+ .verdict-good on the frame. Each frame restyles itself by setting Expressive
+ Code's own variables: a tinted title bar, a coloured frame edge, a verdict
+ mark before the title. Three signals, never colour alone.
+--------------------------------------------------------------------------- */
+.expressive-code .verdict-bad {
+ --nc-verdict: var(--sl-color-red);
+ --nc-verdict-bg: var(--sl-color-red-low);
+ --nc-verdict-ink: var(--sl-color-red-high);
+ --nc-verdict-mark: '\2715';
+}
+
+.expressive-code .verdict-good {
+ --nc-verdict: var(--sl-color-green);
+ --nc-verdict-bg: var(--sl-color-green-low);
+ --nc-verdict-ink: var(--sl-color-green-high);
+ --nc-verdict-mark: '\2713';
+}
+
+.expressive-code :is(.verdict-bad, .verdict-good) {
+ --ec-brdCol: color-mix(in srgb, var(--nc-verdict) 55%, transparent);
+ --ec-frm-edTabBarBg: var(--nc-verdict-bg);
+ --ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--nc-verdict) 55%, transparent);
+ --ec-frm-edActTabBg: transparent;
+ --ec-frm-edActTabFg: var(--nc-verdict-ink);
+ --ec-frm-edActTabIndTopCol: transparent;
+}
+
+.expressive-code :is(.verdict-bad, .verdict-good) .header .title {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ font-weight: 600;
+}
+
+.expressive-code :is(.verdict-bad, .verdict-good) .header .title::before {
+ content: var(--nc-verdict-mark);
+ display: inline-grid;
+ place-items: center;
+ width: 1.2rem;
+ height: 1.2rem;
+ border-radius: 50%;
+ background: var(--nc-verdict);
+ color: var(--sl-color-black);
+ font-size: 0.6875rem;
+ font-weight: 800;
+}
+
+.expressive-code :is(.verdict-bad, .verdict-good) pre {
+ box-shadow: inset 3px 0 0 var(--nc-verdict);
+}
+
+.expressive-code :is(.verdict-bad, .verdict-good) + * {
+ margin-top: 0.75rem;
+}
+
+/* ---------------------------------------------------------------------------
+ Tables. The ESLint rule tables and the showcase-kit config reference. Wide
+ screens get a dense table; below 40rem each row becomes a stacked entry
+ (name, text, then the badges), labelled from data-label on each cell.
+--------------------------------------------------------------------------- */
+.rule-table-wrap {
+ overflow-x: auto;
+}
+
+.sl-markdown-content :is(.rule-table, .nc-ref) {
+ display: table;
+ width: 100%;
+ font-size: var(--sl-text-sm);
+ border-collapse: collapse;
+}
+
+.sl-markdown-content :is(.rule-table, .nc-ref) :is(th, td) {
+ vertical-align: top;
+}
+
+.sl-markdown-content :is(.rule-table, .nc-ref) thead th {
+ font-family: var(--nc-label-font);
+ font-size: var(--nc-label-size);
+ font-weight: var(--nc-label-weight);
+ letter-spacing: var(--nc-label-tracking);
+ text-transform: var(--nc-label-case);
+ color: var(--sl-color-gray-3);
+ white-space: nowrap;
+}
+
+/* The all-rules index has no table of contents, so it takes a wider column. */
+main:has(.rule-table-all) {
+ --sl-content-width: 68rem;
+}
+
+.sl-markdown-content .rule-table td:first-child {
+ width: 30%;
+}
+
+.sl-markdown-content .rule-table td:first-child code {
+ overflow-wrap: break-word;
+ background: none;
+ padding: 0;
+ font-weight: 600;
+}
+
+/* A rule id as two lines: the namespace, quiet, over the rule's name. Both stay
+ in one code element, so find-in-page still matches the full id. */
+.nc-id-ns {
+ display: block;
+ font-size: 0.85em;
+ font-weight: 400;
+ color: var(--sl-color-gray-3);
+}
+
+/* A package's rows start with a group row: the package name and its count. */
+.sl-markdown-content .rule-table .nc-group th {
+ padding-top: 1.4rem;
+ border-bottom: 1px solid var(--sl-color-gray-4);
+ font-family: var(--nc-font-display);
+ font-size: var(--sl-text-base);
+ font-weight: 600;
+ color: var(--sl-color-white);
+ text-align: start;
+}
+
+.nc-group-count {
+ margin-inline-start: 0.5rem;
+ font: 500 var(--sl-text-2xs) var(--__sl-font-mono);
+ color: var(--sl-color-gray-3);
+}
+
+.sl-markdown-content .nc-ref td:first-child,
+.sl-markdown-content .nc-ref td:last-child {
+ white-space: nowrap;
+}
+
+.sl-markdown-content .nc-ref .sl-badge + .sl-badge {
+ margin-inline-start: 0.25rem;
+}
+
+@media (max-width: 40rem) {
+ .rule-table-wrap {
+ overflow: visible;
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref),
+ .sl-markdown-content :is(.rule-table, .nc-ref) :is(tbody, tr) {
+ display: block;
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) thead {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) tbody tr {
+ padding-block: 0.8rem;
+ border-bottom: 1px solid var(--sl-color-gray-5);
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) td {
+ display: block;
+ width: auto !important;
+ padding: 0;
+ border: 0;
+ white-space: normal;
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) td + td {
+ margin-top: 0.3rem;
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) td:nth-child(n + 3) {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ margin: 0.5rem 0.9rem 0 0;
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) td:nth-child(n + 3):empty {
+ display: none;
+ }
+
+ .sl-markdown-content :is(.rule-table, .nc-ref) td[data-label]:nth-child(n + 3)::before {
+ content: attr(data-label);
+ font-family: var(--nc-label-font);
+ font-size: var(--nc-label-size);
+ font-weight: var(--nc-label-weight);
+ letter-spacing: var(--nc-label-tracking);
+ text-transform: var(--nc-label-case);
+ color: var(--sl-color-gray-3);
+ }
+
+ .sl-markdown-content .rule-table .nc-group {
+ display: block;
+ border: 0;
+ }
+
+ .sl-markdown-content .rule-table .nc-group th {
+ display: block;
+ padding-inline: 0;
+ }
+}
+
+.rule-table-legend,
+.quickstart-note,
+.site-footer-note {
+ font-size: var(--sl-text-sm);
+ color: var(--sl-color-gray-3);
+}
+
+/* Summary numbers over the all-rules index. */
+.sl-markdown-content .nc-stats,
+.nc-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
+ gap: 1px;
+ margin-inline: 0;
+ overflow: hidden;
+ border: 1px solid var(--nc-card-border);
+ border-radius: var(--nc-radius);
+ background: var(--nc-card-border);
+}
+
+.nc-stats > div {
+ display: grid;
+ gap: 0.15rem;
+ padding: 0.75rem 0.9rem;
+ background: var(--nc-card-bg);
+}
+
+.nc-stats dd {
+ order: -1;
+ margin: 0;
+ font-family: var(--nc-font-display);
+ font-size: var(--sl-text-2xl);
+ font-weight: 600;
+ line-height: 1.1;
+ font-variant-numeric: tabular-nums;
+ color: var(--sl-color-white);
+}
+
+/* ---------------------------------------------------------------------------
+ Tabs with many entries (the twelve quick-start tabs) wrap into a row of
+ chips instead of scrolling sideways past the fold.
+--------------------------------------------------------------------------- */
+starlight-tabs:has(.tab:nth-child(6)) [role='tablist'] {
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--sl-color-hairline-light);
+}
+
+starlight-tabs:has(.tab:nth-child(6)) .tab {
+ margin-bottom: 0;
+}
+
+starlight-tabs:has(.tab:nth-child(6)) .tab > [role='tab'] {
+ padding: 0.25rem 0.7rem;
+ border: 1px solid var(--sl-color-gray-5);
+ border-radius: calc(var(--nc-radius) * 2 + 0.25rem);
+ font-family: var(--__sl-font-mono);
+ font-size: var(--sl-text-xs);
+}
+
+starlight-tabs:has(.tab:nth-child(6)) .tab > [role='tab'][aria-selected='true'] {
+ border-color: var(--sl-color-text-accent);
+ background: var(--sl-color-accent-low);
+ color: var(--sl-color-white);
+}
+
+/* ---------------------------------------------------------------------------
+ Who a site is for: the two landing-page lists side by side.
+--------------------------------------------------------------------------- */
+.sl-markdown-content .nc-fit,
+.nc-fit {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
+ gap: 0.75rem;
+}
+
+.nc-fit > div {
+ padding: 1rem 1.1rem;
+ border: 1px solid var(--nc-card-border);
+ border-top: 2px solid var(--sl-color-green);
+ border-radius: var(--nc-radius);
+ background: var(--nc-card-bg);
+ font-size: var(--sl-text-sm);
+}
+
+.nc-fit > div + div {
+ border-top-color: var(--sl-color-orange);
+}
+
+.nc-fit > div > p + p {
+ margin-top: 0.5rem;
+}
+
+/* ---------------------------------------------------------------------------
+ Changelog: a release's version and date as one line.
+--------------------------------------------------------------------------- */
+.sl-markdown-content .nc-release-meta,
+.nc-release-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 0.25rem 0.75rem;
+ font-family: var(--__sl-font-mono);
+ font-size: var(--sl-text-sm);
+ color: var(--sl-color-gray-3);
+}
+
+.nc-release-meta b {
+ font-size: var(--sl-text-lg);
+ font-weight: 600;
+ color: var(--sl-color-white);
+}
+
+.nc-sha {
+ font-family: var(--__sl-font-mono);
+ font-size: var(--sl-text-xs);
+ white-space: nowrap;
+}
diff --git a/site/src/styles/noctcore/expressive-code.mjs b/site/src/styles/noctcore/expressive-code.mjs
new file mode 100644
index 0000000..6d38683
--- /dev/null
+++ b/site/src/styles/noctcore/expressive-code.mjs
@@ -0,0 +1,102 @@
+/* noctcore docs: Expressive Code settings for the Observatory preset.
+
+ // ec.config.mjs
+ import { defineEcConfig } from '@astrojs/starlight/expressive-code';
+ import { noctcoreCodeConfig } from './src/styles/noctcore/expressive-code.mjs';
+
+ export default defineEcConfig({
+ ...noctcoreCodeConfig('observatory'),
+ plugins: [],
+ });
+
+ What noctcoreCodeConfig sets, and why:
+ - themes: a dark and a light theme built from the preset's --nc-code-ink and
+ --nc-syn-* values. Starlight switches between them with its own theme
+ picker, because one is type 'dark' and the other 'light'.
+ - useStarlightUiThemeColors: true, so title bars, tabs and borders follow the
+ preset through Starlight's --sl-* variables. Starlight turns this off by
+ default once a site passes its own themes.
+ - customizeTheme: puts the code area on the preset's --nc-code-bg. Starlight
+ would otherwise use --sl-color-gray-6 in dark mode and -7 in light.
+ - minSyntaxHighlightingColorContrast: 0. By default Expressive Code nudges
+ token colours toward 5.5:1 against a fixed grey it uses for that sum, not
+ against the real code background. These colours are checked against the
+ real background instead: 4.5:1 or more, see contrast.md.
+ Keep these values in step with presets/.css when either changes. */
+import { ExpressiveCodeTheme } from '@astrojs/starlight/expressive-code';
+
+const SYNTAX = {
+ "observatory": {
+ "dark": {
+ "bg": "#0f1526",
+ "ink": "#d9deeb",
+ "keyword": "#c1b8ff",
+ "string": "#92e0e9",
+ "number": "#f0c98f",
+ "function": "#a6e6ee",
+ "property": "#dfe4f0",
+ "comment": "#8a93ad",
+ "punct": "#97a0b8"
+ },
+ "light": {
+ "bg": "#ffffff",
+ "ink": "#1e2745",
+ "keyword": "#4a36c4",
+ "string": "#0a6873",
+ "number": "#8a4b00",
+ "function": "#0c4e58",
+ "property": "#1e2745",
+ "comment": "#5c6682",
+ "punct": "#58617c"
+ }
+ }
+};
+
+/* TextMate scopes per token role, for the TypeScript, JavaScript, shell and
+ JSON grammars the docs use. */
+const SCOPES = [
+ ['comment', ['comment', 'punctuation.definition.comment'], 'italic'],
+ ['keyword', ['keyword', 'storage', 'storage.type', 'storage.modifier', 'keyword.control', 'constant.language', 'variable.language']],
+ ['string', ['string', 'string.template', 'punctuation.definition.string', 'punctuation.definition.template-expression']],
+ ['number', ['constant.numeric', 'constant.other']],
+ ['function', ['entity.name.function', 'support.function', 'meta.function-call entity.name.function', 'entity.name.type', 'support.type', 'support.class', 'entity.name.class', 'entity.name.command']],
+ ['property', ['variable.other.property', 'meta.object-literal.key', 'support.type.property-name', 'entity.other.attribute-name']],
+ ['punct', ['punctuation', 'meta.brace', 'keyword.operator']],
+];
+
+function theme(preset, type) {
+ const c = SYNTAX[preset][type];
+ return new ExpressiveCodeTheme({
+ name: `noctcore-${preset}-${type}`,
+ type,
+ colors: { 'editor.background': c.bg, 'editor.foreground': c.ink },
+ tokenColors: SCOPES.map(([key, scope, fontStyle]) => ({
+ scope,
+ settings: fontStyle ? { foreground: c[key], fontStyle } : { foreground: c[key] },
+ })),
+ });
+}
+
+/** The dark and light themes for one preset. */
+export function noctcoreCodeThemes(preset = 'observatory') {
+ if (!SYNTAX[preset]) throw new Error(`Unknown noctcore preset "${preset}". Use one of: ${Object.keys(SYNTAX).join(', ')}.`);
+ return [theme(preset, 'dark'), theme(preset, 'light')];
+}
+
+/** Everything ec.config.mjs needs for one preset; spread it and add the site's plugins. */
+export function noctcoreCodeConfig(preset = 'observatory') {
+ return {
+ themes: noctcoreCodeThemes(preset),
+ useStarlightUiThemeColors: true,
+ minSyntaxHighlightingColorContrast: 0,
+ customizeTheme(theme) {
+ theme.styleOverrides.frames = {
+ ...theme.styleOverrides.frames,
+ editorBackground: 'var(--nc-code-bg)',
+ terminalBackground: 'var(--nc-code-bg)',
+ editorActiveTabBackground: 'var(--nc-code-bg)',
+ };
+ return theme;
+ },
+ };
+}
diff --git a/site/src/styles/noctcore/presets/observatory.css b/site/src/styles/noctcore/presets/observatory.css
new file mode 100644
index 0000000..8d71a34
--- /dev/null
+++ b/site/src/styles/noctcore/presets/observatory.css
@@ -0,0 +1,249 @@
+/* noctcore docs preset: Observatory
+ An editorial star atlas: a serif with optical sizes, graticule hairlines and one calm column, for guides you read top to bottom beside framed screenshots.
+ Type: Newsreader for headings, Newsreader for text, JetBrains Mono for code.
+ Needs: @fontsource-variable/newsreader, @fontsource-variable/jetbrains-mono.
+ Dark is the default, as in Starlight: :root holds the dark values and
+ :root[data-theme='light'] overrides them. Load after base.css and
+ components.css. Contrast: every text pair passes WCAG AA, see contrast.md. */
+@import '@fontsource-variable/newsreader/opsz.css';
+@import '@fontsource-variable/newsreader/opsz-italic.css';
+@import '@fontsource-variable/jetbrains-mono';
+
+:root,
+::backdrop {
+ /* Type, shape and labels */
+ --nc-font-display: 'Newsreader Variable', 'Newsreader', ui-serif, Georgia, serif;
+ --nc-font-body: 'Newsreader Variable', 'Newsreader', ui-serif, Georgia, serif;
+ --nc-font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
+ --nc-label-font: var(--nc-font-mono);
+ --nc-label-case: uppercase;
+ --nc-label-tracking: 0.12em;
+ --nc-label-size: 0.6875rem;
+ --nc-label-weight: 500;
+ --nc-radius: 0.125rem;
+ --nc-measure: 42rem;
+ --sl-text-body: 1.0625rem;
+ --sl-line-height: 1.7;
+ --sl-text-h1: clamp(2.5rem, 1.75rem + 2.5vw, 3.25rem);
+ --sl-text-h2: clamp(1.625rem, 1.25rem + 1.25vw, 2rem);
+
+ /* Surfaces */
+ --nc-bg: #0a0f1d;
+ --nc-surface: #0a0f1d;
+ --nc-raised: #0f1526;
+ --nc-line: #242d47;
+ --nc-line-soft: #161d33;
+
+ /* Ink */
+ --nc-ink: #f1f3fa;
+ --nc-ink-2: #dfe4f0;
+ --nc-text: #c3cadb;
+ --nc-muted: #97a0b8;
+ --nc-faint: #56607d;
+
+ /* Accent */
+ --nc-link: #8fdde8;
+ --nc-on-link: #0a0f1d;
+ --nc-accent: #a79cff;
+ --nc-accent-soft: #171a38;
+ --nc-accent-strong: #8fdde8;
+
+ /* Signals: Starlight asides and badges, the ESLint verdict frames and rule badges */
+ --nc-note-bg: #0d1a28;
+ --nc-note-edge: #4dd0e1;
+ --nc-note-ink: #a6e6ee;
+ --nc-tip-bg: #15142f;
+ --nc-tip-edge: #8b7cff;
+ --nc-tip-ink: #cdc6ff;
+ --nc-caution-bg: #1f1a10;
+ --nc-caution-edge: #e2b04e;
+ --nc-caution-ink: #f1d49a;
+ --nc-danger-bg: #231219;
+ --nc-danger-edge: #f07a7a;
+ --nc-danger-ink: #f7bcbc;
+ --nc-success-bg: #0e1f1a;
+ --nc-success-edge: #56d68f;
+ --nc-success-ink: #a6e9c3;
+
+ /* Components: cards and package cards */
+ --nc-card-bg: #0a0f1d;
+ --nc-card-border: #242d47;
+
+ /* Code: frames read these; the Expressive Code theme carries the syn-* values */
+ --nc-code-inline: #161d33;
+ --nc-code-bg: #0f1526;
+ --nc-code-ink: #d9deeb;
+ --nc-syn-keyword: #c1b8ff;
+ --nc-syn-string: #92e0e9;
+ --nc-syn-number: #f0c98f;
+ --nc-syn-function: #a6e6ee;
+ --nc-syn-property: #dfe4f0;
+ --nc-syn-comment: #8a93ad;
+ --nc-syn-punct: #97a0b8;
+
+ /* Motif: the window-frame backdrop */
+ --nc-motif:
+ radial-gradient(circle at 23% 41%, rgba(241, 243, 250, 0.85) 0 1px, transparent 1.6px) 13px 29px / 113px 97px,
+ radial-gradient(circle at 71% 13%, rgba(143, 221, 232, 0.7) 0 1px, transparent 1.6px) 41px 7px / 167px 139px,
+ radial-gradient(circle at 47% 83%, rgba(241, 243, 250, 0.55) 0 0.8px, transparent 1.3px) 5px 53px / 229px 181px,
+ radial-gradient(circle at 88% 64%, rgba(167, 156, 255, 0.6) 0 0.9px, transparent 1.4px) 71px 23px / 191px 157px,
+ linear-gradient(160deg, #1b1f4a, #0a0f1d 60%);
+}
+
+:root[data-theme='light'],
+[data-theme='light'] ::backdrop {
+ /* Surfaces */
+ --nc-bg: #f6f8fc;
+ --nc-surface: #f6f8fc;
+ --nc-raised: #ffffff;
+ --nc-line: #cdd5e5;
+ --nc-line-soft: #e3e8f2;
+
+ /* Ink */
+ --nc-ink: #0d1430;
+ --nc-ink-2: #1e2745;
+ --nc-text: #2e3752;
+ --nc-muted: #58617c;
+ --nc-faint: #8a93ad;
+
+ /* Accent */
+ --nc-link: #4034b8;
+ --nc-on-link: #ffffff;
+ --nc-accent: #5a4ad6;
+ --nc-accent-soft: #e9e7fb;
+ --nc-accent-strong: #262066;
+
+ /* Signals: Starlight asides and badges, the ESLint verdict frames and rule badges */
+ --nc-note-bg: #e6f3f6;
+ --nc-note-edge: #177f8c;
+ --nc-note-ink: #0c4e58;
+ --nc-tip-bg: #ecebfa;
+ --nc-tip-edge: #5a4ad6;
+ --nc-tip-ink: #332a8f;
+ --nc-caution-bg: #f8efdf;
+ --nc-caution-edge: #b87a10;
+ --nc-caution-ink: #6b4207;
+ --nc-danger-bg: #f9e7e6;
+ --nc-danger-edge: #c63f36;
+ --nc-danger-ink: #85211a;
+ --nc-success-bg: #e3f2e9;
+ --nc-success-edge: #1d8a50;
+ --nc-success-ink: #12532f;
+
+ /* Components: cards and package cards */
+ --nc-card-bg: #ffffff;
+ --nc-card-border: #cdd5e5;
+
+ /* Code: frames read these; the Expressive Code theme carries the syn-* values */
+ --nc-code-inline: #e8ecf5;
+ --nc-code-bg: #ffffff;
+ --nc-code-ink: #1e2745;
+ --nc-syn-keyword: #4a36c4;
+ --nc-syn-string: #0a6873;
+ --nc-syn-number: #8a4b00;
+ --nc-syn-function: #0c4e58;
+ --nc-syn-property: #1e2745;
+ --nc-syn-comment: #5c6682;
+ --nc-syn-punct: #58617c;
+
+ /* Motif: the window-frame backdrop */
+ --nc-motif:
+ radial-gradient(circle at 23% 41%, rgba(241, 243, 250, 0.85) 0 1px, transparent 1.6px) 13px 29px / 113px 97px,
+ radial-gradient(circle at 71% 13%, rgba(143, 221, 232, 0.7) 0 1px, transparent 1.6px) 41px 7px / 167px 139px,
+ radial-gradient(circle at 47% 83%, rgba(241, 243, 250, 0.55) 0 0.8px, transparent 1.3px) 5px 53px / 229px 181px,
+ radial-gradient(circle at 88% 64%, rgba(167, 156, 255, 0.6) 0 0.9px, transparent 1.4px) 71px 23px / 191px 157px,
+ linear-gradient(160deg, #262066, #0d1430 65%);
+}
+
+/* Structure */
+:root:not([data-theme='light']) body {
+ background-image:
+ linear-gradient(to bottom, transparent 0, var(--sl-color-black) 26rem),
+ radial-gradient(circle, rgba(241, 243, 250, 0.55) 0 0.5px, transparent 1px),
+ radial-gradient(circle, rgba(143, 221, 232, 0.45) 0 0.75px, transparent 1.25px);
+ background-size: 100% 100%, 173px 139px, 257px 211px;
+ background-repeat: no-repeat, repeat, repeat;
+}
+
+.page > .header {
+ border-bottom: 0;
+ background-image:
+ repeating-linear-gradient(90deg, var(--sl-color-gray-5) 0 1px, transparent 1px 2rem),
+ linear-gradient(var(--sl-color-gray-5), var(--sl-color-gray-5));
+ background-size: 100% 0.3125rem, 100% 1px;
+ background-position: left bottom;
+ background-repeat: repeat-x, no-repeat;
+}
+
+.sidebar-pane {
+ border-inline-end: 1px solid var(--sl-color-hairline-light);
+}
+
+.group-label .large,
+starlight-toc h2,
+.sl-markdown-content th {
+ font-family: var(--sl-font-mono);
+ font-size: 0.6875rem;
+ font-weight: 500;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--sl-color-gray-3);
+}
+
+.sidebar-content a[aria-current='page'] {
+ background: transparent;
+ color: var(--sl-color-white);
+ box-shadow: inset 2px 0 0 var(--sl-color-text-accent);
+ border-radius: 0;
+}
+
+h1#_top,
+.hero h1 {
+ font-weight: 400;
+ font-variation-settings: 'opsz' 72;
+ letter-spacing: -0.02em;
+}
+
+.sl-markdown-content h2 {
+ font-weight: 400;
+ font-variation-settings: 'opsz' 60;
+ padding-top: 1.75rem;
+ border-top: 1px solid var(--sl-color-hairline-light);
+}
+
+.sl-markdown-content h2[id]::before {
+ content: '\00a7\00a0' attr(id);
+ display: block;
+ margin-bottom: 0.75rem;
+ font: 500 0.6875rem/1 var(--sl-font-mono);
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--sl-color-gray-3);
+}
+
+.starlight-aside {
+ border: 1px solid var(--sl-color-asides-border);
+ border-inline-start-width: 3px;
+}
+
+.starlight-aside__title {
+ font-family: var(--sl-font-mono);
+ font-size: 0.75rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.sl-link-card {
+ background: transparent;
+ border-width: 1px 0 0;
+ border-radius: 0;
+ padding-inline: 0;
+ box-shadow: none;
+}
+
+.sl-link-card .title,
+.package-card-short {
+ font-family: var(--nc-font-display);
+ font-style: italic;
+ font-weight: 400;
+}
diff --git a/site/src/styles/site.css b/site/src/styles/site.css
new file mode 100644
index 0000000..5456b47
--- /dev/null
+++ b/site/src/styles/site.css
@@ -0,0 +1,210 @@
+/* showcase-kit docs: the site's own rules.
+ Loaded last, after the noctcore theme (noctcore/base.css, components.css
+ and presets/observatory.css), so every colour here is a Starlight --sl-*
+ variable or an --nc-* token the preset sets for dark and light. */
+
+/* ---------------------------------------------------------------------------
+ Hero. base.css puts the brand gradient on the primary action with
+ night-navy text; the hover keeps that text instead of Starlight's.
+--------------------------------------------------------------------------- */
+
+.hero .sl-link-button.primary:hover,
+.hero .sl-link-button.primary:focus-visible {
+ color: var(--nc-night);
+ filter: brightness(1.08);
+}
+
+/* The tagline is the page's opening paragraph, in the .nc-lead type. */
+.hero .tagline {
+ max-width: 38rem;
+ font-size: var(--sl-text-lg);
+ line-height: 1.6;
+ color: var(--sl-color-gray-2);
+ text-wrap: pretty;
+}
+
+/* The splash's hero banner: a real output of the kit, shown as a figure under
+ the hero rather than in its image slot, because it carries another app's
+ name. The kit already framed it, so it is a plain image, not an .nc-shot. */
+.splash-shot {
+ margin: 0 0 2.5rem;
+}
+
+.splash-shot img {
+ display: block;
+ width: 100%;
+ height: auto;
+ border: 1px solid var(--sl-color-hairline-light);
+ border-radius: calc(var(--nc-radius) * 1.25);
+}
+
+.splash-shot figcaption {
+ margin-top: 0.75rem;
+ font-size: var(--sl-text-sm);
+ line-height: 1.55;
+ color: var(--sl-color-gray-3);
+}
+
+/* ---------------------------------------------------------------------------
+ Site title. The wordmark override (SiteTitle.astro) replaces Starlight's
+ component, and with it the component's scoped layout: these are its values,
+ with a tighter gap between the wordmark, the separator and the product.
+--------------------------------------------------------------------------- */
+
+.site-title {
+ align-items: center;
+ gap: 0.5rem;
+ min-width: 0;
+ font-size: var(--sl-text-h4);
+ white-space: nowrap;
+ text-decoration: none;
+}
+
+.site-title img {
+ height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
+ width: auto;
+ margin-inline-end: 0.25rem;
+}
+
+/* Below Starlight's 50em breakpoint the header has room for the logo, the
+ search and menu buttons and about 15rem of title. The wordmark and the
+ product in monospace at h4 size are too wide for a 390px phone, base size fits. */
+@media (max-width: 50em) {
+ .site-title {
+ font-size: var(--sl-text-base);
+ }
+}
+
+/* ---------------------------------------------------------------------------
+ Tables. The guides' Markdown tables scroll sideways inside the column; at
+ phone width their last column ("Meaning") would get squeezed to a word per
+ line, so it gets a floor. The generated reference tables are .nc-ref
+ (components.css) and stack into entries instead.
+--------------------------------------------------------------------------- */
+
+@media (max-width: 40rem) {
+ .sl-markdown-content table:not(.nc-ref) td:last-child {
+ min-width: 16rem;
+ }
+}
+
+.reference-wrap {
+ overflow-x: auto;
+}
+
+/* components.css keeps an .nc-ref table's first and last columns on one line,
+ for a key and a column of badges. In the reference the last column is prose
+ (Meaning, Message) and a union type can be long: both wrap. */
+.sl-markdown-content .nc-ref td.reference-meaning,
+.sl-markdown-content .nc-ref td.reference-type {
+ white-space: normal;
+}
+
+/* Stacked, prose reads as a paragraph under its label, not as a chip beside it. */
+@media (max-width: 40rem) {
+ .sl-markdown-content .nc-ref td.reference-meaning {
+ display: block;
+ margin-inline-end: 0;
+ }
+
+ .sl-markdown-content .nc-ref td.reference-meaning[data-label]::before {
+ display: block;
+ margin-bottom: 0.2rem;
+ }
+}
+
+/* ---------------------------------------------------------------------------
+ What one config produces: the splash's cards, one per output, each linking
+ to its guide (GuideOutputs.astro). On the preset's card surface.
+--------------------------------------------------------------------------- */
+
+.sl-markdown-content .link-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
+ gap: 0.75rem;
+ padding: 0;
+ list-style: none;
+}
+
+.sl-markdown-content .link-grid .link-card {
+ margin: 0;
+}
+
+.link-card a {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ height: 100%;
+ padding: 1rem 1.1rem;
+ overflow: hidden;
+ border: 1px solid var(--nc-card-border);
+ border-radius: var(--nc-radius);
+ background: var(--nc-card-bg);
+ color: var(--sl-color-gray-2);
+ text-decoration: none;
+}
+
+.link-card a::before {
+ content: '';
+ position: absolute;
+ inset: 0 0 auto;
+ height: 2px;
+ background: var(--nc-gradient);
+ opacity: 0;
+}
+
+.link-card a:hover,
+.link-card a:focus-visible {
+ border-color: var(--sl-color-accent);
+}
+
+.link-card a:hover::before,
+.link-card a:focus-visible::before {
+ opacity: 1;
+}
+
+.link-card-name {
+ font-family: var(--nc-font-display);
+ font-size: var(--sl-text-lg);
+ line-height: 1.25;
+ color: var(--sl-color-white);
+}
+
+.link-card-desc {
+ font-size: var(--sl-text-sm);
+ line-height: 1.5;
+}
+
+.link-card-meta {
+ margin-top: auto;
+ font: 500 var(--sl-text-xs) / 1.4 var(--__sl-font-mono);
+ color: var(--sl-color-gray-3);
+}
+
+/* components.css sets the note's size and colour. */
+.site-footer-note {
+ margin-top: 1.5rem;
+}
+
+/* ---------------------------------------------------------------------------
+ Section headings. Starlight wraps each Markdown h2 in .sl-heading-wrapper
+ and sets the heading inline, so the preset's hairline over an h2 only spans
+ the heading's text. The wrapper carries it instead, across the column.
+--------------------------------------------------------------------------- */
+
+.sl-markdown-content .sl-heading-wrapper.level-h2 {
+ padding-top: 1.75rem;
+ border-top: 1px solid var(--sl-color-hairline-light);
+}
+
+.sl-markdown-content .sl-heading-wrapper.level-h2 > h2 {
+ padding-top: 0;
+ border-top: 0;
+}
+
+/* The preset sets table heads in mono capitals. Code in a head is a literal
+ (a URL, a key), so it keeps its case. */
+.sl-markdown-content th code {
+ text-transform: none;
+}
diff --git a/site/src/styles/theme.css b/site/src/styles/theme.css
deleted file mode 100644
index 23fdc33..0000000
--- a/site/src/styles/theme.css
+++ /dev/null
@@ -1,287 +0,0 @@
-/* noctcore docs theme.
- The noctcore brand: a night sky (near-black navy, not violet), a crescent moon
- that runs from violet (#8b7cff) on one limb to cyan (#4dd0e1) on the other,
- and a white wordmark over a short violet-to-cyan rule. Surfaces here are that
- navy; the gradient is the signature and cyan does the link and highlight work.
- Starlight is dark by default, so :root is the dark theme and
- :root[data-theme='light'] overrides it. */
-
-:root {
- --sl-font: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
- --sl-font-mono: 'JetBrains Mono Variable', ui-monospace, 'SFMono-Regular', monospace;
- --nc-font-display: 'Space Grotesk Variable', ui-sans-serif, system-ui, sans-serif;
-
- /* Brand constants. Decorative only (gradients, the moon): never body text. */
- --nc-violet: #8b7cff;
- --nc-cyan: #4dd0e1;
- --nc-gradient: linear-gradient(90deg, var(--nc-violet), var(--nc-cyan));
- --nc-night: #0b1020;
-
- /* Starlight maps text-accent to accent-high in dark mode: links, the
- selected sidebar item and the primary button all use it, so it is the
- brand cyan. accent (violet) drives hover borders and focus. */
- --sl-color-accent-low: #1b2450;
- --sl-color-accent: #8b7cff;
- --sl-color-accent-high: #5fd6e5;
-
- --sl-color-white: #eef2fb;
- --sl-color-gray-1: #dde3f1;
- --sl-color-gray-2: #b7c0d8;
- --sl-color-gray-3: #939fbf;
- --sl-color-gray-4: #5b6a90;
- --sl-color-gray-5: #26324f;
- --sl-color-gray-6: #141c33;
- --sl-color-gray-7: #10172b;
- --sl-color-black: #0b1020;
-
- /* Cyan as text: the card meta line. Same hue family as the links. */
- --nc-glow: #5fd6e5;
- --nc-card: #10172b;
- --nc-card-hover: #161f3a;
-
- --sl-content-width: 50rem;
-}
-
-:root[data-theme='light'] {
- /* Light mode is genuinely light: white paper, navy ink, the violet darkened
- until it holds 4.5:1 as link text, cyan deepened to teal for the same
- reason. The gradient constants stay the brand hexes because they only
- ever sit under navy text or on their own. */
- --sl-color-accent-low: #e7e5ff;
- --sl-color-accent: #5a4ad6;
- --sl-color-accent-high: #2a2470;
-
- --sl-color-white: #0b1020;
- --sl-color-gray-1: #232c44;
- --sl-color-gray-2: #3a4560;
- --sl-color-gray-3: #5a6684;
- --sl-color-gray-4: #7b86a4;
- --sl-color-gray-5: #c9d1e3;
- --sl-color-gray-6: #e9edf7;
- --sl-color-gray-7: #f5f7fc;
- --sl-color-black: #ffffff;
-
- --nc-glow: #0b6f7e;
- --nc-card: #f5f7fc;
- --nc-card-hover: #eef1f9;
-}
-
-/* Night sky. A faint violet and cyan glow at the top of the page in dark
- mode, the way the banner lifts from black at the horizon. Light mode gets
- none of this: paper stays paper. */
-:root:not([data-theme='light']) body {
- background-image:
- radial-gradient(70rem 30rem at 20% -8rem, rgba(139, 124, 255, 0.16), transparent 65%),
- radial-gradient(50rem 24rem at 90% -10rem, rgba(77, 208, 225, 0.1), transparent 65%);
- background-repeat: no-repeat;
-}
-
-/* The wordmark is monospace white over a gradient rule in the banner; the
- site title follows it. */
-.site-title {
- font-family: var(--sl-font-mono);
- font-weight: 600;
- letter-spacing: -0.02em;
- color: var(--sl-color-white) !important;
-}
-
-.site-title span {
- text-overflow: ellipsis;
-}
-
-/* Below Starlight's 50em breakpoint the header has room for the logo, the
- search and menu buttons and about 15rem of title. Monospace at h4 size
- plus the version pill is too wide for a 390px phone, base size fits. */
-@media (max-width: 50em) {
- .site-title {
- font-size: var(--sl-text-base);
- }
-}
-
-/* Hero: the banner's short violet-to-cyan rule under the title, and the
- primary action carries the gradient with night-navy text, which holds AA
- against both ends in both modes. */
-.hero h1::after {
- content: '';
- display: block;
- width: 6rem;
- height: 0.25rem;
- margin-top: 1rem;
- border-radius: 999px;
- background: var(--nc-gradient);
-}
-
-/* Starlight centres the hero below 50rem and starts it above; the rule
- follows the title. */
-@media (max-width: 49.99rem) {
- .hero h1::after {
- margin-inline: auto;
- }
-}
-
-.hero .sl-link-button.primary {
- background: var(--nc-gradient);
- border-color: transparent;
- color: var(--nc-night);
-}
-
-.hero .sl-link-button.primary:hover,
-.hero .sl-link-button.primary:focus-visible {
- color: var(--nc-night);
- filter: brightness(1.08);
-}
-
-.site-title,
-.sl-markdown-content h1,
-.sl-markdown-content h2,
-h1#_top,
-.hero h1 {
- font-family: var(--nc-font-display);
- letter-spacing: -0.015em;
-}
-
-/* ---------------------------------------------------------------------------
- Version pill.
-
- SiteTitle.astro puts the current release next to the wordmark as a small
- pill that links to the changelog.
---------------------------------------------------------------------------- */
-
-.site-title-group {
- display: flex;
- align-items: center;
- gap: 0.6rem;
- min-width: 0;
-}
-
-.version-pill {
- display: inline-block;
- padding: 0.05rem 0.5rem;
- border-radius: 999px;
- font-family: var(--sl-font-mono);
- font-size: var(--sl-text-xs);
- font-weight: 600;
- white-space: nowrap;
- text-decoration: none;
- color: var(--sl-color-accent-high);
- background: var(--sl-color-accent-low);
-}
-
-.version-pill:hover,
-.version-pill:focus-visible {
- color: var(--sl-color-white);
-}
-
-/* ---------------------------------------------------------------------------
- Tables.
-
- At phone width a key table's last column ("Meaning") gets squeezed to a
- word per line. Give it a floor and let the table scroll sideways instead.
---------------------------------------------------------------------------- */
-
-@media (max-width: 40rem) {
- .sl-markdown-content table td:last-child {
- min-width: 16rem;
- }
-}
-
-/* ---------------------------------------------------------------------------
- Landing page.
---------------------------------------------------------------------------- */
-
-.hero .tagline {
- max-width: 38rem;
-}
-
-.link-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
- gap: 0.75rem;
- padding: 0 !important;
- list-style: none;
-}
-
-.link-grid .link-card {
- margin: 0 !important;
-}
-
-.link-card a {
- position: relative;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
- height: 100%;
- padding: 1rem 1.1rem;
- border: 1px solid var(--sl-color-gray-5);
- border-radius: 0.6rem;
- background: var(--nc-card);
- color: var(--sl-color-gray-2);
- text-decoration: none;
- transition: border-color 0.15s ease, background 0.15s ease;
-}
-
-.link-card a::before {
- content: '';
- position: absolute;
- inset: 0 0 auto 0;
- height: 3px;
- background: var(--nc-gradient);
- opacity: 0;
- transition: opacity 0.15s ease;
-}
-
-.link-card a:hover,
-.link-card a:focus-visible {
- border-color: var(--sl-color-accent);
- background: var(--nc-card-hover);
-}
-
-.link-card a:hover::before,
-.link-card a:focus-visible::before {
- opacity: 1;
-}
-
-.link-card-name {
- font-family: var(--sl-font-mono);
- font-size: var(--sl-text-sm);
- font-weight: 600;
- color: var(--sl-color-white);
-}
-
-.link-card-desc {
- font-size: var(--sl-text-sm);
- line-height: 1.45;
-}
-
-.link-card-meta {
- margin-top: auto;
- font-size: var(--sl-text-xs);
- color: var(--nc-glow);
-}
-
-/* The splash's hero banner: a real output of the kit, shown as a figure under
- the hero rather than in its image slot, because it carries another app's name. */
-.splash-shot {
- margin: 0 0 2.5rem;
-}
-
-.splash-shot img {
- display: block;
- width: 100%;
- height: auto;
- border-radius: 0.75rem;
- border: 1px solid var(--sl-color-gray-5);
-}
-
-.splash-shot figcaption {
- margin-top: 0.75rem;
- font-size: var(--sl-text-sm);
- color: var(--sl-color-gray-3);
-}
-
-.site-footer-note {
- margin-top: 1.5rem;
- font-size: var(--sl-text-xs);
- color: var(--sl-color-gray-3);
-}
|