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