/*
 * Catppuccin Latte (light) and Mocha (dark).
 *
 * Palette values appear exactly once, as custom properties. Component rules
 * below must reference the semantic tokens (--bg, --fg, …), never a raw hex.
 *
 * Theme selection is `prefers-color-scheme` by default, overridable by a
 * `data-theme` attribute on <html> so a server can honour a cookie without any
 * JavaScript. There is deliberately no in-page toggle: a CSS-only one cannot
 * persist across navigations, which is worse than respecting the system.
 */

:root {
  /* Latte */
  --ctp-base: #eff1f5;
  --ctp-mantle: #e6e9ef;
  --ctp-crust: #dce0e8;
  --ctp-surface0: #ccd0da;
  --ctp-text: #4c4f69;
  --ctp-subtext0: #6c6f85;
  --ctp-subtext1: #5c5f77;
  --ctp-blue: #1e66f5;
  --ctp-mauve: #8839ef;

  --bg: var(--ctp-base);
  --bg-alt: var(--ctp-mantle);
  --border: var(--ctp-surface0);
  --fg: var(--ctp-text);
  /* subtext1, not subtext0: Latte's subtext0 is 4.37:1 on the base — under the
     4.5:1 RGAA 3.2 asks for body text, before any glass is involved. The proof
     in `src/theme.rs` resolves these tokens out of this file, so the pairing is
     checked rather than assumed. */
  --fg-muted: var(--ctp-subtext1);
  /* Catppuccin Latte's blue is 4.34:1 on its own base and cannot reach 4.5:1 at
     any surface opacity, so it is darkened toward the text colour rather than
     replaced: still recognisably the palette's blue, and conformant. Mocha's
     blue clears the bar untouched and overrides this below. */
  --link: color-mix(in srgb, var(--ctp-blue) 80%, var(--ctp-text));
  --accent: var(--ctp-mauve);

  /* Glass. `--glass-alpha` is the opacity of every surface that carries text,
     and the readability of the site hangs on it: the colour behind the text is
     chosen per request, so it can only be argued about as a bound. See
     `src/theme.rs`, whose tests read these numbers straight out of this file
     and fail the build if the worst case drops under 4.5:1 (RGAA 3.2). Do not
     edit them without re-running `cargo test`. */
  --glass-alpha: 0.88;
  --glass-bg: color-mix(in srgb, var(--bg) calc(var(--glass-alpha) * 100%), transparent);
  /* Opacity of the recessed blocks — code, the summary — which sit *on* the
     sheet rather than replacing it. Proven on top of the sheet's own bound. */
  --recess-alpha: 0.7;
  --glass-blur: 20px;
  /* Two stable URLs, one per palette. The server answers each with a different
     one of sixteen patterns on every request — that, and nothing in this file,
     is where the randomness lives. Chosen in the cascade rather than inside the
     SVG because an external image can only see `prefers-color-scheme`, and this
     site also honours `data-theme` so a serving layer can act on a cookie. */
  --bg-pattern: url("/bg/latte.svg");

  --measure: 40rem;
  /* Sidebar geometry. On a wide screen the summary is lifted out of the sheet
     and into the margin beside it; these two say how much room that takes. */
  --toc-width: 13rem;
  --toc-gap: 2rem;
  /* The sheet's inner padding, named because the wide layout has to reach back
     across it to put the summary in the margin. */
  --sheet-pad: 1.5rem;
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: ui-monospace, "Cascadia Code", "Fira Code", monospace;
}

/* Mocha */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ctp-base: #1e1e2e;
    --ctp-mantle: #181825;
    --ctp-crust: #11111b;
    --ctp-surface0: #313244;
    --ctp-text: #cdd6f4;
    --ctp-subtext0: #a6adc8;
    --ctp-subtext1: #bac2de;
    --ctp-blue: #89b4fa;
    --ctp-mauve: #cba6f7;

    --link: var(--ctp-blue);
    --bg-pattern: url("/bg/mocha.svg");
  }
}

