/* CSS custom properties (the --variable-name syntax) are just named values —
   defined once here, referenced everywhere else as var(--name). The payoff
   for theming: swapping a handful of variable definitions restyles the whole
   page, instead of hunting down every hardcoded color in the stylesheet. */
:root {
    --bg: #fbfaf7;
    --fg: #1d1c1a;
    --muted: #736f66;
    --border: #e6e2d9;
    --accent: #3c5a6b;
    --accent-hover: #2e4551;
    --accent-fg: #ffffff;
    --accent-bg: #f2efe7;
    --danger: #a53f3f;

    /* The comment-color palette (Comment.color, models.py) — a small,
       fixed, already-theme-aware set rather than a free-form picker;
       see that column's own comment for why. Named for what they look
       like in light mode; html.theme-dark below swaps in darker,
       desaturated variants of the same five so none of them turn into a
       glaring block of saturated color against a dark background. */
    --color-yellow: #e8d477;
    --color-green: #8fbf8f;
    --color-blue: #7fa8c9;
    --color-pink: #d98fae;
    --color-purple: #a888c9;

    /* Prose (post titles, body copy, comments) uses a serif stack — this is
       a writing site, and serif type reads as more "considered" for long-
       form text. UI chrome (nav, buttons, labels, meta text) uses the
       system sans stack instead, which is what keeps controls looking like
       controls rather than like part of the writing. */
    --font-serif: Charter, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
    --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
    --font-mono: ui-monospace, "SF Mono", Consolas, monospace;

    /* Elevation + surface tokens (added in the moderate visual refresh).
       --surface is a slightly raised fill for "cards" (post-list items, the
       dashboard summary panel) that reads as sitting a step above the page
       ground; --shadow-sm/-md are the two depths those cards use at rest and
       on hover. --radius is the one shared corner radius so cards, inputs,
       and buttons round consistently instead of each picking their own value.
       Defined here as named tokens for the same reason every color is: the
       look can be retuned in one place, and dark mode just swaps the values
       (see html.theme-dark) rather than every card needing its own dark rule.
       In light mode --surface is pure white, standing just clear of the warm
       off-white page background (--bg). */
    --surface: #ffffff;
    --shadow-sm: 0 1px 2px rgba(29, 28, 26, 0.05);
    --shadow-md: 0 6px 18px rgba(29, 28, 26, 0.10);
    --radius: 0.55rem;
}

/* There is deliberately no `@media (prefers-color-scheme: dark)` block
   here. An earlier version had one, to guess a first-time visitor's OS
   setting — but `html`'s class is always exactly "theme-light" or
   "theme-dark" now (see inject_theme() in app.py), never anything else,
   so a media-query fallback could never actually be reached; keeping it
   would have been dead CSS pretending to still do something. The :root
   block above already *is* the light palette, serving as the base default
   before either explicit class is layered on. */

html.theme-light {
    --bg: #fbfaf7;
    --fg: #1d1c1a;
    --muted: #736f66;
    --border: #e6e2d9;
    --accent: #3c5a6b;
    --accent-hover: #2e4551;
    --accent-fg: #ffffff;
    --accent-bg: #f2efe7;
    --danger: #a53f3f;
    --color-yellow: #e8d477;
    --color-green: #8fbf8f;
    --color-blue: #7fa8c9;
    --color-pink: #d98fae;
    --color-purple: #a888c9;
    --surface: #ffffff;
    --shadow-sm: 0 1px 2px rgba(29, 28, 26, 0.05);
    --shadow-md: 0 6px 18px rgba(29, 28, 26, 0.10);
    --radius: 0.55rem;
}

html.theme-dark {
    --bg: #16181a;
    --fg: #e9e6df;
    --muted: #948f83;
    --border: #2c2e2f;
    --accent: #85adc0;
    --accent-hover: #9fc0d1;
    --accent-fg: #12181a;
    --accent-bg: #1e2022;
    --danger: #d17a7a;
    --color-yellow: #a68b2e;
    --color-green: #4c8a5c;
    --color-blue: #4a7ba6;
    --color-pink: #a65a7a;
    --color-purple: #7a5aa6;
    /* A hair lighter than --bg (#16181a) so a card reads as raised against
       the page rather than blending into it — dark mode's equivalent of the
       white-on-off-white separation light mode gets for free. Shadows go
       deeper/darker than light mode's since a soft shadow barely registers
       against a dark background; the raised --surface fill does most of the
       "this is a card" work here, the shadow just reinforces it. */
    --surface: #1e2124;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.30);
    --shadow-md: 0 6px 18px rgba(0, 0, 0, 0.45);
    --radius: 0.55rem;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    font-family: var(--font-sans);
    line-height: 1.5;
}

a {
    color: var(--accent);
}

/* Custom scrollbar, site-wide — the one piece of chrome left at its
   plain OS default in an otherwise fully themed page, most noticeable
   on the comment-thread panel (static/js/highlights.js), which scrolls
   on its own independent of the page. scrollbar-color/scrollbar-width
   (Firefox) and the ::-webkit-scrollbar family (Chrome/Safari/Edge)
   are two separate, non-overlapping mechanisms — neither browser
   camp supports the other's — so both are declared; every color comes
   from the same custom properties as everything else, which is what
   makes this switch themes automatically with no dark-mode-specific
   rule needed here at all. */
html {
    scrollbar-color: var(--border) var(--bg);
    scrollbar-width: thin;
}

::-webkit-scrollbar {
    width: 0.8rem;
    height: 0.8rem;
}

::-webkit-scrollbar-track {
    background: var(--bg);
}

/* The thumb's own border, in the track's color rather than transparent,
   is what gives it a padded, floating "pill" look instead of filling
   the full scrollbar width edge-to-edge — a border on a scrollbar thumb
   clips to the padding box the same way it would on any other element,
   opening a visible gap between thumb and track on every side. */
::-webkit-scrollbar-thumb {
    background-color: var(--border);
    border-radius: 0.5rem;
    border: 0.2rem solid var(--bg);
}

::-webkit-scrollbar-thumb:hover {
    background-color: var(--muted);
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* A single centered column, capped in width, is what makes long-form
   writing readable — the eye loses its place on a line of text that spans
   a full wide monitor. This is most of the entire "layout system" this
   site needs. Content inside defaults to the serif stack, since almost
   everything in .page is either a post title or prose; the handful of UI
   controls (buttons, inputs, labels, meta text) override it back below. */
.page {
    max-width: 38rem;
    margin: 0 auto;
    padding: 3rem 1.5rem 5rem;
    font-family: var(--font-serif);
    font-size: 1.0625rem;
    line-height: 1.7;
}

.page label,
.page .meta,
.page button,
.page input,
.page select,
.page .section-heading {
    font-family: var(--font-sans);
}

/* --- Masthead / nav --- */

/* Sticky masthead (added in the refresh) — the nav rides along at the top
   of the viewport so switching tracks / search / theme is always one reach
   away without scrolling back up, the same behavior every reading site of
   this kind has. z-index 50 keeps it above ordinary page content but below
   the floating comment-thread / link-popup panels (z-index 100) and the
   focus-mode overlay (998+), so those still layer on top of it. The
   background is the opaque page color specifically so scrolled content
   doesn't ghost through it; a slightly translucent fill with backdrop-blur
   was tempting but reads as muddy over this site's warm off-white ground,
   so a solid fill plus a soft shadow does the "floating strip" job instead.
   Heading scroll-margin below is the necessary partner to this: an in-page
   #anchor jump (the table of contents) would otherwise land the target
   heading hidden underneath this bar. */
nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1.5rem;
    padding: 1.1rem 1.5rem;
    border-bottom: 1px solid var(--border);
    font-family: var(--font-sans);
    position: sticky;
    top: 0;
    z-index: 50;
    background: var(--bg);
    box-shadow: 0 1px 0 var(--border), 0 2px 12px rgba(0, 0, 0, 0.03);
}

.brand {
    font-family: var(--font-serif);
    font-size: 1.2rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--fg);
    text-decoration: none;
    padding: 0.3rem 0.5rem;
    margin-left: -0.5rem;
    border-radius: 0.3rem;
    transition: background-color 0.15s ease;
}

.brand:hover {
    background: var(--accent-bg);
}

.nav-links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem 0.3rem;
    margin-left: auto;
}

/* Padding + border-radius here (rather than the negative-margin trick used
   on .brand, which has nothing beside it) is what gives each link its own
   hover-highlight "pill," matching .theme-toggle's hover treatment below —
   previously only the toggle button had this, and every other nav item
   just changed text color with no background, an inconsistency between
   items that are otherwise all part of the same row. Real (non-negative)
   padding, with the row's own gap sized to match, keeps adjacent pills
   from ever touching — including when the row wraps onto a second line on
   narrow screens, where a negative-margin version of this would have let
   wrapped rows' hover backgrounds collide vertically. */
