Files

1346 lines
37 KiB
CSS

body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
:root {
color-scheme: light dark;
--bg-base: light-dark(#FFFFFF, #0a0a0a);
--bg-surface: light-dark(#F9F9F9, #0f0f0f);
--bg-elevated: light-dark(#F1F1F1, #151515);
--bg-hover: light-dark(#E5E5E5, #1f1f1f);
--fg: light-dark(var(--fg-dark), var(--fg-light));
--fg-mute: light-dark(var(--fg-mute-dark), var(--fg-mute-light));
--bg-accent: light-dark(#0977d7, #0977d7);
--bg-accent-hover: color-mix(in srgb, var(--bg-accent), black 25%);
--bg-accent-click: color-mix(in srgb, var(--bg-accent), black 45%);
--fg-inv: light-dark(var(--fg-light), var(--fg-dark));
--fg-mute-inv: light-dark(var(--fg-mute-light), var(--fg-mute-dark));
--fg-light: #EAEAEA;
--fg-dark: #1A1A1A;
--fg-mute-light: #5A5A5A;
--fg-mute-dark: #7A7A7A;
--nav-button-padding: 1rem;
--nav-button-inside-gap: .5rem;
--content-pad: 3.5rem;
--padded-gap: 3rem;
--card-container-gap: 1rem;
--card-bottom-padding: .5rem;
--card-border-radius: .5rem;
--button-padding: 1rem;
--button-border-radius: .6rem;
--nav-height: 4rem; /* corrected at runtime by nav.js to the real value */
}
:root[data-theme="light"] {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
}
* {
transition: background-color 0.2s ease, color 0.2s ease;
}
.padded {
display: flex;
flex-direction: column;
padding: var(--content-pad);
gap: var(--padded-gap);
}
body {
background-color: var(--bg-base);
color: var(--fg);
font-family: "Roboto", sans-serif;
font-optical-sizing: auto;
font-weight: normal;
font-style: normal;
font-variation-settings: "wdth" 100;
}
/* Reusable <nexvane-nav>/<nexvane-footer> wrapper tags (see
scripts/components/nav-footer.js) should be invisible to layout —
the real <nav> and <footer> they render stay direct visual children
of <body>. */
nexvane-nav,
nexvane-footer {
display: contents;
}
nav {
top: 0;
z-index: 10000;
position: sticky;
background-color: var(--bg-surface);
display: flex;
flex-direction: row;
justify-content: space-between;
user-select: none;
}
/* .nav-topbar, .nav-links and .nav-right are structural wrappers only.
On desktop they stay display:contents so their children behave exactly
like direct flex children of <nav>, reproducing the original layout
pixel-for-pixel. They only become real boxes on mobile (see media query). */
.nav-topbar,
.nav-menu,
.nav-links,
.nav-right {
display: contents;
}
.nav-right > :first-child {
margin-left: auto;
}
nav .nav-hamburger {
display: none;
}
/* --- LOGO ---
One SVG, colored via CSS mask + background-color (same technique as
the download/manual card icons in index.css) instead of two separate
light/dark PNGs — background-color inherits the already
theme-aware --fg variable, so light/dark mode "just works" without
swapping image files or crossfading between them. */
.nav-logo-mark {
width: 2rem;
height: 2rem;
flex-shrink: 0;
background-color: var(--fg);
-webkit-mask: url("../img/logo/nexvane-logo.svg") center / contain no-repeat;
mask: url("../img/logo/nexvane-logo.svg") center / contain no-repeat;
}
.nav-button {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--nav-button-inside-gap);
padding: .85rem var(--nav-button-padding);
position: relative;
color: inherit;
text-decoration: none;
}
/* <p> keeps its default user-agent margin otherwise — invisible on
desktop where the nav's own fixed height already clips it, but on
mobile each stacked row sizes to its content, so that margin turns
into a large, obviously-unintended gap between rows. */
.nav-button p {
margin: 0;
}
.nav-button:hover {
background-color: var(--bg-hover);
cursor: pointer;
}
.nav-button-icon {
margin-right: .5rem;
}
/* --- DROPDOWN CSS --- */
.dropdown-content {
display: none;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
background-color: var(--bg-elevated);
min-width: 200px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.5);
z-index: 100;
}
.dropdown-right {
left: auto;
right: 0;
}
.nav-button:hover .dropdown-content {
display: flex;
}
.dropdown-content a {
color: var(--fg);
padding: 1rem var(--nav-button-padding);
text-decoration: none;
font-size: 0.95rem;
}
.dropdown-content a:hover {
background-color: var(--bg-hover);
}
/* --- BANNER --- */
.banner {
height: 70vh;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.banner > p {
padding: 0;
margin: 0;
user-select: none;
color: var(--fg-light);
}
.darkened-banner {
background-color: rgba(0, 0, 0, 0.5);
background-blend-mode: darken;
}
.banner-title {
font-weight: bolder;
font-size: 5rem;
}
.banner-subheading {
font-weight: bold;
font-size: 2rem;
}
/*--- CARDS ---*/
.card-container {
display: grid;
gap: var(--card-container-gap);
justify-content: center;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), calc(33.333% - 0.7rem)));
}
.card {
padding: .5rem;
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: 1fr;
height: 75vh;
background-color: var(--bg-elevated);
border-radius: var(--card-border-radius);
transition: transform 0.2s ease, background-color 0.2s ease;
}
.card:hover {
background-color: var(--bg-hover);
cursor: pointer;
user-select: none;
transform: scale(1.01);
}
.card-top {
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
.card-bottom {
padding: var(--card-bottom-padding);
gap: var(--card-bottom-padding);
display: flex;
flex-direction: column;
}
.card-text {
margin: 0;
line-height: 1.5;
}
.card-title {
font-size: 1.5rem;
font-weight: bold;
margin: 1rem 0 1rem 0;
}
/*--- BUTTONS ---*/
.button {
color: var(--fg-inv);
font-weight: bold;
background-color: var(--bg-accent);
text-decoration: none;
padding: var(--button-padding);
border-radius: var(--button-border-radius);
text-align: center;
margin-top: auto;
}
.button:hover {
background-color: var(--bg-accent-hover);
color: var(--fg);
}
.button:active {
background-color: var(--bg-accent-click);
}
h1 {
font-size: 2.5rem;
align-self: center;
}
/*--- FOOTER ---*/
footer
{
background-color: var(--bg-surface);
display: flex;
flex-direction: row;
justify-content: center;
}
.dashed-text-div
{
color: var(--fg-mute);
display: flex;
flex-direction: row;
gap: 1rem;
}
.dashed-text-div > * + *::before
{
content: "-";
margin-right: 1rem;
}
/* =========================================================
DOC LAYOUT (sidebar + content + footer)
A reusable two-column layout for docs-style pages: a
navbar-styled sidebar (search box + collapsible node/leaf
tree — see the SIDEBAR block below, it's a separate,
generic component, not docs-specific) sticks below the
navbar and scrolls independently, next to a content column
that ends in the footer — the footer sits under the content
only, not full-bleed, and is in normal document flow (not
sticky).
========================================================= */
.doc-layout {
display: flex;
align-items: flex-start;
}
/* =========================================================
LANGUAGE MODAL (generic — see scripts/components/lang-modal.js)
========================================================= */
.lang-modal-overlay {
position: fixed;
inset: 0;
z-index: 20000;
/* display:none by default; JS toggles the hidden attribute to show
it. This can't just be one unconditional `display: flex` — an
author-level display declaration always overrides the browser's
own [hidden]{display:none} rule (author CSS beats UA stylesheet
regardless of specificity), which would make `hidden = true` a
no-op and the modal impossible to close. */
display: none;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
padding: 1rem;
}
.lang-modal-overlay:not([hidden]) {
display: flex;
}
.lang-modal {
width: 100%;
max-width: 320px;
background-color: var(--bg-elevated);
border-radius: var(--card-border-radius);
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.lang-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
}
.lang-modal-header p {
font-weight: bold;
margin: 0;
}
.lang-modal-close {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--fg-mute);
font-size: 1rem;
cursor: pointer;
padding: .25rem;
border-radius: var(--button-border-radius);
}
.lang-modal-close:hover {
background-color: var(--bg-hover);
color: var(--fg);
}
.lang-modal-options {
display: flex;
flex-direction: column;
padding: 0 0 .5rem 0;
}
.lang-modal-option {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
box-sizing: border-box;
padding: .75rem 1.25rem;
background: none;
border: none;
font: inherit;
font-size: .95rem;
color: var(--fg);
text-align: left;
cursor: pointer;
}
.lang-modal-option:hover {
background-color: var(--bg-hover);
}
.lang-modal-check {
color: var(--bg-accent);
opacity: 0;
}
.lang-modal-option.active .lang-modal-check {
opacity: 1;
}
.sidebar {
position: sticky;
top: var(--nav-height);
flex-shrink: 0;
width: 280px;
height: calc(100vh - var(--nav-height));
display: flex;
flex-direction: column;
background-color: var(--bg-surface);
}
.sidebar-search {
position: relative;
display: flex;
align-items: center;
gap: .5rem;
padding: 0 .85rem;
height: 2.85rem;
flex-shrink: 0;
color: var(--fg-mute);
}
.sidebar-search input {
flex: 1;
min-width: 0;
background: none;
border: none;
outline: none;
color: var(--fg);
font: inherit;
font-size: .9rem;
}
.sidebar-search input::placeholder {
color: var(--fg-mute);
}
/* Suggestions float below the search box instead of pushing the tree
down, so results can come from any registered SiteSearch entries —
not just this sidebar's own tree. */
.sidebar-search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 20;
background-color: var(--bg-elevated);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
max-height: 60vh;
overflow-y: auto;
}
.sidebar-search-result {
display: flex;
flex-direction: column;
gap: .1rem;
padding: .5rem .85rem;
color: var(--fg);
text-decoration: none;
}
.sidebar-search-result:hover {
background-color: var(--bg-hover);
}
.sidebar-search-result-title {
font-size: .85rem;
}
.sidebar-search-result-desc {
font-size: .75rem;
color: var(--fg-mute);
}
.sidebar-tree {
flex: 1;
overflow-y: auto;
padding: .25rem 0;
/* Reserve the scrollbar's own space up front (rather than only once
the tree is tall enough to need it) so rows never have to resize
or sit flush against a track that just popped in. */
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: var(--bg-hover) transparent;
}
.sidebar-tree::-webkit-scrollbar {
width: 8px;
}
.sidebar-tree::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-tree::-webkit-scrollbar-thumb {
background-color: var(--bg-hover);
border-radius: 4px;
}
.sidebar-tree-list {
list-style: none;
margin: 0;
padding: 0;
}
/* Only nested lists indent — each level of nesting shifts its own
children further right. Rows without a chevron reserve the same
width via .sidebar-tree-spacer (below), so this indentation is the
ONLY thing that changes with depth — a child's icon always sits to
the right of its parent's, never level with or before it. */
.sidebar-tree-list .sidebar-tree-list {
padding-left: .85rem;
}
/* Each row is chevron-hit (toggle) + link (navigate), side by side, so
a node can both expand/collapse AND go to its own page — the chevron
is a small dedicated hit target, the label is the rest of the row.
Hover is on the row so either half lights up the whole row. */
.sidebar-tree-row {
display: flex;
align-items: stretch;
width: 100%;
}
.sidebar-tree-row:hover {
background-color: var(--bg-hover);
}
.sidebar-tree-chevron-hit,
.sidebar-tree-spacer {
flex-shrink: 0;
width: 1.5rem;
}
.sidebar-tree-chevron-hit {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--fg-mute);
cursor: pointer;
}
.sidebar-tree-chevron {
font-size: .65rem;
transition: transform 0.15s ease;
}
.sidebar-tree-chevron-hit[aria-expanded="true"] .sidebar-tree-chevron {
transform: rotate(90deg);
}
.sidebar-tree-link {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: .4rem;
padding: .4rem .75rem .4rem .1rem;
color: var(--fg);
text-decoration: none;
background: none;
border: none;
font: inherit;
font-size: .88rem;
text-align: left;
cursor: pointer;
}
.sidebar-tree-icon {
flex-shrink: 0;
font-size: .85rem;
color: var(--fg-mute);
}
/* The exact page you're on: full row highlight (covers the chevron too,
when there is one — a leaf's link is the row's only child anyway, so
this still just colors that one element for leafs). */
.sidebar-tree-row.active {
background-color: var(--bg-hover);
}
.sidebar-tree-row.active .sidebar-tree-link,
.sidebar-tree-row.active .sidebar-tree-icon {
color: var(--bg-accent);
}
/* A section that CONTAINS the active page: a lighter touch (just the
icon), so you can see which part of the tree you're in without it
competing with the one row that's actually "you are here". */
.sidebar-tree-row.ancestor-active > .sidebar-tree-link > .sidebar-tree-icon {
color: var(--bg-accent);
}
.doc-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.doc-content {
display: flex;
flex-direction: column;
padding: var(--content-pad);
}
/* =========================================================
DOC CONTENT (headings, prose, callouts, tables, TOC — the
visual language for writing documentation pages, all scoped
to .doc-content so it never touches the marketing page's own
h1 usage elsewhere on the site).
========================================================= */
/* Spacing: a flat `gap` on .doc-content gave every pair of
elements the same large space, so a heading looked just as
far from ITS OWN paragraph as from the next section entirely
— nothing read as "grouped". Margins let that differ: tight
right under a heading, roomy before the next h2/h3. */
.doc-content > * {
margin: 0;
}
.doc-content > * + * {
margin-top: 1.5rem;
}
.doc-content > h2 {
margin-top: 3rem;
}
.doc-content > h3 {
margin-top: 2rem;
}
.doc-content > h1 + * {
margin-top: 1rem;
}
.doc-content > h2 + *,
.doc-content > h3 + * {
margin-top: .6rem;
}
.doc-content h1,
.doc-content h2,
.doc-content h3 {
scroll-margin-top: calc(var(--nav-height) + 1rem);
}
.doc-content h1 {
font-size: 2.5rem;
align-self: flex-start;
}
.doc-content h2 {
font-size: 1.65rem;
}
.doc-content h3 {
font-size: 1.2rem;
}
.doc-content p {
line-height: 1.6;
}
.doc-content ul,
.doc-content ol {
padding-left: 1.4rem;
line-height: 1.6;
}
.doc-content li + li {
margin-top: .35rem;
}
.doc-content a {
color: var(--bg-accent);
text-decoration: none;
}
.doc-content a:hover {
text-decoration: underline;
}
/* --- Callout / admonition boxes: scripts/components/doc-callout.js ---
Blended against --bg-elevated (not "transparent") so the tint stays
clearly visible even against a near-black page background — a
low-opacity color mixed toward transparent barely shifts anything
when the base it's sitting on is already almost black. */
.doc-callout {
--callout-color: var(--bg-accent);
display: flex;
flex-direction: column;
gap: .35rem;
padding: 1rem 1.25rem;
border-radius: var(--card-border-radius);
border: 1px solid color-mix(in srgb, var(--callout-color) 50%, transparent);
background-color: color-mix(in srgb, var(--callout-color) 18%, var(--bg-elevated));
}
.doc-callout[type="tip"] {
--callout-color: #1f9d55;
}
.doc-callout[type="warning"] {
--callout-color: #c88719;
}
.doc-callout[type="error"] {
--callout-color: #d64545;
}
.doc-callout-label {
display: flex;
align-items: center;
gap: .5rem;
font-weight: bold;
color: var(--callout-color);
}
.doc-callout-body {
color: var(--fg);
line-height: 1.6;
}
.doc-callout-body > :first-child {
margin-top: 0;
}
.doc-callout-body > :last-child {
margin-bottom: 0;
}
/* --- Tabs: scripts/components/doc-tabs.js ---
The tab strip and panels are deliberately plain: a bottom-border
indicator on the active tab and a hairline box around the whole
thing, so it reads as "a variant switch for the content beneath it"
rather than competing for attention with the callouts above. */
.doc-tabs {
display: block;
margin: 1.5rem 0;
border: 1px solid var(--bg-hover);
border-radius: var(--card-border-radius);
overflow: hidden;
}
.doc-tabs-list {
display: flex;
flex-wrap: wrap;
background-color: var(--bg-elevated);
border-bottom: 1px solid var(--bg-hover);
}
.doc-tabs-tab {
appearance: none;
background: none;
border: none;
/* Sits flush against .doc-tabs-list's own border-bottom so the
active indicator can sit exactly on top of it without a seam. */
border-bottom: 2px solid transparent;
margin-bottom: -1px;
padding: .65rem 1.1rem;
font: inherit;
font-size: .9rem;
font-weight: 600;
color: var(--fg-mute);
cursor: pointer;
}
.doc-tabs-tab:hover {
color: var(--fg);
background-color: var(--bg-hover);
}
.doc-tabs-tab.active {
color: var(--bg-accent);
border-bottom-color: var(--bg-accent);
}
.doc-tabs-panel {
/* doc-tab is never registered as a custom element (it's a pure
content container, styled and toggled entirely from here), so
without this it defaults to the browser's display:inline for
unknown elements — which doesn't establish a proper block
context for the <p>/<pre> inside it, so the padding below ends
up not actually insetting them (that's what was clipping the
first couple characters of every panel's text). */
display: block;
padding: 1.25rem;
/* Matches .doc-tabs-list's own background rather than staying
transparent — otherwise the only thing distinguishing the panel
from the plain page behind it is a 1px border, so the padding
around plain-text content reads as if it isn't there at all. */
background-color: var(--bg-elevated);
}
/* The display:block above ties with the browser's own [hidden] rule
for specificity, and an author rule wins that tie — so without
this, setting display here would quietly un-hide every inactive
panel at once. */
.doc-tabs-panel[hidden] {
display: none;
}
.doc-tabs-panel > :first-child {
margin-top: 0;
}
.doc-tabs-panel > :last-child {
margin-bottom: 0;
}
/* A <pre> normally has its own background/border/padding (see below)
so it reads as a distinct block in ordinary prose. Inside a tab
panel that's already that same card, `pre` inheriting its own box
on top doubles up: a visible seam plus a second ring of padding
stacked on the panel's, which is what actually produced the odd
dead-space gap under the tab strip. Flatten it back to plain text
in a box so the panel's own padding is the only spacing at play. */
.doc-tabs-panel pre {
margin: 0;
padding: 0;
border: none;
background: none;
border-radius: 0;
}
/* --- Inline code & code blocks ---
Nothing styled these before now — harmless while content was plain
prose, but the tabs above exist specifically to hold code samples,
so an unstyled <pre> would undercut the very feature it's meant to
demo. */
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: .9em;
}
:not(pre) > code {
background-color: var(--bg-hover);
padding: .15em .4em;
border-radius: .3em;
}
pre {
background-color: var(--bg-elevated);
border: 1px solid var(--bg-hover);
border-radius: var(--card-border-radius);
padding: 1rem 1.25rem;
overflow-x: auto;
}
pre code {
font-size: .85rem;
line-height: 1.6;
}
/* --- Syntax-highlighted code blocks: scripts/components/doc-code.js ---
Shiki (real TextMate-grammar highlighting, VS Code's own Dark+/
Light+ themes) generates a fully self-styled <pre class="shiki">
with each token's colors set as inline styles/CSS variables — it
still lands on a plain `pre` element, though, so the sizing/border
rules above apply to it same as any other code block automatically.
All that's left is wiring Shiki's dual-theme CSS variables to this
site's own [data-theme] toggle (scripts/theme.js) instead of the
prefers-color-scheme media query Shiki defaults to, since here the
dark/light switch is a manual click, not just an OS setting.
The :not(...) exclusion matters once Twoslash is involved (see
below): its hover popups are themselves <span>s nested inside this
same <pre>, and since this rule needs !important to win over
Shiki's own inline per-token colors, it would otherwise force the
popup's background/text to match the surrounding code instead of
rendering as a distinct floating tooltip. */
[data-theme="light"] .shiki,
[data-theme="light"] .shiki span:not(.twoslash-popup-container, .twoslash-popup-container *) {
color: var(--shiki-light) !important;
background-color: var(--shiki-light-bg) !important;
font-style: var(--shiki-light-font-style) !important;
font-weight: var(--shiki-light-font-weight) !important;
text-decoration: var(--shiki-light-text-decoration) !important;
}
[data-theme="dark"] .shiki,
[data-theme="dark"] .shiki span:not(.twoslash-popup-container, .twoslash-popup-container *) {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
/* --- Tree-sitter output: scripts/components/doc-code.js ---
A real parser's output (currently just C++ — see TREE_SITTER_LANGS
in that file), not a TextMate grammar's, so there's no dual-theme
CSS-variable dance to do here: this is our own plain `tok-*` classes
with no inline styles competing for the cascade, so a normal (no
!important needed) color per class per side of the [data-theme]
toggle is all it takes. Colors match Dark+/Light+ token-for-token
where the concepts line up (type/function/variable/etc.), so a
tree-sitter block and a Shiki block sitting near each other on the
same page read as one consistent palette. */
[data-theme="dark"] .tree-sitter .tok-keyword { color: #569CD6; }
[data-theme="dark"] .tree-sitter .tok-type { color: #4EC9B0; }
[data-theme="dark"] .tree-sitter .tok-function,
[data-theme="dark"] .tree-sitter .tok-function-special { color: #DCDCAA; }
[data-theme="dark"] .tree-sitter .tok-variable,
[data-theme="dark"] .tree-sitter .tok-property { color: #9CDCFE; }
[data-theme="dark"] .tree-sitter .tok-variable-builtin { color: #569CD6; }
[data-theme="dark"] .tree-sitter .tok-string { color: #CE9178; }
[data-theme="dark"] .tree-sitter .tok-number { color: #B5CEA8; }
[data-theme="dark"] .tree-sitter .tok-comment { color: #6A9955; }
[data-theme="dark"] .tree-sitter .tok-constant { color: #4FC1FF; }
[data-theme="dark"] .tree-sitter .tok-label { color: #C8C8C8; }
[data-theme="light"] .tree-sitter .tok-keyword { color: #0000FF; }
[data-theme="light"] .tree-sitter .tok-type { color: #267F99; }
[data-theme="light"] .tree-sitter .tok-function,
[data-theme="light"] .tree-sitter .tok-function-special { color: #795E26; }
[data-theme="light"] .tree-sitter .tok-variable,
[data-theme="light"] .tree-sitter .tok-property { color: #001080; }
[data-theme="light"] .tree-sitter .tok-variable-builtin { color: #0000FF; }
[data-theme="light"] .tree-sitter .tok-string { color: #A31515; }
[data-theme="light"] .tree-sitter .tok-number { color: #098658; }
[data-theme="light"] .tree-sitter .tok-comment { color: #008000; }
[data-theme="light"] .tree-sitter .tok-constant { color: #0070C1; }
[data-theme="light"] .tree-sitter .tok-label { color: #383838; }
/* --- Twoslash hover popups: @shikijs/twoslash + twoslash-cdn, loaded
by scripts/components/doc-code.js only for a `twoslash`-marked code
block. The popup's own look (border, shadow, layout) comes from the
library's own style-rich.css, loaded alongside it — these just hand
its color variables this site's own palette for each half of the
[data-theme] toggle instead of the library's fixed light-grey
default, so the popup doesn't look out of place against a dark
page. */
[data-theme="dark"] {
--twoslash-border-color: var(--bg-hover);
--twoslash-popup-bg: var(--bg-elevated);
--twoslash-popup-color: var(--fg);
--twoslash-code-font: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
[data-theme="light"] {
--twoslash-border-color: var(--bg-hover);
--twoslash-popup-bg: var(--bg-elevated);
--twoslash-popup-color: var(--fg);
--twoslash-code-font: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* --- Tables ---
border-collapse:collapse fights border-radius (collapsed borders
render on a layer the table's own corner-clipping doesn't reliably
catch, especially on the last row) — border-collapse:separate with
zero spacing gets the same visual seam between rows while keeping
the rounded corners intact. Row background uses --bg-elevated, not
--bg-surface — the latter is only 5/255 brighter than the page
background, which read as no card at all. */
.doc-content table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
border-radius: var(--card-border-radius);
overflow: hidden;
}
.doc-content th {
text-align: left;
background-color: var(--bg-elevated);
color: var(--fg-mute);
font-weight: normal;
font-size: .85rem;
padding: .9rem 1.1rem;
}
.doc-content td {
padding: 1.1rem;
border-top: 1px solid var(--bg-hover);
background-color: var(--bg-elevated);
vertical-align: top;
}
.doc-content td:first-child {
font-weight: bold;
}
.doc-content-row {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: var(--padded-gap);
}
.doc-content-row > .doc-content {
flex: 1;
min-width: 0;
padding: var(--content-pad) 0 var(--content-pad) var(--content-pad);
}
/* --- "On this page" TOC: scripts/components/doc-toc.js ---
Mirrors the left <sidebar>'s own layout language (sticky, flush
under the nav, full viewport height, internal scroll) rather than
floating as a separate rounded card — the two read as one family
instead of the sidebar being "real" and the TOC being a tooltip
that happens to sit still. The one deliberate difference: the
background stays semi-transparent (see .doc-toc-panel below) since
an opaque panel this size, this close to the sidebar, reads as a
second competing nav rather than a lightweight companion to the
content.
<doc-toc> itself only ever handles POSITION (sticky column, full
height — see the responsive overrides below for how that collapses
once there's no room for a third column). .doc-toc-panel only ever
handles APPEARANCE (background, borders). Keeping those separate is
what makes the narrow-viewport version a one-property-at-a-time
change instead of a second copy of the whole component. */
doc-toc {
display: block;
position: sticky;
top: var(--nav-height);
/* Defensively low: sticky elements with no z-index of their own can
behave inconsistently across browsers as they scroll past other
positioned content — this guarantees the nav (z-index 10000)
always wins regardless. */
z-index: 1;
flex-shrink: 0;
width: 220px;
box-sizing: border-box;
height: calc(100vh - var(--nav-height));
}
.doc-toc-panel {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
border-left: 1px solid var(--bg-hover);
/* Semi-transparent rather than a solid fill like the sidebar's —
this column is a secondary, at-a-glance aid, not a second place
to navigate from, so it shouldn't compete at the same visual
weight. Blurred so scrolling content behind it reads as tinted
rather than as raw visual noise through a hard-edged cutout. */
background-color: color-mix(in srgb, var(--bg-surface) 40%, transparent);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
overflow: hidden;
}
.doc-toc-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
height: 2.85rem;
padding: 0 .85rem;
cursor: pointer;
user-select: none;
flex-shrink: 0;
}
.doc-toc-summary:hover {
background-color: var(--bg-hover);
}
/* Hide the browser's own disclosure triangle — the chevron icon
below is the one and only toggle indicator. */
.doc-toc-summary::-webkit-details-marker {
display: none;
}
.doc-toc-summary::marker {
content: "";
}
.doc-toc-heading {
font-size: .75rem;
font-weight: bold;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--fg-mute);
}
.doc-toc-chevron {
font-size: .65rem;
color: var(--fg-mute);
flex-shrink: 0;
transition: transform 0.15s ease;
}
.doc-toc-panel[open] > .doc-toc-summary .doc-toc-chevron {
transform: rotate(90deg);
}
.doc-toc-nav {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: .25rem 0;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: var(--bg-hover) transparent;
}
.doc-toc-nav::-webkit-scrollbar {
width: 8px;
}
.doc-toc-nav::-webkit-scrollbar-track {
background: transparent;
}
.doc-toc-nav::-webkit-scrollbar-thumb {
background-color: var(--bg-hover);
border-radius: 4px;
}
.doc-toc-list {
list-style: none;
margin: 0;
padding: 0;
}
.doc-toc-list .doc-toc-list {
padding-left: .85rem;
}
.doc-toc-link {
display: block;
padding: .4rem .85rem;
border-left: 2px solid transparent;
color: var(--fg-mute);
text-decoration: none;
font-size: .85rem;
line-height: 1.4;
}
.doc-toc-link:hover {
background-color: var(--bg-hover);
color: var(--fg);
}
.doc-toc-link.active {
color: var(--bg-accent);
border-left-color: var(--bg-accent);
font-weight: 600;
}
/* =========================================================
RESPONSIVE / MOBILE OVERRIDES
Everything above this point defines the desktop (and base)
appearance and is left untouched. Everything below only
applies at smaller viewport widths and adapts layout,
spacing and the nav into a collapsible mobile menu.
========================================================= */
@media (max-width: 1200px) {
/* The right-side "on this page" column needs real room alongside
BOTH the left sidebar and the main content — sidebar (280px)
plus TOC (260px) plus padding/gaps eats well over half of an
860px-wide viewport, leaving the content column squeezed down
to almost nothing. Rather than just dropping the TOC (which
loses it entirely on every phone and tablet), it moves out of
the row and becomes an ordinary block ahead of the content —
same <details> panel, just no longer pinned to a column. */
.doc-content-row {
flex-direction: column;
/* The row's default align-items:flex-start (needed on desktop so
the TOC column doesn't stretch to the tallest sibling) means a
stacked child sizes to its own content width instead of
filling the line — without this override the TOC panel here
would end up only as wide as its text, not the full column. */
align-items: stretch;
}
.doc-content-row > .doc-content {
padding-right: var(--content-pad);
}
doc-toc {
display: block;
position: static;
width: auto;
height: auto;
/* Comes first in the DOM (after <main> — see manual.html) so
it reads as part of the page's normal top-to-bottom flow;
flexbox is what actually puts it before the content
visually without needing to touch the markup. */
order: -1;
}
.doc-toc-panel {
height: auto;
max-height: none;
border: 1px solid var(--bg-hover);
background-color: var(--bg-elevated);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.doc-toc-nav {
max-height: 40vh;
}
}
@media (max-width: 860px) {
:root {
--content-pad: 1.25rem;
--padded-gap: 2rem;
--nav-button-padding: 1.25rem;
}
/* --- NAV --- */
nav {
flex-direction: column;
align-items: stretch;
}
.nav-topbar,
.nav-menu,
.nav-links,
.nav-right {
display: flex;
}
.nav-topbar {
flex-direction: row;
justify-content: space-between;
align-items: stretch;
width: 100%;
flex-shrink: 0;
}
/* .nav-menu is the single collapsible/scrollable region below the
always-visible topbar, so the hamburger stays reachable and no
nav content can ever end up taller than the viewport with no way
to reach it. */
.nav-menu {
flex-direction: column;
width: 100%;
max-height: 0;
overflow-y: hidden;
transition: max-height 0.25s ease;
}
nav.nav-open .nav-menu {
max-height: calc(100vh - 4.5rem);
overflow-y: auto;
}
.nav-links {
flex-direction: column;
width: 100%;
}
/* Unlike .nav-links (Download/Documentation/About, which stay
full-width stacked rows), .nav-right pairs the user menu with the
settings gear side by side on their own row — the gear reads as
"goes with Username", not as a fourth unrelated stacked item. */
.nav-right {
flex-direction: row;
align-items: stretch;
justify-content: space-between;
width: 100%;
}
.nav-right > :first-child {
margin-left: 0;
}
nav .nav-hamburger {
display: flex;
background: none;
border: none;
margin: 0;
color: inherit;
font: inherit;
font-size: 1.4rem;
appearance: none;
}
nav .nav-hamburger::-moz-focus-inner {
border: 0;
padding: 0;
}
/* Only the items inside the collapsible menu should become
full-width stacked rows — the topbar's logo and hamburger stay
their natural inline size. */
.nav-menu .nav-button {
width: 100%;
box-sizing: border-box;
/* Let the dropdown-content (width 100%) drop onto its own line
below the label instead of sitting beside it. */
flex-wrap: wrap;
}
/* .nav-right's two buttons sit side by side (see above), so while
closed they size to their own content instead of each claiming
the full row — otherwise "Username" and the gear would each
still stack as their own full-width line. Once opened, though,
a button goes back to full width (flex-wrap: wrap on .nav-right
above then pushes its now-full-width sibling onto the next
line) so its dropdown-content has the same full row to expand
into that .nav-links items get, instead of being squeezed into
whatever width the closed pill happened to be. */
.nav-right {
flex-wrap: wrap;
}
.nav-right > .nav-button {
width: auto;
flex-wrap: nowrap;
}
.nav-right > .nav-button.open {
width: 100%;
flex-wrap: wrap;
}
/* Hover-triggered dropdowns don't work well on touch; disable hover
opening on mobile and rely on the .open class toggled by nav.js */
.nav-button:hover .dropdown-content {
display: none;
}
.nav-button.dropdown.open .dropdown-content {
display: flex;
}
/* Every dropdown in the mobile menu — .nav-links (Download,
Documentation, About) and .nav-right (Username, the gear) alike
— drops its contents inline, full-width, right under the label
it belongs to, rather than floating as a small anchored panel
that can run off the edge of a narrow screen. */
.nav-menu .dropdown-content {
position: static;
box-shadow: none;
min-width: 0;
width: 100%;
background-color: var(--bg-hover);
margin-top: .5rem;
}
.nav-menu .dropdown-content a {
padding-left: calc(var(--nav-button-padding) * 2);
}
/* --- BANNER --- */
.banner {
height: 45vh;
}
.banner-title {
font-size: 2.75rem;
}
.banner-subheading {
font-size: 1.25rem;
}
/* --- HEADINGS --- */
h1 {
font-size: 1.9rem;
}
/* --- CARDS ---
CSS Grid's auto-fit centers the whole track group, but a leftover
card in an incomplete last row still sits in the first track
(flush left) rather than being centered itself. Flexbox with
wrap + justify-content:center centers each row's items
individually, so a lone leftover card sits centered instead of
left-aligned, while still keeping 2-3 cards per row when they fit. */
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.card {
flex: 0 1 min(100%, 320px);
box-sizing: border-box;
/* A fixed-height image row + an auto text row means the image
is always the same size no matter how long the card's text
is, and the card itself grows to fit the text instead of
squeezing the image to make room for it. */
height: auto;
grid-template-rows: 190px auto;
}
.card-title {
font-size: 1.25rem;
margin: .75rem 0;
}
/* --- FOOTER ---
Stays a horizontal row; it only wraps onto a second line if it
truly can't fit, it never stacks fully vertical. */
.dashed-text-div {
flex-wrap: wrap;
justify-content: center;
text-align: center;
}
footer {
padding: 1.5rem 0;
}
/* --- DOC LAYOUT ---
The sticky/independently-scrolling sidebar only makes sense next
to a wide content column. On mobile it stacks above the content
as a normal (non-sticky) block instead, capped in height with
its own scroll so a long tree doesn't push the content way down. */
.doc-layout {
flex-direction: column;
}
.sidebar {
position: static;
width: 100%;
height: auto;
max-height: 50vh;
}
.doc-content {
padding: 1.25rem;
}
/* The right-side "on this page" TOC only makes sense next to a wide
content column too — there's no room for a third column here. */
.doc-content-row {
flex-direction: column;
}
.doc-content-row > .doc-content {
padding: 1.25rem;
}
.doc-toc-nav {
max-height: 35vh;
}
}
@media (max-width: 480px) {
:root {
--content-pad: 1rem;
}
.banner-title {
font-size: 2.1rem;
}
.banner-subheading {
font-size: 1.05rem;
}
.card {
grid-template-rows: 160px auto;
}
h1 {
font-size: 1.6rem;
}
}