:root[data-theme="dark"] {
  --ctp-base: #1e1e2e;
  --ctp-mantle: #181825;
  --ctp-crust: #11111b;
  --ctp-surface0: #313244;
  --ctp-text: #cdd6f4;
  --ctp-subtext0: #a6adc8;
  --ctp-subtext1: #bac2de;
  --ctp-blue: #89b4fa;
  --ctp-mauve: #cba6f7;

  --link: var(--ctp-blue);
  --bg-pattern: url("/bg/mocha.svg");
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0 1rem;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  /* RGAA 10.4: text must survive a 200% zoom, hence relative units throughout. */
  font-size: 1.0625rem;
  line-height: 1.6;
}

/*
 * The pattern.
 *
 * A network of pipes — long orthogonal runs with rounded elbows, in the manner
 * of `pipes.sh` — generated by the SSG as an SVG. The two URLs below are
 * stable, and the server answers each with a different one of sixteen patterns
 * on every request, so a reload repaints the page without a line of JavaScript
 * and without the HTML itself having to change.
 *
 * Drawn rather than declared: a path is not something custom properties can
 * express, so this rule only places the image. Everything about *what* is in it
 * — the geometry, and the colour band the contrast proof depends on — lives in
 * `src/theme.rs`.
 *
 * Fixed, so the pattern stays put while the text scrolls over it: that is what
 * reads as depth, and it is also why `cover` is safe — the layer is always
 * exactly the viewport, so the image never repeats or shifts. `z-index: -1`
 * keeps it above the page background, which the body propagates to the canvas,
 * and below every element.
 */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--bg-pattern);
  background-size: cover;
  background-position: center;
}

/*
 * Glass: one sheet, and only one.
 *
 * The whole page — masthead, content, footer — is a single pane of `--bg` at
 * `--glass-alpha`, with the colour fields blurred behind it. An earlier version
 * gave every region its own pane; it fragmented the page into a stack of cards
 * that met at their edges, and reading it was busier than reading the text. One
 * sheet is also the stronger accessibility position: there is no way to add a
 * region that ends up outside it, and text off a pane sits on a backdrop chosen
 * per request that nothing has bounded.
 *
 * The alpha is not a taste decision — see the tokens at the top of this file
 * and the proof in `src/theme.rs`.
 *
 * `backdrop-filter` is a progressive enhancement in the strict sense: where it
 * is missing the surface is simply translucent, which is still legible, because
 * the contrast bound assumes nothing about the blur.
 */
.sheet {
  /* The same width on every page, whatever is in it. An earlier version grew
     the sheet on articles that had a summary; the page then changed size as you
     moved between the listing and a post, which reads as the layout shifting
     under you rather than as two views of one site. */
  max-width: calc(var(--measure) + 2 * var(--sheet-pad));
  margin: 1rem auto 2rem;
  padding: 0 var(--sheet-pad);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.6);
  backdrop-filter: blur(var(--glass-blur)) saturate(1.6);
  border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
  border-radius: 1rem;
  /* A light top edge and a soft drop shadow: the two cues that make a flat
     translucent rectangle read as a pane sitting above the page. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 35%, transparent),
    0 1px 2px color-mix(in srgb, #000 6%, transparent),
    0 8px 24px -12px color-mix(in srgb, #000 30%, transparent);
}

/* Inside the sheet the regions are separated by a hairline, not by a gap: the
   masthead and the footer are parts of one page, and a rule says so without
   cutting the page into pieces. */
.site-header {
  border-bottom: 1px solid var(--border);
}

.site-footer {
  border-top: 1px solid var(--border);
}

/* Every page's content column, whatever generated it: an article body, the
   listing, the 404 page. Centring it here — rather than on `main`'s children —
   is what keeps the listing aligned with the prose, and is immune to a later
   `margin: 0` on a list inside it. */
.prose,
.site-header,
.site-footer {
  max-width: var(--measure);
  margin-inline: auto;
}

/*
 * Transparency is the first thing to drop when the system asks for plainness.
 *
 * `prefers-reduced-transparency` is the setting a visitor turns on precisely
 * because layered translucency is hard for them to read; honouring it means
 * opaque surfaces and no colour fields at all, not a thinner version of the
 * same effect.
 */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more), (forced-colors: active) {
  body::before {
    display: none;
  }

  .sheet,
  article:has(> .toc) > .toc {
    background: var(--bg);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-color: var(--border);
    box-shadow: none;
  }
}