.nav-links a {
    color: var(--muted);
    text-decoration: none;
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    /* A link's own box always auto-sizes to hug its text + this padding
       exactly (width: auto, the default) — there's no such thing as text
       "overflowing" it. What actually reads as a cramped hover pill is
       just too little padding relative to the letter-spaced, uppercase
       text sitting inside it; letter-spacing in particular adds space
       after every character including the last one, so a tight right
       padding reads as even tighter than the same number on the left.
       Generous, slightly asymmetric padding (a bit more on the right) is
       the actual fix — not a box-model bug, a padding value that was too
       small for how the text was styled. */
    padding: 0.5rem 0.85rem 0.5rem 0.75rem;
    border-radius: 0.3rem;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.nav-links a:hover {
    color: var(--fg);
    background: var(--accent-bg);
}

/* display: contents makes this <form> invisible to layout — its one child
   (the toggle button) sits directly in the .nav-links flex row as if the
   <form> wrapper weren't there at all, instead of adding an extra box that
   would throw off the row's alignment and gaps. */
.theme-toggle-form {
    display: contents;
}

/* Same display: contents trick as .theme-toggle-form above — this form's
   one child (the search box) sits directly in the .nav-links row. */
.nav-search-form {
    display: contents;
}

.nav-search-form input[type="text"] {
    width: 8rem;
    padding: 0.35rem 0.6rem;
    font-size: 0.8rem;
    border: 1px solid var(--border);
    border-radius: 0.3rem;
    background: var(--bg);
    color: var(--fg);
}

.nav-search-form input[type="text"]:focus {
    outline: none;
    border-color: var(--accent);
}

/* Emoji glyphs carry their own internal padding that varies by font/OS,
   which is why sizing this with just font-size/line-height (the first
   attempt) left the sun/moon looking uncentered or clipped depending on
   the platform's emoji font. inline-flex + fixed width/height centers the
   glyph the same way regardless of its font metrics, and gives the button
   an actual fixed-size hit target instead of shrink-wrapping to whatever
   the glyph's natural box happens to be. */
.theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    background: none;
    border: none;
    border-radius: 50%;
    padding: 0;
    font-size: 1.05rem;
    line-height: 1;
    cursor: pointer;
}

.theme-toggle:hover {
    background: var(--accent-bg);
}

/* --- Headings ---
   .page-title is the one big heading per page (a post's title, "Drafts",
   a track name, ...) — always exactly one per page, always this style.
   .section-heading is a small-caps divider between distinct blocks on the
   *same* page ("Comments", "Danger zone") — there can be several per page.
   Neither is a bare `h1`/`h2` element selector, on purpose: a post's own
   Markdown body can contain real `# Heading` content of its own (rendered
   inside .prose below), and that needs its own, different, unrelated
   heading treatment — not to be silently swept up by page-chrome styling
   just because it happens to also be an <h1> or <h2> tag. */
.page-title {
    font-family: var(--font-serif);
    font-size: 2rem;
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: -0.01em;
    margin: 0 0 0.75rem;
}

.section-heading {
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 1rem;
}

/* --- Rendered post content ---
   Markdown-to-HTML output (the `markdown` filter) lands here, wrapped in
   .prose specifically so headings the post's author wrote (# Heading in
   their Markdown) get their own contained heading scale, instead of
   matching .page-title or .section-heading above. */
/* scroll-margin-top on every heading a table-of-contents link can target,
   so a #anchor jump leaves the heading clear of the now-sticky nav above
   instead of tucked underneath it. Applied to all levels (not just the
   h2/h3 _toc_ify links at, since a post's own in-body links could point at
   any heading id the toc extension generated). */
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
    scroll-margin-top: 4.5rem;
}

.prose h1 {
    font-size: 1.5rem;
    margin: 2rem 0 0.75rem;
}

.prose h2 {
    font-size: 1.25rem;
    margin: 1.75rem 0 0.6rem;
}

.prose h3 {
    font-size: 1.05rem;
    margin: 1.5rem 0 0.5rem;
}

.prose p,
.prose ul,
.prose ol {
    margin: 0 0 1.1rem;
}

.prose blockquote {
    margin: 0 0 1.1rem;
    padding-left: 1rem;
    border-left: 3px solid var(--border);
    color: var(--muted);
}

/* Epigraph — gwern-feature-ideas.md's "worth building" list. A quote a
   writer opens a post with (a quip, a line from the book being
   discussed) reads differently from an ordinary in-body blockquote, so
   it gets different styling: no left border/rule (a blockquote mid-post
   is "quoting something," an epigraph is "setting the tone before the
   post starts," a visually quieter role), italic, and centered rather
   than left-aligned. No markup convention needed beyond "the post's
   Markdown happens to open with a blockquote" — :first-child is exactly
   that check, since .prose only ever contains the rendered body, never
   the post title (a separate .page-title element outside it). */
/* The second selector covers the one case the plain :first-child version
   misses: on a long post, _toc_ify (app.py) prepends a <nav class="toc">
   as the first child of .prose, which pushes an opening-blockquote epigraph
   out of :first-child position and silently dropped its styling. Matching
   "a blockquote sitting immediately after the leading ToC nav" restores it,
   so an epigraph reads the same whether or not the post is long enough to
   have grown a table of contents above it. (post_revision_view and the feed
   render without _toc_ify, so the plain :first-child selector still applies
   there — both cases are covered.) */
.prose > blockquote:first-child,
.prose > nav.toc:first-child + blockquote {
    border-left: none;
    padding-left: 0;
    font-style: italic;
    text-align: center;
    font-size: 1.05rem;
}

/* Dropcap — the lowest-effort item on gwern-feature-ideas.md's list,
   named there as exactly that: gwern.net's own reasoning is that this is
   a purely typographic touch, not a functional feature. :first-of-type
   (not :first-child) is deliberate — it finds the first <p> regardless
   of whether an epigraph blockquote comes before it, so a post that
   opens with one still gets its dropcap on the first real paragraph of
   body text, not on nothing. */
.prose > p:first-of-type::first-letter {
    font-family: var(--font-serif);
    font-size: 3.4rem;
    font-weight: 600;
    line-height: 0.8;
    float: left;
    margin: 0.1rem 0.4rem 0 0;
    color: var(--accent);
}

/* A real bug, not a hypothetical: if a highlighted comment's anchor text
   starts at the very first word of the post (_inject_highlights, app.py
   — entirely possible; a reader can select from the beginning), that
   word arrives wrapped in an inline <mark>, and the browser still
   resolves ::first-letter through it — but a float-and-3.4rem-sized
   glyph nested inside a <mark> with its own background/border-bottom
   doesn't lay out cleanly in any browser tested: the enlarged letter and
   the mark's own underline collide and overlap instead of both
   rendering as intended. :has() (Chrome 105+/Safari 15.4+/Firefox
   121+ — safe to rely on for this site's tiny, known readership) lets
   this rule detect that exact case and back out cleanly with `all:
   revert` — no dropcap that one time, rather than a broken one. */
.prose > p:first-of-type:has(> mark:first-child)::first-letter {
    all: revert;
}

/* Link icons — a small marker after any link leaving this site, so a
   reader can tell before clicking rather than needing to hover for the
   internal-link popup (static/js/link-popup.js) to (not) appear. Every
   internal link on this site is written as a relative /posts/... path
   (see that script's own selector) — never an absolute URL, even to this
   site's own domain — so "starts with http" is already an exact,
   sufficient test for "this leaves the site," no need to also exclude
   scripta.pw's own domain the way a more defensive selector would. */
.prose a[href^="http"]::after {
    content: " ↗";
    font-size: 0.85em;
}

/* Admonitions — _admonition_ify() in app.py adds .admonition plus a
   per-label modifier class (.admonition-note/-warning/-epistemic-status)
   to the *paragraph* written as "**Note:** ..."/"**Warning:** ..."/
   "**Epistemic status:** ..." (not the surrounding <blockquote> — see
   that function's own comment for why: Python-Markdown merges
   consecutive blockquotes with nothing but a blank line between them
   into one shared <blockquote>, so the paragraph is the only element
   guaranteed to correspond 1:1 with one admonition). font-style/
   text-align are set explicitly, not left to inherit, specifically to
   survive the one real overlap case: an admonition that's *also* the
   first thing in the post sits inside a <blockquote> the epigraph rule
   above also matches, and italic/centered are inherited properties —
   without overriding them here, that inheritance would win regardless
   of any specificity/order tie-break, and a Note callout would come out
   looking like a quote. The :has() rule strips the *outer* blockquote's
   own border/padding/background whenever it contains an admonition, so
   what's visible is one clean callout box, not two nested left borders
   — supported in every current major browser, no fallback needed. */
