Replace the fixed 100rem two-column breakpoint with 'columns: 30rem' so CSS fits as many >=30rem columns as the article's width allows, and let long (.multicol) articles grow past the 78rem cap (4:1 share against the gutters, keeping them symmetric for the .wide breakout math). Also treat mid-article h1s as full-width column separators like h2s.
862 lines
22 KiB
CSS
862 lines
22 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;
|
||
/* Overlay scrollbars where supported (Chromium/WebKit, deprecated but
|
||
working): a scrollbar that appears covers a strip of the layout instead
|
||
of shifting it, and no space is reserved while absent — so 100vw always
|
||
equals the body width and .wide breakout margins stay exact. Ignored by
|
||
Firefox (unknown value), which keeps its thin classic scrollbar. */
|
||
overflow-y: overlay;
|
||
/* Lightweight scrollbar styling: thin, theme-muted thumb on a transparent
|
||
track where supported (Firefox: the two properties; WebKit: the rules
|
||
below). Full restyling deliberately not attempted. */
|
||
scrollbar-width: thin;
|
||
scrollbar-color: color-mix(in srgb, var(--muted) 45%, transparent) transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 0.5rem;
|
||
height: 0.5rem;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: color-mix(in srgb, var(--muted) 45%, transparent);
|
||
border-radius: 0.25rem;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
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. When the page
|
||
sets no banner of its own, the backend inlines the active theme's SVG
|
||
artwork here instead (pagerite/themes/{theme}/banner.svg). */
|
||
#page-banner {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* :not(style, script): author-level display:block would override the UA's
|
||
display:none on those and render their source as banner text. */
|
||
#page-banner>*:not(style, script) {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
/* 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. */
|
||
transform: scale(1.25) translateY(var(--pry, 0px));
|
||
will-change: transform;
|
||
}
|
||
|
||
#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: the center track keeps 78rem as its minimum and then
|
||
takes a 4:1 share of any extra space against each gutter, so the article
|
||
grows without limit and its column count follows its width (see the
|
||
`columns: 30rem` rule below). The gutters stay symmetric, so the .wide
|
||
breakout math (50% − 50vw) still centers on the viewport — and at very
|
||
wide windows the gutters still hold the 12rem sidebar. */
|
||
body:has(.multicol) #content {
|
||
grid-template-columns: minmax(0, 1fr) minmax(78rem, 4fr) minmax(0, 1fr);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* The editor host lives inside #content: it starts below the banner and
|
||
ends above the footer. The panel itself sticks to the viewport while
|
||
scrolling (but never taller than the content area). */
|
||
.editor-host {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
left: calc(0px - var(--editor-w));
|
||
width: var(--editor-w);
|
||
}
|
||
|
||
.editor-root.overlay {
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
max-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%);
|
||
}
|
||
}
|
||
|
||
/* The banner's own pen: opens the site editor (banner + structure).
|
||
Qualified with `button` to beat the later .edit-link rule's left offset
|
||
(both classes apply to the same element). */
|
||
button.banner-edit-link {
|
||
position: absolute;
|
||
top: 0.6rem;
|
||
right: 1.25rem;
|
||
left: auto;
|
||
z-index: 10;
|
||
font: inherit;
|
||
border: none;
|
||
cursor: pointer;
|
||
background: none;
|
||
padding: 0;
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
#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 {
|
||
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 buttons injected by pagerite.js when Paskia SSO is in use.
|
||
Both sit in the banner's top-right corner, to the right of the site pen,
|
||
and are styled like the edit pens. */
|
||
button.login-link,
|
||
button.profile-link {
|
||
position: absolute;
|
||
top: 0.6rem;
|
||
right: 0.5rem;
|
||
z-index: 10;
|
||
font: inherit;
|
||
border: none;
|
||
cursor: pointer;
|
||
background: none;
|
||
padding: 0;
|
||
opacity: 0.7;
|
||
text-shadow: 0 0 0.1em black;
|
||
}
|
||
|
||
button.login-link:hover,
|
||
button.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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
table {
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
border: 1px solid var(--line);
|
||
padding: 0.35rem 0.8rem;
|
||
}
|
||
|
||
/* Images and figures */
|
||
img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
figure {
|
||
margin: 0 0 1.5rem;
|
||
}
|
||
|
||
figcaption {
|
||
color: var(--muted);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Captions of full-bleed images: centered and kept to a readable width. */
|
||
figure:has(.wide) figcaption {
|
||
max-width: 65ch;
|
||
margin-inline: auto;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Positioning via brace-attribute classes: {.right}, {.left}, {.wide} */
|
||
figure:has(.right),
|
||
img.right {
|
||
float: right;
|
||
margin: 0.3rem 0 1rem 1.5rem;
|
||
}
|
||
|
||
figure:has(.left),
|
||
img.left {
|
||
float: left;
|
||
margin: 0.3rem 1.5rem 1rem 0;
|
||
}
|
||
|
||
/* .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). */
|
||
figure:has(.wide),
|
||
img.wide {
|
||
display: block;
|
||
/* no inline strut/descender gaps around the image */
|
||
width: 100vw;
|
||
max-width: none;
|
||
margin-inline: calc(50% - 50vw);
|
||
/* negative margins also kill the UA figure margin */
|
||
/* Follow the docked editor's margin-left transition smoothly. */
|
||
transition: margin-left 0.25s ease;
|
||
}
|
||
|
||
/* A paragraph wrapping only a wide image must not add its line height. */
|
||
p:has(> img.wide:only-child) {
|
||
line-height: 0;
|
||
}
|
||
|
||
/* A captioned image is a <figure>, and the positioning class lives on the
|
||
img inside it: only the figure may float/shift, never the img itself,
|
||
or the caption would wrap beside the image instead of sitting below.
|
||
The image fills the figure's width. */
|
||
figure:has(.right) img,
|
||
figure:has(.left) img {
|
||
float: none;
|
||
margin: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
figure:has(.wide) img {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
margin: 0;
|
||
}
|
||
|
||
/* Floats take a fixed share of the text column rather than sizing by the
|
||
image's intrinsic width, which varies wildly (SVGs have none, and
|
||
min-content collapses them). A percentage also scales correctly inside
|
||
multi-column segments, where the column is the containing block. The
|
||
caption wraps within that width. A width attribute ({.right width=300})
|
||
overrides the default on uncaptioned images. */
|
||
figure:has(.right),
|
||
figure:has(.left),
|
||
img.right:not([width]),
|
||
img.left:not([width]) {
|
||
width: 30%;
|
||
}
|
||
|
||
/* 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), so the .wide breakout
|
||
margin is a plain constant. 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 uncapped here too: same 12rem reservation for the
|
||
sidebar, but the center track keeps growing (see the .multicol rule
|
||
at #content). The article's left edge stays at 12rem either way, so
|
||
the constant .wide breakout margin below remains correct. */
|
||
body:has(#sidebar):has(.multicol):not(.editing) #content {
|
||
grid-template-columns: 12rem minmax(78rem, 4fr) minmax(0, 1fr);
|
||
}
|
||
|
||
body:has(#sidebar) figure:has(.wide),
|
||
body:has(#sidebar) img.wide:not(figure img) {
|
||
margin-inline: -13.25rem 0;
|
||
}
|
||
}
|
||
|
||
/* Editing: shrink the bleed to the space right of the docked editor. */
|
||
body.editing figure:has(.wide),
|
||
body.editing img.wide:not(figure img) {
|
||
width: calc(100vw - var(--editor-w));
|
||
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2);
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
.footnote {
|
||
font-size: 0.85rem;
|
||
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;
|
||
}
|