/* ==========================================================================
   Site layer for sam.bristow.nz — sits on top of the brutalist system in
   base.css. The theme owns components (buttons, cards, rows, prose);
   this file owns identity: palette, type roles, page chrome and layout.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Palette
   Ink on paper, one reserved accent, and five working hues.

   AMBER is reserved. It marks what is live or selected — the hero band, the
   active nav item, the pressed filter, the emphasised diagram node — and is
   never used as a category or a status. Nothing else may borrow it.

   The five working hues carry two steps each, because the two jobs have
   different contrast requirements:

     --<hue>        the mark step. Borders, icons, diagram strokes, and the
                    source of the 20% wash. Needs 3:1 against the page.
     --<hue>-text   the text step, solved to clear 4.5:1 against the code
                    block background, which is lighter than the page.

   The set was validated as a categorical palette rather than chosen by eye:
   all five clear the lightness band, the chroma floor and the normal-vision
   separation floor in both schemes. Worst colour-blind separation is ΔE 7.3
   (deuteranopia, dark mode), inside the band that is only acceptable where a
   second cue carries the meaning — which holds here, since every diagram node
   has a label and every admonition has an icon and a heading.

   Order matters: assigned rust, indigo, plum, spruce, gold. That sequence was
   picked because it maximises the separation of adjacent pairs; reordering
   them makes some pairs harder to tell apart.
   -------------------------------------------------------------------------- */
:root {
    --amber: 255 179 0;   /* #ffb300 — reserved: live / selected, never a category */
    --ink: 17 19 16;      /* #111310 — near-black, faint green cast */

    --rust: 192 74 30;
    --indigo: 63 92 184;
    --plum: 150 60 134;
    --spruce: 16 121 79;
    --gold: 166 122 8;
    --footer-bg: #e4e3da;  /* one step darker than the page: the plinth */

    --rust-text: #b2441c;
    --indigo-text: #3f5cb8;
    --plum-text: #963c86;
    --spruce-text: #10764d;
    --gold-text: #856206;

    /* Remap the theme's semantic tokens onto the site palette. Deliberately
       NOT --color/--primary/--secondary: the theme flips those for dark mode
       from a plain :root, so setting them here would outrank that block in
       both schemes and leave dark text on a dark ground. */
    /* Secondary text. 0.55 was the eyeballed value and it measured 4.03:1 on
       the page — under the 4.5:1 floor for text this small. 0.62 clears it on
       the page and on the darker footer, in both schemes.

       An alpha rather than a finished colour, because a custom property is
       substituted where it is DECLARED, not where it is used: a --muted built
       from var(--color) here would bake in the page's ink and carry it into
       .hero-band, whose --color is pinned to ink-on-amber. That put white text
       on the amber in dark mode. Applying the alpha at each point of use
       resolves --color in whatever scope is asking for it. */
    --muted-alpha: 0.62;

    --accent: var(--amber);
    --contrast-accent: var(--ink);

    /* Status slots the theme already draws admonitions from. It uses each as a
       border, a 20% wash behind body text, and the icon colour. */
    --success: var(--spruce);
    --contrast-success: 255 255 255;
    --warning: var(--gold);
    --contrast-warning: 255 255 255;
    --danger: var(--rust);
    --contrast-danger: 255 255 255;
    --info: var(--indigo);
    --contrast-info: 255 255 255;
}

/* --------------------------------------------------------------------------
   2. Colour scheme
   Three states. `system` is the default and sets no attribute at all, so the
   theme's own prefers-color-scheme block does the work. `light` and `dark`
   stamp data-theme on <html> and override it in both directions.

   The dark values below mirror the guarded block in base.css — if you
   change one, change the other.
   -------------------------------------------------------------------------- */
:root[data-theme="dark"] {
    --body-bg: #242423;
    --primary: 221 221 221;
    --contrast-primary: 0 0 0;
    --color: 221 221 221;
    --filled: 0 0 0;
    --contrast-filled: 221 221 221;
    --contrast-secondary: 221 221 221;
    --border-alpha: 0.35;

    --rust: 201 90 46;
    --indigo: 95 128 212;
    --plum: 178 81 144;
    --spruce: 31 154 104;
    --gold: 176 131 12;
    --footer-bg: #191918;

    --rust-text: #d57f5c;
    --indigo-text: #7894db;
    --plum-text: #c67fad;
    --spruce-text: #3ca77c;
    --gold-text: #b89024;
}

/* The same dark steps for someone on System with a dark OS, who never stamps
   data-theme at all. base.css carries the equivalent guard for the base
   tokens; these are the palette's own. Mirror of the block above — change one,
   change both. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --rust: 201 90 46;
        --indigo: 95 128 212;
        --plum: 178 81 144;
        --spruce: 31 154 104;
        --gold: 176 131 12;
        --footer-bg: #191918;

        --rust-text: #d57f5c;
        --indigo-text: #7894db;
        --plum-text: #c67fad;
        --spruce-text: #3ca77c;
        --gold-text: #b89024;
    }
}

/* Tell the browser which way native scrollbars and form controls should go. */
:root[data-theme="dark"] {
    color-scheme: dark;
}

:root[data-theme="light"] {
    color-scheme: light;
}

/* The theme switch. It lives at the foot of the page, not the head: with
   System as the default the control is rarely needed, and it was taking the
   most valuable spot on every page to say so.

   A pill with a sliding thumb — the one deliberately round thing in a design
   made of hairlines and square corners, because that is what reads as a
   switch. Still a fieldset of native radios underneath, so arrow keys and
   group semantics come for free and no ARIA is invented.

   Three states, so sun and moon need a third companion. A half-filled circle
   is the settled convention for "follow the system" (it is what macOS uses for
   Auto) and it survives being monochrome at 14px, which a monitor glyph does
   not. It is never asked to carry the meaning alone: the visible "Theme" label
   gives context and each radio has its name in visually-hidden text. */
/* Right-aligned even when it is alone on the row, or wraps onto a new one. */
.theme-control {
    display: inline-flex;
    align-items: center;
    gap: 0.625rem;
    margin-left: auto;
}

.theme-control[hidden] {
    display: none;
}

.theme-control-label {
    color: rgb(var(--color) / var(--muted-alpha));
}

.theme-switch {
    position: relative;
    display: flex;
    flex-direction: row;
    gap: 0;
    padding: 0.1875rem;
    margin: 0;
    min-inline-size: auto;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 10rem;
    background: rgb(var(--color) / 0.06);
}

.theme-switch label {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0;
    width: 1.75rem;
    height: 1.75rem;
    border: none;
    border-radius: 10rem;
    cursor: pointer;
    color: rgb(var(--color) / var(--muted-alpha));
    transition: color var(--transition-duration) ease-out;
}

.theme-switch label svg {
    width: 0.875rem;
    height: 0.875rem;
    display: block;
}

.theme-switch input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
}

.theme-switch label:hover {
    color: rgb(var(--color));
}

/* The selected icon sits on the amber thumb. */
.theme-switch label:has(:checked) {
    color: rgb(var(--contrast-accent));
}