.prose p.admonition {
    border-left: 3px solid var(--border);
    background: var(--accent-bg);
    padding: 0.75rem 1rem;
    margin: 0 0 1.1rem;
    font-style: normal;
    text-align: left;
    color: var(--fg);
}

.prose p.admonition-note {
    border-left-color: var(--accent);
}

.prose p.admonition-warning {
    border-left-color: var(--danger);
}

.prose p.admonition-epistemic-status {
    border-left-color: var(--muted);
}

.prose blockquote:has(> p.admonition) {
    border-left: none;
    padding-left: 0;
}

/* --- Table of contents (_toc_ify() in app.py, long posts only) --- */

.prose nav.toc {
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 1rem 1.25rem;
    margin: 0 0 1.8rem;
    background: var(--accent-bg);
}

.prose nav.toc .toc-label {
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 0.6rem;
}

.prose nav.toc ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.prose nav.toc li {
    margin: 0.35rem 0;
}

.prose nav.toc li.toc-sub {
    margin-left: 1.25rem;
    font-size: 0.92rem;
}

.prose nav.toc a {
    color: var(--fg);
    text-decoration: none;
}

.prose nav.toc a:hover {
    color: var(--accent);
    text-decoration: underline;
}

/* --- Collapsible sections (::: collapse ::: convention, _collapse_ify() in
   app.py) — plain <details>/<summary>, no JS needed for the toggle itself --- */

.prose details.collapse {
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.75rem 1rem;
    margin: 0 0 1.1rem;
}

.prose details.collapse[open] {
    padding-bottom: 1rem;
}

.prose details.collapse summary {
    cursor: pointer;
    font-weight: 600;
    color: var(--fg);
}

.prose details.collapse summary:hover {
    color: var(--accent);
}

.prose details.collapse > *:last-child {
    margin-bottom: 0;
}

.prose details.collapse[open] summary {
    margin-bottom: 0.75rem;
}

/* --- Sidenotes (footnotes rendered inline, gwern.net/Tufte-CSS style) ---
   _sidenote_ify() in app.py rewrites Python-Markdown's ordinary endnotes
   (reference in the text, note text in a list at the very bottom) into a
   <label>+checkbox+<span> trio sitting right at the reference point. That
   restructuring is what makes the rest of this pure CSS: no JavaScript
   computes a position or shows/hides anything — the browser's own
   :checked pseudo-class does that, and a plain CSS float does the rest.
   counter-reset here (not on <body>) means numbering restarts at 1 for
   each post, matching what a reader expects from footnotes. */
.prose {
    counter-reset: sidenote-counter;
}

/* Every sidenote selector below is scoped with a leading .prose — not just
   for semantic scoping, but because a bare `.sidenote-toggle`/
   `.sidenote-number` (specificity: one class) loses to the site's generic
   `.page label { display: block; text-transform: uppercase; ... }` rule
   (specificity: one class + one element) further up this file. .prose's
   own <label> is a real <label> element, so without this it silently
   inherited that form-field styling — block display breaking it out of
   the paragraph's inline flow, plus an uppercase transform — even though
   nothing about it is actually a form field. Caught by inspecting
   getComputedStyle() on the rendered label directly, not by eye: at a
   glance the numeral still looked plausible, just misplaced. Two classes
   in a descendant selector (.prose .sidenote-number) outweighs one class
   plus one element (.page label) in CSS's specificity comparison — the
   number of classes is compared before the element type is — so this is
   the smallest change that reliably wins regardless of source order. */

/* The checkbox itself is never meant to be seen — only its label (the
   small numeral) is. It stays a real, focusable, tabbable checkbox rather
   than `display: none` (which would also remove it from keyboard/tab
   order) — visually hidden, not functionally hidden. */
.prose .sidenote-toggle {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.prose .sidenote-number {
    display: inline;
    margin: 0;
    text-transform: none;
    counter-increment: sidenote-counter;
    cursor: pointer;
}

.prose .sidenote-number::after {
    content: counter(sidenote-counter);
    font-size: 0.7em;
    vertical-align: super;
    color: var(--accent);
    margin: 0 0.15em;
}

/* Narrow screens (below the sidenote breakpoint further down): the note
   stays hidden until its numeral is tapped, then appears inline right
   after it — the same "floating footnote" fallback gwern.net uses on
   mobile, just built with a checkbox instead of JS. */
.prose .sidenote {
    display: none;
    font-family: var(--font-sans);
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--muted);
    background: var(--accent-bg);
    border-radius: 0.3rem;
    padding: 0.6rem 0.8rem;
    margin: 0.5rem 0;
}

.prose .sidenote-toggle:checked ~ .sidenote {
    display: block;
}

/* Wide screens: .page's own centered 38rem column already leaves a wide,
   permanently-blank margin on either side — this is exactly the unused
   space Tufte-CSS-style sidenotes are meant to reclaim. The note becomes
   permanently visible (no tap needed) and floats into that margin: a
   negative margin-right wider than the note itself pushes it clear of the
   main text column before its own width is subtracted back off, landing
   it a few rem past .page's edge rather than overlapping the body copy.
   See the README for the exact math and why these particular numbers. */
@media (min-width: 78rem) {
    .prose .sidenote-toggle {
        display: none;
    }

    .prose .sidenote-number {
        cursor: default;
    }

    .prose .sidenote {
        display: block;
        float: right;
        clear: right;
        width: 13rem;
        margin: 0.15rem -16rem 0.5rem 0;
        padding: 0 0 0 0.75rem;
        border-left: 2px solid var(--border);
        border-radius: 0;
        background: none;
        font-size: 0.8rem;
    }
}

/* --- Flash messages --- */

.flash {
    font-family: var(--font-sans);
    font-size: 0.9rem;
    background: var(--accent-bg);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 0.3rem;
    padding: 0.7rem 1rem;
    margin: 0 0 1.5rem;
}

/* --- Meta text (dates, word counts, byline) --- */

.meta {
    color: var(--muted);
    font-size: 0.85rem;
}

/* static/js/editor.js's live word count (post editor only — see its own
   data-word-count-for wiring) — same visual weight as .meta above (the
   published post's own word-count/reading-time line), just computed
   against the textarea's current, unsaved value instead. */
.word-count {
    display: block;
    color: var(--muted);
    font-size: 0.8rem;
    margin-top: 0.3rem;
}

/* Auto-save status (static/js/editor.js's wireAutosave) — the background
   save used to be entirely silent (see post_autosave in app.py), which
   meant a writer had no way to tell it was working, or when it last ran.
   This is a quiet, non-intrusive confirmation in the same muted register as
   the word count beside it — "Saving…" while a request is in flight,
   "Draft saved · 14:32" once it lands — never a modal or a flash, just a
   line of small text that updates in place. Empty (and so invisible) until
   the first real save happens. */
.autosave-status {
    display: inline;
    margin-left: 0.75rem;
    color: var(--muted);
    font-size: 0.8rem;
    font-family: var(--font-sans);
    transition: opacity 0.3s ease;
}

.autosave-status.saved {
    color: var(--accent);
}

/* Reading-progress bar (static/js/reading-progress.js, post pages only) — a
   thin accent line pinned to the very top edge of the viewport that fills
   left-to-right as the reader moves through a post's body, the same
   at-a-glance "how far in am I" cue long-form reading sites use. z-index 60
   sits it just above the sticky nav (50) so it rides along the top edge
   rather than scrolling under the masthead. Purely progressive
   enhancement: the element is created by JS, so with scripting off there's
   simply no bar and nothing else changes. */
.reading-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    width: 0;
    background: var(--accent);
    z-index: 60;
    transition: width 0.1s linear;
    pointer-events: none;
}

/* static/js/editor.js's draft-recovery banner — same "notice" shape as
   .flash above (accent-bg fill, accent left border), not a new visual
   language, since it's the same kind of thing: a message about the
   form's own state, just one with two actions attached instead of zero. */
.draft-recovery-banner {
    font-family: var(--font-sans);
    font-size: 0.85rem;
    background: var(--accent-bg);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 0.3rem;
    padding: 0.6rem 0.9rem;
    margin: 0 0 0.6rem;
}

/* --- Forms --- */

.page label {
    display: block;
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 1.25rem 0 0.4rem;
}

.page p:first-child label {
    margin-top: 0;
}

