The admin inbox leaves the short-lived /_m page and joins the visitor panel as one overlay (ContactOverlay shell over paskia's shared blur backdrop, centered dialog, full-screen on mobile): the corner ✉️ icon opens the visitor thread for visitors and the wider sidebar inbox for admins, while {contact:} trigger buttons always open the visitor side so admins can test both. The icon highlights unread state without extra connections: visitors via localStorage seen/last-admin timestamps, admins via the contact_unread count added to the /_api/settings probe, both kept live by the bundle once loaded. MessagesView owns its admin socket only while the overlay is open.
1973 lines
60 KiB
CSS
1973 lines
60 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(var(--text) 50%, var(--accent));
|
||
/* Inline code tint in body paragraphs: halfway between text and muted.
|
||
Both endpoints are theme constants, so the mix is a definite color
|
||
per theme — themes may also pin it outright. Outside paragraphs code
|
||
inherits the context's color (accent headings stay accent). */
|
||
--code-inline: color-mix(var(--text), var(--muted));
|
||
/* Selection fill for page text and the CodeMirror editors; themes
|
||
override when the accent tint clashes with accent-colored text. */
|
||
--selection-bg: color-mix(var(--accent) 30%, transparent);
|
||
/* Referer-badge chip in the analytics viewer: a translucent neutral wash,
|
||
deliberately NOT themed — the chip sits behind transparent favicons, so
|
||
black-on-transparent and white-on-transparent glyphs must both stay
|
||
legible on every theme (a slight whitening keeps black glyphs readable
|
||
on dark bars without a glaring solid-white chip). Being translucent, it
|
||
takes the page's tone, so its text follows the theme's colors. */
|
||
--badge-bg: #aaaaaa44;
|
||
--badge-text: var(--text);
|
||
--badge-muted: var(--muted);
|
||
/* 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: var(--selection-bg);
|
||
color: inherit;
|
||
}
|
||
|
||
/* Text size classes for any block ({.small} {.large} {.huge}, set via the
|
||
format bar or by hand): em units on the single 1rem base scale, so they
|
||
compose with the theme's typography. */
|
||
.small {
|
||
font-size: 0.7em;
|
||
}
|
||
|
||
.large {
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.huge {
|
||
font-size: 3em;
|
||
}
|
||
|
||
/* 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(var(--muted) 45%, transparent) transparent;
|
||
}
|
||
|
||
.os-scrollbar {
|
||
--os-size: 0.5rem;
|
||
--os-thumb-bg: color-mix(var(--muted) 45%, transparent);
|
||
--os-thumb-hover-bg: color-mix(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);
|
||
/* 1rem exactly: the text scale (headings, .small/.large/.huge) keys off
|
||
one base size — no per-context tweaking, or consistent sizing becomes
|
||
impossible. */
|
||
font-size: 1rem;
|
||
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.95em;
|
||
}
|
||
|
||
#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 start gutter (grid columns are
|
||
flow-relative: on RTL pages the whole composition mirrors), so it
|
||
appearing or disappearing never shifts the article; the end 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 start 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 start lane gets its own track at every width, so the
|
||
sidebar never overlaps the article and the article leans on the
|
||
viewport's end 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;
|
||
}
|
||
|
||
/* The corner ✉️ (docs/messages.md) is in the pens row for everyone, so
|
||
needs its own positioning context for the unread markers: the same
|
||
accent dot as the trigger buttons (visitors: a new reply) or a count
|
||
pill (admins: unanswered threads). The marker lifts the icon out of the
|
||
icon-btn dimming so it reads clearly. */
|
||
.contact-icon {
|
||
position: relative;
|
||
}
|
||
|
||
.contact-icon.has-unread {
|
||
opacity: 1;
|
||
}
|
||
|
||
.contact-icon.has-unread::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: -0.2em;
|
||
right: -0.25em;
|
||
width: 0.5em;
|
||
height: 0.5em;
|
||
border-radius: 50%;
|
||
background: var(--accent);
|
||
box-shadow: 0 0 0 0.12em var(--bg);
|
||
}
|
||
|
||
.contact-icon .n {
|
||
position: absolute;
|
||
top: -0.5em;
|
||
right: -0.7em;
|
||
min-width: 1.3em;
|
||
padding: 0.05em 0.3em;
|
||
border-radius: 1em;
|
||
background: var(--accent);
|
||
color: var(--bg);
|
||
font-size: 0.62em;
|
||
font-weight: 700;
|
||
line-height: 1.3;
|
||
text-align: center;
|
||
opacity: 1;
|
||
}
|
||
|
||
#sidebar {
|
||
grid-column: 1;
|
||
/* Pinned to the page's start 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;
|
||
/* The extra inline-start padding (the viewport-edge side, mirroring with
|
||
the direction) matches main's side padding. */
|
||
padding: 1rem;
|
||
padding-inline: 1.25rem 1rem;
|
||
border-end-start-radius: 0.5rem;
|
||
background: color-mix(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-inline-start: -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 cards: a category page (and the content-less category
|
||
404) lists its published children after the markdown content — one
|
||
card per child (a child without a page of its own is represented by
|
||
its first leaf page; see _cards in views.py). The row bleeds to full
|
||
page width (div.wide); every card has the same fixed 16/10 shape,
|
||
scaling only with the available width. Cards come in two modes
|
||
following the child's twitter:card selection (_card_large — the
|
||
per-article override, else the card image's dimensions): large cards
|
||
are covered entirely by the page's card 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; small cards (.card.compact)
|
||
split at the golden ratio: a square cover in the top part with the
|
||
title beside it, the article description below. The card is one <a>
|
||
holding only phrasing-level spans; the
|
||
spans lay out as blocks. */
|
||
/* Equal-width columns (grid, not flex: no differential shrink, no
|
||
cross-axis stretch — every card keeps the fixed 16/10 shape, scaling
|
||
only with the available width). Cards stop growing at their cap; the
|
||
row centers in the bleed then. */
|
||
.cards {
|
||
display: grid;
|
||
grid-auto-flow: column;
|
||
grid-auto-columns: minmax(0, 24em);
|
||
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;
|
||
}
|
||
|
||
/* Phones: the cards stack vertically instead of shrinking to slivers. */
|
||
@media (max-width: 48rem) {
|
||
.cards {
|
||
grid-auto-flow: row;
|
||
grid-auto-columns: unset;
|
||
grid-template-columns: minmax(0, 24em);
|
||
}
|
||
}
|
||
|
||
.card {
|
||
/* Fill the (equal-width) grid column; the height comes only from the
|
||
fixed aspect ratio (align-self: start — the row must not stretch it).
|
||
Overflowing content is clipped. */
|
||
width: 100%;
|
||
align-self: start;
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
aspect-ratio: 16 / 10;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
border-radius: 0.5em;
|
||
/* Lifted from the page background (a touch of the text color) so cards
|
||
read as distinct surfaces. */
|
||
background: color-mix(var(--surface) 88%, var(--text));
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
box-shadow: 0 0 0.1em 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: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
overflow: hidden;
|
||
margin-top: auto;
|
||
padding: 0.75em 1.25em 0.9em;
|
||
background: color-mix(var(--bg) 65%, transparent);
|
||
/* Pinned: the article a:hover accent must not leak through the card. */
|
||
color: var(--text);
|
||
font-family: var(--font-heading);
|
||
font-size: 1.25em;
|
||
font-weight: 900;
|
||
line-height: 1.25;
|
||
/* Hyphenation needs the card's lang (the target article's language). */
|
||
hyphens: auto;
|
||
}
|
||
|
||
/* 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: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 4;
|
||
line-clamp: 4;
|
||
overflow: hidden;
|
||
/* Four lines exactly, with an ellipsis on overflow: the text itself is
|
||
truncated server-side (_cards), this is just the safety net. */
|
||
line-height: 1.4;
|
||
padding: 0.4em 1.25em 0.9em;
|
||
background: color-mix(var(--bg) 55%, transparent);
|
||
color: var(--muted);
|
||
font-size: 0.95em;
|
||
text-align: start;
|
||
hyphens: auto;
|
||
}
|
||
|
||
/* Small cards (_card_large false; the twitter "summary" style): externally
|
||
the same 16/10 shape as the large card, split horizontally in two
|
||
sub-grids at the golden ratio — the top part (the image, its full
|
||
height, with the title beside it at the bottom) takes φ, the bottom
|
||
part (the description — which only the small format carries — above
|
||
spare space) takes 1, so the image's bottom edge sits at ~62% of the
|
||
card instead of centering wherever the differing title/description
|
||
sizes land it. The title and description carry the translucent band
|
||
(the same band color as the large cards' title) as their own
|
||
backgrounds. Imageless cards are just the gradient with the title
|
||
spanning the full width. */
|
||
.card.compact {
|
||
display: grid;
|
||
/* minmax(0, …): the parts never grow past the card — overlong text is
|
||
line-clamped (title/description) instead of pushing the layout. */
|
||
grid-template-rows: minmax(0, 1.618fr) minmax(0, 1fr);
|
||
background: linear-gradient(
|
||
135deg,
|
||
color-mix(var(--surface) 88%, var(--text)),
|
||
var(--bg)
|
||
);
|
||
}
|
||
|
||
/* Top part: spare space above the title row. The image column is as wide
|
||
as the top part is tall — the card is 16/10 and the top is φ/(φ+1) of
|
||
its height, i.e. ~38.6% of its width — so a square image at that width
|
||
fills the whole top part. */
|
||
.card.compact .top {
|
||
display: grid;
|
||
grid-template-columns: 38.6% 1fr;
|
||
grid-template-rows: minmax(0, 1fr) auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* The image fills its whole grid area (column 1, both rows); cover crops
|
||
rather than letterboxes. */
|
||
.card.compact img.cover {
|
||
position: static;
|
||
grid-column: 1;
|
||
grid-row: 1 / 3;
|
||
z-index: 2;
|
||
width: 100%;
|
||
height: 100%;
|
||
margin: 0;
|
||
object-fit: cover;
|
||
background: none;
|
||
}
|
||
|
||
.card.compact .title {
|
||
/* Full width: the translucent band runs behind the image to the left
|
||
edge (the image paints over it, z-index 2), the text indented past
|
||
the image column. */
|
||
grid-column: 1 / -1;
|
||
grid-row: 2;
|
||
z-index: 1;
|
||
margin: 0;
|
||
padding: 0.4em 0.6em 0 calc(38.6% + 0.6em);
|
||
background: color-mix(var(--bg) 65%, transparent);
|
||
}
|
||
|
||
/* No image: no indentation, the text starts at the left edge. */
|
||
.card.compact .top:not(:has(img.cover)) .title {
|
||
padding-left: 0.6em;
|
||
}
|
||
|
||
/* No description: the title supplies the band's bottom padding. */
|
||
.card.compact:not(:has(.desc)) .title {
|
||
padding-bottom: 0.4em;
|
||
}
|
||
|
||
/* Bottom part: the description above spare space. */
|
||
.card.compact .bottom {
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
min-height: 0;
|
||
}
|
||
|
||
.card.compact .desc {
|
||
grid-column: 1 / -1;
|
||
grid-row: 1;
|
||
z-index: 1;
|
||
margin: 0;
|
||
padding: 0.4em 0.6em;
|
||
background: color-mix(var(--bg) 65%, transparent);
|
||
/* Three lines fit the bottom part (⅜ of the card) even at the card cap;
|
||
the line clamp adds the ellipsis past that. */
|
||
-webkit-line-clamp: 3;
|
||
line-clamp: 3;
|
||
overflow: hidden;
|
||
max-height: 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. flow-root makes the list a
|
||
block formatting context: beside a floated figure the whole list box
|
||
clears the float (line boxes alone would dodge it while the marker
|
||
box, outdented from the li's padding edge, stayed under the image),
|
||
so the gap becomes the figure's margin plus the full --list-indent. */
|
||
article :is(ul, ol:not([type])) {
|
||
list-style: none;
|
||
padding-inline-start: 0;
|
||
--list-indent: 2em;
|
||
display: flow-root;
|
||
}
|
||
|
||
article :is(ul, ol:not([type])) > li {
|
||
padding-inline-start: var(--list-indent);
|
||
}
|
||
|
||
article ul li::before {
|
||
content: "🔹";
|
||
display: inline-block;
|
||
margin-inline-start: 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-inline-start: calc(-1 * var(--list-indent));
|
||
width: var(--list-indent);
|
||
text-align: start;
|
||
}
|
||
|
||
/* Task lists: real clickable checkboxes. The checkbox stands in for the
|
||
list marker — taken out of flow, start-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;
|
||
inset-inline-start: 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;
|
||
}
|
||
|
||
/* Emoji/symbol icon buttons and links: dim until hovered. */
|
||
.icon-btn {
|
||
padding: 0;
|
||
font: inherit;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.icon-btn:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* {contact: Label} directive: the button opening the visitor contact
|
||
panel (docs/messages.md). A standalone accent button in the article
|
||
flow; .has-unread (set by contact-main) marks a new admin reply.
|
||
Filled with the accent and hovered like the accent-filled navbar tab:
|
||
text in --ink (nitro; --bg elsewhere), hover switches to the bright
|
||
--tab-hover with the same glow (text-shadow, which fades dimmer, so
|
||
the color stays bright). */
|
||
.contact-trigger {
|
||
position: relative;
|
||
margin: 0.3em 0.1em;
|
||
padding: 0.35em 1.1em;
|
||
font: inherit;
|
||
color: var(--ink, var(--bg));
|
||
background: var(--accent);
|
||
border: 1px solid var(--accent);
|
||
border-radius: 1.2em;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.contact-trigger:hover {
|
||
color: var(--tab-hover, var(--bg));
|
||
text-shadow: 0 0 0.1em;
|
||
}
|
||
|
||
.contact-trigger:focus-visible {
|
||
outline: 2px solid var(--accent);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.contact-trigger.has-unread::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: -0.25em;
|
||
right: -0.25em;
|
||
width: 0.6em;
|
||
height: 0.6em;
|
||
border-radius: 50%;
|
||
background: var(--accent);
|
||
box-shadow: 0 0 0 0.15em var(--bg);
|
||
}
|
||
|
||
.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 */
|
||
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.1em;
|
||
vertical-align: 0.3em;
|
||
margin-inline-start: 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-inline-start: 0.4rem;
|
||
opacity: 0.35;
|
||
}
|
||
|
||
/* Login/profile buttons 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 text-shadow tweak. */
|
||
.editor-pens .login-link,
|
||
.editor-pens .profile-link {
|
||
text-shadow: 0 0 0.1em black;
|
||
}
|
||
|
||
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 start side, centered in
|
||
main — surplus width becomes vacant space, never endless text (with a
|
||
sidebar the article leans to the end edge instead and the sidebar's
|
||
track is the start 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 are placed into it out of flow, 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)
|
||
are taken out of flow and placed against the article's start edge —
|
||
the same region the nav sidebar overlays. The boxes stay in the
|
||
column segment at their anchor point (the backend render no longer
|
||
splits segments around them); absolute positioning off the article —
|
||
always position: relative — pins the horizontal side to the zone
|
||
regardless of any column layout inside, while the unset top keeps
|
||
the box at the vertical position where it occurs in the text. */
|
||
article.multicol>.colseg,
|
||
article.multicol>h1,
|
||
article.multicol>h2 {
|
||
margin-inline-start: 16rem;
|
||
}
|
||
|
||
article.multicol .margin,
|
||
article.multicol .aside,
|
||
article.multicol figure.margin {
|
||
position: absolute;
|
||
inset-inline-start: 0;
|
||
width: 14rem;
|
||
max-width: none;
|
||
margin: 0.3rem 0 0;
|
||
}
|
||
}
|
||
|
||
@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 start lane at every
|
||
width (see #content): no in-article zone, the text lane runs fluid (up
|
||
to 86rem) and leans on main's end edge — surplus width extends the
|
||
start lane instead of balancing out at the end — and margin boxes
|
||
hang into the lane off the article's start 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 start side stays on the
|
||
viewport's start edge, growing toward the article). */
|
||
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-inline-start: 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.margin {
|
||
position: absolute;
|
||
/* Attached to the article's start border (1.25rem gap), hanging into
|
||
the start lane and growing 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 beside the end-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;
|
||
inset-inline-start: calc(-1.25rem - var(--box-w));
|
||
margin: 0.3rem 0 0;
|
||
}
|
||
}
|
||
|
||
/* A shrink-wrapped figure (explicit image width) centers in the plain
|
||
layout; inside a column the centering looks adrift — align to the start
|
||
edge. Floated figures keep their own margins (the text gap). */
|
||
.multicol .colseg.cols figure:has(img[width]):not(:has(.left), :has(.right), .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 start 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;
|
||
margin-inline-start: -0.5rem;
|
||
padding: 0;
|
||
padding-inline-start: 0.25rem;
|
||
border-inline-start: 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 start 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-inline-start = border +
|
||
padding-inline-start). Bottom-only margins like everything else in
|
||
articles; inner paragraphs carry no margins of their own. */
|
||
.admonition,
|
||
.markdown-alert {
|
||
margin: 0 0 1rem;
|
||
margin-inline-start: -1.15rem;
|
||
padding: 0.4rem 0.9rem;
|
||
border-inline-start: 0.25rem solid var(--admonition-color, var(--accent));
|
||
border-start-end-radius: 0.3rem;
|
||
border-end-end-radius: 0.3rem;
|
||
background: color-mix(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-inline-end: 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; {.margin} / ::: margin
|
||
is a plainer margin note, and figures take the class directly on the
|
||
<figure> (the renderer moves it off the img), floating like {.left}.
|
||
Where the layout has room for a side zone — multicol pages, the
|
||
sidebar's track, the wide single-column gutter (see the article
|
||
section and the figure rules below) — the boxes are taken out of flow
|
||
and absolutely positioned into it, off the article's start border, each
|
||
at the vertical spot where it occurs in the text (boxes occurring
|
||
closer together than their heights may overlap — keep them apart);
|
||
otherwise they stay in-column start floats (consecutive floats stack
|
||
via clear: inline-start). Headings already clear floats, so in-column
|
||
boxes never bleed into the next section. */
|
||
.aside {
|
||
float: inline-start;
|
||
clear: inline-start;
|
||
width: 30%;
|
||
max-width: 20rem;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-end: 1.2rem;
|
||
padding: 0.6rem 0.9rem;
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
background: color-mix(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: inline-start;
|
||
clear: inline-start;
|
||
width: 30%;
|
||
max-width: 20rem;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-end: 1.2rem;
|
||
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 leading border must
|
||
extend margin-inline-start by the border width to keep this
|
||
alignment. */
|
||
margin-inline: -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);
|
||
}
|
||
|
||
/* Inline code: a slight nudge toward the muted tone rather than a fixed
|
||
color, so code inside accent-colored headings keeps the heading's hue
|
||
and the distinction stays subtle everywhere. color-mix resolves against
|
||
the inherited color (currentColor in a color declaration refers to the
|
||
inherited value), shifting it 30% toward --muted. */
|
||
code:not(pre code) {
|
||
padding-inline: 0.2em;
|
||
/* Never break inside a code span. word-break: keep-all would not
|
||
suffice: a hard hyphen is an explicit break opportunity (UAX #14),
|
||
which keep-all does not suppress — `--arg` could still split after
|
||
the dashes. Inline code is short, so nowrap is safe here. */
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* In body paragraphs the text color is a known constant (--text), so
|
||
code can take a fixed tint (--code-inline) instead of an unreliable
|
||
mix off the inherited color. A paragraph never wraps pre, so no guard
|
||
is needed here. */
|
||
p code {
|
||
color: var(--code-inline);
|
||
}
|
||
|
||
code:not(pre code):first-child {
|
||
padding-inline-start: 0;
|
||
}
|
||
|
||
/* 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: start;
|
||
background: linear-gradient(180deg,
|
||
var(--table-head-a, color-mix(var(--accent) 10%, var(--surface))),
|
||
var(--table-head-b, color-mix(var(--accent) 18%, var(--surface))));
|
||
}
|
||
|
||
td {
|
||
background: linear-gradient(160deg,
|
||
color-mix(var(--table-tint, var(--accent)) 5%, transparent),
|
||
transparent 75%);
|
||
}
|
||
|
||
tbody tr+tr td {
|
||
border-top: 1px solid color-mix(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 — except {.margin}, which the renderer moves onto
|
||
the figure itself — 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} float to the text column's end/start
|
||
edge — the class names are author-facing and fixed, but the sides follow
|
||
the text direction (in RTL, .left floats right) — defaulting to 30% of
|
||
the column and capped at half of it. */
|
||
figure:has(.right) {
|
||
float: inline-end;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-start: 1em;
|
||
}
|
||
|
||
figure:has(.left) {
|
||
float: inline-start;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-end: 1em;
|
||
}
|
||
|
||
/* The same floats for other blocks: ::: left / ::: right containers
|
||
(rendered div.left/right), paragraphs, code fences, blockquotes and
|
||
tables all take the class directly ({.right} at the end of a
|
||
paragraph's last line, a trailing {.left} line after a fence, ...). */
|
||
:is(div, p, pre, blockquote, table).right {
|
||
float: inline-end;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-start: 1em;
|
||
}
|
||
|
||
:is(div, p, pre, blockquote, table).left {
|
||
float: inline-start;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-end: 1em;
|
||
}
|
||
|
||
/* 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 (the class moves onto the figure wrapper at render —
|
||
see markdown.py) float to the start edge like {.left} ones — until
|
||
they fall into the side zone (see the composition rules up in the
|
||
article section). */
|
||
figure.margin {
|
||
float: inline-start;
|
||
width: 30%;
|
||
max-width: 50%;
|
||
margin: 0.3rem 0 1rem;
|
||
margin-inline-end: 1em;
|
||
}
|
||
|
||
/* Click-to-enlarge (pagerite.js): article figure images open in a
|
||
full-viewport lightbox — the image as large as fits with the caption
|
||
below; click or Esc closes. The backdrop keeps a hint of the page
|
||
behind it (blur + dark tint) rather than going solid black; themes
|
||
retune it via --lightbox-bg / --lightbox-text. */
|
||
article figure img {
|
||
cursor: zoom-in;
|
||
}
|
||
|
||
#lightbox {
|
||
--lightbox-bg: rgb(0 0 0 / 0.68);
|
||
--lightbox-text: #e8e8e8;
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 100;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--lightbox-bg);
|
||
backdrop-filter: blur(1rem) saturate(0.85);
|
||
/* Wheel/touch scrolling stops at the overlay instead of chaining to
|
||
the page behind it. overscroll-behavior needs a scroll container,
|
||
hence overflow: auto — the content is clamped to fit, so it never
|
||
actually scrolls. */
|
||
overflow: auto;
|
||
overscroll-behavior: contain;
|
||
cursor: zoom-out;
|
||
}
|
||
|
||
#lightbox img {
|
||
max-width: 100vw;
|
||
/* Flex-shrink does the vertical fit: the image yields exactly the
|
||
space the caption needs, no fixed reservation. */
|
||
max-height: 100%;
|
||
flex: 0 1 auto;
|
||
min-height: 0;
|
||
object-fit: contain;
|
||
border-radius: 3px;
|
||
box-shadow: 0 1rem 4rem rgb(0 0 0 / 0.55);
|
||
}
|
||
|
||
#lightbox .caption {
|
||
max-width: 65ch;
|
||
padding: 0.8rem 1rem;
|
||
color: var(--lightbox-text);
|
||
font-size: 0.95em;
|
||
text-align: center;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
#lightbox {
|
||
animation: lightbox-in 0.18s ease-out;
|
||
}
|
||
|
||
#lightbox img {
|
||
animation: lightbox-img 0.22s ease-out;
|
||
}
|
||
|
||
@keyframes lightbox-in {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@keyframes lightbox-img {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.96);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Wide single-column pages: margin boxes lean into the vacant start-side
|
||
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). Out of flow like on multicol pages: the box hangs off
|
||
the article's start border, growing with the gutter up to 150% (18rem),
|
||
its end side 1.25rem off the 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.margin {
|
||
position: absolute;
|
||
--box-w: min(18rem, (100vw - 78rem) / 2 - 1.25rem);
|
||
width: var(--box-w);
|
||
max-width: none;
|
||
inset-inline-start: calc(-1.25rem - var(--box-w));
|
||
margin: 0.3rem 0 0;
|
||
}
|
||
}
|
||
|
||
/* In the wide symmetric gutters (where the sidebar overlays the flexible
|
||
start gutter rather than a reserved track) the sidebar flexes with the
|
||
gutter up to 150% — its start 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 start 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 to the end edge, so the
|
||
bleed extends past the surplus and the lane to the true viewport start
|
||
edge — sliding under the translucent sidebar — and past main's end
|
||
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: start;
|
||
/* 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
|
||
start 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 and blocks fall back to
|
||
plain full-width (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.margin {
|
||
float: none;
|
||
width: 100%;
|
||
max-width: none;
|
||
margin: 0 auto 1.5rem;
|
||
}
|
||
|
||
/* Non-figure floated blocks flatten to plain full-width blocks too. */
|
||
:is(div, p, pre, blockquote, table):is(.right, .left) {
|
||
float: none;
|
||
width: auto;
|
||
max-width: none;
|
||
margin: 0 0 1rem;
|
||
}
|
||
|
||
/* 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 start-side 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: start;
|
||
}
|
||
|
||
.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. */
|