/* One thumb that slides, rather than three backgrounds that blink. */
.theme-thumb {
    position: absolute;
    top: 0.1875rem;
    left: 0.1875rem;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 10rem;
    background: rgb(var(--accent));
    transition: transform var(--transition-duration) ease-out;
}

.theme-switch:has(input[value="light"]:checked) .theme-thumb {
    transform: translateX(0);
}

.theme-switch:has(input[value="dark"]:checked) .theme-thumb {
    transform: translateX(100%);
}

.theme-switch:has(input[value="system"]:checked) .theme-thumb {
    transform: translateX(200%);
}

/* The radio holds focus but is invisible, so ring its label. */
.theme-switch label:has(:focus-visible) {
    outline: solid 0.1875rem rgb(var(--color));
    outline-offset: 0.0625rem;
}

/* Without JS the control cannot do anything, so don't show it. theme.js
   removes this as soon as it runs. */
.theme-switch[hidden] {
    display: none;
}

/* --------------------------------------------------------------------------
   3. Type roles
   Bricolage Grotesque, pushed narrow and heavy, carries every display line.
   Archivo does the reading. DM Mono does the labelling — dates, ticks,
   eyebrows, anything that behaves like instrument text.
   -------------------------------------------------------------------------- */
h1, h2, .display {
    font-family: var(--font-display);
    font-stretch: 78%;
    font-weight: 700;
    letter-spacing: -0.02em;
}

h3, h4, h5, h6 {
    font-weight: 600;
}