/* The "Publish now" checkbox needed its own rule: `.page label` above sets
   display: block for the text-field labels (a label sits on its own line
   above its input), but applying that same block display to a checkbox's
   label pushed the label text onto a separate line below the checkbox
   instead of sitting next to it — checkbox and label read as two
   disconnected pieces instead of one control. .checkbox-field makes the
   *wrapping* element the flex row and puts the label back to inline,
   rather than nesting a <label> around the checkbox (a <label> containing
   another <label> is invalid HTML and leaves click/focus behavior undefined). */
.checkbox-field {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 1.25rem 0 0.4rem;
}

.checkbox-field label {
    display: inline;
    margin: 0;
}

/* WTForms' ListWidget (used by MultiCheckboxField in forms.py, for the
   account-creation track picker) renders its options as a plain <ul><li>
   list — this undoes the browser's default list indent/bullets and gives
   each checkbox+label pair the same "sit on one line together" treatment
   .checkbox-field gives a single checkbox above. */
.checkbox-group ul {
    list-style: none;
    padding: 0;
    margin: 0.4rem 0;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.checkbox-group li {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* Stands in for a <p> specifically where a field can't be wrapped in one
   (see the comment in admin.html) — matches a browser's default paragraph
   margin so the Tracks field lines up with the plain <p>-wrapped fields
   above and below it. */
.field-block {
    margin: 1em 0;
}

.checkbox-group label {
    display: inline;
    margin: 0;
}

input[type="checkbox"] {
    /* accent-color re-themes the browser's native checkbox to match the
       site's palette — one property, no need to hand-build a custom
       checkbox out of extra markup and a hidden real input underneath it. */
    accent-color: var(--accent);
    width: 1rem;
    height: 1rem;
    margin: 0;
}

input[type="text"],
input[type="password"],
select,
textarea {
    background: var(--bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 0.3rem;
    padding: 0.55rem 0.7rem;
    font: inherit;
    font-family: var(--font-sans);
    font-size: 0.95rem;
    width: 100%;
    max-width: 100%;
}

/* input[type="file"] gets none of the theming above — file inputs don't
   match input[type="text"] etc.'s selector, and left alone render as the
   browser's own unstyled, always-light native control (a white "Choose
   File" button, black text) regardless of the page's theme, the one
   remaining input on this site that didn't already fit the dark palette.
   ::file-selector-button is the standard hook (Chrome/Edge/Firefox/Safari
   all support it) for restyling just the button part to match every
   other button on the site; the filename text beside it is styled
   directly on the input itself. */
input[type="file"] {
    color: var(--muted);
    font-size: 0.85rem;
}

input[type="file"]::file-selector-button {
    background: var(--accent);
    color: var(--accent-fg);
    border: 1px solid var(--accent);
    border-radius: 0.3rem;
    padding: 0.5rem 0.9rem;
    font: inherit;
    font-family: var(--font-sans);
    font-size: 0.85rem;
    cursor: pointer;
    margin-right: 0.7rem;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

input[type="file"]::file-selector-button:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
}

/* Same "input and button need a real gap on one row" reasoning as
   .search-form below — an <input type="file"> and its Upload button have
   no default gap of their own either. */
.upload-form {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    flex-wrap: wrap;
}

/* The generic input[type="text"] rule above sets width: 100% — correct for
   a field alone on its own line, but here it meant the input claimed the
   entire row's width, leaving no room for the Search button beside it and
   forcing it onto the next line with zero margin between them (an <input>
   and <button> have no default gap of their own). display: flex plus an
   explicit flex: 1 on the input (overriding the 100% for this context
   specifically) puts them on one row with a real gap, and lets the button
   size to its own content instead of stretching full-width too. */
.search-form {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.search-form input[type="text"] {
    flex: 1;
    width: auto;
}

/* Every textarea that accepts some amount of Markdown (the post editor,
   and — since fenced code/inline code became a thing comments render —
   every comment textarea too) gets a monospace font; the title field and
   other plain single-line inputs don't share this class and stay in the
   sans body font, since Markdown source (headings, ``` fences, list
   markers, code) is genuinely easier to write and align in monospace,
   and plain prose isn't. */
.markdown-input {
    font-family: var(--font-mono);
}

/* Toggled by wireImageUpload's dragenter/dragleave/drop handlers
   (editor.js) — a visible cue that dropping an image here does
   something, before the drop actually happens. */
.markdown-input.drag-over {
    outline: 2px dashed var(--accent);
    outline-offset: -2px;
}

/* static/js/editor.js builds one of these directly above any
   .markdown-input textarea whose data-toolbar attribute it recognizes.
   Originally styled with no border or background at rest at all (quiet
   nav-link-style chrome, on the theory that a 6-button row didn't need
   much visual weight) — that stopped working once footnote/admonition/
   collapse/list/table snippet buttons brought this up to 14 buttons on
   the full editor: a wall of borderless uppercase text wrapping across
   three lines read as a run-on sentence, not a toolbar, since nothing
   marked where one button ended and the next began until the cursor was
   already on top of it. The bordered strip below plus a real border on
   each button at rest (not just on hover) is what fixes that — every
   button now reads as its own pressable chip immediately, not only on
   hover. */
.markdown-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.5rem;
    padding: 0.5rem;
    background: var(--accent-bg);
    border: 1px solid var(--border);
    border-radius: 0.4rem;
}

/* A button's own box always auto-sizes to hug its text + padding exactly
   (width: auto) — text can't actually overflow it. What read as a
   too-small hover box was too little padding for uppercase,
   letter-spaced text at this size, the same issue .nav-links a had
   above. Two changes here instead of just more padding, since 14 of
   these sit in one crowded strip rather than a handful in a nav bar:
   normal sentence case instead of uppercase+letter-spacing (a big part
   of what made each label feel like it was straining against its own
   box), and a size increase (0.72rem was genuinely hard to read, not
   just tight) — both buy back enough horizontal room to afford real
   padding without needing more lines than before. */
.markdown-toolbar button {
    background: var(--bg);
    color: var(--muted);
    border: 1px solid var(--border);
    border-radius: 0.3rem;
    padding: 0.42rem 0.75rem;
    font-family: var(--font-sans);
    font-size: 0.8rem;
    font-weight: 500;
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.markdown-toolbar button:hover {
    background: var(--accent);
    color: var(--accent-fg);
    border-color: var(--accent);
}

/* The Focus mode toggle is a mode switch, not an insertion action like
   everything to its left, so it's marked as its own small group with a
   left divider rather than just blending into the run of formatting/
   snippet buttons — but deliberately *not* pushed to the row's far
   right with margin-left: auto the way an earlier version of this rule
   did. auto-margin positioning depends entirely on how much room is
   left on whichever line the wrapping flex row happens to end on, which
   at 14 buttons wide is unpredictable — the result was this button
   stranded alone on its own otherwise-empty final line, visually
   disconnected from the row it's actually part of (reported directly:
   "focus mode seems to be oddly positioned"). Left in the normal flow,
   it wraps exactly like every other button and always sits directly
   beside its neighbors. */
.markdown-toolbar button.focus-mode-toggle {
    margin-left: 0.2rem;
    padding-left: 0.95rem;
    border-left: 2px solid var(--border);
}

/* --- Focus mode (static/js/editor.js's toggleFocusMode/wireFocusMode) ---
   No DOM reparenting: the textarea and its toolbar are simply repositioned
   fixed on top of everything else via this one class on <body>, so every
   listener already attached to them (autosave, word count, draft
   recovery) keeps working untouched — it's still the same elements. */

body.focus-mode {
    overflow: hidden;
}

body.focus-mode::before {
    content: "";
    position: fixed;
    inset: 0;
    background: var(--bg);
    z-index: 998;
}

body.focus-mode .markdown-toolbar {
    position: fixed;
    top: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 999;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    padding: 0.3rem;
}

body.focus-mode .markdown-input[data-toolbar="full"] {
    position: fixed;
    top: 4.5rem;
    bottom: 2rem;
    left: 50%;
    transform: translateX(-50%);
    width: min(700px, 90vw);
    z-index: 999;
    font-size: 1.1rem;
    line-height: 1.7;
    padding: 2rem;
    resize: none;
    box-shadow: 0 0 0 1px var(--border);
}

/* --- Buttons ---
   The bare element selector below is the default look — solid, filled,
   "the main action on this form" (Save, Log in, Search, Post comment).
   .button-secondary and .button-danger are opt-in overrides for the two
   places that need a visually different weight: a lesser action (Upload)
   and a destructive one (Delete post). */
button,
input[type="submit"] {
    background: var(--accent);
    color: var(--accent-fg);
    border: 1px solid var(--accent);
    border-radius: 0.3rem;
    padding: 0.55rem 1.1rem;
    font: inherit;
    font-family: var(--font-sans);
    font-size: 0.85rem;
    letter-spacing: 0.02em;
    cursor: pointer;
    /* Eased color change so hover reads as a smooth state change rather than
       an instant flip — the same 0.15s ease already used on the nav links,
       toolbar buttons, and cards, applied here so every interactive surface
       animates consistently. Deliberately only transition/hover-color and
       nothing heavier (no box-shadow or transform): the bare `button`
       selector is reused by the flat text buttons (.link-button, the theme
       toggle, the toolbar, the comment-thread icons), which override the
       fill but would still inherit a shadow or a press-nudge from here —
       keeping this to color alone is what lets those stay visually flat. */
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

button:hover,
input[type="submit"]:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
}

.button-secondary {
    background: transparent;
    color: var(--fg);
    border-color: var(--border);
}

.button-secondary:hover {
    background: transparent;
    border-color: var(--fg);
}

.button-danger {
    background: transparent;
    color: var(--danger);
    border-color: var(--danger);
}

.button-danger:hover {
    background: var(--danger);
    color: #ffffff;
}

/* --- Code blocks ---
   codehilite (the markdown filter's syntax-highlighting extension) wraps
   every fenced code block as <div class="highlight"><pre><code>...</code>
   </pre></div>. `pre` is deliberately the ONLY element in that chain that
   gets a background, border, and border-radius — .highlight is reset to
   be fully invisible instead of also carrying its own box. Pygments'
   generated CSS (pygments.css) ships its own hardcoded `.highlight {
   background: ... }` rule by default; if left in, that gave the *outer*
   div a flat-cornered background sitting directly behind pre's *rounded*
   corners, so a sliver of the wrong-colored square box showed through at
   each corner. pygments.css has that rule stripped out at generation time
   for exactly this reason — this reset is the other half of that fix. */
.highlight {
    background: transparent;
    margin: 0;
    padding: 0;
}

pre {
    overflow-x: auto;
    margin: 0;
    padding: 0.9rem 1rem;
    border-radius: 0.4rem;
    border: 1px solid var(--border);
    background: var(--accent-bg);
    font-size: 0.9rem;
}

code {
    font-family: var(--font-mono);
}

/* --- Tables (the `tables` Markdown extension, _render_body_html in app.py) ---
   Styled to match the rest of the prose: a sans header row on the accent-bg
   tint (the same fill admonitions and the ToC use), hairline borders in the
   site's --border color, and comfortable cell padding. display: block +
   overflow-x: auto is the standard wrapper-less way to let a table wider than
   the reading column scroll on its own — the same overflow treatment `pre`
   (code blocks) and .katex-display already use — instead of forcing the whole
   page sideways on a narrow screen. Scoped to .prose so it only ever touches
   a post author's own Markdown tables, never any layout the site itself
   builds out of other elements. */
.prose table {
    border-collapse: collapse;
    width: 100%;
    margin: 0 0 1.1rem;
    font-size: 0.95rem;
    display: block;
    overflow-x: auto;
}

.prose th,
.prose td {
    border: 1px solid var(--border);
    padding: 0.5rem 0.7rem;
    text-align: left;
}

/* A CSS text-align always beats an HTML align="" attribute, so the flat
   "text-align: left" above would quietly cancel the per-column alignment
   the tables extension emits (align="center"/"right", see app.py). These
   attribute-matched rules put alignment back in CSS's hands at the same
   layer, so a ":---:" or "---:" column actually centers/right-aligns
   instead of being overridden back to left. */
.prose th[align="center"],
.prose td[align="center"] {
    text-align: center;
}

.prose th[align="right"],
.prose td[align="right"] {
    text-align: right;
}

.prose thead th {
    background: var(--accent-bg);
    font-family: var(--font-sans);
    font-weight: 600;
}

/* --- Math (static/vendor/katex/, math-render.js) ---
   KaTeX's own CSS already uses `color: currentColor` throughout (checked
   directly in katex.min.css, not assumed) — every formula already
   inherits .prose's real text color, light or dark theme alike, with no
   override needed here at all. The one real gap: this KaTeX version's own
   .katex-display rule has no overflow handling, and .katex itself sets
   `white-space: nowrap` — so a display equation wider than the reading
   column would force the whole page to scroll horizontally instead of
   just the equation, the same failure mode `pre` (code blocks, above)
   already solved for exactly this reason. Same fix, same one property. */
.katex-display {
    overflow-x: auto;
    overflow-y: hidden;
}

/* --- Post lists (track list, drafts, reader dashboard) --- */

/* Post lists render as cards now (the moderate visual refresh), not a run
   of bottom-bordered rows — each post its own raised --surface panel with a
   real gap between them, so a list of posts reads as a set of distinct
   things to pick from rather than one dense block of dividers. The hover
   lift (a deeper shadow + a 1px rise + an accent-tinted border) is the
   "these are clickable" cue the flat rows never had. The whole <li> isn't a
   link, so the title <a> stays the actual click target; the card just makes
   the row it sits in feel like one. .related-posts overrides this back to a
   flat list on purpose — see its own rule. */
ul.post-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
}

ul.post-list li {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1rem 1.15rem;
    box-shadow: var(--shadow-sm);
    transition: box-shadow 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}

/* :has(a) so a placeholder <li> ("No posts yet.", which has no link) stays
   a plain flat message and doesn't grow a hover lift that goes nowhere. */
ul.post-list li:has(a):hover {
    box-shadow: var(--shadow-md);
    border-color: var(--accent);
    transform: translateY(-1px);
}

ul.post-list a {
    font-family: var(--font-serif);
    font-size: 1.15rem;
    font-weight: 500;
    color: var(--fg);
    text-decoration: none;
}

ul.post-list a:hover {
    color: var(--accent);
}

ul.post-list .meta {
    display: block;
    margin-top: 0.35rem;
}

/* The tag links inside a card's own meta line are the one exception to
   "meta is a block on its own line" above — they should read as inline
   chips within that line, not each break onto a new one. */
ul.post-list .meta a {
    font-family: var(--font-sans);
    font-size: inherit;
    color: var(--muted);
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

ul.post-list .meta a:hover {
    color: var(--accent);
}

/* --- Similar posts / backlinks (post_view.html, _similar_posts and
   _backlinking_posts in app.py) — reuses ul.post-list's own item styling
   above; this only lays the (up to two) columns side by side. Nested
   inside the "About this post" <details> now (see .about-post-toggle
   below) rather than sitting as its own top-level page section, which is
   why this has no border/heavy margin of its own anymore — the <details>
   it lives in already provides that visual boundary once, rather than
   this block adding a second one nested just inside it. --- */

.related-posts {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2rem;
    margin-top: 1rem;
}

.related-posts ul.post-list a {
    font-size: 1rem;
}

/* Flatten the card treatment back out inside "About this post" — this
   block is deliberately quiet chrome tucked in a <details>, so its similar/
   backlink lists stay plain text rows (no surface fill, border, shadow, or
   hover lift) rather than a grid of little cards competing with the post
   itself. Reuses ul.post-list's markup for the link styling; only the card
   shell is undone here. */
.related-posts ul.post-list {
    gap: 0.25rem;
}

.related-posts ul.post-list li {
    background: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0.15rem 0;
}

.related-posts ul.post-list li:has(a):hover {
    box-shadow: none;
    border-color: transparent;
    transform: none;
}

@media (max-width: 640px) {
    .related-posts {
        grid-template-columns: 1fr;
        gap: 1rem;
    }
}

/* --- Admin panel --- */

.account-name {
    font-family: var(--font-serif);
    font-size: 1.15rem;
    font-weight: 500;
}

/* Each pending-post / account row in admin.html has one or two small forms
   (Approve/Reject, or just Delete) sitting side by side. Plain <form>
   elements have no layout relationship to each other by default — this is
   the same "elements touch with no gap" class of bug the search box hit
   earlier, headed off here from the start rather than fixed after the
   fact. */
.admin-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.6rem;
}

.admin-actions button {
    font-size: 0.8rem;
    padding: 0.4rem 0.8rem;
}

/* --- Dashboard summary list (owner view: track counts + drafts count) ---
   This was previously an unstyled bare <ul> — default browser bullets and
   default underlined blue links, sitting right below the fully-styled nav
   and page title. It's a different shape of list than ul.post-list (a
   title + a count, not a title + meta line), so it gets its own rule
   rather than force-fitting it into that one. */
/* The owner/writer dashboard's counts (Finance N posts, Drafts N, ...) sit
   in a single raised card now rather than a bare divided list — one panel
   of at-a-glance stats, matching the card language the post lists below it
   use. The internal per-row dividers stay (they separate the stats within
   the one card); the card's own border/fill/shadow is what's new. */
ul.summary-list {
    list-style: none;
    padding: 0.4rem 1.15rem;
    margin: 0 0 1.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

ul.summary-list li {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 0.8rem 0;
    border-bottom: 1px solid var(--border);
}

ul.summary-list li:last-child {
    border-bottom: none;
}

ul.summary-list a,
ul.summary-list .stat-label {
    font-family: var(--font-serif);
    font-size: 1.1rem;
    color: var(--fg);
    text-decoration: none;
}

ul.summary-list a:hover {
    color: var(--accent);
}

/* --- Comments --- */

.comment {
    padding: 1.1rem 0;
    border-top: 1px solid var(--border);
}

.comment:first-of-type {
    border-top: none;
    padding-top: 0;
}

.comment .meta {
    margin-bottom: 0.35rem;
}

.author-badge {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
    border: 1px solid var(--accent);
    border-radius: 0.25rem;
    padding: 0.05rem 0.35rem;
    margin-left: 0.4rem;
}

.question-badge {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    border: 1px solid var(--border);
    border-radius: 0.25rem;
    padding: 0.05rem 0.35rem;
    margin-left: 0.4rem;
}

.private-badge {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--danger);
    border: 1px solid var(--danger);
    border-radius: 0.25rem;
    padding: 0.05rem 0.35rem;
    margin-left: 0.4rem;
}

/* --color-yellow, not --danger like .private-badge above — a correction
   sitting right next to the Author badge it always accompanies would be
   easy to misread as another "private"-style warning if the two shared
   the same red. Reusing the existing comment-color palette (already
   theme-aware — see :root and html.theme-dark) instead of introducing a
   one-off new color for a single badge. */
.correction-badge {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-yellow);
    border: 1px solid var(--color-yellow);
    border-radius: 0.25rem;
    padding: 0.05rem 0.35rem;
    margin-left: 0.4rem;
}

.comment-body {
    /* Comments are mostly still plain escaped text (see app.py's
       _render_comment_body for the one exception — a fenced code block
       gets real syntax highlighting, everything else stays plain) —
       pre-wrap is what makes a line break the commenter actually typed
       show up as one, without needing to convert it to a <br>. This is a
       <div> now, not a <p>, specifically so a highlighted code block's
       <pre> (block-level content) can legally sit inside it — nesting
       block content inside a <p> is exactly the HTML content-model
       violation that broke the admin panel's checkbox list earlier in
       this project (see README) — a <div> has no such restriction. */
    white-space: pre-wrap;
    line-height: 1.6;
}

.comment-body pre {
    /* The one place pre-wrap above needs to NOT apply — a code block's
       own whitespace is already handled by its highlight/pygments
       styling, and inheriting pre-wrap on top of that would double up. */
    white-space: pre;
}

/* Comment color tags (Comment.color, models.py) — a small left accent,
   only when a color was actually chosen; an ordinary comment (the
   overwhelming majority) keeps today's plain look untouched. box-shadow,
   not border-left: a real border-left occupies its own layout width (3px
   border + the padding-left added to clear it, ~15px total here before),
   stacking visually with .comment-replies' own 1rem indent step — enough
   that a colored reply sitting at the exact same tree depth as an
   uncolored sibling looked like it was nested one level deeper than it
   actually was, reported directly as a reply-nesting bug that traced
   back to this, not to the reply/threading logic (which turned out to be
   correct all along — see README). box-shadow draws the same colored
   line without taking up any box width itself, so only the small
   padding-left below (well under a full nesting step) is left to
   account for. */
.comment.comment-color-yellow { box-shadow: -3px 0 0 var(--color-yellow); padding-left: 0.4rem; }
.comment.comment-color-green { box-shadow: -3px 0 0 var(--color-green); padding-left: 0.4rem; }
.comment.comment-color-blue { box-shadow: -3px 0 0 var(--color-blue); padding-left: 0.4rem; }
.comment.comment-color-pink { box-shadow: -3px 0 0 var(--color-pink); padding-left: 0.4rem; }
.comment.comment-color-purple { box-shadow: -3px 0 0 var(--color-purple); padding-left: 0.4rem; }

/* --- Color picker (templates/_color_picker.html) ---
   Same zero-JS "hide the real input, style a sibling instead" trick
   already used for sidenotes and multi-track checkboxes elsewhere in
   this app — the radio itself is invisible but still real, focusable,
   and keyboard-operable; only its appearance is replaced. */
.color-picker {
    display: inline-flex;
    gap: 0.4rem;
    margin: 0.4rem 0;
    vertical-align: middle;
}

.color-swatch-input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.color-swatch {
    display: inline-block;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    border: 2px solid var(--border);
    cursor: pointer;
}

.color-swatch-input:checked + .color-swatch {
    border-color: var(--fg);
}

.color-swatch-input:focus-visible + .color-swatch {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.color-swatch-none { background: var(--bg); }
.color-swatch-yellow { background: var(--color-yellow); }
.color-swatch-green { background: var(--color-green); }
.color-swatch-blue { background: var(--color-blue); }
.color-swatch-pink { background: var(--color-pink); }
.color-swatch-purple { background: var(--color-purple); }

/* A reply's replies (templates/_comment_macros.html's recursive
   render_comment) nest inside this — the left border plus indent is what
   makes a "tree" actually read as one visually, at any depth, without
   needing depth-aware styling (each level just nests one more of these). */
.comment-replies {
    margin: 0.8rem 0 0 1rem;
    padding-left: 1rem;
    border-left: 2px solid var(--border);
}

.comment-replies .comment {
    /* padding-top/-bottom only — see the same note on .comment-thread
       .comment above; a colored reply nested here hit the identical
       shorthand-padding-clobbers-padding-left bug. */
    padding-top: 0.8rem;
    padding-bottom: 0.8rem;
}

/* Reply is a <details> (templates/_comment_macros.html), same zero-JS
   disclosure trick already used elsewhere on this site — "Reply" is the
   always-visible <summary>, the textarea/button only exist in the DOM,
   not hidden via JS, until a reader actually wants to use them. */
.comment-reply-toggle {
    margin-top: 0.5rem;
    font-size: 0.85rem;
}

.comment-reply-toggle summary {
    display: inline-block;
    cursor: pointer;
    color: var(--muted);
}

.comment-reply-toggle summary:hover {
    color: var(--accent);
}

.comment-reply-form {
    margin-top: 0.5rem;
}

.comment-reply-form textarea {
    font-size: 0.85rem;
    min-height: 3.5rem;
}

.comment-reply-form button {
    margin-top: 0.4rem;
    font-size: 0.8rem;
    padding: 0.4rem 0.8rem;
}

/* The root, bottom-of-post comment form (post_view.html) — collapsed
   behind the same <details> disclosure a reply already uses, but styled
   with more visual weight than a quiet "Reply" text link: this is the
   *only* invitation to comment most readers ever see on a page, not one
   of several nested reply prompts scattered through an existing thread,
   so it gets a bordered, button-like summary instead of .comment-reply-
   toggle summary's plain muted text. The form itself (declared inline in
   post_view.html, not given the compact .comment-reply-form treatment)
   keeps its original, more prominent field sizing — collapsing it
   shouldn't also shrink it once it's actually open. */
.new-comment-toggle {
    margin-top: 1.5rem;
    font-size: 1rem;
}

.new-comment-toggle summary {
    display: inline-block;
    cursor: pointer;
    color: var(--fg);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0.3rem;
    padding: 0.55rem 1rem;
    font-size: 0.85rem;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.new-comment-toggle summary:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.new-comment-toggle[open] summary {
    margin-bottom: 1rem;
}

/* "More options" on the post form (post_form.html) — Knowledge Topic and
   "revisits," collapsed by default. Reuses .comment-reply-toggle's own
   quiet summary treatment (no bordered button chip like .new-comment-
   toggle above gets — this sits inside a form the writer has already
   committed to, not a first invitation to act the way "Write a comment"
   is) with just enough of its own margin to read as a clearly separate
   block from the Tags field above and the Body field below. */
.more-options-toggle {
    margin: 1.25rem 0;
}

.more-options-toggle > p:first-of-type label {
    margin-top: 0;
}

/* "About this post" (post_view.html) — the consolidated revisits/
   revisited-by/similar-posts/backlinks disclosure, replacing what used
   to be up to four separate blocks scattered above and below the post
   body. Same quiet .comment-reply-toggle summary treatment as "More
   options" above; a section boundary of its own (top/bottom border) is
   what the standalone .related-posts grid used to provide before it
   moved in here, so this <details> carries that instead now. */
.about-post-toggle {
    margin: 1.5rem 0;
    padding: 1rem 0;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

.about-post-toggle[open] summary {
    margin-bottom: 0.75rem;
}

.about-post-toggle p.meta:last-of-type {
    margin-bottom: 0;
}

/* Previous/next adjacent-post links (post_view.html, app.py's
   _adjacent_posts) — a plain .meta line would left-align both, but
   "previous" reads naturally on the left and "next" on the right (the
   same left/right convention as a book's own page-turn arrows), so this
   one gets its own rule instead of reusing .meta as-is. */
.post-nav {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    margin: 1.5rem 0;
}

/* .inline-form + .link-button together let the comment-delete action
   (owner-only moderation) sit inside the meta line next to the username
   and date, instead of appearing as a full standalone button — a <form>
   is a block-level element by default, and its <button> gets the same
   filled/bordered look as every other button on the site unless told
   otherwise here. */
.inline-form {
    display: inline;
}

.link-button {
    display: inline;
    background: none;
    border: none;
    padding: 0;
    color: var(--muted);
    font-size: inherit;
    font-family: inherit;
    text-decoration: underline;
    cursor: pointer;
}

.link-button:hover {
    color: var(--danger);
}

/* .link-button's hover-red is right for the destructive comment-delete
   action it was written for, but "Save for later" isn't destructive
   either direction (saving or unsaving) — this override keeps the same
   plain-text-button shape while swapping in the site's ordinary accent
   hover instead of the danger color. */
.save-toggle:hover {
    color: var(--accent);
}

/* --- Text-highlight comments (app.py's _inject_highlights,
   static/js/highlights.js, templates/_comment_thread.html) ---
   The <mark> app.py wraps a comment's anchor text in. Neutral by
   default — a muted tint matching the site's palette rather than a
   typical yellow-highlighter mark, since most of these are meant to
   read as "someone left a note here," not as emphasis. A root comment
   with a chosen Comment.color gets a colored *underline* instead (the
   .comment-color-* overrides below) — the background stays the same
   neutral tint every highlight already has, on purpose: a real solid-
   color background reads as the writer's own emphasis on that text,
   which isn't what a commenter's color tag means. An earlier version of
   this tinted the whole background per color instead and was corrected
   directly: it made the color look like it was highlighting the post's
   own words, not tagging a comment about them. */
mark.highlight {
    background: var(--accent-bg);
    border-bottom: 2px solid var(--accent);
    color: inherit;
    cursor: pointer;
    padding: 0.05em 0;
}

/* :hover alone is enough for a highlight that's a single <mark> — the
   common case, plain prose. highlight-hover (static/js/highlights.js) is
   for the other case: a highlight split across several <mark> fragments
   because it crosses a syntax-highlighting token boundary, where :hover
   alone would only ever light up whichever one fragment the cursor is
   directly over. Same visual result either way, just driven by two
   different triggers. */
mark.highlight:hover,
mark.highlight.highlight-hover {
    background: var(--border);
}

mark.highlight.comment-color-yellow { border-bottom-color: var(--color-yellow); }
mark.highlight.comment-color-green { border-bottom-color: var(--color-green); }
mark.highlight.comment-color-blue { border-bottom-color: var(--color-blue); }
mark.highlight.comment-color-pink { border-bottom-color: var(--color-pink); }
mark.highlight.comment-color-purple { border-bottom-color: var(--color-purple); }

/* The floating thread panel — one per highlighted comment, all rendered
   hidden; static/js/highlights.js shows/positions whichever one matches
   the <mark> that was clicked. Visually the same "floating card" as
   .link-popup (same position: absolute reasoning — see that rule's own
   comment), reused here for a comment thread instead of a link preview.
   highlights.js also reuses this exact class for the "create a new
   highlight" panel (.highlight-create), so both stay visually consistent
   without duplicating the card styling. */
.comment-thread {
    position: absolute;
    z-index: 100;
    width: min(24rem, calc(100vw - 1.5rem));
    min-width: 16rem;
    max-width: calc(100vw - 1.5rem);
    max-height: min(22rem, calc(100vh - 1.5rem));
    min-height: 6rem;
    /* A thread can get genuinely long — a real discussion, several
       replies deep, maybe code in more than one of them — and a fixed
       24rem-wide card stops being a comfortable place to read that, not
       just a starting size. resize (both axes; overflow: auto on both,
       not just overflow-y, is what makes the resize handle show up in
       every browser) lets a reader drag it larger; max-width still caps
       *that* a little short of the full viewport so it can't be dragged
       into covering the page entirely by accident.
       max-height is deliberately not the same kind of "almost the full
       viewport" cap max-width is, and it took two rounds to land on that
       on purpose, not by accident — worth walking through both:
       (1) max-height used to be a flat 70vh, which silently capped
       *manual resize* too, not just the panel's own default size: a
       panel whose content already reached 70vh on open (an ordinary
       thread with a code block, on plenty of real screens) had zero
       vertical headroom left to resize into, while max-width's far more
       generous cap meant horizontal resize kept working — reported as
       "resize vertically, not just horizontally," confirmed by matching
       computed max-height against the panel's actual height, both
       385px, to the pixel, before the fix.
       (2) Raising max-height to match max-width's own near-full-viewport
       cap fixed that, but broke the *other* direction of the same
       tension: every panel's *default* size (not just how far it could
       be dragged) grew right along with the ceiling, since CSS resize
       has no way to cap a box's auto/content-driven height differently
       from how far a reader can manually drag it past that — reported
       directly, right after, as "the default vertical length is too
       long." A fixed, modest max-height (22rem) here is what keeps a
       panel's *default* appearance compact regardless of how long its
       content actually is; static/js/highlights.js's own resize-corner
       mousedown handler (already there for the resize-then-closes bug)
       raises max-height to the same generous calc(100vh - 1.5rem) cap
       max-width uses, at the exact moment a reader actually starts
       dragging to resize — so the default stays short, and manual
       resize still has the same real headroom either way. */
    overflow: auto;
    resize: both;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0.5rem;
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);
    padding: 1rem 1.1rem;
    font-family: var(--font-sans);
    font-size: 0.9rem;
}

.comment-thread[hidden] {
    display: none;
}

/* Drag handle (static/js/highlights.js) — negative margins pull it out
   to the panel's own edges (undoing .comment-thread's own padding just
   for this strip) so it reads as a distinct title bar, not more of the
   padded content area. A dedicated strip, not "drag from anywhere in the
   panel": dragging from the comment text itself would fight with
   ordinary text selection, and dragging from a reply button would fight
   with clicking it.
   flex, not a fixed height with the pin/close buttons absolutely
   positioned on top of it: the first version sized this strip and
   positioned the close button as two totally independent rules that
   just happened to roughly line up, and didn't — the header's own
   border-bottom was drawn straight across the middle of the close
   button. Real flex children are vertically centered by construction,
   whatever height this strip ends up being; the height itself is now
   just "however tall the buttons + their padding need," not a
   hand-picked number. */
.comment-thread-header {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    margin: -1rem -1.1rem 0.6rem;
    padding: 0.15rem 0.3rem;
    border-bottom: 1px solid var(--border);
    border-radius: 0.5rem 0.5rem 0 0;
    cursor: grab;
    transition: background-color 0.15s ease;
}

/* Neutral at rest, a tint of --accent-bg on hover — the same "this is
   interactive" language .theme-toggle/.brand/.nav-links a all already
   use elsewhere, so a plain unlabeled strip still reads as grabbable
   before a reader ever tries dragging it. */
.comment-thread-header:hover {
    background: var(--accent-bg);
}

.comment-thread-header:active {
    cursor: grabbing;
}

.comment-thread .comment {
    /* padding-top/-bottom only, not the padding shorthand: a colored
       comment's own .comment-color-* rule sets padding-left separately
       (see above), and this rule used to override that back to 0 with
       shorthand padding — same specificity, later in the file, so it
       won every time — leaving a colored comment's accent border flush
       against its text with no gap at all. */
    padding-top: 0.6rem;
    padding-bottom: 0.6rem;
}

.comment-thread .comment:first-of-type {
    padding-top: 0;
}

/* Pin and close are both plain icon buttons living inside
   .comment-thread-header now (flex children, see above) — same quiet
   "no border/background until hovered" treatment as .theme-toggle, so
   neither one competes visually with the drag handle they sit inside. */
.comment-thread-pin,
.comment-thread-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 0.3rem;
    color: var(--muted);
    line-height: 1;
    cursor: pointer;
    padding: 0.25rem 0.35rem;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.comment-thread-pin:hover,
.comment-thread-close:hover {
    background: var(--accent-bg);
    color: var(--fg);
}

.comment-thread-close {
    font-size: 1.1rem;
}

.comment-thread-pin {
    font-size: 0.85rem;
    opacity: 0.55;
}

/* Filled/opaque + tinted accent once pinned — the same "on" language a
   checked checkbox or a selected color swatch elsewhere on this site
   uses (a visibly different state, not just a label), so a pinned
   thread is obviously pinned at a glance rather than requiring a reader
   to remember whether they clicked it. */
.comment-thread-pin.pinned {
    opacity: 1;
    color: var(--accent);
}

.comment-thread-pin.pinned:hover {
    color: var(--accent-hover);
}

.highlight-create textarea {
    font-size: 0.85rem;
    min-height: 4rem;
}

.highlight-create button[type="submit"] {
    margin-top: 0.4rem;
    font-size: 0.8rem;
    padding: 0.4rem 0.8rem;
}

/* --- Post-form section dividers (Upload an image, Danger zone) --- */

.section-block {
    margin-top: 2.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--border);
}

/* --- Writing-streak heatmap (dashboard.html, _writing_streak in app.py) --- */

.heatmap-grid {
    display: flex;
    gap: 3px;
    overflow-x: auto;
    padding-bottom: 0.25rem;
}

.heatmap-week {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.heatmap-cell {
    display: block;
    width: 11px;
    height: 11px;
    border-radius: 2px;
    background: var(--border);
    /* A cell with real activity is a real <a> now (dashboard.html), not
       just a <span> — display: block (rather than relying only on flex-
       item blockification from the .heatmap-week parent) and no
       underline are what keep a clickable cell looking identical to a
       plain, unlinked one. */
    text-decoration: none;
}

.heatmap-cell:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.heatmap-empty {
    background: transparent;
}

.heatmap-level-0 {
    background: var(--border);
}

.heatmap-level-1 {
    background: var(--accent);
    opacity: 0.35;
}

.heatmap-level-2 {
    background: var(--accent);
    opacity: 0.6;
}

.heatmap-level-3 {
    background: var(--accent);
    opacity: 0.8;
}

.heatmap-level-4 {
    background: var(--accent);
    opacity: 1;
}

.danger-zone {
    margin-top: 1.5rem;
    padding: 1.1rem 1.25rem 1.4rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    background: var(--accent-bg);
}

.danger-zone .section-heading {
    color: var(--danger);
    margin-bottom: 0.75rem;
}

/* --- Link popups (static/js/link-popup.js) ---
   A single shared floating card, positioned in JS via inline top/left.
   position: absolute, not fixed — an earlier version used fixed while the
   JS math added window.scrollY/scrollX to a getBoundingClientRect() (a
   viewport-relative) value, which only produces the right answer for
   absolute positioning (document-relative); with fixed (viewport-
   relative), that scroll offset was being double-counted, so the popup
   would drift further from its link the more the page had been scrolled.
   Never caught by testing because the pages tested on weren't scrolled
   far enough for the drift to be visible. absolute also means the popup
   now scrolls naturally with the page instead of staying fixed on screen
   while its trigger link scrolls away underneath it — the more correct
   behavior anyway. Hidden by default; the [hidden] attribute the JS
   toggles is what actually shows/hides it, this file only styles its
   appearance once visible. Relies on no ancestor between this element and
   <html> setting its own position: relative/absolute/fixed — that would
   change what "document-relative" means and break the math; none
   currently do (.page and <body> are both position: static). */
.link-popup {
    position: absolute;
    z-index: 100;
    width: min(22rem, calc(100vw - 1.5rem));
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0.5rem;
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);
    padding: 1rem 1.1rem;
    font-family: var(--font-sans);
}

.link-popup[hidden] {
    display: none;
}

.link-popup-title {
    font-family: var(--font-serif);
    font-size: 1.05rem;
    font-weight: 600;
    margin: 0 0 0.3rem;
    padding-right: 1.5rem; /* keeps the title clear of the close button in the corner */
}

.link-popup-excerpt {
    font-size: 0.9rem;
    color: var(--fg);
    margin: 0.6rem 0;
}

/* Only ever present on a Wikipedia popup (_wikipedia_preview.html) — an
   internal post preview has no equivalent image. float: left + a fixed
   small size keeps a tall thumbnail from pushing the popup's height
   around unpredictably; text wraps naturally around it the same way a
   Wikipedia infobox image behaves on the article page itself. */
.link-popup-thumbnail {
    float: left;
    width: 3.5rem;
    height: 3.5rem;
    object-fit: cover;
    border-radius: 0.3rem;
    margin: 0.1rem 0.7rem 0.4rem 0;
}

.link-popup a {
    font-size: 0.85rem;
}

.link-popup-close {
    position: absolute;
    top: 0.5rem;
    right: 0.6rem;
    background: none;
    border: none;
    color: var(--muted);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.2rem;
    /* Only reachable once the popup is pinned open (see link-popup.js) —
       otherwise a plain hover preview would show a close button that does
       nothing useful, since mousing away already dismisses it. */
    display: none;
}

.link-popup.pinned .link-popup-close {
    display: block;
}

/* has-popup marks a link the JS is actually watching (i.e. it resolved to
   a real internal post link or a Wikipedia article link — see
   popupTarget() in link-popup.js) — a small dotted underline distinguishes
   it from an ordinary link before the reader ever hovers it, the same way
   gwern.net marks up popup-enabled links. */
a.has-popup {
    text-decoration-style: dotted;
    text-underline-offset: 0.2em;
}

/* Printing/PDF-export view of a post. Everything here only matters on
   paper (or a "Save as PDF" dialog, which uses the same media query) — it
   never affects the on-screen site. The goal is a post that reads like a
   printed page: no site chrome, no interactive affordances that mean
   nothing on paper (a <details> toggle, a comment form, a color picker),
   just the title, its metadata, and the prose, using the page's full
   width instead of the deliberately narrow on-screen reading column. */
@media print {
    nav,
    .theme-toggle,
    .markdown-toolbar,
    .word-count,
    .new-comment-toggle,
    .about-post-toggle,
    .related-posts,
    .post-nav,
    form,
    .flash {
        display: none !important;
    }

    /* Edit/History/"Back to Finance"-style links are only meaningful in a
       live, clickable page — on paper they're dead text pointing nowhere.
       Previous/next-post links (.post-nav, above) are the same idea, just
       added later (the reading-shortcuts feature came after this print
       stylesheet did) — same "dead text pointing nowhere" reasoning,
       just hidden by class instead of by href pattern since .post-nav's
       own links point at ordinary /posts/<slug> URLs indistinguishable
       from a link inside the post's own prose. */
    p:has(> a[href*="/edit"]),
    p:has(> a[href*="/history"]) {
        display: none;
    }

    .prose {
        max-width: none;
        width: 100%;
    }

    /* Keeps a heading from printing as the last line on one page with its
       own section starting on the next, and stops a code block from being
       sliced in half across a page boundary. */
    h1, h2, h3, h4, h5, h6 {
        break-after: avoid;
    }

    pre {
        break-inside: avoid;
    }

    /* .katex-display's own overflow-x: auto (above) is exactly right
       on-screen — a too-wide equation scrolls inside its own box instead
       of forcing the whole page wider. On paper there's no scrollbar to
       reveal that hidden overflow at all, so the same rule would silently
       clip part of the equation out of the printed page instead. visible
       lets it print in full, past the box's own edge if it has to — an
       equation running a little wide is a real but minor cosmetic
       issue; a silently truncated formula is actually wrong. */
    .katex-display {
        overflow-x: visible;
    }
}

/* Revision diff (post_revision_diff.html) — a plain <pre> rather than
   .prose's code-block styling, since this isn't a code sample, just
   line-tagged Markdown source. Colors reuse the same green/red pairing
   readers already associate with "added"/"removed" from every code-host
   diff view, kept desaturated enough to sit next to this site's muted
   palette instead of looking like a foreign UI dropped in. */
pre.diff-view {
    white-space: pre-wrap;
    word-wrap: break-word;
    font-size: 0.85rem;
    line-height: 1.6;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--accent-bg);
}

.diff-line {
    display: block;
    padding: 0 0.25rem;
}

.diff-added {
    background: rgba(143, 191, 143, 0.25);
}

.diff-removed {
    background: rgba(165, 63, 63, 0.15);
    text-decoration: line-through;
    text-decoration-color: var(--danger);
}

.diff-context {
    color: var(--muted);
}
