/* 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 ; 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 , 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 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. 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-left: 0; --list-indent: 2em; display: flow-root; } 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