.eyebrow,
.mono,
time {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    line-height: 1.2;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.eyebrow {
    display: block;
    color: rgb(var(--color) / var(--muted-alpha));
    margin-bottom: 1rem;
}

/* Sections announce themselves with a mono label, the way a rack unit is
   labelled. The rule after it is the theme's own hairline, extended. */
.band > .eyebrow {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* A link inside an eyebrow heading (the year headings on a dated index) is
   navigation, not prose, so it owes the 24px target minimum like the nav does.
   Inline-block rather than inline-flex, so it still sits in the heading's flex
   row beside the hairline rule. */
.eyebrow a {
    display: inline-block;
    min-height: 1.5rem;
    line-height: 1.5rem;
}

.band > .eyebrow::after {
    content: "";
    flex: 1;
    border-top: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

a {
    text-underline-offset: 0.15em;
    text-decoration-thickness: 0.0625rem;
}

/* Keyboard users get a visible ring — the theme ships none.
   The ring is drawn in --color, not the accent: amber measures 1.63:1 against
   the cream ground and 1.79:1 against a white card, well under the 3:1 that
   WCAG 2.4.11 asks of a focus indicator. Ink is ~17:1 in light and ~12:1 in
   dark. The accent survives as an inner ring, where it is decoration rather
   than the thing carrying the contrast. */
:focus-visible {
    outline: solid 0.1875rem rgb(var(--color));
    outline-offset: 0.1875rem;
    box-shadow: 0 0 0 0.1875rem rgb(var(--accent));
}

/* Visible only when focused: the first stop for a keyboard user on every page,
   which otherwise means tabbing five nav links and a theme switch to reach the
   content. Moved with transform rather than display so it stays focusable. */
.skip-link {
    position: absolute;
    z-index: 100;
    top: 0.5rem;
    left: 0.5rem;
    padding: 0.625rem 1rem;
    background: rgb(var(--accent));
    color: rgb(var(--contrast-accent));
    border: solid 0.0625rem rgb(var(--color));
    border-radius: 0.25rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    transform: translateY(-250%);
    transition: transform var(--transition-duration) ease-out;
}

.skip-link:focus {
    transform: none;
}

/* main takes focus when the skip link is followed, without becoming a tab stop. */
main:focus {
    outline: none;
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* --------------------------------------------------------------------------
   4. Document model
   -------------------------------------------------------------------------- */

/* The theme is built as an app shell: html and body are pinned to 100% height
   and `main` is the scroll container, so the document itself never scrolls.
   For a content site that costs you the page scrollbar, URL-bar collapse on
   mobile, working anchor jumps and scroll restoration. Hand scrolling back to
   the document. */
html,
body {
    height: auto;
    min-height: 100%;
}

/* A column, so a short page still puts the footer at the bottom of the window
   instead of leaving a band of page background below it. */
body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

main {
    overflow: visible;
    flex: 1;
    grid-column: auto;
    grid-row: auto;
}

/* Anchor targets clear the sticky header instead of hiding under it. */
html {
    scroll-padding-top: 5rem;
}

/* A stretched link covers the whole card, which makes the target generous but
   lays an overlay over the text so none of it can be selected. Lifting the
   card's own content above the overlay gives selection back; the surrounding
   whitespace stays clickable. A card must contain exactly one link for this to
   behave — a second one would sit under the overlay and be unreachable. */
.card > *,
.entry > * {
    position: relative;
}

.card a.stretched-link,
.entry a.stretched-link {
    position: static;
}

/* `a.stretched-link::before` is absolutely positioned to cover its card. With
   no positioned ancestor it anchors to the viewport instead, turning every one
   into a full-page click target that swallows the whole document. The theme
   sets position:relative on article.product-card for this reason but not on
   .card, so pin it on every container this site stretches a link inside. */
.card,
.entry,
.note-card {
    position: relative;
}

/* --------------------------------------------------------------------------
   5. Page chrome
   -------------------------------------------------------------------------- */

/* The theme reserves the bare `nav` element for its app-shell sidebar: a black
   fixed panel, flex column, hidden below 1024px. This site has no such sidebar,
   so hand `nav` back to the classes that style it. Matching the theme's own
   `nav:not(.open)` specificity is what makes the width reset stick. */
nav:not(.app-nav) {
    flex-direction: row;
    background-color: transparent;
    color: inherit;
    overflow: visible;
    position: static;
    width: auto;
    grid-row: auto;
    grid-column: auto;
    font-size: inherit;
    line-height: inherit;
}

nav:not(.app-nav) > * {
    --color: inherit;
}

/* The theme also makes a bare `aside` a fixed 40vw right-hand drawer. Sidenotes
   need the element for what it actually means, so take it back the same way. */
aside:not(.drawer) {
    display: block;
    position: static;
    inset: auto;
    z-index: auto;
    width: auto;
    overflow: visible;
    background-color: transparent;
    color: inherit;
    padding: 0;
    gap: 0;
    border-left: none;
    grid-column: auto;
}

aside:not(.drawer) > * {
    --color: inherit;
}

.note-sidebar {
    display: grid;
    gap: 1rem;
    align-content: start;
}

/* Now that these sit outside <main>, the theme's `main > *` padding no longer
   reaches them; match its gutter so the page keeps one measure. */
.topbar,
.site-footer {
    padding: 1rem;
}

@media (min-width: 1024px) {
    .topbar,
    .site-footer {
        padding-left: max((100% - 71.25rem) / 2, 4rem);
        padding-right: max((100% - 71.25rem) / 2, 4rem);
    }
}

.site-footer {
    background-color: var(--footer-bg);
    border-top: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    padding-top: 4rem;
    padding-bottom: 4rem;
}

.topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-index-header);
    background-color: var(--body-bg);
    border-bottom: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    padding-top: 1rem !important;
    padding-bottom: 1rem !important;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 2rem;
}

.wordmark {
    min-height: 1.5rem;
    font-family: var(--font-mono);
    font-size: 0.875rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 0.625rem;
}

/* Carrier-lock indicator: solid when the page is live, which is always. */
.wordmark::before {
    content: "";
    width: 0.625rem;
    height: 0.625rem;
    background: rgb(var(--accent));
    border: solid 0.0625rem rgb(var(--color));
    flex-shrink: 0;
}

.topbar nav {
    display: flex;
    gap: 1.5rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* 24px minimum target (WCAG 2.5.8). These measured 33x20 before. */
.topbar nav a {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
    text-decoration: none;
    padding-bottom: 0.125rem;
    border-bottom: solid 0.125rem transparent;
}

.topbar nav a:hover {
    border-bottom-color: rgb(var(--color) / 0.3);
}

.topbar nav a[aria-current="page"] {
    border-bottom-color: rgb(var(--accent));
}

/* --------------------------------------------------------------------------
   6. Hero — amber band, one enormous line
   -------------------------------------------------------------------------- */
.hero-band {
    background-color: rgb(var(--accent));
    /* Pin the local palette so borders, text and buttons stay ink-on-amber
       in dark mode too, where the theme would otherwise flip them light. */
    --color: var(--ink);
    --filled: 255 255 255;
    --contrast-filled: var(--ink);
    --primary: var(--ink);
    --contrast-primary: 255 255 255;
    --border-alpha: 1;
    /* Muted ink on amber is a harder ground than muted ink on paper: the page's
       0.62 measures 4.17:1 there, under the 4.5:1 this text size needs. 0.72
       gets it to 5.3:1 and still reads as secondary against the headline. */
    --muted-alpha: 0.72;
    color: rgb(var(--color));
    display: grid;
    gap: 2rem;
}

.hero-band h1 {
    font-size: clamp(2.75rem, 7.5vw, 5.5rem);
    line-height: 0.95;
    max-width: 20ch;
    margin: 0;
}

.lede {
    font-size: 1.125rem;
    line-height: 1.5;
    max-width: 58ch;
}

/* A [.lead] paragraph in a post already sits in the reading column, so it
   takes the column's measure rather than the hero's. */
.rich-text .lede {
    max-width: none;
}

.hero-actions-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

/* The hairline under the band is not the band's: base.css draws it as the top
   border of whatever follows (`main > :not(:first-child)`). A home page with
   no published section has nothing following, so the amber stopped mid-air and
   the page read as unfinished.
   Draw it on main's own ::after, which is a box in main's flow immediately
   below the hero and — being main's, not the band's — inherits the page
   palette. The same border declared inside .hero-band would resolve against
   the pinned ink-on-amber values and paint a near-black line that disappears
   against a dark page. main itself cannot carry it either: `flex: 1` grows it
   to the footer, which is where its bottom border would land. */
main:has(> .hero-band:only-child)::after {
    content: "";
    display: block;
    border-top: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

/* --------------------------------------------------------------------------
   7. Bands and lists
   -------------------------------------------------------------------------- */
.band {
    display: grid;
    gap: 1.5rem;
    align-content: start;
}

.band-head a {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
}

.band-head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.5rem;
    justify-content: space-between;
    align-items: baseline;
}

/* The masthead band on interior pages. The amber is spent on the home hero;
   these open quietly and let the index do the talking. */
.page-head {
    gap: 1rem;
}

.page-head h1 {
    font-size: clamp(2.25rem, 6vw, 4rem);
    line-height: 1;
    margin: 0;
}

.tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
}

.pagination-row {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    justify-content: space-between;
    align-items: center;
}

.breadcrumbs {
    display: block;
    margin-bottom: 1.5rem;
}

/* Breadcrumb links measured 16px tall — navigation, not prose, so the 24px
   minimum applies to them. That min-height makes a link taller than the plain
   span of the current page, which then sat high against the rest of the trail;
   centring the item aligns the child and the "/" separator alike. */
.breadcrumbs ol li {
    display: inline-flex;
    align-items: center;
}

.breadcrumbs ol li a {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
}

.breadcrumbs[role="navigation"] ol {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgb(var(--color) / var(--muted-alpha));
}

/* A framed list of entries. The theme's .rows would give the same frame but
   forces grid-column:1/-1 onto every grandchild, which collapses the date
   gutter below — cheaper to own eight lines than to out-specify it. */
.index-list {
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.25rem;
    background-color: rgb(var(--filled));
    color: rgb(var(--contrast-filled));
    overflow: hidden;
}

.index-list > * {
    --color: var(--contrast-filled);
}

.index-list > * + * {
    border-top: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

/* An index entry: mono date in the gutter, title doing the work. */
.entry {
    display: grid;
    gap: 0.25rem 1.5rem;
    padding: 1.25rem 1rem;
    position: relative;
    align-items: baseline;
}

.entry time {
    color: rgb(var(--color) / var(--muted-alpha));
    white-space: nowrap;
}

.entry h3 {
    font-size: 1.25rem;
    line-height: 1.25;
    margin: 0;
}

.entry p {
    color: rgb(var(--color) / 0.7);
    max-width: 62ch;
}

.entry .tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.5rem;
}

.entry:hover {
    background: rgb(var(--accent) / 0.16);
}

@media (min-width: 640px) {
    .entry {
        grid-template-columns: 7rem 1fr;
    }

    /* Pin the date to the gutter and send everything else down column 2, so
       the row count doesn't have to be known in advance. */
    .entry time {
        grid-column: 1;
        grid-row: 1;
        padding-top: 0.375rem;
    }

    .entry > :not(time) {
        grid-column: 2;
    }
}

/* Small tag pills — the theme's .pill is chunky, these sit inside list rows. */
.tag {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 0.1875rem 0.4375rem;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.25rem;
    text-decoration: none;
    white-space: nowrap;
}

/* Interactive pills — month links, filter buttons — need the target minimum.
   A plain <span class="tag"> is not a target and is left alone. */
a.tag,
button.tag {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
}

.tag[aria-pressed="true"] {
    background: rgb(var(--accent));
    color: rgb(var(--contrast-accent));
}

/* --------------------------------------------------------------------------
   8. Long-form reading
   -------------------------------------------------------------------------- */
.reading {
    max-width: 42rem;
}

.article-head h1 {
    font-size: clamp(2rem, 5.5vw, 3.5rem);
    line-height: 1.02;
    margin-bottom: 1rem;
}

.article-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
    align-items: center;
    color: rgb(var(--color) / var(--muted-alpha));
}

.rich-text h2 {
    font-size: 1.75rem;
    margin-top: 2.5rem;
    padding-top: 1.25rem;
    border-top: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text blockquote {
    border-left: solid 0.25rem rgb(var(--accent));
    font-size: 1.125rem;
}

.rich-text blockquote footer {
    margin-top: 0.5rem;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: rgb(var(--color) / var(--muted-alpha));
}

.rich-text blockquote cite {
    font-style: italic;
    text-transform: none;
    letter-spacing: 0;
}

/* A verse keeps the line breaks the author typed. pre-wrap rather than pre so
   a long line still wraps instead of forcing the page sideways. */
.rich-text blockquote.verse {
    white-space: pre-wrap;
}

/* A worked example, set apart from the argument without being a code block. */
.rich-text figure.example {
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.375rem;
    background: rgb(var(--color) / 0.03);
    padding: 1rem 1.25rem;
}

.rich-text figure.example > div > :not(:last-child) {
    margin-bottom: 0.75rem;
}

/* The tick is decorative — convert_ulist puts the state in adjacent
   visually-hidden text, so a screen reader hears it rather than inferring it
   from a glyph. The box is drawn either way so the column stays aligned. */
.rich-text ul.checklist {
    list-style: none;
    padding-left: 0;
}

.rich-text ul.checklist > li {
    display: flex;
    gap: 0.625rem;
    align-items: baseline;
}

.rich-text .check {
    flex: none;
    width: 1rem;
    height: 1rem;
    align-self: center;
    display: grid;
    place-items: center;
    font-size: 0.75rem;
    line-height: 1;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.1875rem;
}

/* #highlighted# — the browser default is a flat yellow that fights the warm
   paper and, worse, reads as amber, which this palette reserves for what is
   live or selected. Plum is the one hue carrying no status meaning (rust,
   indigo, spruce and gold are spoken for by the admonitions), so a highlight
   contrasts with the page without claiming to be one. Wash only: the text
   stays currentColor, which keeps it above 12:1 in light and 9:1 in dark. */
.rich-text mark {
    color: inherit;
    /* Drawn like a marker pen rather than a filled box: the sweep is heaviest
       just after the start and thins out as the nib runs dry, the rounded
       corners are lopsided the way a hand-drawn stroke is, and the negative
       margin lets the ink overrun the word slightly at both ends. */
    margin: 0 -0.4em;
    padding: 0.1em 0.4em;
    border-radius: 0.8em 0.3em;
    background: transparent;
    background-image: linear-gradient(
        to right,
        rgb(var(--plum) / 0.08),
        rgb(var(--plum) / 0.4) 4%,
        rgb(var(--plum) / 0.24)
    );
    /* Over a line break each fragment gets its own stroke, ends and all,
       instead of one gradient stretched across both. */
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
}

/* In forced colours the sweep is the only thing saying "highlighted", and a
   gradient is exactly what that mode throws away — so hand the job back to the
   system's own marked-text pair, which is guaranteed to be visible against
   whatever the user has chosen. */
@media (forced-colors: active) {
    .rich-text mark {
        background-image: none;
        background-color: Mark;
        color: MarkText;
    }
}

/* --- link affordances ---------------------------------------------------
   Some links say something before you click them: one that leaves the site,
   one that opens a mail client, one that hands you a document, one that starts
   a download, and one that hands you a PGP key. Each gets a small mark in the
   link's own colour.

   Drawn as a mask over currentColor rather than as an icon font: the mark then
   follows the link colour, survives dark mode and forced colours, needs no
   extra font request, and the pseudo-element's content is a non-breaking
   space, so a screen reader announces nothing where a font glyph would read
   out a private-use codepoint.

   rel="external" is written by the converter, which is the only place that
   knows the site's own host — see lib/site_converter.rb — and by hand in the
   templates for the few links they emit directly. All of this is scoped
   to .rich-text so nav, footer and card links stay plain, and so the reference
   list's DOIs do not turn a dense bibliography into a field of arrows.
   ------------------------------------------------------------------------ */
.rich-text a[rel~="external"]::after,
.rich-text a[href$=".pdf" i]::after,
.rich-text a[href$=".zip" i]::after,
.rich-text a[href$=".tar.gz" i]::after,
.rich-text a[href$=".tgz" i]::after,
.rich-text a[href$=".7z" i]::after,
.rich-text a[href$=".asc" i]::after,
.rich-text a[type="application/pgp-keys"]::after,
.rich-text a[href^="mailto:"]::before {
    content: "\a0";
    display: inline-block;
    width: 0.85em;
    min-height: 0.85em;
    vertical-align: -0.02em;
    background: currentColor;
    -webkit-mask-image: var(--link-icon);
    mask-image: var(--link-icon);
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-position: 50% 50%;
    mask-position: 50% 50%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

.rich-text a[rel~="external"]::after,
.rich-text a[href$=".pdf" i]::after,
.rich-text a[href$=".zip" i]::after,
.rich-text a[href$=".tar.gz" i]::after,
.rich-text a[href$=".tgz" i]::after,
.rich-text a[href$=".7z" i]::after,
.rich-text a[href$=".asc" i]::after,
.rich-text a[type="application/pgp-keys"]::after {
    margin-left: 0.2em;
}

.rich-text a[href^="mailto:"]::before {
    margin-right: 0.25em;
}

/* Order matters below: a link can be both off-site and a file, and every one of
   these selectors carries the same specificity, so the later rule wins. The
   file marks come after the external one deliberately — what you get back
   matters more than where it came from. */
.rich-text a[rel~="external"]::after {
    --link-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.991 20.991a4 4 0 0 0 4-4v-10a4 4 0 0 0-4-4h-10a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10Zm0-2h-10a2 2 0 0 1-2-2v-10a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2Zm-8-3c.256 0 .523-.086.72-.28l4.28-4.282v3.562h2v-6a1 1 0 0 0-1-1h-6v2h3.562l-4.28 4.281a1.03 1.03 0 0 0 0 1.438c.195.195.462.281.718.281Z' fill='%23000'/%3E%3C/svg%3E");
}

.rich-text a[href^="mailto:"]::before {
    --link-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.953 4.002c-2.034 0-3.626.514-3.907 2.469-.09.626.108 1.242.563 1.687.226.22.465.484.78.75.794.669 1.805 1.42 2.75 2.094 2.604 1.85 4.659 3 5.876 3 1.217 0 3.272-1.15 5.875-3 .947-.673 1.958-1.426 2.75-2.094.316-.266.555-.528.78-.75a1.944 1.944 0 0 0 .564-1.687C21.703 4.516 20.11 4 18.077 4H5.953Zm-3.938 6.156v5.844a4 4 0 0 0 4 4h12a4 4 0 0 0 4-4v-5.844a26.122 26.122 0 0 1-3.031 2.5c-2.836 2.008-5.383 3.344-6.97 3.344-1.585 0-4.132-1.336-6.968-3.344a26.037 26.037 0 0 1-3.031-2.5Z' fill='%23000'/%3E%3C/svg%3E");
}

/* A PDF is a document you leave the browser for, so it is marked as a document
   rather than as a download. */
.rich-text a[href$=".pdf" i]::after {
    --link-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.59 2.004c-1.95 0-3.562 1.48-3.562 3.344V18.66c0 1.864 1.611 3.344 3.562 3.344h8.876c1.951 0 3.562-1.48 3.562-3.344V8.973c0-3.563-3.439-6.97-7-6.97H7.59Zm6.438 2.125c1.772.47 3.416 2.108 3.875 3.875H15.59c-.889 0-1.562-.63-1.562-1.344V4.129Zm-5 3.875h1a1 1 0 0 1 0 2h-1a1 1 0 0 1 0-2Zm0 4h6a1 1 0 0 1 0 2h-6a1 1 0 0 1 0-2Zm0 4h6a1 1 0 0 1 0 2h-6a1 1 0 0 1 0-2Z' fill='%23000'/%3E%3C/svg%3E");
}

/* Every spelling of "an archive" a reader is likely to meet. The `i` flag is
   there because a filename's extension is not reliably lower case. */
.rich-text a[href$=".zip" i]::after,
.rich-text a[href$=".tar.gz" i]::after,
.rich-text a[href$=".tgz" i]::after,
.rich-text a[href$=".7z" i]::after {
    --link-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.991 2.003a1 1 0 0 0-1 1v10h-3l4 4 4-4h-3v-10a1 1 0 0 0-1-1zm-8 12a1 1 0 0 0-1 1v2a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-2a1 1 0 0 0-2 0v2a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2v-2a1 1 0 0 0-1-1z' fill='%23000'/%3E%3C/svg%3E");
}

/* type= is how hand-written HTML declares a key; .asc is what the AsciiDoc
   `link:` macro can express, and what a public key file is usually called. */
.rich-text a[href$=".asc" i]::after,
.rich-text a[type="application/pgp-keys"]::after {
    --link-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 25' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M21.6 9.61a7.2 7.2 0 0 1-9.29 6.892L9.6 19.21H7.2v2.4H2.4v-4.8l5.11-5.108A7.2 7.2 0 1 1 21.6 9.61Zm-7.2-4.8a1.2 1.2 0 0 0 0 2.4 2.4 2.4 0 0 1 2.4 2.4 1.2 1.2 0 0 0 2.4 0 4.8 4.8 0 0 0-4.8-4.8Z' fill='%23000'/%3E%3C/svg%3E");
}

/* kbd:[] / btn:[] / menu:[] — small, monochrome, in the site's mono face.

   base.css resets vertical-align to bottom on every element, which is there to
   kill the descender gap under an <img>. On an inline box carrying its own
   font size it does something else: it pins the box to the bottom of the line
   box instead of sitting its text on the baseline. A kbd is 12px inside 16px
   prose, so the key dropped two pixels below the sentence it sits in and hung
   under the descenders.

   Every box in the chain needs saying, not just the visible one: Asciidoctor
   wraps a two-key sequence in <span class="keyseq">, and it is the wrapper
   that was being bottom-aligned, carrying both keys down with it. Same for the
   <b>s inside a menuseq. Measured on the sink afterwards: the key's label foot
   lands on the prose's, and the cap sits 13px above the baseline and 7px
   below.
   ------------------------------------------------------------------------ */
.rich-text .keyseq,
.rich-text kbd,
.rich-text .button,
.rich-text .menuseq,
.rich-text .menuseq b {
    vertical-align: baseline;
}

.rich-text kbd {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    padding: 0.0625rem 0.375rem;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-bottom-width: 0.125rem;
    border-radius: 0.1875rem;
    white-space: nowrap;
}

.rich-text .button,
.rich-text .menuseq {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    font-weight: 400;
}

.rich-text .button {
    padding: 0.0625rem 0.375rem;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.1875rem;
}

.rich-text .menuseq .caret {
    color: rgb(var(--color) / var(--muted-alpha));
    font-weight: 400;
}

.rich-text pre,
.rich-text code {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
}

.rich-text :not(pre) > code {
    /* The theme sets overflow-wrap:anywhere on html/body, which shreds an
       identifier like `life-cold-init` to fit a narrow table column. Let the
       column take the width it needs instead. */
    overflow-wrap: normal;
    background: rgb(var(--color) / 0.08);
    padding: 0.0625rem 0.3125rem;
    border-radius: 0.1875rem;
}

.rich-text pre {
    background: rgb(var(--color) / 0.05);
    padding: 1rem;
    overflow-x: auto;
    white-space: pre;
}

/* --- callouts -----------------------------------------------------------
   A <1> against a line of code, and its explanation in the list underneath.
   convert_inline_callout writes the bare numeral, so the number is real text
   in both places rather than generated content — a screen reader reads it,
   and user-select keeps it out of anything copied from the block.

   Ink rather than a hue: rust, indigo, spruce and gold are the admonitions,
   plum is the highlight and amber is reserved for what is live or selected.
   A callout points, it does not classify — so it takes the text colour and
   knocks the numeral out in the page's own paper.
   ------------------------------------------------------------------------ */
.rich-text .conum,
.rich-text ol.colist > li::before {
    display: inline-block;
    width: 1.4em;
    height: 1.4em;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-style: normal;
    font-weight: 500;
    line-height: 1.4em;
    text-align: center;
    border-radius: 50%;
    background: rgb(var(--color));
    color: rgb(var(--filled));
    -webkit-user-select: none;
    user-select: none;
}

/* The list Asciidoctor pairs with those markers. Its own numbering would run
   alongside the discs, so drop it and count the discs instead. */
.rich-text ol.colist {
    list-style: none;
    padding-left: 0;
    counter-reset: colist;
}

.rich-text ol.colist > li {
    display: flex;
    gap: 0.625rem;
    align-items: baseline;
    counter-increment: colist;
}

.rich-text ol.colist > li::before {
    content: counter(colist);
    flex: none;
}

/* A filled disc is exactly what forced colours throw away, so hand the mark
   back as an outline the system is guaranteed to paint. */
@media (forced-colors: active) {
    .rich-text .conum,
    .rich-text ol.colist > li::before {
        border: solid 0.0625rem CanvasText;
        background: Canvas;
        color: CanvasText;
    }
}

.rich-text figure img {
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text figcaption,
.rich-text .figcaption {
    font-family: var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 0.6875rem !important;
    color: rgb(var(--color) / var(--muted-alpha));
}

/* Prev/next at the end of a post. */
.post-nav {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
}

.post-nav a {
    text-decoration: none;
    display: grid;
    gap: 0.375rem;
    padding: 1.25rem;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.25rem;
    transition: box-shadow var(--transition-duration) ease-out,
                transform var(--transition-duration) ease-out;
}

.post-nav a:hover {
    transform: translate(-0.25rem, -0.25rem);
    box-shadow: 0.25rem 0.25rem 0 rgb(var(--color));
}

.post-nav a[rel="next"] {
    text-align: right;
}



/* Description lists: a term and its explanation. The term carries the weight,
   the body is inset behind a hairline so a run of them reads as a list rather
   than as paragraphs. */
.rich-text dl {
    display: block;
    margin-bottom: 1rem;
}

/* The theme appends ":" to every term; wanted for [horizontal], not for a
   term that is a whole sentence. */
.rich-text dl dt::after {
    content: none;
}

/* [horizontal] keeps the theme's two-column grid. */
.rich-text dl.horizontal {
    display: grid;
    grid-template-columns: fit-content(40%);
    column-gap: 0.75rem;
}

.rich-text dl.horizontal dt {
    margin-top: 0;
}

.rich-text dl.horizontal dt::after {
    content: ":";
}

.rich-text dl.horizontal dd {
    margin-top: 0;
    padding-left: 0;
    border-left: none;
}

.rich-text dt {
    font-weight: 600;
    margin-top: 1.25rem;
}

.rich-text dt:first-child {
    margin-top: 0;
}

.rich-text dd {
    margin: 0.375rem 0 0;
    padding-left: 1rem;
    border-left: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text dd > p:last-child {
    margin-bottom: 0;
}


/* --- tables -------------------------------------------------------------
   The theme reflows a table into one card per row below 1024px by setting
   display:grid on the table, block on its children and contents on tbody.
   That strips every implicit ARIA role — measured at 600px, thead resolved to
   display:none and tbody to display:contents, so the table vanished from the
   accessibility tree entirely. Keep real table display at all widths and let a
   wide table scroll instead; the scroll container is focusable so it can be
   scrolled from the keyboard.

   The look is the theme's own: the body is a filled card with hairline rules
   between rows and rounded outer corners, and the column labels sit above it
   with no fill and no border. Rounded corners need border-collapse:separate,
   so the rules are placed per-edge to avoid doubling up. */
.table-scroll {
    overflow-x: auto;
    margin-bottom: 1rem;
}

.rich-text table {
    display: table;
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.9375rem;
    gap: 0;
}

.rich-text table > thead {
    display: table-header-group;
}

.rich-text table > tbody {
    display: table-row-group;
}

.rich-text table tr {
    display: table-row;
}

.rich-text table th,
.rich-text table td {
    display: table-cell;
    padding: 0.75rem 1rem;
    text-align: left;
    vertical-align: top;
    border: none;
}

/* Column labels: above the card, not part of it. */
.rich-text table thead th {
    font-weight: 600;
    white-space: nowrap;
    padding-top: 0;
    background: none;
}

/* The body is one filled card. */
.rich-text table tbody tr {
    background-color: rgb(var(--filled));
    color: rgb(var(--contrast-filled));
}

.rich-text table tbody td {
    --color: var(--contrast-filled);
    border-top: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text table tbody td:first-child {
    border-left: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text table tbody td:last-child {
    border-right: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text table tbody tr:last-child td {
    border-bottom: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
}

.rich-text table tbody tr:first-child td:first-child {
    border-top-left-radius: 0.25rem;
}

.rich-text table tbody tr:first-child td:last-child {
    border-top-right-radius: 0.25rem;
}

.rich-text table tbody tr:last-child td:first-child {
    border-bottom-left-radius: 0.25rem;
}

.rich-text table tbody tr:last-child td:last-child {
    border-bottom-right-radius: 0.25rem;
}

/* --------------------------------------------------------------------------
   9. Sidenotes
   An aside in a post sits out in the right margin on wide screens and folds
   back into the flow below 1120px, where there is no margin to sit in. The
   number is a CSS counter driven by the reference marker, so inserting a note
   renumbers everything after it for free.

   Numbers are real text emitted by the converter (Asciidoctor already numbers
   footnotes in document order), not a CSS counter — so they are announced,
   selectable and copyable.

   Two placements, one class:
     <span class="sidenote">   inside a <p>, right after its ref marker. Floats
                               level with that exact line. Best for short notes.
     <aside class="sidenote">  between paragraphs. Floats level with the
                               paragraph below it. Best for longer asides.
   Both are valid HTML where they sit — <aside> is not allowed inside <p>,
   which is why the inline form is a <span>.
   -------------------------------------------------------------------------- */
.sidenote-ref {
    text-decoration: none;
    padding: 0 0.1875em;
}

.sidenote-ref sup {
    font-family: var(--font-mono);
    font-size: 0.6875em;
    font-weight: 500;
    border-bottom: solid 0.0625rem rgb(var(--color) / 0.35);
}

.sidenote-ref:hover sup {
    border-bottom-color: rgb(var(--accent));
}

/* Narrow screens: a bordered inset in the flow. The amber left edge marks it
   as marginal; a blockquote uses the same amber but without the box. */
.sidenote {
    display: block;
    margin: 1.25rem 0;
    padding: 0.75rem 1rem;
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-left: solid 0.1875rem rgb(var(--accent));
    border-radius: 0.25rem;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: rgb(var(--color) / 0.8);
}

.sidenote-num {
    font-family: var(--font-mono);
    font-weight: 500;
    color: rgb(var(--color));
    padding-right: 0.5rem;
}

/* 1120px is the point where the 42rem reading column, a 2rem gutter and a
   14rem note all clear the theme's page padding. Below it they would collide. */
@media (min-width: 1120px) {
    .rich-text .sidenote {
        float: right;
        clear: right;
        width: 14rem;
        margin: 0.375rem -16rem 1.25rem 0;
        padding: 0.5rem 0 0;
        border: none;
        border-top: solid 0.125rem rgb(var(--accent));
        border-radius: 0;
    }
}

/* --------------------------------------------------------------------------
   10. Diagrams and images

   Diagrams must be INLINE <svg>, not <img src="x.svg">. An <img> renders in an
   isolated document: no page CSS, no site fonts, no currentColor, no dark mode.
   Inline SVG inherits all four, which is the whole trick to making draw.io,
   PlantUML and Graphviz output look like one hand drew them.

   Never key a diagram to prefers-color-scheme — not via <picture media>, not
   via a <style> block inside the SVG. Those see the OS, not the site's
   Light/Dark/System switch, so they desync the moment someone picks Light on a
   dark machine. Inherit from currentColor instead and it is theme-agnostic.

   The class vocabulary below is all tools/svg-clean.py emits. Keep it small.
   -------------------------------------------------------------------------- */
.diagram {
    background-color: rgb(var(--filled));
    color: rgb(var(--contrast-filled));
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.25rem;
    padding: 1.25rem;
    margin-bottom: 1.5rem;
}

.diagram > * {
    --color: var(--contrast-filled);
}

/* The caption takes the row under the diagram; enhance.js puts an Expand
   button at its end when the diagram is showing smaller than drawn. Scoped
   to the figure: the dialog is a .diagram too, and display here would beat
   the browser's own rule that hides it while closed. */
figure.diagram {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 1rem;
    align-items: baseline;
}

figure.diagram > .diagram-body {
    grid-column: 1 / -1;
}

.diagram-expand {
    grid-column: 2;
    cursor: pointer;
}

.diagram-expand[hidden] {
    display: none;
}

/* A diagram that will not fit scrolls rather than shrinking to illegible. */
.diagram-body {
    overflow-x: auto;
}

/* Drawn size: svg-clean.py sets width and height from the viewBox, one unit to
   one CSS pixel, so text is the size it was drawn at in every diagram. A
   narrow one is centred, never stretched. enhance.js adds .fit, letting a wide
   one shrink to the column down to its data-min-width; without it, it scrolls
   at drawn size, which is always legible. */
.diagram-body svg {
    display: block;
    height: auto;
    max-width: none;
    margin-inline: auto;
    color: rgb(var(--color));
    font-family: var(--font-mono);
    /* Only for text the tool left unsized: a size it set is the one it laid
       the label out at, so it has to win. */
    font-size: 0.75rem;
}

/* --- the vocabulary an exported diagram is normalised down to --- */
.diagram-body.fit svg {
    max-width: 100%;
}

/* The full-size view: the whole screen, scrolling both ways. */
/* The theme centres a dialog with a translate and gives it an offset shadow;
   both are undone here, or the full-screen view is pushed off the corner. */
.diagram-zoom {
    inset: 0;
    transform: none;
    width: 100%;
    height: 100%;
    min-width: 0;
    max-width: none;
    max-height: none;
    margin: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
}

.diagram-zoom[open] {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

/* Centred both ways by auto margins in a flex box, which, unlike
   align-items: center, still lets an oversized diagram scroll to its start. */
.diagram-zoom > .diagram-body {
    flex: 1;
    display: flex;
    overflow: auto;
}

.diagram-zoom svg {
    margin: auto;
    flex: none;
}

.diagram-close {
    align-self: flex-end;
    cursor: pointer;
}

.diagram-zoom::backdrop {
    background: rgb(0 0 0 / 0.6);
}

.diagram svg text {
    fill: currentColor;
    font-family: var(--font-mono);
    /* No letter-spacing: draw.io lays labels out without it, and
       tools/drawio-export.py wraps them on that measure. */
}

.diagram svg .d-line {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.25;
}

/* Opaque so connectors passing behind a node are covered, and it follows the
   card surface rather than being pinned to a light-mode colour. */
.diagram svg .d-box {
    fill: rgb(var(--filled));
    stroke: currentColor;
    stroke-width: 1.25;
}

/* Filled node: reads as "this one matters", same job amber does everywhere. */
.diagram svg .d-accent {
    fill: rgb(var(--accent));
    stroke: currentColor;
    stroke-width: 1.25;
}

.diagram svg .d-accent + text,
.diagram svg text.on-accent {
    fill: rgb(var(--contrast-accent));
}

/* Category fills. Washes rather than solids, so the label on top stays
   currentColor and reads in both schemes without a contrast colour per hue —
   and so a category never competes with .d-accent, which is the only solid. */
.diagram svg .d-rust {
    fill: rgb(var(--rust) / 0.22);
    stroke: currentColor;
    stroke-width: 1.25;
}
.diagram svg .d-indigo {
    fill: rgb(var(--indigo) / 0.22);
    stroke: currentColor;
    stroke-width: 1.25;
}
.diagram svg .d-plum {
    fill: rgb(var(--plum) / 0.22);
    stroke: currentColor;
    stroke-width: 1.25;
}
.diagram svg .d-spruce {
    fill: rgb(var(--spruce) / 0.22);
    stroke: currentColor;
    stroke-width: 1.25;
}
.diagram svg .d-gold {
    fill: rgb(var(--gold) / 0.22);
    stroke: currentColor;
    stroke-width: 1.25;
}

/* Secondary weight for guides, grids and annotations. */
.diagram svg .d-muted {
    stroke: currentColor;
    opacity: 0.4;
    fill: none;
}

.diagram svg text.d-muted {
    stroke: none;
    fill: currentColor;
}

/* --- raster --- */
.rich-text figure img,
.figure img {
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.25rem;
}

/* A raster diagram that cannot be recoloured — a scanned sketch, a screenshot
   of a tool — can be inverted instead. Mirrors the palette's three-state logic
   so it follows the switch, not the OS. NEVER put this on a photograph. */
:root[data-theme="dark"] .invert-on-dark {
    filter: invert(1) hue-rotate(180deg);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .invert-on-dark {
        filter: invert(1) hue-rotate(180deg);
    }
}

/* Photographs keep their own colour; they get the frame and nothing else. */
.photo img {
    width: 100%;
    display: block;
}

/* [.stand-in] on an image block: a draft placeholder for a photo not taken
   yet. The placeholder tile is square; cropping it to the usual photo shape
   keeps the spacing around it honest. */
.photo.stand-in img {
    aspect-ratio: 16 / 9;
    object-fit: cover;
}

/* [.rounded] on an image block: a softer corner than the default frame. */
.photo.rounded img {
    border-radius: 0.75rem;
}

/* Book covers sit small and square-shouldered, with the theme's hard shadow. */
.cover {
    width: 5.5rem;
    flex-shrink: 0;
    align-self: start;
    border: solid 0.0625rem rgb(var(--color));
    box-shadow: var(--box-shadow-1);
    border-radius: 0;
}

/* --------------------------------------------------------------------------
   11. Code highlighting

   Rouge runs at build time and emits short token classes. The theme ships
   .hljs-* rules from a different highlighter, so these map Rouge's vocabulary
   onto the same palette. Same three-state dance as the main colour scheme:
   base values light, redefined for explicit dark and for OS-dark-unless-forced.
   -------------------------------------------------------------------------- */
:root {
    --hl-comment: rgb(var(--color) / var(--muted-alpha));
    --hl-keyword: var(--gold-text);
    --hl-string:  var(--spruce-text);
    --hl-number:  var(--indigo-text);
    --hl-name:    var(--plum-text);
    --hl-title:   var(--rust-text);
    /* Diff and error markers. The text steps, not --danger/--success: those
       are mark steps, solved to 3:1 against the page, and they measure 4.06:1
       and 3.26:1 on the code surface. A deleted line is text and needs 4.5. */
    --hl-deleted:  var(--rust-text);
    --hl-inserted: var(--spruce-text);
}

.rich-text pre .c,  .rich-text pre .c1, .rich-text pre .cm,
.rich-text pre .cs, .rich-text pre .cd { color: var(--hl-comment); font-style: italic; }

.rich-text pre .k,  .rich-text pre .kc, .rich-text pre .kd, .rich-text pre .kn,
.rich-text pre .kp, .rich-text pre .kr, .rich-text pre .kt,
.rich-text pre .ow { color: var(--hl-keyword); }

/* .sa is the prefix on f"", r"" and b"" — part of the literal, so it takes the
   string step. Left out, the f sits in plain ink detached from its own string. */
.rich-text pre .s,  .rich-text pre .s1, .rich-text pre .s2, .rich-text pre .sa,
.rich-text pre .sb, .rich-text pre .sc, .rich-text pre .sd, .rich-text pre .se,
.rich-text pre .sh, .rich-text pre .si, .rich-text pre .sr, .rich-text pre .ss,
.rich-text pre .sx                                          { color: var(--hl-string); }

.rich-text pre .m,  .rich-text pre .mb, .rich-text pre .mf, .rich-text pre .mh,
.rich-text pre .mi, .rich-text pre .mo, .rich-text pre .il { color: var(--hl-number); }

.rich-text pre .na, .rich-text pre .nb, .rich-text pre .nc, .rich-text pre .nd,
.rich-text pre .ne, .rich-text pre .ni, .rich-text pre .nl, .rich-text pre .nn,
.rich-text pre .no, .rich-text pre .nt, .rich-text pre .nv, .rich-text pre .nx,
.rich-text pre .bp, .rich-text pre .vc, .rich-text pre .vg,
.rich-text pre .vi { color: var(--hl-name); }

.rich-text pre .nf, .rich-text pre .fm, .rich-text pre .py { color: var(--hl-title); }

/* A preprocessor line is the one construct C++ shows that the five hues have
   no slot for. It reads as two things it already has: `#include` is a keyword
   of the preprocessor's own language, and <omnetpp.h> is a string literal —
   which is what Rouge calls it. No sixth hue, and amber stays reserved. */
.rich-text pre .cp  { color: var(--hl-keyword); }
.rich-text pre .cpf { color: var(--hl-string); }

/* A console block is a transcript: the prompt and the returned output are
   scenery, and the line you would actually retype is the only ink on it. */
.rich-text pre .gp, .rich-text pre .go { color: var(--hl-comment); }

/* Diff headers are addressing, not content — same treatment. */
.rich-text pre .gh, .rich-text pre .gu { color: var(--hl-comment); }

.rich-text pre .err { color: var(--hl-deleted); }
.rich-text pre .gd  { color: var(--hl-deleted); }
.rich-text pre .gi  { color: var(--hl-inserted); }

/* An unnumbered aside: a [sidebar] block, which has no reference point in the
   text to number against. Same margin treatment, no number. */
.sidenote-plain .sidenote-num {
    display: none;
}

/* --------------------------------------------------------------------------
   12. Citations and references
   Inline citations are quiet — a hairline underline, no colour — so a
   paragraph with four of them still reads as prose. The reference list at the
   foot carries the weight.
   -------------------------------------------------------------------------- */
.cite {
    text-decoration: none;
    white-space: nowrap;
    border-bottom: solid 0.0625rem rgb(var(--color) / 0.35);
}

.cite:hover {
    border-bottom-color: rgb(var(--accent));
    border-bottom-width: 0.125rem;
}

/* Only ever visible if a build error was ignored; loud on purpose. */
.cite-missing {
    background: rgb(var(--danger) / 0.15);
    border-bottom: solid 0.125rem rgb(var(--danger));
    padding: 0 0.125rem;
}

/* A work's title can run to 20 words; the post h1 scale would swamp the page. */
.item-head h1 {
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    line-height: 1.1;
}

.references {
    margin-top: 2.5rem;
}

.references ol {
    display: grid;
    gap: 0.875rem;
    margin-left: 1.125rem;
    font-size: 0.875rem;
    line-height: 1.5;
}

.references li {
    padding-left: 0.25rem;
}

.references .ref-title {
    font-weight: 600;
}

.references .ref-more {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-decoration: none;
    color: rgb(var(--color) / var(--muted-alpha));
    margin-left: 0.25rem;
}

.references .ref-more:hover {
    color: rgb(var(--color));
}

/* The palette specimen at /palette/ — a development tool, built only with
   --palette. */
.swatches {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
    gap: 1rem;
}

.swatch {
    border: solid 0.0625rem rgb(var(--color) / var(--border-alpha));
    border-radius: 0.25rem;
    padding: 1rem;
    display: grid;
    gap: 0.375rem;
    align-content: start;
}

.swatch h3 {
    font-family: var(--font-mono);
    font-size: 0.875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin: 0;
}

.swatch p {
    margin: 0;
    font-size: 0.875rem;
}

.swatch-chips {
    display: flex;
    gap: 0.25rem;
    margin-bottom: 0.5rem;
}

.chip {
    flex: 1;
    height: 3rem;
    border: solid 0.0625rem rgb(var(--color));
    border-radius: 0.125rem;
}

.swatch-vars {
    color: rgb(var(--color) / var(--muted-alpha));
    font-size: 0.6875rem !important;
    letter-spacing: 0.04em;
}

/* The audit page at /a11y/ — a development tool, built only with --a11y. */
.audit-pass {
    color: rgb(var(--success));
}

.audit-fail {
    color: rgb(var(--danger));
    font-weight: 600;
}

#results .entry ul {
    margin: 0.5rem 0 0 1rem;
    font-size: 0.875rem;
    display: grid;
    gap: 0.375rem;
}

/* --------------------------------------------------------------------------
   13. Notes
   Notes are working material, not publications — denser, and each one carries
   how long it's been since it was touched.
   -------------------------------------------------------------------------- */
.notes-layout {
    align-items: start;
}

.note-groups {
    display: grid;
    gap: 3rem;
}

.note-group {
    display: grid;
    gap: 1rem;
}

.note-index {
    display: grid;
    gap: 0.125rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    position: sticky;
    top: 5rem;
}

.note-index a {
    text-decoration: none;
    padding: 0.4375rem 0.625rem;
    border-left: solid 0.125rem transparent;
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
}

.note-index a:hover {
    background: rgb(var(--color) / 0.06);
}

.note-index a[aria-current="true"] {
    border-left-color: rgb(var(--accent));
    background: rgb(var(--accent) / 0.18);
}

.note-index .count {
    color: rgb(var(--color) / var(--muted-alpha));
}

.note-card,
.book-card {
    display: grid;
    gap: 0.75rem;
    align-content: start;
}

.note-card h3,
.book-card h3 {
    font-size: 1.0625rem;
    line-height: 1.3;
    margin: 0;
}

.note-card p,
.book-card p {
    font-size: 0.875rem;
    color: rgb(var(--color) / 0.7);
    margin: 0;
}

.note-card footer,
.book-card footer {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    justify-content: space-between;
    margin-top: auto;
    padding-top: 0.25rem;
}

/* Staleness bar: six segments, lit by how recently the note was touched. */
.freshness {
    display: flex;
    gap: 0.125rem;
    height: 0.375rem;
}

.freshness > i {
    flex: 1;
    background: rgb(var(--color) / 0.1);
}

.freshness > i.on {
    background: rgb(var(--accent));
}


/* Author line under a book title. */
.byline {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    color: rgb(var(--color) / var(--muted-alpha));
}

/* --------------------------------------------------------------------------
   14. Footer
   -------------------------------------------------------------------------- */
.site-footer {
    display: grid;
    gap: 1.5rem;
    text-align: left;
}

.footer-bottom {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: center;
    justify-content: space-between;
    margin-top: 0.5rem;
}

.site-footer .links a {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
}

.site-footer .links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.5rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   15. Quality floor
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-delay: 0ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}
