1092 lines
30 KiB
CSS
1092 lines
30 KiB
CSS
/* Base structural and functional styles for Pagerite pages and editors.
|
||
Color choices are intentionally conservative CSS variables so themes can
|
||
override them without fighting layout or interaction rules. */
|
||
@import url("./fonts/fonts.css");
|
||
@import url("./pygments.css");
|
||
|
||
:root {
|
||
/* The base theme is light, period — this also resolves the light-dark()
|
||
code palette below (themes declare their own color-scheme). */
|
||
color-scheme: light;
|
||
--bg: #ffffff;
|
||
--surface: #f5f5f5;
|
||
--text: #1a1a1a;
|
||
--muted: #666666;
|
||
--accent: #0056b3;
|
||
--accent2: #003d80;
|
||
--line: #0000001a;
|
||
/* Links stay quiet — mostly text color with a hint of the accent —
|
||
and light up to the full accent on hover. */
|
||
--link: color-mix(in oklab, var(--text) 50%, var(--accent));
|
||
/* Code highlighting palette, consumed by pygments.css: complete light and
|
||
dark sets (background included), resolved by light-dark() from the
|
||
used color-scheme. A theme picks a set simply by declaring
|
||
color-scheme; a fixed dark code block on a light page pins the dark
|
||
set with `pre { color-scheme: dark }`. Only override individual
|
||
--code-* variables for specific theming needs. */
|
||
--code-bg: light-dark(var(--surface), #0d1117);
|
||
--code-text: light-dark(#24292f, #e6edf3);
|
||
--code-muted: light-dark(#6e7781, #6e7681);
|
||
--code-comment: light-dark(#59636e, #8b949e);
|
||
--code-keyword: light-dark(#cf222e, #ff7b72);
|
||
--code-builtin: light-dark(#0550ae, #79c0ff);
|
||
--code-string: light-dark(#0a3069, #a5d6ff);
|
||
--code-function: light-dark(#8250df, #d2a8ff);
|
||
--code-type: light-dark(#953800, #f0883e);
|
||
--code-tag: light-dark(#116329, #7ee787);
|
||
--code-error: light-dark(#d1242f, #f85149);
|
||
--code-added: light-dark(#1a7f37, #56d364);
|
||
--code-hl: light-dark(#fff8c5, #6e7681);
|
||
/* One variable per available font family (faces in fonts/fonts.css);
|
||
everything else references these, so font stacks live here only. */
|
||
--font-source-sans: "Source Sans 3", system-ui, sans-serif;
|
||
--font-source-serif: "Source Serif 4", serif;
|
||
--font-fraunces: "Fraunces", serif;
|
||
--font-literata: "Literata", serif;
|
||
--font-playfair: "Playfair Display", serif;
|
||
--font-cormorant: "Cormorant", serif;
|
||
--font-inter: "Inter", system-ui, sans-serif;
|
||
--font-montserrat: "Montserrat", system-ui, sans-serif;
|
||
--font-fira-code: "Fira Code", ui-monospace, monospace;
|
||
--font-cause: "Cause", system-ui, sans-serif;
|
||
--font-exo2: "Exo 2", system-ui, sans-serif;
|
||
--font-new-rocker: "New Rocker", "Playfair Display", serif;
|
||
--font-body: var(--font-source-sans);
|
||
--font-heading: var(--font-source-serif);
|
||
/* The brand follows the heading font unless overridden separately. */
|
||
--font-brand: var(--font-heading);
|
||
--font-code: var(--font-fira-code);
|
||
/* Width of the docked editor panel (used both here for shifting the page
|
||
and in the Vue editor's own styles). */
|
||
--editor-w: min(46rem, 50vw);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html {
|
||
scroll-behavior: smooth;
|
||
/* Native-scrollbar fallback styling (JS off or before pagerite.js runs):
|
||
thin, theme-muted thumb on a transparent track. With JS the scrollbars
|
||
are replaced by OverlayScrollbars (see pagerite.js) — floating,
|
||
auto-hidden scrollbars styled by the --os-* variables below, so they
|
||
never reserve layout space or shift the page when appearing. */
|
||
scrollbar-width: thin;
|
||
scrollbar-color: color-mix(in srgb, var(--muted) 45%, transparent) transparent;
|
||
}
|
||
|
||
.os-scrollbar {
|
||
--os-size: 0.5rem;
|
||
--os-thumb-bg: color-mix(in srgb, var(--muted) 45%, transparent);
|
||
--os-thumb-hover-bg: color-mix(in srgb, var(--muted) 65%, transparent);
|
||
--os-thumb-active-bg: var(--muted);
|
||
--os-track-bg: transparent;
|
||
--os-thumb-border-radius: 0.25rem;
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-body);
|
||
font-size: 1.05rem;
|
||
line-height: 1.65;
|
||
/* Tabular numerals wherever the active font supports them; avoids
|
||
numbers jumping in width as counters/values change. */
|
||
font-variant-numeric: tabular-nums;
|
||
margin: 0;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
/* Full-bleed elements (.wide) size to 100vw, which counts the vertical
|
||
scrollbar; clip the few stray pixels instead of scrolling. */
|
||
overflow-x: clip;
|
||
/* Full height even on short pages: the footer sits at the bottom and the
|
||
docked editor (sized by #content) never collapses. */
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#content {
|
||
flex: 1;
|
||
}
|
||
|
||
/* Full-width banner: image header with the brand and nav overlaid.
|
||
Base provides a plain fallback; themes override with artwork. */
|
||
#banner {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
min-height: 11rem;
|
||
background: linear-gradient(135deg, var(--surface), var(--bg));
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
/* Per-page banner content (img, styled div, inline SVG...) fills the
|
||
banner; swapped along with #nav/#main on fetch-navigation. The backend
|
||
inlines the effective banner design's SVG artwork here first
|
||
(pagerite/themes/{design}/banner.svg, marked svg[data-design]), then the
|
||
page's own banner code after it. */
|
||
#page-banner {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
/* Stack the design artwork and the page's own banner code on top of
|
||
each other (artwork first): the banner is a background layer, author
|
||
code overlays it. A single child behaves exactly as before. The track
|
||
is explicitly banner-sized: an auto row would size to the content
|
||
(an SVG's intrinsic aspect ratio makes it far taller than the banner),
|
||
and children's height:100% would resolve against that bloated row. */
|
||
display: grid;
|
||
grid-template: 100% / 100%;
|
||
}
|
||
|
||
/* :not(style, script): author-level display:block would override the UA's
|
||
display:none on those and render their source as banner text. No
|
||
width/height: the default stretch alignment fills the track without a
|
||
percentage-resolution cycle. */
|
||
#page-banner>*:not(style, script) {
|
||
grid-area: 1 / 1;
|
||
display: block;
|
||
object-fit: cover;
|
||
/* Banner artwork is bottom-anchored: the meaningful content (horizon,
|
||
ground, characters) sits at the bottom, so on wide viewports — where
|
||
the fixed-height banner crops the artwork vertically — the sky/top is
|
||
what scrolls out of view. object-position covers replaced elements
|
||
(img, canvas); inline SVG is anchored by the transform-origin below
|
||
together with preserveAspectRatio="...YMax slice" in the artwork. */
|
||
object-position: bottom;
|
||
/* Scroll parallax: pagerite.js sets --pry on <html>; the banner stays
|
||
windowed in place while the artwork drifts inside it. The scale
|
||
provides overscan so the drift never reveals an edge; scaling from the
|
||
bottom keeps the artwork's bottom edge pinned to the banner's. */
|
||
transform: scale(1.25) translateY(var(--pry, 0px));
|
||
transform-origin: bottom;
|
||
will-change: transform;
|
||
}
|
||
|
||
/* The design artwork wrapper (div[data-design]) takes the sizing and
|
||
parallax above; an svg inside it fills it (it is what used to be the
|
||
direct child before designs got a wrapper). */
|
||
#page-banner [data-design]>svg {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
#brand,
|
||
#nav {
|
||
position: relative;
|
||
}
|
||
|
||
#brand {
|
||
font-family: var(--font-brand);
|
||
font-weight: 700;
|
||
font-size: 2.4rem;
|
||
text-decoration: none;
|
||
/* One line always: pagerite.js shrinks the font size to fit instead of
|
||
wrapping (the themed size is the maximum). */
|
||
white-space: nowrap;
|
||
margin: auto 1.25rem 0;
|
||
padding-top: 1.5rem;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* Nav overlaid at the bottom of the banner */
|
||
#nav {
|
||
font-size: 1.3em;
|
||
padding: 0.35rem 1.25rem;
|
||
}
|
||
|
||
#nav ul {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
gap: 0.25rem 1.5rem;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
}
|
||
|
||
#nav a {
|
||
text-decoration: none;
|
||
color: var(--text);
|
||
}
|
||
|
||
#nav ul ul a {
|
||
color: var(--muted);
|
||
}
|
||
|
||
#nav a:hover {
|
||
color: var(--accent);
|
||
}
|
||
|
||
#nav span {
|
||
color: var(--muted);
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
#nav .current {
|
||
color: var(--accent);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Sidebar + main row. A symmetric grid: the article column is sized by the
|
||
viewport alone (never by content), with equally sized flexible gutters
|
||
on both sides. The sidebar sits in the left gutter, so it appearing or
|
||
disappearing never shifts the article; the right gutter balances it.
|
||
The outer tracks are minmax(0, 1fr) — a plain 1fr has an `auto` minimum,
|
||
which let the 12rem sidebar expand its track at narrow widths and push
|
||
the article off-center; now the sidebar overlays the gutter edge instead
|
||
(it is translucent/blurred exactly so it can pass over content, and
|
||
full-bleed .wide images slide underneath it). */
|
||
#content {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 78rem) minmax(0, 1fr);
|
||
/* The docked editor pushes the content (not the header) right. */
|
||
transition: margin-left 0.25s ease;
|
||
}
|
||
|
||
/* Long articles (.multicol is added by pagerite.js based on content length)
|
||
lift the 78rem cap and scrap the right gutter: a 1fr left gutter (which
|
||
holds the overlaying sidebar) and the article taking all the rest, out
|
||
to the right viewport edge. The column count follows the width (see the
|
||
`columns: 30rem` rule below). The .wide breakout is re-anchored to the
|
||
left gutter below (the article is no longer viewport-centered). */
|
||
body:has(.multicol) #content {
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 4fr);
|
||
}
|
||
|
||
body.editing #content {
|
||
margin-left: var(--editor-w);
|
||
padding-left: 1rem;
|
||
/* gap between the docked editor and the content */
|
||
/* No overflow clipping here: .editor-host lives outside this box
|
||
(negative left), and .wide shrink-wraps to the remaining space. */
|
||
}
|
||
|
||
/* The sidebar's gutter space is needed by the editor instead. */
|
||
body.editing #sidebar {
|
||
display: none;
|
||
}
|
||
|
||
/* While editing, the window itself does not scroll: the editor panel is
|
||
exactly the remaining window height, and only the article area (#main)
|
||
scrolls. Without the editor, normal full-page scroll applies. */
|
||
body.editing {
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body.editing #content {
|
||
min-height: 0;
|
||
grid-template-rows: 100%;
|
||
}
|
||
|
||
body.editing #main {
|
||
height: 100%;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* The editor host lives inside #content: it starts below the banner and
|
||
ends above the footer. With the page scroll locked while editing, the
|
||
panel exactly fills that space — the full available window height. */
|
||
.editor-host {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
left: calc(0px - var(--editor-w));
|
||
width: var(--editor-w);
|
||
}
|
||
|
||
.editor-root.overlay {
|
||
height: 100%;
|
||
background: var(--bg);
|
||
animation: editor-slide-in 0.25s ease;
|
||
}
|
||
|
||
.editor-root.overlay.closing {
|
||
transform: translateX(-100%);
|
||
transition: transform 0.25s ease;
|
||
}
|
||
|
||
@keyframes editor-slide-in {
|
||
from {
|
||
transform: translateX(-100%);
|
||
}
|
||
}
|
||
|
||
/* Banner-area pens (banner editor, site editor) and auth buttons live in a
|
||
single flex container pinned to the banner's top-right corner. */
|
||
.editor-pens {
|
||
position: absolute;
|
||
top: 0.6rem;
|
||
right: 0.5rem;
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
}
|
||
|
||
.editor-pens button,
|
||
.editor-pens a {
|
||
position: static;
|
||
font: inherit;
|
||
border: none;
|
||
cursor: pointer;
|
||
background: none;
|
||
padding: 0;
|
||
text-decoration: none;
|
||
}
|
||
|
||
#sidebar {
|
||
grid-column: 1;
|
||
/* Pinned to the page's left edge (not the article's) and kept in view
|
||
while scrolling. Translucent + blurred rather than an opaque box, so
|
||
full-bleed .wide images can pass underneath without a hard edge. */
|
||
justify-self: start;
|
||
align-self: start;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
width: 12rem;
|
||
max-height: 100vh;
|
||
overflow-y: auto;
|
||
padding: 1rem 1rem 1rem 1.25rem;
|
||
border-radius: 0 0 0.5rem 0;
|
||
background: color-mix(in srgb, var(--bg) 75%, transparent);
|
||
backdrop-filter: blur(0.5rem);
|
||
}
|
||
|
||
#sidebar ul {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.8em;
|
||
line-height: 1.0;
|
||
}
|
||
|
||
/* Third level (and deeper): nested like the first level of article lists —
|
||
slight indent, emoji marker — while the sidebar's own top level (the
|
||
section's direct children) stays plain, without indicators or indent. */
|
||
#sidebar ul ul {
|
||
gap: 0.5em;
|
||
margin-top: 0.5em;
|
||
padding-inline-start: 1em;
|
||
}
|
||
|
||
#sidebar ul ul li::before {
|
||
content: "🔹";
|
||
display: inline-block;
|
||
margin-left: -1.3em;
|
||
width: 1.3em;
|
||
}
|
||
|
||
#sidebar a {
|
||
text-decoration: none;
|
||
color: var(--muted);
|
||
}
|
||
|
||
#sidebar a:hover {
|
||
color: var(--accent);
|
||
}
|
||
|
||
#sidebar .current {
|
||
color: var(--accent);
|
||
font-weight: 500;
|
||
}
|
||
|
||
main {
|
||
grid-column: 2;
|
||
/* No top padding: a leading wide image sits flush under the banner, and
|
||
text-first pages get their spacing from the h1's top margin instead. */
|
||
padding: 0 1.25rem 3rem;
|
||
}
|
||
|
||
article h1,
|
||
article h2,
|
||
article h3 {
|
||
font-family: var(--font-heading);
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
article h1 {
|
||
font-size: 2.2rem;
|
||
margin: 2rem 0 1.2rem;
|
||
}
|
||
|
||
/* Margin strategy: bottom-only inside articles. Top margins misalign
|
||
column tops and collapse unpredictably; spacing comes from below. */
|
||
article p,
|
||
article ul,
|
||
article ol,
|
||
article dl,
|
||
article blockquote,
|
||
article pre,
|
||
article figure,
|
||
article table {
|
||
margin-top: 0;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
article h3 {
|
||
margin: 1.4rem 0 0.4rem;
|
||
}
|
||
|
||
/* Lists: small diamond emoji markers — blue 🔹 on odd nesting levels,
|
||
orange 🔸 on even. The marker occupies a 1em outdented box so wrapped
|
||
lines align. */
|
||
article ul {
|
||
list-style: none;
|
||
padding-inline-start: 1em;
|
||
}
|
||
|
||
article ul li::before {
|
||
content: "🔹";
|
||
display: inline-block;
|
||
margin-left: -1.3em;
|
||
width: 1.3em;
|
||
}
|
||
|
||
article ul ul li::before {
|
||
content: "🔸";
|
||
}
|
||
|
||
article ul ul ul li::before {
|
||
content: "🔹";
|
||
}
|
||
|
||
/* Task lists: real checkboxes (not emoji), clickable in the public page. */
|
||
article .task-list-item::before {
|
||
content: none;
|
||
}
|
||
|
||
article .task-list-item-checkbox {
|
||
cursor: pointer;
|
||
margin-inline-end: 0.35em;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
article {
|
||
position: relative;
|
||
}
|
||
|
||
.edit-link {
|
||
position: absolute;
|
||
top: 0.2rem;
|
||
/* In the left gutter, on the same side as the docked editor panel. */
|
||
left: -2.2rem;
|
||
z-index: 2;
|
||
/* stay above full-bleed .wide images */
|
||
font: inherit;
|
||
background: none;
|
||
border: none;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
opacity: 0.7;
|
||
text-shadow: 0 0 0.1em black;
|
||
}
|
||
|
||
/* pagerite.js tucks the pen at the end of the article's first h1. */
|
||
article h1 .edit-link {
|
||
position: static;
|
||
font-size: 1.1rem;
|
||
vertical-align: 0.3em;
|
||
margin-left: 0.4rem;
|
||
}
|
||
|
||
.edit-link:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Login/profile links injected by pagerite.js when Paskia SSO is in use.
|
||
They live inside the .editor-pens flex container in the banner's top-right
|
||
corner and inherit its reset; keep only their opacity/text-shadow tweaks. */
|
||
.editor-pens a.login-link,
|
||
.editor-pens a.profile-link {
|
||
opacity: 0.7;
|
||
text-shadow: 0 0 0.1em black;
|
||
}
|
||
|
||
.editor-pens a.login-link:hover,
|
||
.editor-pens a.profile-link:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
article p,
|
||
article li,
|
||
article dd {
|
||
text-align: justify;
|
||
hyphens: auto;
|
||
}
|
||
|
||
/* Multi-column reading, but only for long articles (pagerite.js adds
|
||
.multicol based on content length and splits the body into .colseg segments
|
||
separated by full-width h2s and wide figures; only segments with enough
|
||
text get .cols). No fixed breakpoint: `columns: 30rem` lets CSS fit as
|
||
many columns of at least 30rem as the article's current width allows —
|
||
since .multicol also uncaps the article width (see #content above), a
|
||
wider window simply yields more columns. */
|
||
.multicol .colseg.cols {
|
||
columns: 30rem;
|
||
column-gap: 3.5rem;
|
||
column-rule: 1px solid var(--line);
|
||
}
|
||
|
||
.multicol .colseg {
|
||
margin-bottom: 1rem;
|
||
|
||
p,
|
||
li {
|
||
break-inside: avoid-column;
|
||
}
|
||
|
||
figure,
|
||
pre,
|
||
blockquote,
|
||
table,
|
||
dl {
|
||
break-inside: avoid;
|
||
}
|
||
}
|
||
|
||
article h2 {
|
||
font-size: 1.5rem;
|
||
margin: 2.2rem 0 0.6rem;
|
||
}
|
||
|
||
article a {
|
||
color: var(--link);
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
}
|
||
|
||
article a:hover {
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* Blockquotes: inner paragraphs carry no margins (spacing comes from the
|
||
blockquote itself, bottom-only like everything else in articles). The
|
||
negative left margin pushes the bar out past the text edge, so quoted
|
||
text aligns with the surrounding paragraphs — same trick as code blocks. */
|
||
blockquote {
|
||
margin: 0 0 1rem -0.5rem;
|
||
padding: 0 0 0 0.25rem;
|
||
border-left: 0.25rem solid var(--accent2);
|
||
color: var(--muted);
|
||
}
|
||
|
||
blockquote p {
|
||
margin: 0;
|
||
}
|
||
|
||
/* Admonitions (markdown !!! note/warning/...): a lightweight callout in
|
||
the blockquote idiom — accent bar and a faint wash, recolored per type.
|
||
Bottom-only margins like everything else in articles; inner paragraphs
|
||
carry no margins of their own. */
|
||
.admonition {
|
||
margin: 0 0 1rem;
|
||
padding: 0.4rem 0.9rem;
|
||
border-left: 0.25rem solid var(--admonition-color, var(--accent));
|
||
border-radius: 0 0.3rem 0.3rem 0;
|
||
background: color-mix(in srgb, var(--admonition-color, var(--accent)) 7%, transparent);
|
||
}
|
||
|
||
.admonition> :last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.admonition-title {
|
||
margin: 0 0 0.2rem;
|
||
font-weight: 600;
|
||
color: var(--admonition-color, var(--accent));
|
||
}
|
||
|
||
.admonition.tip,
|
||
.admonition.important,
|
||
.admonition.hint,
|
||
.admonition.success {
|
||
--admonition-color: var(--accent2);
|
||
}
|
||
|
||
.admonition.warning,
|
||
.admonition.caution,
|
||
.admonition.danger,
|
||
.admonition.failure {
|
||
--admonition-color: var(--accent3);
|
||
}
|
||
|
||
pre {
|
||
overflow-x: auto;
|
||
padding: 0.5rem 0.8rem;
|
||
/* Code text aligns with the surrounding paragraphs: the box extends
|
||
past them by its own padding. Themes that add a left border must
|
||
extend margin-left by the border width to keep this alignment. */
|
||
margin-left: -0.8rem;
|
||
margin-right: -0.8rem;
|
||
background: var(--code-bg);
|
||
border-radius: 4px;
|
||
position: relative;
|
||
}
|
||
|
||
/* Inline code integrates with the text, no box of its own */
|
||
p code,
|
||
li code {
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
/* Click-to-copy button (added by pagerite.js) */
|
||
.copy {
|
||
position: absolute;
|
||
top: 0.35rem;
|
||
right: 0.35rem;
|
||
font: inherit;
|
||
font-size: 0.75rem;
|
||
padding: 0.15rem 0.6rem;
|
||
color: var(--muted);
|
||
background: var(--bg);
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
pre:hover .copy,
|
||
.copy:focus-visible {
|
||
opacity: 1;
|
||
}
|
||
|
||
.copy.copied {
|
||
color: var(--accent);
|
||
}
|
||
|
||
code {
|
||
font-family: var(--font-code);
|
||
font-size: 0.88em;
|
||
}
|
||
|
||
/* Tables separate by color, not lines: the header is a soft vertical
|
||
gradient tinted with the theme's accent (themes can override the
|
||
--table-head-* stops outright), body cells carry a very faint diagonal
|
||
wash of --table-tint that lifts them apart, and only a whisper of a row
|
||
separator remains. */
|
||
table {
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
padding: 0.35rem 0.8rem;
|
||
border: none;
|
||
}
|
||
|
||
th {
|
||
text-align: left;
|
||
background: linear-gradient(180deg,
|
||
var(--table-head-a, color-mix(in srgb, var(--accent) 10%, var(--surface))),
|
||
var(--table-head-b, color-mix(in srgb, var(--accent) 18%, var(--surface))));
|
||
}
|
||
|
||
td {
|
||
background: linear-gradient(160deg,
|
||
color-mix(in srgb, var(--table-tint, var(--accent)) 5%, transparent),
|
||
transparent 75%);
|
||
}
|
||
|
||
tbody tr+tr td {
|
||
border-top: 1px solid color-mix(in srgb, var(--table-tint, var(--accent)) 12%, transparent);
|
||
}
|
||
|
||
/* Definition lists are laid out as a lightweight two-column grid — terms
|
||
in an accent column with a sensible minimum width, descriptions filling
|
||
the rest — with no borders or fills (tables carry the heavier shading).
|
||
Explicit grid columns keep runs of multiple <dt>s or <dd>s stacking
|
||
correctly. */
|
||
dl {
|
||
display: grid;
|
||
grid-template-columns: minmax(6rem, max-content) 1fr;
|
||
}
|
||
|
||
dt {
|
||
grid-column: 1;
|
||
font-weight: 550;
|
||
color: var(--accent);
|
||
}
|
||
|
||
dd {
|
||
grid-column: 2;
|
||
margin: 0;
|
||
}
|
||
|
||
dt,
|
||
dd {
|
||
padding: 0.2rem 0;
|
||
}
|
||
|
||
/* Images and figures
|
||
|
||
Markdown images standing alone in a paragraph render as a block
|
||
<figure> (with <figcaption> when the image has a title); the
|
||
brace-attribute positioning class ({.left}, {.right}, {.wide}) lives
|
||
on the img inside, but only the figure is ever positioned, so the
|
||
caption stays below the image. Raw <img> HTML written by the author
|
||
stays inline and unstyled beyond these defaults. */
|
||
img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* Figures are block containers filling the column; the image fills the
|
||
figure. The auto inline margins only take effect once a rule below
|
||
shrinks the width (centering the figure); at 100% they compute to 0. */
|
||
figure {
|
||
margin: 0 auto 1.5rem;
|
||
width: 100%;
|
||
}
|
||
|
||
/* The image fills the figure, except when it carries an explicit width
|
||
attribute — that width is a browser presentational hint which CSS
|
||
cannot restore once overridden, so such images are simply left alone. */
|
||
figure img:not([width]) {
|
||
width: 100%;
|
||
}
|
||
|
||
/* Floated figures: {.right} / {.left}, defaulting to 30% of the column
|
||
and capped at half of it. */
|
||
figure:has(.right) {
|
||
float: right;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 0 1rem 1em;
|
||
}
|
||
|
||
figure:has(.left) {
|
||
float: left;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 1em 1rem 0;
|
||
}
|
||
|
||
/* An image with an explicit width attribute shrink-wraps instead: the
|
||
figure fits the image and, per the auto inline margins above, centers
|
||
in the column. Placed after the percentage widths above so it
|
||
overrides them at equal specificity. */
|
||
figure:has(img[width]) {
|
||
width: fit-content;
|
||
}
|
||
|
||
/* .wide is full bleed: edge to edge of the viewport (or of the space right
|
||
of the docked editor), while staying in flow so it keeps its vertical
|
||
space. The article column is centered in the available space, so negative
|
||
symmetric margins of (50% of the column − 50% of the target width) break
|
||
out of the column and center the element on the same center — making the
|
||
left edge land exactly at the viewport's edge (or the editor's right
|
||
border). The sidebar stacks above it (z-index + translucent blur).
|
||
|
||
The rules below re-anchor the bleed for the layouts where the article
|
||
is not viewport-centered; each just overrides width/margin-inline, and
|
||
later rules win at equal specificity. The analytics dashboard uses the
|
||
same breakout directly on its container (div.wide — it is the page's
|
||
whole content, not a figure). */
|
||
figure:has(.wide),
|
||
div.wide {
|
||
width: 100vw;
|
||
max-width: none;
|
||
margin-inline: calc(50% - 50vw);
|
||
/* Follow the docked editor's margin-left transition smoothly. */
|
||
transition: margin-left 0.25s ease;
|
||
}
|
||
|
||
/* Full bleed means edge to edge — no rounded corners. */
|
||
figure:has(.wide) img {
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* .wide on multicol pages: the article is not viewport-centered (no right
|
||
gutter), so the bleed anchors at the left gutter — the 1fr share of the
|
||
1fr + 4fr grid, i.e. 20vw — plus main's padding, and spans on to the
|
||
right viewport edge. */
|
||
body:has(.multicol) figure:has(.wide) {
|
||
margin-inline: calc(-20vw - 1.25rem) 0;
|
||
}
|
||
|
||
/* Editing: shrink the bleed to the space right of the docked editor. */
|
||
body.editing figure:has(.wide) {
|
||
width: calc(100vw - var(--editor-w));
|
||
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2);
|
||
}
|
||
|
||
/* Editing + multicol: the left gutter is 1/5 of the space right of the
|
||
editor, and the bleed also crosses #content's 1rem editing gap plus
|
||
main's 1.25rem left padding (the gutter shrink from the padding roughly
|
||
cancels the rounding): −2rem in total. */
|
||
body.editing:has(.multicol) figure:has(.wide) {
|
||
margin-inline: calc((100vw - var(--editor-w)) / -5 - 2rem) 0;
|
||
}
|
||
|
||
/* Narrow windows with a sidebar: below 102rem the symmetric gutters can no
|
||
longer both hold the 12rem sidebar, so #content reserves it with a fixed
|
||
left track (see the matching media query below) and the article always
|
||
starts at 12rem (+ main's 1.25rem padding) — the bleed margin is a plain
|
||
constant. Scoped by :has(#sidebar) since the sidebar element is omitted
|
||
entirely on pages without sub-navigation, and excluded while editing,
|
||
where the editing rules above apply instead. */
|
||
@media (max-width: 102rem) {
|
||
body:has(#sidebar):not(.editing) figure:has(.wide) {
|
||
margin-inline: -13.25rem 0;
|
||
}
|
||
}
|
||
|
||
figcaption {
|
||
color: var(--muted);
|
||
line-height: 1;
|
||
font-size: 0.85rem;
|
||
hyphens: auto;
|
||
-webkit-hyphens: auto;
|
||
text-wrap: pretty;
|
||
text-align: left;
|
||
/* Never let a long caption stretch a shrink-to-fit figure wider than the
|
||
image; the caption wraps at the figure's width instead. */
|
||
width: 0;
|
||
min-width: 100%;
|
||
}
|
||
|
||
/* Full-bleed figures: center the caption, shrink-wrapped to its text and
|
||
capped at a readable width. The min-width shrink-wrap trick above would
|
||
win over max-width (min beats max per spec), so it is reset here. */
|
||
figure:has(.wide) figcaption {
|
||
width: fit-content;
|
||
min-width: 0;
|
||
max-width: 65ch;
|
||
margin-inline: auto;
|
||
}
|
||
|
||
/* Section headings start below any floated figures of the previous
|
||
section, so images never overflow into the next one. */
|
||
article h1,
|
||
article h2 {
|
||
clear: both;
|
||
}
|
||
|
||
/* Narrow windows with a sidebar: below 102rem the symmetric gutters can no
|
||
longer both hold the 12rem sidebar, so reserve its space with a fixed
|
||
left track instead of letting it overlap the article. The article then
|
||
always starts at 12rem (+ main's 1.25rem padding), which the matching
|
||
.wide breakout rule in the images section relies on. Scoped by
|
||
:has(#sidebar) since the sidebar element is omitted entirely on pages
|
||
without sub-navigation. */
|
||
@media (max-width: 102rem) {
|
||
body:has(#sidebar):not(.editing) #content {
|
||
grid-template-columns: 12rem minmax(0, 78rem) minmax(0, 1fr);
|
||
}
|
||
|
||
/* Long articles stay fluid here too: same 12rem reservation for the
|
||
sidebar, then the article takes everything to the right viewport
|
||
edge. The article's left edge stays at 12rem either way, so the
|
||
constant .wide breakout margin remains correct. */
|
||
body:has(#sidebar):has(.multicol):not(.editing) #content {
|
||
grid-template-columns: 12rem minmax(0, 1fr);
|
||
}
|
||
}
|
||
|
||
/* Phones and other narrow viewports: single-column layout with the
|
||
sidebar lifted above the article as a wrapping link strip, and no
|
||
floated figures — .left/.right fall back to plain centered figures
|
||
(explicit img widths still shrink-wrap), while .wide keeps its full
|
||
viewport bleed. */
|
||
@media (max-width: 48rem) {
|
||
#content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#sidebar {
|
||
position: static;
|
||
width: auto;
|
||
max-height: none;
|
||
overflow-y: visible;
|
||
border-radius: 0;
|
||
padding: 0.5rem 1rem;
|
||
}
|
||
|
||
/* Only the main level becomes a horizontal wrapping strip; submenus stay
|
||
vertical blocks attached under their parent item. */
|
||
#sidebar > ul {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem 1.2rem;
|
||
}
|
||
|
||
figure:has(.right),
|
||
figure:has(.left) {
|
||
float: none;
|
||
width: 100%;
|
||
max-width: none;
|
||
margin: 0 auto 1.5rem;
|
||
}
|
||
|
||
/* An explicit img width still shrink-wraps the figure (redeclared: this
|
||
block comes after the desktop rule at equal specificity). */
|
||
figure:has(img[width]) {
|
||
width: fit-content;
|
||
}
|
||
|
||
/* The 102rem sidebar/multicol .wide margins assume a left sidebar
|
||
column; with the sidebar on top the article is viewport-wide and the
|
||
plain centered bleed applies again. */
|
||
body:has(#sidebar):not(.editing) figure:has(.wide),
|
||
body:has(.multicol) figure:has(.wide) {
|
||
margin-inline: calc(50% - 50vw);
|
||
}
|
||
}
|
||
|
||
/* Scroll reveal (pagerite.js adds .reveal/.in; JS off = fully visible) */
|
||
.reveal {
|
||
opacity: 0;
|
||
translate: 0 14px;
|
||
transition:
|
||
opacity 0.6s ease,
|
||
translate 0.6s ease;
|
||
}
|
||
|
||
.reveal.in {
|
||
opacity: 1;
|
||
translate: 0 0;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
html {
|
||
scroll-behavior: auto;
|
||
}
|
||
|
||
.reveal {
|
||
opacity: 1;
|
||
translate: none;
|
||
transition: none;
|
||
}
|
||
}
|
||
|
||
.task-list-item {
|
||
list-style: none;
|
||
}
|
||
|
||
/* Published/updated line, expanded from the {dates} tag in the markdown
|
||
(typically placed right after the article's h1). */
|
||
.dateline {
|
||
color: var(--muted);
|
||
font-size: 0.85rem;
|
||
text-align: left;
|
||
}
|
||
|
||
.footnotes {
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* The footer element is kept (the editor host ends above it) but currently
|
||
empty and zero-height. */
|
||
footer {
|
||
padding: 0;
|
||
}
|
||
|
||
/* Rotating-cube page transition, adapted from termotohtori.fi.
|
||
FRAGILE: do not tweak; the view-transition pseudo-tree is picky. */
|
||
::view-transition {
|
||
perspective: 1000px;
|
||
inset: 0;
|
||
}
|
||
|
||
::view-transition-group(root),
|
||
::view-transition-image-pair(root) {
|
||
transform-style: preserve-3d;
|
||
isolation: auto;
|
||
}
|
||
|
||
::view-transition-old(root),
|
||
::view-transition-new(root) {
|
||
mix-blend-mode: normal;
|
||
backface-visibility: hidden;
|
||
animation: none;
|
||
}
|
||
|
||
@keyframes group-rotate {
|
||
to {
|
||
transform: rotateY(-90deg);
|
||
}
|
||
}
|
||
|
||
@keyframes fade-out-a-bit {
|
||
to {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
@keyframes fade-in-a-bit {
|
||
from {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
::view-transition-group(root) {
|
||
transform-origin: 50% 50% -50vw;
|
||
animation: 300ms ease-in-out forwards group-rotate;
|
||
}
|
||
|
||
::view-transition-old(root) {
|
||
animation: 300ms ease-in-out forwards fade-out-a-bit;
|
||
}
|
||
|
||
::view-transition-new(root) {
|
||
transform-origin: 0 0;
|
||
transform: rotateY(90deg);
|
||
inset: 0 auto 0 100%;
|
||
animation: 300ms ease-in-out forwards fade-in-a-bit;
|
||
}
|
||
|
||
/* Reverse direction for browser back navigation (same geometry, mirrored). */
|
||
@keyframes group-rotate-back {
|
||
to {
|
||
transform: rotateY(90deg);
|
||
}
|
||
}
|
||
|
||
html.nav-back::view-transition-group(root) {
|
||
animation-name: group-rotate-back;
|
||
}
|
||
|
||
html.nav-back::view-transition-new(root) {
|
||
transform-origin: 100% 0;
|
||
transform: rotateY(-90deg);
|
||
inset: 0 100% 0 auto;
|
||
}
|
||
|
||
/* Same-section navigation: a plain crossfade instead of the cube. These
|
||
rules only override animation/geometry, leaving the FRAGILE block's
|
||
perspective and layering untouched. The old snapshot stays fully opaque
|
||
underneath while the new one fades in on top — never a dip to black. */
|
||
@keyframes nav-fade-in {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
html.nav-fade::view-transition-group(root) {
|
||
animation: none;
|
||
}
|
||
|
||
html.nav-fade::view-transition-old(root) {
|
||
animation: none;
|
||
}
|
||
|
||
html.nav-fade::view-transition-new(root) {
|
||
transform: none;
|
||
inset: 0;
|
||
animation: 200ms ease-in-out nav-fade-in;
|
||
}
|