Mirror content layout with text direction; admin stays LTR

This commit is contained in:
2026-09-03 20:01:23 +00:00
parent d6d07db2c5
commit e6a0c57446
8 changed files with 141 additions and 118 deletions
+2 -2
View File
@@ -19,8 +19,8 @@ Pagerite is a single-user CMS/blog. This document records the initial high-level
- Content is written in **Markdown** with powerful extensions (tables, footnotes, code highlighting, etc.).
- **Embedded HTML is passed through unfiltered**, including inline scripts and other dynamic content the author wants to post. This is safe by the single-trusted-author assumption above.
- Renderer: **markdown-it-py** with mdit-py-plugins (footnotes, definition lists, task lists, brace-attributes, admonitions and `::: name` containers — generic `<div class="name">` wrappers (the name may be followed by brace attributes: `::: aside {.right}`), of which `::: aside` floats as a muted side box and `{.margin}` / `::: margin` marks any block a margin note — on all but phone widths they are taken out of flow into the side zone at the article's left (the region the nav sidebar overlays, or the sidebar's own track when the layout reserves one) and the text never moves — and `::: nocols` opts its section out of column layout; tables and strikethrough from the default preset), GitHub-style alerts (`> [!NOTE]` / TIP / IMPORTANT / WARNING / CAUTION, rendered in the admonition callout styling), with `html=True` for raw passthrough, `typographer=True` for SmartyPants-style replacements in body text (curly quotes, `--` / `---` → en / em dashes, `...` → ellipsis, `(c)` → ©, etc.), and `breaks=True` so single line breaks inside paragraphs become `<br>` — including inside blockquotes, where every newline is kept and a blank `>` line starts a new paragraph. Code spans/blocks and raw HTML are left untouched. Fenced code blocks are highlighted server-side with **Pygments** (`nowrap` spans styled by `/_assets/pygments-*.css`, which maps every token class onto the `--code-*` variables; the base stylesheet defines light and dark palette sets resolved via `light-dark()`, so each theme gets the set matching its `color-scheme` and may only retint `--code-bg` to keep the well in the page's color family); a JS copy button appears on hover. Should this prove limiting, we implement our own renderer on top of html5tagger, which we already use for all HTML generation.
- **Files are content-addressed.** Uploads (`PUT /_api/files/{filename}`) are stored on disk (`<hostname>/files/`, RAM-cached uncompressed + zstd) by content hash — blake3, first 6 bytes hex + original extension — and served immutable from `/_f/…`. Raster images (not GIF) and SVGs (rasterized) are recompressed via mediapreview: the original is kept as `{hash}.orig{ext}` (internal only, never served — it may carry EXIF data; SVG originals stay servable as `{hash}.svg`) while pages link the extension-less `/_f/{hash}` and the server picks from the derivatives (`{hash}.avif` / `{hash}.webp` / `{hash}.jpg`) by Accept header — a format only when listed explicitly (`image/avif` → AVIF, `image/webp` → WebP, otherwise JPEG), with `vary: accept`; an explicit extension in the URL pins the format. Absolute URLs that survive page renames and dedupe identical content; pages no longer own files. An image standing alone in its paragraph becomes a block `<figure>` — with `<figcaption>` when it has a title; images inline with text and raw `<img>` HTML stay plain inline images. Positioning is by attribute classes: `![alt](/_f/… "Caption"){.right}``{.right}`, `{.left}` float at 30% of the text column (the caption wraps within it; an explicit `width=300` makes the figure shrink-wrap the image instead), `{.margin}` makes it a margin note, placed in the side zone left of the text on all but phone widths, `{.wide}` goes full bleed (viewport edge to edge, or up to the docked editor; the sidebar stacks on top of it); plain attributes like `width=300` work too. The same brace syntax on a block's last line (no blank line between) applies to the whole block: a paragraph ending with `{.wide}` becomes a full-width element that breaks out of the column layout, and space-separated at the end of a text line (`some text {.small}`) the braces likewise belong to the block — a space is what keeps them off an image or link ending the line, which keep their own directly-attached attrs; text size classes `{.small}` / `{.large}` / `{.huge}` (em-based) work on any block; written on the line after a block it applies to that preceding block — this is how headings, `::: containers` and code fences take classes (a wide code fence goes full bleed like a wide figure). Headings (h1/h2) clear floats, so images never overflow into the next section.
- Renderer: **markdown-it-py** with mdit-py-plugins (footnotes, definition lists, task lists, brace-attributes, admonitions and `::: name` containers — generic `<div class="name">` wrappers (the name may be followed by brace attributes: `::: aside {.right}`), of which `::: aside` floats as a muted side box and `{.margin}` / `::: margin` marks any block a margin note — on all but phone widths they are taken out of flow into the side zone at the article's start edge (left in LTR, right in RTL — the region the nav sidebar overlays, or the sidebar's own track when the layout reserves one) and the text never moves — and `::: nocols` opts its section out of column layout; tables and strikethrough from the default preset), GitHub-style alerts (`> [!NOTE]` / TIP / IMPORTANT / WARNING / CAUTION, rendered in the admonition callout styling), with `html=True` for raw passthrough, `typographer=True` for SmartyPants-style replacements in body text (curly quotes, `--` / `---` → en / em dashes, `...` → ellipsis, `(c)` → ©, etc.), and `breaks=True` so single line breaks inside paragraphs become `<br>` — including inside blockquotes, where every newline is kept and a blank `>` line starts a new paragraph. Code spans/blocks and raw HTML are left untouched. Fenced code blocks are highlighted server-side with **Pygments** (`nowrap` spans styled by `/_assets/pygments-*.css`, which maps every token class onto the `--code-*` variables; the base stylesheet defines light and dark palette sets resolved via `light-dark()`, so each theme gets the set matching its `color-scheme` and may only retint `--code-bg` to keep the well in the page's color family); a JS copy button appears on hover. Should this prove limiting, we implement our own renderer on top of html5tagger, which we already use for all HTML generation.
- **Files are content-addressed.** Uploads (`PUT /_api/files/{filename}`) are stored on disk (`<hostname>/files/`, RAM-cached uncompressed + zstd) by content hash — blake3, first 6 bytes hex + original extension — and served immutable from `/_f/…`. Raster images (not GIF) and SVGs (rasterized) are recompressed via mediapreview: the original is kept as `{hash}.orig{ext}` (internal only, never served — it may carry EXIF data; SVG originals stay servable as `{hash}.svg`) while pages link the extension-less `/_f/{hash}` and the server picks from the derivatives (`{hash}.avif` / `{hash}.webp` / `{hash}.jpg`) by Accept header — a format only when listed explicitly (`image/avif` → AVIF, `image/webp` → WebP, otherwise JPEG), with `vary: accept`; an explicit extension in the URL pins the format. Absolute URLs that survive page renames and dedupe identical content; pages no longer own files. An image standing alone in its paragraph becomes a block `<figure>` — with `<figcaption>` when it has a title; images inline with text and raw `<img>` HTML stay plain inline images. Positioning is by attribute classes: `![alt](/_f/… "Caption"){.right}``{.right}`, `{.left}` float at 30% of the text column, to its end/start edge following the text direction (the caption wraps within it; an explicit `width=300` makes the figure shrink-wrap the image instead), `{.margin}` makes it a margin note, placed in the side zone at the text's start edge on all but phone widths, `{.wide}` goes full bleed (viewport edge to edge, or up to the docked editor; the sidebar stacks on top of it); plain attributes like `width=300` work too. The same brace syntax on a block's last line (no blank line between) applies to the whole block: a paragraph ending with `{.wide}` becomes a full-width element that breaks out of the column layout, and space-separated at the end of a text line (`some text {.small}`) the braces likewise belong to the block — a space is what keeps them off an image or link ending the line, which keep their own directly-attached attrs; text size classes `{.small}` / `{.large}` / `{.huge}` (em-based) work on any block; written on the line after a block it applies to that preceding block — this is how headings, `::: containers` and code fences take classes (a wide code fence goes full bleed like a wide figure). Headings (h1/h2) clear floats, so images never overflow into the next section.
## Page structure and navigation
+2 -1
View File
@@ -168,7 +168,8 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
</script>
<template>
<div class="analytics-view">
<!-- Untranslated admin dashboard: always LTR, like the editor panel. -->
<div class="analytics-view" lang="en" dir="ltr">
<div class="analytics-panel">
<header>
<h1>Analytics</h1>
+117 -98
View File
@@ -288,8 +288,10 @@ body {
/* 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.
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
@@ -305,16 +307,16 @@ body {
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
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 left lane gets its own track at every width, so the
/* 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 right edge (surplus extends the lane). The lane is flexible:
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
@@ -398,7 +400,7 @@ body.editing #sidebar {
#sidebar {
grid-column: 1;
/* Pinned to the page's left edge (not the article's) and kept in view
/* 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;
@@ -409,8 +411,11 @@ body.editing #sidebar {
width: 12rem;
max-height: 100vh;
overflow-y: auto;
padding: 1rem 1rem 1rem 1.25rem;
border-radius: 0 0 0.5rem 0;
/* 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);
}
@@ -437,7 +442,7 @@ body.editing #sidebar {
#sidebar ul ul li::before {
content: "🔹";
display: inline-block;
margin-left: -1.3em;
margin-inline-start: -1.3em;
width: 1.3em;
}
@@ -578,7 +583,7 @@ main {
background: color-mix(var(--bg) 30%, transparent);
color: var(--muted);
font-size: 0.95rem;
text-align: left;
text-align: start;
hyphens: none;
}
@@ -679,19 +684,19 @@ article * + h6 {
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;
padding-inline-start: 0;
--list-indent: 2em;
display: flow-root;
}
article :is(ul, ol:not([type])) > li {
padding-left: var(--list-indent);
padding-inline-start: var(--list-indent);
}
article ul li::before {
content: "🔹";
display: inline-block;
margin-left: calc(-1 * var(--list-indent));
margin-inline-start: calc(-1 * var(--list-indent));
width: var(--list-indent);
text-align: center;
}
@@ -713,13 +718,13 @@ article ol:not([type]) > li::before {
content: counter(item) ".";
color: var(--muted);
display: inline-block;
margin-left: calc(-1 * var(--list-indent));
margin-inline-start: calc(-1 * var(--list-indent));
width: var(--list-indent);
text-align: left;
text-align: start;
}
/* Task lists: real clickable checkboxes. The checkbox stands in for the
list marker — taken out of flow, left-aligned in the indent box and
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 {
@@ -732,7 +737,7 @@ article .task-list-item::before {
article .task-list-item-checkbox {
position: absolute;
left: 0;
inset-inline-start: 0;
top: calc(0.5lh - .15ex);
translate: 0 -50%;
font-size: inherit;
@@ -772,7 +777,7 @@ article h1 .edit-link {
position: static;
font-size: 1.1rem;
vertical-align: 0.3em;
margin-left: 0.4rem;
margin-inline-start: 0.4rem;
}
/* Section pens sit at the end of anchored h2s, dimmer than the page pen
@@ -781,7 +786,7 @@ article h2 .edit-section {
position: static;
font-size: 0.85rem;
vertical-align: 0.35em;
margin-left: 0.4rem;
margin-inline-start: 0.4rem;
opacity: 0.35;
}
@@ -811,10 +816,10 @@ article dd {
}
/* The long-article composition (.multicol): a fluid but bounded text
lane with a 16rem side zone at the article's left, centered in main —
surplus width becomes vacant space, never endless text (with a sidebar
the article leans right instead and the sidebar's track is the left
lane; see below). (The backend render splits the body into .colseg
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
@@ -832,7 +837,7 @@ article.multicol {
@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 left edge —
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 —
@@ -842,14 +847,14 @@ article.multicol {
article.multicol>.colseg,
article.multicol>h1,
article.multicol>h2 {
margin-left: 16rem;
margin-inline-start: 16rem;
}
article.multicol .margin,
article.multicol .aside,
article.multicol figure.margin {
position: absolute;
left: 0;
inset-inline-start: 0;
width: 14rem;
max-width: none;
margin: 0.3rem 0 0;
@@ -870,11 +875,11 @@ article.multicol {
}
}
/* With a sidebar, the sidebar's 12rem track IS the left lane at every
/* 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 right edge — surplus width extends the
left lane instead of balancing out on the right — and margin boxes
hang into the lane off the article's left border, sliding under the
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.) */
@@ -884,8 +889,8 @@ article.multicol {
margin-inline: auto 0;
}
/* The sidebar fills the flexible lane (its left side stays on the
viewport's left edge, growing rightward). */
/* 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%;
}
@@ -893,29 +898,29 @@ article.multicol {
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.colseg,
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>h1,
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>h2 {
margin-left: 0;
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 left border (1.25rem gap), hanging into
the left lane and growing leftward with it: 12rem when the lane is
/* 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 left of the right-leaning article).
(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;
left: calc(-1.25rem - var(--box-w));
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 — left-align.
Floated figures keep their own margins (the text gap). */
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;
}
@@ -988,13 +993,15 @@ article a:hover {
/* Blockquotes: spacing comes from the blockquote itself (bottom-only like
everything else in articles); inner paragraphs keep only the gap between
them. The negative left margin pushes the bar out past the text edge, so
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 -0.5rem;
padding: 0 0 0 0.25rem;
border-left: 0.25rem solid var(--accent2);
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);
}
@@ -1009,17 +1016,19 @@ blockquote p + p {
/* Admonitions (markdown !!! note/warning/...) and GitHub-style alerts
(> [!NOTE] ...): a lightweight callout in the blockquote idiom — accent
bar and a faint wash, recolored per type, with a type emoji on the
title. The negative left margin pushes bar and wash out past the text
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-left = border + padding-left).
Bottom-only margins like everything else in articles; inner paragraphs
carry no margins of their own. */
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 -1.15rem;
margin: 0 0 1rem;
margin-inline-start: -1.15rem;
padding: 0.4rem 0.9rem;
border-left: 0.25rem solid var(--admonition-color, var(--accent));
border-radius: 0 0.3rem 0.3rem 0;
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);
}
@@ -1037,7 +1046,7 @@ blockquote p + p {
.admonition-title::before,
.markdown-alert-title::before {
padding-right: 0.35em;
padding-inline-end: 0.35em;
}
.admonition.note .admonition-title::before,
@@ -1099,18 +1108,19 @@ blockquote p + p {
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 left border, each
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 left floats (consecutive floats stack
via clear: left). Headings already clear floats, so in-column boxes
never bleed into the next section. */
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: left;
clear: left;
float: inline-start;
clear: inline-start;
width: 30%;
max-width: 20rem;
margin: 0.3rem 1.2rem 1rem 0;
margin: 0.3rem 0 1rem;
margin-inline-end: 1.2rem;
padding: 0.6rem 0.9rem;
font-size: 0.9rem;
color: var(--muted);
@@ -1140,11 +1150,12 @@ blockquote p + p {
}
.margin {
float: left;
clear: left;
float: inline-start;
clear: inline-start;
width: 30%;
max-width: 20rem;
margin: 0.3rem 1.2rem 1rem 0;
margin: 0.3rem 0 1rem;
margin-inline-end: 1.2rem;
font-size: 0.9rem;
color: var(--muted);
}
@@ -1153,10 +1164,10 @@ 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;
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;
@@ -1196,7 +1207,7 @@ p code {
}
code:not(pre code):first-child {
padding-left: 0;
padding-inline-start: 0;
}
/* Click-to-copy button (added by pagerite.js) */
@@ -1241,7 +1252,7 @@ td {
}
th {
text-align: left;
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))));
@@ -1313,20 +1324,24 @@ figure img:not([width]) {
width: 100%;
}
/* Floated figures: {.right} / {.left}, defaulting to 30% of the column
and capped at half of it. */
/* 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: right;
float: inline-end;
width: 30%;
max-width: 50%;
margin: 0.3rem 0 1rem 1em;
margin: 0.3rem 0 1rem;
margin-inline-start: 1em;
}
figure:has(.left) {
float: left;
float: inline-start;
width: 30%;
max-width: 50%;
margin: 0.3rem 1em 1rem 0;
margin: 0.3rem 0 1rem;
margin-inline-end: 1em;
}
/* The same floats for other blocks: ::: left / ::: right containers
@@ -1334,17 +1349,19 @@ figure:has(.left) {
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: right;
float: inline-end;
width: 30%;
max-width: 50%;
margin: 0.3rem 0 1rem 1em;
margin: 0.3rem 0 1rem;
margin-inline-start: 1em;
}
:is(div, p, pre, blockquote, table).left {
float: left;
float: inline-start;
width: 30%;
max-width: 50%;
margin: 0.3rem 1em 1rem 0;
margin: 0.3rem 0 1rem;
margin-inline-end: 1em;
}
/* An image with an explicit width attribute shrink-wraps instead: the
@@ -1356,13 +1373,15 @@ figure:has(img[width]) {
}
/* {.margin} figures (the class moves onto the figure wrapper at render —
see markdown.py) float left like {.left} ones — until they fall into
the side zone (see the composition rules up in the article section). */
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: left;
float: inline-start;
width: 30%;
max-width: 50%;
margin: 0.3rem 1em 1rem 0;
margin: 0.3rem 0 1rem;
margin-inline-end: 1em;
}
/* Click-to-enlarge (pagerite.js): article figure images open in a
@@ -1439,12 +1458,12 @@ article figure img {
}
}
/* Wide single-column pages: margin boxes lean into the vacant left
/* 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 left border, growing with the gutter up to 150% (18rem),
its right side 1.25rem off the border. */
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,
@@ -1453,14 +1472,14 @@ article figure img {
--box-w: min(18rem, (100vw - 78rem) / 2 - 1.25rem);
width: var(--box-w);
max-width: none;
left: calc(-1.25rem - var(--box-w));
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
left gutter rather than a reserved track) the sidebar flexes with the
gutter up to 150% — its left side stays on the viewport's edge. */
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%);
@@ -1509,7 +1528,7 @@ body.editing pre.wide {
/* Narrow single-column pages with a sidebar: below 102rem the symmetric
gutters can no longer both hold the sidebar, so #content reserves it
with a flexible left track (see the matching media query below) and the
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
@@ -1536,9 +1555,9 @@ body:has(.multicol) pre.wide {
}
/* Multicol with a sidebar track (≥48rem, see #content): main starts at
the flexible lane's width and the article leans right, so the bleed
extends left past the surplus and the lane to the true viewport edge —
sliding under the translucent sidebar — and right past main's
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),
@@ -1556,7 +1575,7 @@ figcaption {
hyphens: auto;
-webkit-hyphens: auto;
text-wrap: pretty;
text-align: left;
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;
@@ -1582,7 +1601,7 @@ article h2 {
/* Narrow windows with a sidebar: below 102rem the symmetric gutters can no
longer both hold the 12rem sidebar, so reserve its space with a flexible
left track instead of letting it overlap the article (multicol pages use
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
@@ -1702,10 +1721,10 @@ article h2 {
width: fit-content;
}
/* The single-column sidebar .wide margins assume a left sidebar column;
with the sidebar on top the article is viewport-wide and the plain
centered bleed applies again. (Multicol pages need no override: their
cqw bleed is exact at any width.) */
/* 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);
}
@@ -1746,7 +1765,7 @@ article h2 {
.dateline {
color: var(--muted);
font-size: 0.85rem;
text-align: left;
text-align: start;
}
.footnotes {
+5 -4
View File
@@ -111,12 +111,13 @@ article h3 {
}
blockquote {
border-left-color: var(--accent);
border-inline-start-color: var(--accent);
background: color-mix(var(--accent) 6%, transparent);
padding: 0.4rem 0.9rem;
/* Keep the quoted text on the paragraph edge: the tinted box extends
past it by its own border/padding, like code blocks. */
margin: 0 -0.9rem 1rem calc(-0.25rem - 0.9rem);
margin: 0 0 1rem;
margin-inline: calc(-0.25rem - 0.9rem) -0.9rem;
border-radius: 6px;
}
@@ -125,9 +126,9 @@ blockquote {
bar stays in both. */
pre {
border: 1px solid transparent;
border-left: 0.25rem solid var(--accent);
border-inline-start: 0.25rem solid var(--accent);
/* Text on the paragraph edge: the box extends by padding + border. */
margin-left: calc(-0.8rem - 0.25rem);
margin-inline-start: calc(-0.8rem - 0.25rem);
border-radius: 6px;
}
+8 -7
View File
@@ -144,9 +144,9 @@ article h1 {
font-weight: 700;
padding-bottom: 0.5rem;
/* The hazard-stripe underline breaks out of the page box: the negative
right margin extends the h1's box (and thus its background) all the
way to the viewport's right edge. */
margin-right: calc((100% - 100vw) / 2);
end margin extends the h1's box (and thus its background) all the
way to the viewport's edge on that side. */
margin-inline-end: calc((100% - 100vw) / 2);
background:
linear-gradient(-55deg,
transparent 0 0.2rem,
@@ -186,20 +186,21 @@ article ul ul ul li::before {
}
blockquote {
border-left-color: var(--accent2);
border-inline-start-color: var(--accent2);
background: color-mix(var(--accent2) 6%, transparent);
padding: 0.25rem 0.75rem;
/* Keep the quoted text on the paragraph edge: the tinted box extends
past it by its own border/padding, like code blocks. */
margin: 0 -0.75rem 1rem -1rem;
margin: 0 0 1rem;
margin-inline: -1rem -0.75rem;
}
/* Code follows the color scheme; the dark-scheme well joins the violet
family (--code-bg above). The orange side bar stays in both. */
pre {
border-left: 0.25rem solid var(--accent);
border-inline-start: 0.25rem solid var(--accent);
/* Text on the paragraph edge: the box extends by padding + border. */
margin-left: calc(-0.8rem - 0.25rem);
margin-inline-start: calc(-0.8rem - 0.25rem);
border-radius: 3px;
}
+2 -2
View File
@@ -66,7 +66,7 @@ article ul li::before {
content: "◆";
color: var(--accent);
font-size: 0.8em;
margin-left: calc(-1 * var(--list-indent) / 0.8);
margin-inline-start: calc(-1 * var(--list-indent) / 0.8);
width: calc(var(--list-indent) / 0.8);
}
@@ -80,7 +80,7 @@ article ul ul ul li::before {
}
blockquote {
border-left-color: var(--accent2);
border-inline-start-color: var(--accent2);
}
/* Code panels sit slightly lighter than the page; the token colors come
+2 -2
View File
@@ -160,7 +160,7 @@ main::before {
top edge and a grassy shadow. */
#sidebar {
background: linear-gradient(160deg, #f4faddd9, #d9eec5cf);
border-right: 1px solid #ffffff80;
border-inline-end: 1px solid #ffffff80;
border-bottom: 1px solid var(--line);
box-shadow: 0 0.3rem 1rem #4f913b1f;
border-radius: 1rem;
@@ -225,7 +225,7 @@ article ul ul ul li::before {
/* Quotes get a grassy edge and a wash of sunlight. */
blockquote {
color: #4d6849;
border-left-color: var(--accent2);
border-inline-start-color: var(--accent2);
background: linear-gradient(90deg, #fff0a238, transparent 70%);
padding-top: 0.25rem;
padding-bottom: 0.25rem;
+3 -2
View File
@@ -349,8 +349,9 @@ def _layout(
property attributes, everything else (description, twitter:*) as name.
``lang`` is the served language for <html lang>; an RTL language (ar,
fa, ...) also puts dir="rtl" on <html> (the editor panel carries its own
lang="en" dir="ltr", so it is unaffected). ``canonical`` and
fa, ...) also puts dir="rtl" on <html> (the editor panel and the
analytics dashboard carry their own lang="en" dir="ltr", so they are
unaffected). ``canonical`` and
``alternates`` ((hreflang, href) pairs) are the page's language URLs
(see docs/localization.md), emitted right after the viewport and before
the social tags: canonical first, then the hreflang alternates.