1607 lines
48 KiB
CSS
1607 lines
48 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);
|
||
/* Target x-height ratio for code text: set to the body font's ratio
|
||
(here Source Sans 3's 0.478) so font-size-adjust can scale the code
|
||
font to the same optical height. Themes retune it to their body font
|
||
(measured: Literata 0.507, Inter 0.546, Montserrat 0.517, Cause 0.5). */
|
||
--code-x-height: 0.478;
|
||
/* 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;
|
||
}
|
||
|
||
::selection {
|
||
background: color-mix(var(--accent) 30%, transparent);
|
||
color: inherit;
|
||
}
|
||
|
||
/* Links never underline — including SVG link text, which the UA stylesheet
|
||
underlines by default. */
|
||
a {
|
||
text-decoration: none;
|
||
}
|
||
|
||
html {
|
||
scroll-behavior: smooth;
|
||
/* No rubber-band bounce past the page ends (macOS trackpads): the banner
|
||
parallax in --pry is driven by scrollY and overscroll would let the
|
||
artwork drift beyond its designed range. */
|
||
overscroll-behavior: none;
|
||
/* 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;
|
||
/* Height scales down proportionally on small screens: 13rem at 800px
|
||
(50rem) viewport, shrinking with the smaller of viewport width/height
|
||
(vmin) below that, floored at 8rem. */
|
||
height: clamp(8rem, 26vmin, 13rem);
|
||
box-sizing: content-box;
|
||
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;
|
||
/* Scales down proportionally on small screens, same curve as the banner
|
||
height: 2.4rem at 800px, shrinking with vmin below that. */
|
||
font-size: clamp(1.4rem, 4.8vmin, 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;
|
||
/* Shrink-wrap to the text: as a flex child of the column-direction
|
||
#banner it would otherwise stretch full-width, making the empty banner
|
||
area beside the text a link to the front page. */
|
||
align-self: flex-start;
|
||
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);
|
||
}
|
||
|
||
/* Long articles (.multicol comes from the backend render, based on
|
||
content length — code excluded) lift the 78rem cap: main takes the full
|
||
width and the article composes itself inside it — fluid, bounded text
|
||
lanes with the surplus left vacant (see the article layout rules
|
||
below). The left track — main always sits in column 2 — collapses to
|
||
zero when the page has no sidebar; the sidebar then simply overlays
|
||
the vacant zone, as it does on single-column pages. */
|
||
body:has(.multicol) #content {
|
||
grid-template-columns: 0 minmax(0, 1fr);
|
||
}
|
||
|
||
/* With a sidebar the left lane gets its own track at every width, so the
|
||
sidebar never overlaps the article and the article leans on the
|
||
viewport's right edge (surplus extends the lane). The lane is flexible:
|
||
12rem when space is tight, growing up to 150% (18rem) once the viewport
|
||
exceeds the article's 88.5rem (86rem + main's side padding). The --lane
|
||
variable doubles as the measure for the margin boxes and the .wide
|
||
bleed below. (Inert below 48rem, where #content becomes a flex column
|
||
with the sidebar on top.) */
|
||
body:has(#sidebar):has(.multicol):not(.editing) #content {
|
||
--lane: clamp(12rem, 100vw - 88.5rem, 18rem);
|
||
grid-template-columns: var(--lane) minmax(0, 1fr);
|
||
}
|
||
|
||
body.editing #content {
|
||
margin-left: var(--editor-w);
|
||
/* No padding/gap here: the article area starts flush at the panel's right
|
||
edge so that full-bleed .wide images (anchored to that edge below) line
|
||
up with it exactly. */
|
||
}
|
||
|
||
/* The sidebar's gutter space is needed by the editor instead. */
|
||
body.editing #sidebar {
|
||
display: none;
|
||
}
|
||
|
||
/* While editing, the window keeps scrolling normally (the overlay
|
||
scrollbars take no layout space, so the vw-based .wide bleed stays exact
|
||
and no horizontal scrollbar appears). The editor panel is fixed to the
|
||
viewport's left edge; main.js sets its top each scroll frame — the
|
||
banner's bottom edge while the banner is visible, else the viewport top.
|
||
The panel scrolls internally. */
|
||
.editor-host {
|
||
position: fixed;
|
||
top: 0;
|
||
/* main.js: banner bottom while visible, else 0 */
|
||
bottom: 0;
|
||
left: 0;
|
||
width: var(--editor-w);
|
||
/* Above the sidebar, .edit-link and the banner's top-right pens
|
||
(z-index 10) while sliding in/out (the host now lives at the end of
|
||
<body>, so it needs its own stacking level). */
|
||
z-index: 10;
|
||
}
|
||
|
||
.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: 1.8em;
|
||
}
|
||
|
||
#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;
|
||
/* The layout container for the article composition: the multicol stage
|
||
(lane count), the margin fall and the .wide bleed respond to the
|
||
actual available width here — editor inset included — via container
|
||
queries and cqw units. */
|
||
container-type: inline-size;
|
||
}
|
||
|
||
/* Child-entry card stacks: a category page (and the content-less category
|
||
404) lists its published children after the markdown content — one column
|
||
per child, the child's whole subtree flattened into the column in menu
|
||
order (see _cards in views.py). The row bleeds to full page width
|
||
(div.wide): the columns first grow to fill it, then shrink rather than
|
||
wrap. Every card has the same fixed 16/10 shape, covered entirely by the
|
||
page's share image (og:image heuristics, as a background — a gradient
|
||
placeholder when it has none) with the title overlaid on a translucent
|
||
band at the bottom. The card is one <a> holding only phrasing-level
|
||
spans; the spans lay out as blocks. */
|
||
.cards {
|
||
display: flex;
|
||
/* Stacks stop growing at their cap; center the row in the bleed then. */
|
||
justify-content: center;
|
||
gap: 1.25rem;
|
||
margin-top: 2.5rem;
|
||
/* The full-bleed breakout (div.wide rules) lands the edges exactly on the
|
||
viewport's; this keeps the cards themselves off the edges. */
|
||
padding-inline: 1.25rem;
|
||
}
|
||
|
||
.cards .stack {
|
||
/* Grow to fill the row (up to the cap — full-width rows would make huge
|
||
cards), shrink (not wrap) when there are too many. */
|
||
flex: 1 1 0;
|
||
max-width: 24rem;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.25rem;
|
||
}
|
||
|
||
/* Phones: the columns stack vertically instead of shrinking to slivers.
|
||
Text-only cards (gradient cover + description) then fit their content —
|
||
the fixed 16/10 shape only makes sense for image covers. */
|
||
@media (max-width: 48rem) {
|
||
.cards {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.card:has(.desc) {
|
||
aspect-ratio: auto;
|
||
}
|
||
}
|
||
|
||
.card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
aspect-ratio: 16 / 10;
|
||
/* Allow shrinking below the text's min-content width: without this the
|
||
text cards hold their stack wider than the image-only stacks. */
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
border-radius: 0.5rem;
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
box-shadow: 0 0 0.1rem black;
|
||
}
|
||
|
||
.card:hover {
|
||
transform: scale(1.02);
|
||
box-shadow: 0 0 0.3rem black;
|
||
}
|
||
|
||
/* The cover fills the whole card; the text spans are positioned so they
|
||
paint above it. */
|
||
.card .cover {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(135deg, var(--surface), var(--bg));
|
||
background-size: cover;
|
||
background-position: center;
|
||
}
|
||
|
||
.card .title {
|
||
position: relative;
|
||
display: block;
|
||
margin-top: auto;
|
||
padding: 0.75rem 1.25rem 0.9rem;
|
||
background: color-mix(var(--bg) 40%, transparent);
|
||
/* Pinned: the article a:hover accent must not leak through the card. */
|
||
color: var(--text);
|
||
font-family: var(--font-heading);
|
||
font-size: 1.25rem;
|
||
font-weight: 900;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
/* Image-less cards carry the description under the title, extending the
|
||
same translucent band. */
|
||
.card .title:has(+ .desc) {
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.card .desc {
|
||
position: relative;
|
||
display: block;
|
||
overflow: hidden;
|
||
/* Four lines exactly: the text itself is truncated server-side (_cards),
|
||
this is just the safety net. max-height spans the lines plus the
|
||
vertical padding (border-box), so nothing bleeds past the clip. */
|
||
line-height: 1.4;
|
||
max-height: calc(4 * 1.4em + 1.3rem);
|
||
padding: 0.4rem 1.25rem 0.9rem;
|
||
background: color-mix(var(--bg) 30%, transparent);
|
||
color: var(--muted);
|
||
font-size: 0.95rem;
|
||
text-align: left;
|
||
hyphens: none;
|
||
}
|
||
|
||
article h1,
|
||
article h2,
|
||
article h3,
|
||
article h4,
|
||
article h5,
|
||
article h6 {
|
||
font-family: var(--font-heading);
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
article h1 {
|
||
font-size: 2.2rem;
|
||
margin: 0 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,
|
||
article h2,
|
||
article h3,
|
||
article h4,
|
||
article h5,
|
||
article h6 {
|
||
margin-top: 0;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
/* Headings separate from the text above via a top margin on the sibling
|
||
combinator: a heading that is the first child of a container (an aside,
|
||
an admonition, a .colseg column segment, ...) gets no gap, and browsers
|
||
truncate the margin at column breaks, so column tops stay aligned. The
|
||
one exception is a page-top heading: main carries no top padding, so a
|
||
leading h1/h2 keeps its spacing from the banner. */
|
||
article h3,
|
||
article h4,
|
||
article h5,
|
||
article h6 {
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
article * + h1,
|
||
article > h1:first-child {
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
article * + h2,
|
||
article > h2:first-child {
|
||
margin-top: 2.2rem;
|
||
}
|
||
|
||
/* A section heading right after the page title needs no separator of its
|
||
own — the h1's bottom margin already sets it off. */
|
||
article h1 + h2 {
|
||
margin-top: 0;
|
||
}
|
||
|
||
article * + h3,
|
||
article * + h4,
|
||
article * + h5,
|
||
article * + h6 {
|
||
margin-top: 1.4rem;
|
||
}
|
||
|
||
/* Margin boxes are compact: the section-separator gaps above also reach
|
||
headings inside .aside/.margin (they follow the box's paragraphs, so
|
||
the sibling combinator matches) — bring them back to plain block
|
||
spacing. First-child headings keep their zero via the sibling
|
||
combinator, as everywhere. */
|
||
.aside * + :is(h1, h2, h3, h4, h5, h6),
|
||
.margin * + :is(h1, h2, h3, h4, h5, h6) {
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
/* Lists: small diamond emoji markers — blue 🔹 on odd nesting levels,
|
||
orange 🔸 on even; ordered lists get muted counters (an explicit
|
||
[type] attribute keeps the browser's native markers). The marker box
|
||
spans the whole --list-indent (outdented from the text column,
|
||
centered), so the space on both sides of the glyph is equal whatever
|
||
its advance width — emoji diamonds, chevrons, flowers, whatever a
|
||
theme swaps in. The box is em-sized so it tracks the local text size
|
||
(e.g. smaller in asides); a theme that shrinks the glyph via
|
||
font-size must widen the box by the inverse factor (see the purple
|
||
theme) to keep it spanning the indent. */
|
||
article :is(ul, ol:not([type])) {
|
||
list-style: none;
|
||
padding-left: 0;
|
||
--list-indent: 2em;
|
||
}
|
||
|
||
article :is(ul, ol:not([type])) > li {
|
||
padding-left: var(--list-indent);
|
||
}
|
||
|
||
article ul li::before {
|
||
content: "🔹";
|
||
display: inline-block;
|
||
margin-left: calc(-1 * var(--list-indent));
|
||
width: var(--list-indent);
|
||
text-align: center;
|
||
}
|
||
|
||
article ul ul li::before {
|
||
content: "🔸";
|
||
}
|
||
|
||
article ol:not([type]) {
|
||
list-style: none;
|
||
counter-reset: item;
|
||
}
|
||
|
||
article ol:not([type]) > li {
|
||
counter-increment: item;
|
||
}
|
||
|
||
article ol:not([type]) > li::before {
|
||
content: counter(item) ".";
|
||
color: var(--muted);
|
||
display: inline-block;
|
||
margin-left: calc(-1 * var(--list-indent));
|
||
width: var(--list-indent);
|
||
text-align: left;
|
||
}
|
||
|
||
/* Task lists: real clickable checkboxes. The checkbox stands in for the
|
||
list marker — taken out of flow, left-aligned in the indent box and
|
||
centered on the first line's middle, so the item text starts at the
|
||
same edge as every other list item's. */
|
||
article .task-list-item {
|
||
position: relative;
|
||
}
|
||
|
||
article .task-list-item::before {
|
||
content: none;
|
||
}
|
||
|
||
article .task-list-item-checkbox {
|
||
position: absolute;
|
||
left: 0;
|
||
top: calc(0.5lh - .15ex);
|
||
translate: 0 -50%;
|
||
font-size: inherit;
|
||
line-height: inherit;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* The item text is a <label for> the checkbox (tasklists label_after):
|
||
clicking it toggles too — without selecting the text on repeat clicks. */
|
||
article .task-list-item label {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* Section pens sit at the end of anchored h2s, dimmer than the page pen
|
||
until hovered. */
|
||
article h2 .edit-section {
|
||
position: static;
|
||
font-size: 0.85rem;
|
||
vertical-align: 0.35em;
|
||
margin-left: 0.4rem;
|
||
opacity: 0.35;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* The long-article composition (.multicol): a fluid but bounded text
|
||
lane with a 16rem side zone at the article's left, centered in main —
|
||
surplus width becomes vacant space, never endless text (with a sidebar
|
||
the article leans right instead and the sidebar's track is the left
|
||
lane; see below). (The backend render splits the body into .colseg
|
||
segments separated by full-width h2s and .wide elements, tags
|
||
text-heavy segments of several paragraphs .cols — a ::: nocols
|
||
container opts its section out, and column-filling paragraphs are
|
||
marked .breakable — and flags the article .multicol; CSS owns the
|
||
geometry.) Technical content wants a wide lane: up to 42rem single, or
|
||
two fluid lanes (36rem minimum, never more than two) once they fit
|
||
beside the zone, capped at 102rem total. The zone — the region the nav
|
||
sidebar overlays — is a margin indent on the lane content; margin
|
||
boxes float into it, and the text never moves. */
|
||
article.multicol {
|
||
margin-inline: auto;
|
||
max-width: 58rem; /* 42rem lane + 16rem zone */
|
||
}
|
||
|
||
@container (min-width: 45rem) {
|
||
/* The side zone (not on phones): lane content indents 16rem; margin
|
||
boxes ({.margin} / ::: margin blocks, ::: aside, {.margin} figures)
|
||
float at the article's left edge — the same region the nav sidebar
|
||
overlays. Scoped to direct article children (the backend render keeps
|
||
margin blocks out of the column segments); nested ones keep the
|
||
in-column float fallback. */
|
||
article.multicol>.colseg,
|
||
article.multicol>h1,
|
||
article.multicol>h2 {
|
||
margin-left: 16rem;
|
||
}
|
||
|
||
article.multicol>.margin,
|
||
article.multicol>.aside,
|
||
article.multicol>figure:has(.margin) {
|
||
float: left;
|
||
clear: left;
|
||
width: 14rem;
|
||
max-width: none;
|
||
margin: 0.3rem 2rem 1rem 0;
|
||
}
|
||
|
||
/* Wide separators start below any margin box — their bleed must not
|
||
wrap around it. */
|
||
article.multicol>figure:has(.wide),
|
||
article.multicol>div.wide,
|
||
article.multicol>pre.wide {
|
||
clear: left;
|
||
}
|
||
}
|
||
|
||
@container (min-width: 96rem) {
|
||
article.multicol {
|
||
max-width: 102rem;
|
||
}
|
||
|
||
/* Two fluid lanes (36rem minimum) beside the zone, up to the 102rem
|
||
cap — wider windows just add vacant space. */
|
||
.multicol .colseg.cols {
|
||
columns: 36rem 2;
|
||
column-gap: 3.5rem;
|
||
column-rule: 1px solid var(--line);
|
||
}
|
||
}
|
||
|
||
/* With a sidebar, the sidebar's 12rem track IS the left lane at every
|
||
width (see #content): no in-article zone, the text lane runs fluid (up
|
||
to 86rem) and leans on main's right edge — surplus width extends the
|
||
left lane instead of balancing out on the right — and margin boxes
|
||
hang into the lane off the article's left border, sliding under the
|
||
translucent sticky nav, which only ever occupies its top. (Not below
|
||
48rem: there the sidebar becomes a link strip above the article and
|
||
there is no lane to fall into.) */
|
||
@media (min-width: 48rem) {
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol {
|
||
max-width: 86rem;
|
||
margin-inline: auto 0;
|
||
}
|
||
|
||
/* The sidebar fills the flexible lane (its left side stays on the
|
||
viewport's left edge, growing rightward). */
|
||
body:has(#sidebar):has(.multicol):not(.editing) #sidebar {
|
||
width: 100%;
|
||
}
|
||
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.colseg,
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>h1,
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>h2 {
|
||
margin-left: 0;
|
||
}
|
||
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.margin,
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.aside,
|
||
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>figure:has(.margin) {
|
||
float: left;
|
||
clear: left;
|
||
/* Attached to the article's left border (1.25rem gap), hanging into
|
||
the left lane and growing leftward with it: 12rem when the lane is
|
||
tight, up to 150% (18rem) when the track or the surplus has room
|
||
(100cqw - 100% is the surplus left of the right-leaning article).
|
||
The lane (track + main's padding) always guarantees the room. */
|
||
--box-w: min(18rem, var(--lane) + 100cqw - 100% - 1.25rem);
|
||
width: var(--box-w);
|
||
max-width: none;
|
||
margin: 0.3rem 0 1rem calc(-1.25rem - var(--box-w));
|
||
}
|
||
}
|
||
|
||
/* A shrink-wrapped figure (explicit image width) centers in the plain
|
||
layout; inside a column the centering looks adrift — left-align.
|
||
Floated figures keep their own margins (the text gap). */
|
||
.multicol .colseg.cols figure:has(img[width]):not(:has(.left), :has(.right), :has(.margin)) {
|
||
margin-inline: 0;
|
||
}
|
||
|
||
.multicol .colseg {
|
||
margin-bottom: 1rem;
|
||
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
break-after: avoid-column;
|
||
}
|
||
|
||
p,
|
||
li {
|
||
break-inside: avoid-column;
|
||
}
|
||
|
||
/* Column-filling paragraphs (marked .breakable by the backend render)
|
||
are allowed to split — keeping them whole would strand the following
|
||
content in a column of its own. */
|
||
p.breakable {
|
||
break-inside: auto;
|
||
}
|
||
|
||
figure,
|
||
pre,
|
||
blockquote,
|
||
table,
|
||
ul,
|
||
ol,
|
||
dl,
|
||
.admonition,
|
||
.markdown-alert {
|
||
break-inside: avoid;
|
||
}
|
||
}
|
||
|
||
article h2 {
|
||
font-size: 1.5rem;
|
||
/* Bottom-only like every other block (margin-top: 0 comes from the group
|
||
above); the section-separator top margin lives in the sibling rule, and
|
||
a shorthand here would tie on specificity and clobber it. */
|
||
margin-bottom: 0.6rem;
|
||
}
|
||
|
||
/* Heading self-links (section anchors) look exactly like plain heading
|
||
text — click sets the hash, right-click copies the link. */
|
||
article :is(h1, h2) a.anchor,
|
||
article :is(h1, h2) a.anchor:hover {
|
||
color: inherit;
|
||
font-weight: inherit;
|
||
}
|
||
|
||
/* Anchored headings clear the fixed/sticky top nav when scrolled to. */
|
||
article :is(h1, h2)[id] {
|
||
scroll-margin-top: 4rem;
|
||
}
|
||
|
||
article a {
|
||
color: var(--link);
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
}
|
||
|
||
article a:hover {
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* Blockquotes: spacing comes from the blockquote itself (bottom-only like
|
||
everything else in articles); inner paragraphs keep only the gap between
|
||
them. 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;
|
||
}
|
||
|
||
blockquote p + p {
|
||
margin-top: 0.6rem;
|
||
}
|
||
|
||
/* Admonitions (markdown !!! note/warning/...) and GitHub-style alerts
|
||
(> [!NOTE] ...): a lightweight callout in the blockquote idiom — accent
|
||
bar and a faint wash, recolored per type, with a type emoji on the
|
||
title. The negative left margin pushes bar and wash out past the text
|
||
edge so the inner text aligns with surrounding paragraphs — same trick
|
||
as blockquotes and code blocks (margin-left = border + padding-left).
|
||
Bottom-only margins like everything else in articles; inner paragraphs
|
||
carry no margins of their own. */
|
||
.admonition,
|
||
.markdown-alert {
|
||
margin: 0 0 1rem -1.15rem;
|
||
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,
|
||
.markdown-alert> :last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.admonition-title,
|
||
.markdown-alert-title {
|
||
margin: 0 0 0.2rem;
|
||
font-weight: 600;
|
||
color: var(--admonition-color, var(--accent));
|
||
}
|
||
|
||
.admonition-title::before,
|
||
.markdown-alert-title::before {
|
||
padding-right: 0.35em;
|
||
}
|
||
|
||
.admonition.note .admonition-title::before,
|
||
.markdown-alert-note .markdown-alert-title::before {
|
||
content: "ℹ️";
|
||
}
|
||
|
||
.admonition.tip .admonition-title::before,
|
||
.admonition.hint .admonition-title::before,
|
||
.markdown-alert-tip .markdown-alert-title::before {
|
||
content: "✨";
|
||
}
|
||
|
||
.admonition.important .admonition-title::before,
|
||
.markdown-alert-important .markdown-alert-title::before {
|
||
content: "❗";
|
||
}
|
||
|
||
.admonition.success .admonition-title::before {
|
||
content: "✅";
|
||
}
|
||
|
||
.admonition.warning .admonition-title::before,
|
||
.markdown-alert-warning .markdown-alert-title::before {
|
||
content: "⚠️";
|
||
}
|
||
|
||
.admonition.caution .admonition-title::before,
|
||
.markdown-alert-caution .markdown-alert-title::before {
|
||
content: "🔥";
|
||
}
|
||
|
||
.admonition.danger .admonition-title::before,
|
||
.admonition.failure .admonition-title::before {
|
||
content: "⛔";
|
||
}
|
||
|
||
.admonition.tip,
|
||
.admonition.important,
|
||
.admonition.hint,
|
||
.admonition.success,
|
||
.markdown-alert-tip,
|
||
.markdown-alert-important {
|
||
--admonition-color: var(--accent2);
|
||
}
|
||
|
||
.admonition.warning,
|
||
.admonition.caution,
|
||
.admonition.danger,
|
||
.admonition.failure,
|
||
.markdown-alert-warning,
|
||
.markdown-alert-caution {
|
||
--admonition-color: var(--accent3);
|
||
}
|
||
|
||
/* Side boxes: ::: aside is a muted floated box (consecutive asides stack
|
||
via clear: left); {.margin} / ::: margin is a plainer margin note, and
|
||
figures take {.margin} like {.left}. On multicol pages they float in
|
||
the composition's left side zone — or in the sidebar's track when the
|
||
layout reserves one (see the article section); on wide single-column
|
||
pages they lean into the left gutter (with the figure rules below);
|
||
otherwise they stay in-column left floats. Headings already clear
|
||
floats, so boxes never bleed into the next section. */
|
||
.aside {
|
||
float: left;
|
||
clear: left;
|
||
width: 30%;
|
||
max-width: 20rem;
|
||
margin: 0.3rem 1.2rem 1rem 0;
|
||
padding: 0.6rem 0.9rem;
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
background: color-mix(in srgb, var(--accent) 6%, transparent);
|
||
border-radius: 0.3rem;
|
||
}
|
||
|
||
.aside> :last-child,
|
||
.margin> :last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* A figure inside an aside spans the box to its borders (negative
|
||
margins matching the box's padding); the caption keeps the box's
|
||
padding for itself. Explicit-width images keep their shrink-wrap. */
|
||
.aside>figure:not(:has(img[width])) {
|
||
width: auto;
|
||
margin: -0.6rem -0.9rem 1rem;
|
||
}
|
||
|
||
.aside>figure figcaption {
|
||
padding: 0.6rem 0.9rem 0;
|
||
}
|
||
|
||
.aside>figure:last-child figcaption {
|
||
padding-bottom: 0.6rem;
|
||
}
|
||
|
||
.margin {
|
||
float: left;
|
||
clear: left;
|
||
width: 30%;
|
||
max-width: 20rem;
|
||
margin: 0.3rem 1.2rem 1rem 0;
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
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. Instead of a
|
||
fixed em shrink (which can't fit every body/code font pairing — Fira
|
||
Code's x-height ratio 0.525 is taller than Source Sans 3's 0.478 yet
|
||
shorter than Inter's 0.546), font-size-adjust scales whatever code font
|
||
is in use so its x-height matches the body font's ratio. Browsers
|
||
without font-size-adjust get unadjusted 1em code, which is fine. */
|
||
code {
|
||
font-family: var(--font-code);
|
||
font-size-adjust: ex-height var(--code-x-height);
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* {.margin} figures float left like {.left} ones — until they fall into
|
||
the side zone (see the composition rules up in the article section). */
|
||
figure:has(.margin) {
|
||
float: left;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 1em 1rem 0;
|
||
}
|
||
|
||
/* Wide single-column pages: margin boxes lean into the vacant left
|
||
gutter instead (below 104rem the gutter cannot hold the box, and while
|
||
editing the docked panel reshapes the gutters — in both they stay
|
||
plain floats). The box grows with the gutter up to 150% (18rem), its
|
||
right side 1.25rem off the article's left border. */
|
||
@media (min-width: 104rem) {
|
||
body:not(.editing):not(:has(.multicol)) article>.margin,
|
||
body:not(.editing):not(:has(.multicol)) article>.aside,
|
||
body:not(.editing):not(:has(.multicol)) article>figure:has(.margin) {
|
||
float: left;
|
||
clear: left;
|
||
--box-w: min(18rem, (100vw - 78rem) / 2 - 1.25rem);
|
||
width: var(--box-w);
|
||
max-width: none;
|
||
margin: 0.3rem 0 1rem calc(-1.25rem - var(--box-w));
|
||
}
|
||
}
|
||
|
||
/* In the wide symmetric gutters (where the sidebar overlays the flexible
|
||
left gutter rather than a reserved track) the sidebar flexes with the
|
||
gutter up to 150% — its left side stays on the viewport's edge. */
|
||
@media (min-width: 102rem) {
|
||
body:not(.editing) #sidebar {
|
||
width: min(18rem, 100%);
|
||
}
|
||
}
|
||
|
||
/* .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), and code blocks via a trailing {.wide}
|
||
line (fence block attrs land on <pre> itself). */
|
||
figure:has(.wide),
|
||
div.wide,
|
||
pre.wide {
|
||
width: 100vw;
|
||
max-width: none;
|
||
margin-inline: calc(50% - 50vw);
|
||
}
|
||
|
||
/* Full bleed means edge to edge — no rounded corners. */
|
||
figure:has(.wide) img,
|
||
pre.wide {
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* Editing: shrink the bleed to the space right of the docked editor. The
|
||
window keeps its overlay scrollbars while editing, so — unlike a classic
|
||
scrollbar — they take no layout space and the vw math stays exact.
|
||
Multicol pages measure the bleed from main instead (cqw rules below),
|
||
which insets for the editor automatically. */
|
||
body.editing figure:has(.wide),
|
||
body.editing div.wide,
|
||
body.editing pre.wide {
|
||
width: calc(100vw - var(--editor-w));
|
||
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2);
|
||
}
|
||
|
||
/* Narrow single-column pages with a sidebar: below 102rem the symmetric
|
||
gutters can no longer both hold the sidebar, so #content reserves it
|
||
with a flexible left track (see the matching media query below) and the
|
||
article always starts at the lane's width (+ main's 1.25rem padding) —
|
||
the bleed margin measures off --lane. 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. (Multicol pages reserve the same track at every
|
||
width — their rules are below.) */
|
||
@media (max-width: 102rem) {
|
||
body:has(#sidebar):not(.editing):not(:has(.multicol)) figure:has(.wide),
|
||
body:has(#sidebar):not(.editing):not(:has(.multicol)) div.wide,
|
||
body:has(#sidebar):not(.editing):not(:has(.multicol)) pre.wide {
|
||
margin-inline: calc(-1.25rem - var(--lane)) 0;
|
||
}
|
||
}
|
||
|
||
/* .wide on multicol pages: the same edge-to-edge bleed, but measured from
|
||
main (the container) instead of the viewport — cqw includes the editor
|
||
inset automatically, so no editing override is needed. The 2.5rem
|
||
covers main's side padding. */
|
||
body:has(.multicol) figure:has(.wide),
|
||
body:has(.multicol) div.wide,
|
||
body:has(.multicol) pre.wide {
|
||
width: calc(100cqw + 2.5rem);
|
||
margin-inline: calc(50% - 50cqw - 1.25rem);
|
||
}
|
||
|
||
/* Multicol with a sidebar track (≥48rem, see #content): main starts at
|
||
the flexible lane's width and the article leans right, so the bleed
|
||
extends left past the surplus and the lane to the true viewport edge —
|
||
sliding under the translucent sidebar — and right past main's
|
||
padding. */
|
||
@media (min-width: 48rem) {
|
||
body:has(#sidebar):has(.multicol):not(.editing) figure:has(.wide),
|
||
body:has(#sidebar):has(.multicol):not(.editing) div.wide,
|
||
body:has(#sidebar):has(.multicol):not(.editing) pre.wide {
|
||
width: calc(100cqw + 2.5rem + var(--lane));
|
||
margin-inline: calc(100% - 100cqw - 1.25rem - var(--lane)) 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 flexible
|
||
left track instead of letting it overlap the article (multicol pages use
|
||
the same track at every width — see the #content rules above; their
|
||
higher-specificity rule wins there). The lane is 12rem when space is
|
||
tight, growing up to 150% (18rem) once the viewport exceeds the
|
||
article's 80.5rem (78rem + main's side padding), and the sidebar fills
|
||
it. The matching .wide breakout rule in the images section measures off
|
||
--lane. 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 {
|
||
--lane: clamp(12rem, 100vw - 80.5rem, 18rem);
|
||
grid-template-columns: var(--lane) minmax(0, 78rem) minmax(0, 1fr);
|
||
}
|
||
|
||
body:has(#sidebar):not(.editing) #sidebar {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* Phones and other narrow viewports: single-column layout with the
|
||
sidebar lifted above the article as a wrapping link strip, and no
|
||
floats at all — .left/.right/.margin figures fall back to plain
|
||
centered figures (explicit img widths still shrink-wrap), margin boxes
|
||
go full width, while .wide keeps its full viewport bleed. */
|
||
@media (max-width: 48rem) {
|
||
|
||
/* Nav type shrinks fluidly as space runs out. The nav font-size is
|
||
em-based both in base and in every theme override, so scaling the
|
||
banner's font-size (nothing else in the banner is em-sized — brand and
|
||
gaps use rem) reaches the nav through all themes with a single rule.
|
||
2.6vw crosses 1rem at ≈38.5rem, so only genuinely narrow viewports
|
||
shrink. */
|
||
#banner {
|
||
font-size: clamp(0.65rem, 2.6vw, 1rem);
|
||
}
|
||
|
||
/* Tighter margins/padding/gaps: the 1.25rem side gutter is wasted space
|
||
on a phone. */
|
||
#brand {
|
||
margin-inline: 0.6rem;
|
||
}
|
||
|
||
#nav {
|
||
padding: 0.25rem 0.6rem;
|
||
}
|
||
|
||
#nav ul {
|
||
gap: 0.15rem 0.9rem;
|
||
}
|
||
|
||
/* The editor panel takes over the entire viewport: no space left for
|
||
the banner or the page content (main.js pins its top to 0 at these
|
||
widths). */
|
||
body.editing #content {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.editor-host {
|
||
width: 100vw;
|
||
}
|
||
|
||
#content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#sidebar {
|
||
position: static;
|
||
width: auto;
|
||
max-height: none;
|
||
overflow-y: visible;
|
||
border-radius: 0;
|
||
padding: 0.4rem 0.8rem;
|
||
/* Smaller type: the horizontal link strip fits roughly a third more
|
||
items per line. The nested-list gaps below are em-based and shrink
|
||
along. */
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
/* 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.3rem 0.75rem;
|
||
}
|
||
|
||
figure:has(.right),
|
||
figure:has(.left),
|
||
figure:has(.margin) {
|
||
float: none;
|
||
width: 100%;
|
||
max-width: none;
|
||
margin: 0 auto 1.5rem;
|
||
}
|
||
|
||
/* Margin boxes go full width too — no room for side floats on a
|
||
phone. */
|
||
.aside,
|
||
.margin {
|
||
float: none;
|
||
width: auto;
|
||
max-width: none;
|
||
margin: 0 0 1rem;
|
||
}
|
||
|
||
/* 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 single-column sidebar .wide margins assume a left sidebar column;
|
||
with the sidebar on top the article is viewport-wide and the plain
|
||
centered bleed applies again. (Multicol pages need no override: their
|
||
cqw bleed is exact at any width.) */
|
||
body:has(#sidebar):not(.editing):not(: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;
|
||
}
|
||
|
||
/* Page transition styles are NOT here: they live in the transition designs
|
||
(pagerite/themes/{cube,crossfade}/transition.css), injected by the backend
|
||
as #pagerite-transition per the site settings. */
|