Layout: breakable paragraphs across columns, sidebar gets its own flexible track at every width, cols requires multiple paragraphs; h3 size fix in corporate theme

This commit is contained in:
2026-08-28 23:23:51 +00:00
parent 5101a5c5bf
commit 22c4f0e752
5 changed files with 188 additions and 87 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ msgspec Structs for the kanta database. See `docs/content-model.md` for the full
markdown-it-py renderer (html passthrough + attrs, footnote, deflist, tasklists, admon, gfm_autolink, sub/superscript plugins; typographer + breaks on). In bodies with at least three top-level h1/h2 headings (nested ones, e.g. inside `::: aside`, never participate), each gets a slug id (`python-slugify`, mirroring the editor's `slugify.js` — unicode folds to ASCII, separators become single hyphens) unless the author set `{#id}`, and their text is wrapped in a self-link (`a.anchor`) so section links are copyable; anchored headings also carry `data-line` with their markdown source line (the page editor's section pens and piecewise scroll sync key off it); the first in-body h1 is the article title — when the markdown has no h1, `render(title=...)` injects it as `# {title}` so implicit and explicit titles take the same path — it gets no id and doesn't count toward the three, its self-link is `href=""` (scroll to top); shorter articles stay anchor-free, h3+ is never navigable, and duplicates get `-2`/`-3` suffixes. Custom image rule: relative srcs resolve against the page path; an image standing alone in its paragraph becomes a figure (captioned when titled), while inline-with-text images and raw `<img>` HTML stay plain. A `{dates}` line expands to the article's published/updated dateline (`p.dateline`, from `Node.created`/`modified`; left literal in previews of unsaved pages).
`render()` returns a `Rendered(html, multicol)`: the article content segmented for the column layout (there is no wrapper div — segments and bare blocks are direct `<article>` children) — h1/h2 headings, `.wide` blocks and margin-breakout blocks (`.margin`, `::: aside`) stand bare, the runs between them become `<div class="colseg">` (plus `.cols` on segments with enough text, `::: nocols` opting out), and `multicol` flags bodies long enough to columnize (visible-text thresholds, code excluded). `views.py` puts the class on the article; pagerite.css takes it from there (at most two columns, the left-margin breakout, all viewport adaptation).
`render()` returns a `Rendered(html, multicol)`: the article content segmented for the column layout (there is no wrapper div — segments and bare blocks are direct `<article>` children) — h1/h2 headings, `.wide` blocks and margin-breakout blocks (`.margin`, `::: aside`) stand bare, the runs between them become `<div class="colseg">` (plus `.cols` on segments with enough text in at least two paragraphs or one long enough to split across columns, `::: nocols` opting out; in column segments, paragraphs past `BREAKABLE_TEXT` visible characters are marked `.breakable` so they may split across columns), and `multicol` flags bodies long enough to columnize (visible-text thresholds, code excluded). `views.py` puts the class on the article; pagerite.css takes it from there (at most two columns, the left-margin breakout, all viewport adaptation).
## `views.py`
+1 -1
View File
@@ -34,7 +34,7 @@ Pagerite is a single-user CMS/blog. This document records the initial high-level
## Reading experience
- The article column is sized by the **viewport, never by content**: a symmetric grid (`1fr minmax(0, 78rem) 1fr`) with flexible gutters keeps the layout stable across navigation. The sidebar occupies the left gutter, the right gutter balances it. Long articles (flagged `.multicol` by the backend render) lift the cap and become a bounded **composition**, centered in the available space with the surplus left vacant: a fluid text lane (up to 42rem) plus a 16rem **side zone at the article's left** — the region the nav sidebar overlays — which hosts margin boxes (`.margin`, `::: aside`, margin figures) at all but phone widths, without the text ever moving. On pages with a sidebar below ~110rem (where the sidebar gets its own 12rem track) the track is the left lane instead: no in-article zone, the text lane runs fluid up to 86rem, and the boxes fall into the track, sliding under the translucent sticky nav. Once two lanes fit beside the zone (≥96rem available in `main`), the text flows in two fluid lanes (36rem minimum, capped at 102rem total — technical content wants the wider lanes, and wider windows just add vacant space). The stages step by the space actually available in `main` (container queries + `cqw` units, so the docked editor's inset is automatic). `.wide` figures on multicol pages bleed to the viewport edges measured from `main` (`cqw`), sliding under the sidebar. The backend splits the body into `.colseg` segments at h1/h2 headings, `.wide` elements and margin blocks (full-width separators or margin boxes, never inside columns), tagging segments that hold enough text with `.cols` — code blocks are excluded from that measure, and a `::: nocols` container opts its whole section out. On wide single-column pages (≥104rem), margin boxes lean into the vacant left gutter as well.
- The article column is sized by the **viewport, never by content**: a symmetric grid (`1fr minmax(0, 78rem) 1fr`) with flexible gutters keeps the layout stable across navigation. The sidebar occupies the left gutter, the right gutter balances it. Long articles (flagged `.multicol` by the backend render) lift the cap and become a bounded **composition**, centered in the available space with the surplus left vacant: a fluid text lane (up to 42rem) plus a 16rem **side zone at the article's left** — the region the nav sidebar overlays — which hosts margin boxes (`.margin`, `::: aside`, margin figures) at all but phone widths, without the text ever moving. On pages with a sidebar, the sidebar gets its own track at every width — flexible, 12rem when space is tight and growing up to 150% (18rem) once the viewport has room beyond the article, the sidebar keeping its left side on the viewport's edge — and the track is the left lane instead: no in-article zone, the text lane runs fluid up to 86rem leaning on the viewport's right edge (surplus extends the left lane), and the boxes hang into the lane off the article's left border (growing leftward with it, up to 18rem), sliding under the translucent sticky nav. Once two lanes fit beside the zone (≥96rem available in `main`), the text flows in two fluid lanes (36rem minimum, capped at 102rem total — technical content wants the wider lanes, and wider windows just add vacant space). The stages step by the space actually available in `main` (container queries + `cqw` units, so the docked editor's inset is automatic). `.wide` figures on multicol pages bleed to the viewport edges measured from `main` (`cqw`), sliding under the sidebar. The backend splits the body into `.colseg` segments at h1/h2 headings, `.wide` elements and margin blocks (full-width separators or margin boxes, never inside columns), tagging segments that hold enough text in at least two paragraphs (or one long enough to split) with `.cols` — code blocks are excluded from that measure, a `::: nocols` container opts its whole section out, and column-filling paragraphs are marked `.breakable` so they may split across the column gap (shorter paragraphs stay whole). On wide single-column pages (≥104rem), margin boxes lean into the vacant left gutter as well, growing with it up to 18rem.
- A gentle **scroll-reveal** of headings, figures and block-level elements (IntersectionObserver). It is layout-level: articles need no support for it, and `prefers-reduced-motion` disables all motion.
## Styling
+126 -72
View File
@@ -278,22 +278,25 @@ body {
/* 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, centered, 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
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);
}
/* Only when the vacant zone cannot hold the sidebar does it get its own
12rem track — the same compromise single-column pages make below
102rem. Above that, a sidebar's presence changes nothing about the
article. */
@media (max-width: 110rem) {
body:has(#sidebar):has(.multicol):not(.editing) #content {
grid-template-columns: 12rem 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 {
@@ -624,6 +627,16 @@ 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
@@ -768,15 +781,18 @@ 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. (The backend
render splits the body into .colseg segments separated by full-width
h2s and .wide elements, tags text-heavy segments .cols — a ::: nocols
container opts its section out — and flags the article .multicol; CSS
owns the geometry.) Technical content wants a wide lane: up to 42rem
single, or two fluid lanes (36rem minimum, never more than two) once
they fit beside the zone, capped at 102rem total. The zone — the
region the nav sidebar overlays — is a margin indent on the lane
content; margin boxes float into it, and the text never moves. */
surplus width becomes vacant space, never endless text (with a sidebar
the article leans right instead and the sidebar's track is the left
lane; see below). (The backend render splits the body into .colseg
segments separated by full-width h2s and .wide elements, tags
text-heavy segments of several paragraphs .cols — a ::: nocols
container opts its section out, and column-filling paragraphs are
marked .breakable — and flags the article .multicol; CSS owns the
geometry.) Technical content wants a wide lane: up to 42rem single, or
two fluid lanes (36rem minimum, never more than two) once they fit
beside the zone, capped at 102rem total. The zone — the region the nav
sidebar overlays — is a margin indent on the lane content; margin
boxes float into it, and the text never moves. */
article.multicol {
margin-inline: auto;
max-width: 58rem; /* 42rem lane + 16rem zone */
@@ -789,15 +805,15 @@ article.multicol {
overlays. Scoped to direct article children (the backend render keeps
margin blocks out of the column segments); nested ones keep the
in-column float fallback. */
.multicol article>.colseg,
.multicol article>h1,
.multicol article>h2 {
article.multicol>.colseg,
article.multicol>h1,
article.multicol>h2 {
margin-left: 16rem;
}
.multicol article>.margin,
.multicol article>.aside,
.multicol article>figure:has(.margin) {
article.multicol>.margin,
article.multicol>.aside,
article.multicol>figure:has(.margin) {
float: left;
clear: left;
width: 14rem;
@@ -807,9 +823,9 @@ article.multicol {
/* Wide separators start below any margin box — their bleed must not
wrap around it. */
.multicol article>figure:has(.wide),
.multicol article>div.wide,
.multicol article>pre.wide {
article.multicol>figure:has(.wide),
article.multicol>div.wide,
article.multicol>pre.wide {
clear: left;
}
}
@@ -828,35 +844,46 @@ article.multicol {
}
}
/* With a sidebar, below 110rem the sidebar gets its own 12rem track (see
#content). The track IS the left lane then: no in-article zone, the
text lane runs fluid (up to 86rem), and margin boxes fall all the way
left into the track — sliding under the translucent sticky nav, which
only ever occupies its top. (The track never exists once the container
reaches 96rem, so the two-lane rules above never meet it. Not below
/* With a sidebar, the sidebar's 12rem track IS the left lane at every
width (see #content): no in-article zone, the text lane runs fluid (up
to 86rem) and leans on main's right edge — surplus width extends the
left lane instead of balancing out on the right — and margin boxes
hang into the lane off the article's left border, sliding under the
translucent sticky nav, which only ever occupies its top. (Not below
48rem: there the sidebar becomes a link strip above the article and
there is no track to fall into.) */
@media (min-width: 48rem) and (max-width: 110rem) {
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;
}
body:has(#sidebar):has(.multicol):not(.editing) .multicol article>.colseg,
body:has(#sidebar):has(.multicol):not(.editing) .multicol article>h1,
body:has(#sidebar):has(.multicol):not(.editing) .multicol article>h2 {
/* The sidebar fills the flexible lane (its left side stays on the
viewport's left edge, growing rightward). */
body:has(#sidebar):has(.multicol):not(.editing) #sidebar {
width: 100%;
}
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.colseg,
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>h1,
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>h2 {
margin-left: 0;
}
body:has(#sidebar):has(.multicol):not(.editing) .multicol article>.margin,
body:has(#sidebar):has(.multicol):not(.editing) .multicol article>.aside,
body:has(#sidebar):has(.multicol):not(.editing) .multicol article>figure:has(.margin) {
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.margin,
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>.aside,
body:has(#sidebar):has(.multicol):not(.editing) article.multicol>figure:has(.margin) {
float: left;
clear: left;
width: 12rem;
/* Attached to the article's left border (1.25rem gap), hanging into
the left lane and growing leftward with it: 12rem when the lane is
tight, up to 150% (18rem) when the track or the surplus has room
(100cqw - 100% is the surplus left of the right-leaning article).
The lane (track + main's padding) always guarantees the room. */
--box-w: min(18rem, var(--lane) + 100cqw - 100% - 1.25rem);
width: var(--box-w);
max-width: none;
/* From the text's left edge to the page's left edge: half the
centering difference plus the track and main's padding. */
margin: 0.3rem 0 1rem calc(50% - 50cqw - 13.25rem);
margin: 0.3rem 0 1rem calc(-1.25rem - var(--box-w));
}
}
@@ -882,6 +909,13 @@ article.multicol {
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,
@@ -1258,16 +1292,27 @@ figure:has(.margin) {
/* Wide single-column pages: margin boxes lean into the vacant left
gutter instead (below 104rem the gutter cannot hold the box, and while
editing the docked panel reshapes the gutters — in both they stay
plain floats). */
plain floats). The box grows with the gutter up to 150% (18rem), its
right side 1.25rem off the article's left border. */
@media (min-width: 104rem) {
body:not(.editing):not(:has(.multicol)) article>.margin,
body:not(.editing):not(:has(.multicol)) article>.aside,
body:not(.editing):not(:has(.multicol)) article>figure:has(.margin) {
float: left;
clear: left;
width: 12rem;
--box-w: min(18rem, (100vw - 78rem) / 2 - 1.25rem);
width: var(--box-w);
max-width: none;
margin: 0.3rem 0 1rem -13.25rem;
margin: 0.3rem 0 1rem calc(-1.25rem - var(--box-w));
}
}
/* In the wide symmetric gutters (where the sidebar overlays the flexible
left gutter rather than a reserved track) the sidebar flexes with the
gutter up to 150% — its left side stays on the viewport's edge. */
@media (min-width: 102rem) {
body:not(.editing) #sidebar {
width: min(18rem, 100%);
}
}
@@ -1312,19 +1357,19 @@ body.editing pre.wide {
}
/* Narrow single-column pages 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.
(Multicol pages use the same fixed track below 110rem — their rules
are below.) */
gutters can no longer both hold the sidebar, so #content reserves it
with a flexible left track (see the matching media query below) and the
article always starts at the lane's width (+ main's 1.25rem padding) —
the bleed margin measures off --lane. Scoped by :has(#sidebar) since
the sidebar element is omitted entirely on pages without
sub-navigation, and excluded while editing, where the editing rules
above apply instead. (Multicol pages reserve the same track at every
width — their rules are below.) */
@media (max-width: 102rem) {
body:has(#sidebar):not(.editing):not(:has(.multicol)) figure:has(.wide),
body:has(#sidebar):not(.editing):not(:has(.multicol)) div.wide,
body:has(#sidebar):not(.editing):not(:has(.multicol)) pre.wide {
margin-inline: -13.25rem 0;
margin-inline: calc(-1.25rem - var(--lane)) 0;
}
}
@@ -1339,15 +1384,17 @@ body:has(.multicol) pre.wide {
margin-inline: calc(50% - 50cqw - 1.25rem);
}
/* Multicol with a sidebar track (≤110rem, see #content): main starts
12rem in, so the bleed extends left past the track to the true viewport
edge — sliding under the translucent sidebar. */
@media (max-width: 110rem) {
/* Multicol with a sidebar track (≥48rem, see #content): main starts at
the flexible lane's width and the article leans right, so the bleed
extends left past the surplus and the lane to the true viewport edge —
sliding under the translucent sidebar — and right past main's
padding. */
@media (min-width: 48rem) {
body:has(#sidebar):has(.multicol):not(.editing) figure:has(.wide),
body:has(#sidebar):has(.multicol):not(.editing) div.wide,
body:has(#sidebar):has(.multicol):not(.editing) pre.wide {
width: calc(100cqw + 14.5rem);
margin-inline: calc(50% - 50cqw - 13.25rem) 0;
width: calc(100cqw + 2.5rem + var(--lane));
margin-inline: calc(100% - 100cqw - 1.25rem - var(--lane)) 0;
}
}
@@ -1383,16 +1430,23 @@ 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 fixed
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
this same track at every width — see the #content rules above). 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. */
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 {
grid-template-columns: 12rem minmax(0, 78rem) minmax(0, 1fr);
--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%;
}
}
+60 -12
View File
@@ -27,12 +27,16 @@ render() also builds the layout structure: the top-level blocks are
segmented for the column layout — h1/h2 headings, ``.wide`` blocks and
margin-breakout blocks (``.margin``, ``::: aside``) stand on their own,
the runs between them are wrapped in ``<div class="colseg">`` (tagged
``.cols`` when the segment holds enough text, unless a ``::: nocols``
container opts it out). The result carries ``multicol`` when the whole
body justifies columns (views.py puts the class on the article); how
many columns (never more than two), whether the margin breakout applies
and every other viewport adaptation is then pagerite.css's call. The
thresholds measure visible text, code blocks excluded.
``.cols`` when the segment holds enough text — COLS_TEXT — in at least
COLS_PARAS paragraphs or one paragraph long enough to turn .breakable,
unless a ``::: nocols`` container opts it out;
in column segments, paragraphs past BREAKABLE_TEXT are marked
``.breakable`` so they may split across columns). The result carries
``multicol`` when the whole body justifies columns (views.py puts the
class on the article); how many columns (never more than two), whether
the margin breakout applies and every other viewport adaptation is then
pagerite.css's call. The thresholds measure visible text, code blocks
excluded.
markdown-it's typographer is enabled, so body text gets SmartyPants-style
replacements: straight quotes become curly, ``--`` / ``---`` become en / em
@@ -434,12 +438,23 @@ md.core.ruler.push("heading_ids", _heading_ids)
# Text-length thresholds (visible characters, code blocks excluded) for the
# column layout: the article goes .multicol past MULTICOL_TEXT, and a column
# segment gets .cols past COLS_TEXT.
# segment gets .cols past COLS_TEXT — provided it also has at least
# COLS_PARAS paragraphs or a paragraph long enough to turn .breakable: a
# lone unbreakable paragraph would fill a column on its own and strand the
# rest (e.g. a floated figure) in the other, leaving a mostly empty column.
MULTICOL_TEXT = 1800
COLS_TEXT = 600
COLS_PARAS = 2
#: Paragraphs past this visible length are marked .breakable, letting them
#: split across columns (shorter ones stay unbreakable so a paragraph never
#: straddles the column gap).
BREAKABLE_TEXT = 800
_PRE_BLOCK_RE = re.compile(r"<pre\b.*?</pre>", re.S)
_TAG_RE = re.compile(r"<[^>]+>")
_PARA_OPEN_RE = re.compile(r"<p[\s>]")
_PARA_RE = re.compile(r"<p((?:\s[^>]*)?)>(.*?)</p>", re.S)
# Classes that take their block out of the column flow: .wide is a
# full-width separator, .margin/.aside float in the side zone at the
@@ -466,6 +481,28 @@ def _text_len(html: str) -> int:
return len(_TAG_RE.sub("", _PRE_BLOCK_RE.sub("", html)).strip())
def _breakable_paras(html: str) -> str:
"""Mark column-filling paragraphs .breakable so they may split.
Columns keep paragraphs whole (break-inside: avoid-column), but a
paragraph long enough to fill a column would strand everything after
it in a column of its own — these get .breakable, and pagerite.css
lets them split across the column gap. Only applied to .cols segments.
"""
def repl(m: re.Match[str]) -> str:
attrs, body = m.group(1), m.group(2)
if _text_len(body) <= BREAKABLE_TEXT:
return m.group(0)
if 'class="' in attrs:
attrs = attrs.replace('class="', 'class="breakable ', 1)
else:
attrs = f'{attrs} class="breakable"'
return f"<p{attrs}>{body}</p>"
return _PARA_RE.sub(repl, html)
def _top_level_blocks(tokens: list) -> list[list]:
"""Split the token stream into its top-level blocks.
@@ -520,10 +557,12 @@ def render(
The top-level blocks are grouped into column segments: boundary blocks
(h1/h2 headings, .wide, margin-breakout blocks — see _is_boundary) are
rendered bare, the runs between them wrapped in <div class="colseg">.
A segment is tagged .cols when it holds enough text (COLS_TEXT) and no
::: nocols container; the article is .multicol when the whole body
exceeds MULTICOL_TEXT. pagerite.css keys all column and margin-breakout
layout off these classes.
A segment is tagged .cols when it holds enough text (COLS_TEXT) in at
least two paragraphs (COLS_PARAS) or one breakable-length paragraph,
and no ::: nocols container; its long paragraphs are marked .breakable;
the article is .multicol when the whole body exceeds MULTICOL_TEXT.
pagerite.css keys all column and margin-breakout layout off these
classes.
A ``{dates}`` line expands to the article's published/updated dateline
(needs ``created``/``modified``; left as-is in contexts without them,
@@ -562,7 +601,16 @@ def render(
nocols = any(
"nocols" in _classes(t) for t in group if t.type == "container_block_open"
)
cols = " cols" if text_len > COLS_TEXT and not nocols else ""
marked = _breakable_paras(html)
cols = (
" cols"
if text_len > COLS_TEXT
and not nocols
and (len(_PARA_OPEN_RE.findall(html)) >= COLS_PARAS or marked != html)
else ""
)
if cols:
html = marked
parts.append(f'<div class="colseg{cols}">{html}</div>')
html = "".join(parts)
if created is not None and "<p>{dates}</p>" in html:
-1
View File
@@ -106,7 +106,6 @@ article h2 {
article h3 {
font-weight: 700;
font-size: 0.95rem;
letter-spacing: 0.08em;
color: var(--muted);
}