/* RGAA 12.7 — visible only once focused, but always reachable first. */
.skip-link {
  position: absolute;
  left: -9999px;
}

.skip-link:focus {
  left: 1rem;
  top: 1rem;
  z-index: 1;
  padding: 0.5rem 0.75rem;
  background: var(--bg-alt);
  border: 2px solid var(--accent);
}

/* RGAA 10.7: focus is never removed, and is visible against both palettes. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

main:focus {
  outline: none;
}

a {
  color: var(--link);
  /* RGAA 3.1: the underline carries the link, not the colour alone. */
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.site-header {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: baseline;
  justify-content: space-between;
  padding-block: 1.25rem;
}

.site-title {
  font-weight: 700;
  text-decoration: none;
}

nav ul,
.post-list {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.post-list {
  flex-direction: column;
  gap: 1.5rem;
  margin-block: 2rem;
}

.post-list p {
  margin: 0.25rem 0 0;
  color: var(--fg-muted);
}

h1,
h2,
h3 {
  line-height: 1.25;
  text-wrap: balance;
}

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

img {
  max-width: 100%;
  height: auto;
}

figcaption {
  color: var(--fg-muted);
  font-size: 0.9375rem;
}

pre,
code {
  font-family: var(--mono);
  font-size: 0.9375em;
}

pre,
.toc {
  background: color-mix(in srgb, var(--bg-alt) calc(var(--recess-alpha) * 100%), transparent);
  border: 1px solid var(--border);
  border-radius: 0.5rem;
}

pre {
  padding: 1rem;
  overflow-x: auto;
}

/* Section anchors. The whole heading is a link to itself, so the link's
   accessible name is the section title (RGAA 6.1) — but it must not *look* like
   a body link, or every heading would read as a call to action. The affordance
   is the `#` marker, which appears on hover and on keyboard focus; RGAA 10.7's
   focus outline comes from the global :focus-visible rule. */
.heading-anchor {
  color: inherit;
  text-decoration: none;
}

.heading-anchor:hover,
.heading-anchor:focus-visible {
  text-decoration: underline;
}

.heading-anchor-mark {
  margin-left: 0.35ch;
  color: var(--fg-muted);
  /* Not display:none — it would reflow the title when it appears. */
  visibility: hidden;
}

.heading-anchor:hover .heading-anchor-mark,
.heading-anchor:focus-visible .heading-anchor-mark {
  visibility: visible;
}

/* Landing on an anchor must not put the heading flush against the top edge. */
:where(h2, h3, h4, h5, h6)[id] {
  scroll-margin-top: 2rem;
}

/*
 * Table of contents.
 *
 * One `<nav>` in the markup, two presentations. The narrow one — the default,
 * and what a phone gets — is a bordered band between the title and the prose.
 * The wide one, further down, moves it into the left gutter as a sticky column.
 * Nothing is hidden at either size: the same links are present and reachable
 * however the page is being read (RGAA 10.11), only their placement changes.
 */
.toc {
  margin-block: 1.5rem 2rem;
  padding: 0.75rem 1rem;
  font-size: 0.9375rem;
  line-height: 1.45;
}

.toc-title {
  margin: 0 0 0.5rem;
  color: var(--fg-muted);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.toc ol {
  margin: 0;
  padding: 0;
}

/* The section number is the real list marker, not generated content bolted into
   the link: it stays out of the link's accessible name, and a screen reader
   still counts the items. Colour is the only thing overridden. */
.toc > ol {
  padding-left: 2.5rem;
  list-style: decimal-leading-zero;
}

.toc > ol > li::marker {
  color: var(--accent);
  font-size: 0.8125em;
}

.toc li {
  margin-block: 0.3rem;
}

/* Subsections: no numbering of their own, just an indent and a rule, so the
   nesting is legible without competing with the level above. */
.toc ol ol {
  margin-block: 0.3rem;
  padding-left: 0.75rem;
  border-left: 1px solid var(--border);
  list-style: none;
}

.toc ol ol li {
  color: var(--fg-muted);
}

/* A list of links is a navigation block, not links inside a sentence: RGAA 3.1
   is about telling a link apart from surrounding text, and there is none here.
   The underline still appears on hover and on focus. */
.toc a {
  color: inherit;
  text-decoration: none;
}

.toc a:hover,
.toc a:focus-visible {
  color: var(--link);
  text-decoration: underline;
}

/*
 * Wide layout: the summary detaches into the margin.
 *
 * The sheet keeps exactly the width it has on every other page. The article
 * inside it reaches out to the left with a negative margin, so its first grid
 * track lands in the margin *beside* the sheet rather than inside it — the
 * summary becomes a separate block next to the page, and the reading column is
 * untouched.
 *
 * That puts text outside the sheet, on the pattern, where the sheet's contrast
 * bound does not reach. So the summary carries its own surface, with the same
 * `--glass-alpha`: the composite behind its text is then identical to the
 * sheet's, and `src/theme.rs` proves that one case for both.
 *
 * The gap has to swallow the sheet's padding as well as the visual gap, or the
 * prose column would start inside the padding and spill over the sheet's edge.
 *
 * The breakpoint is derived from the geometry — the sheet is 43rem and the
 * summary needs 16.5rem of margin beside it, plus room to breathe — not chosen
 * from a device list. Below it the summary has nowhere to go, and the band
 * above the prose is the right answer. Gating on `:has()` means a browser
 * without it degrades to exactly that band rather than to a broken overhang.
 */
@media (min-width: 78rem) {
  article:has(> .toc) {
    display: grid;
    grid-template-columns: var(--toc-width) minmax(0, var(--measure));
    /* Two explicit rows: title, then body. The summary is placed in the second
       one, which is what gives it the whole height of the prose to stick
       through — a `1 / -1` span would collapse to a single implicit row. */
    grid-template-rows: auto 1fr;
    column-gap: calc(var(--toc-gap) + var(--sheet-pad));
    margin-left: calc(-1 * (var(--toc-width) + var(--toc-gap) + var(--sheet-pad)));
  }

  article:has(> .toc) > header {
    grid-column: 2;
    grid-row: 1;
  }

  article:has(> .toc) > .prose {
    grid-column: 2;
    grid-row: 2;
  }

  article:has(> .toc) > .toc {
    grid-column: 1;
    grid-row: 2;
    align-self: start;
    position: sticky;
    /* Matches the anchors' `scroll-margin-top`, so a heading and the summary
       line up once you have jumped to a section. */
    top: 2rem;
    max-height: calc(100vh - 4rem);
    overflow-y: auto;
    margin: 0;
    padding: 0.9rem 1rem;
    font-size: 0.875rem;
    /* Its own pane, because it is no longer on the sheet. Same alpha, so the
       colour behind its text is the one the proof covers. */
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.6);
    backdrop-filter: blur(var(--glass-blur)) saturate(1.6);
    border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
    /* One accented edge, so the summary reads as belonging to the article it
       sits beside rather than as an unrelated block in the margin. */
    border-left: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);
    border-radius: 0.75rem;
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, #fff 35%, transparent),
      0 1px 2px color-mix(in srgb, #000 6%, transparent),
      0 8px 24px -12px color-mix(in srgb, #000 30%, transparent);
  }

  article:has(> .toc) > .toc > ol {
    padding-left: 2.25rem;
  }
}

/* Prose whose shape carries meaning (ASCII art): monospace is required for the
   figure to hold, but none of the recessed-block chrome is — it reads as body text.
   The size is capped so the full width fits the column instead of scrolling:
   losing the shape off-screen would defeat the point. */
pre.ascii-art {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  color: inherit;
  font-size: min(0.875rem, 2.1vw);
  line-height: 1.2;
}

blockquote {
  margin-inline: 0;
  padding-left: 1rem;
  border-left: 3px solid var(--accent);
  color: var(--fg-muted);
}

table {
  width: 100%;
  border-collapse: collapse;
}

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

.site-footer {
  margin-top: 2rem;
  padding-block: 1.25rem;
  color: var(--fg-muted);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
