/* blog post + talk detail pages */

/* Giscus comments section */
.giscus-section { margin-top: 8px; }
.giscus-section .giscus { margin-top: 16px; }
.giscus-frame { border: none; width: 100%; }

/* Prose content */
.prose { font-size: calc(var(--base-fs, 14px) + 1px); line-height: 1.72; max-width: 68ch; overflow-wrap: break-word; word-break: break-word; }
.prose a { border-bottom: 1px dashed var(--line-strong); padding-bottom: 1px; transition: border-color .15s, color .15s; }
@media (hover: hover) { .prose a:hover { border-color: var(--accent); color: var(--accent); } }
/* Prose headings and code use em so they scale with .prose's --base-fs */
.prose h2 { font-size: 1.2em;  margin: 34px 0 12px; letter-spacing: -0.02em; }
.prose h3 { font-size: 1em;    margin: 26px 0 10px; color: var(--dim); }
.prose h4 { font-size: 0.87em; margin: 20px 0 8px;  color: var(--dim); }
.prose h4::before { content: "#### "; color: var(--accent); }

/* The leading "##"/"###" markdown marker on h2/h3 is a self-link to the section
   (injected by MarkdownService.extractHeadings) — looks like the old ::before
   accent prefix at rest (override the dashed underline .prose a otherwise gives
   every link), with the dashed underline appearing on hover to signal the deep link. */
.prose :is(h2, h3) > .heading-anchor { color: var(--accent); border-bottom: none; }
@media (hover: hover) {
  .prose :is(h2, h3) > .heading-anchor:hover { border-bottom: 1px dashed var(--accent); }
}
.prose p { margin: 12px 0; }
.prose ul, .prose ol { padding-left: 20px; }
.prose li { margin: 4px 0; }
.prose code { font-family: inherit; background: var(--line); padding: 1px 5px; border-radius: 2px; font-size: 0.92em; }
.code-block { position: relative; margin: 16px 0; }
.prose pre {
  background: var(--card); border: 1px dashed var(--line-strong); padding: 16px 18px;
  overflow-x: auto; font-size: 0.87em; line-height: 1.55; margin: 0; position: relative;
}
/* inline-block + min-width:100% so <code> grows to the widest line (not just the
   visible width). This lets a highlighted .code-line's full-bleed background extend
   across the whole horizontally-scrollable area instead of cutting off at the edge.
   The .hljs rule must also carry it: the highlight.js theme sets `pre code.hljs
   { display:block }` at equal specificity and loads after this file, so the plain
   `.prose pre code` would lose the tie — `.prose pre code.hljs` (one more class) wins. */
.prose pre code { background: none; padding: 0; display: inline-block; min-width: 100%; }
.prose pre code.hljs { background: none; padding: 0; color: inherit; display: inline-block; min-width: 100%; }
.prose blockquote { border-left: 2px solid var(--accent); padding-left: 16px; color: var(--dim); margin: 16px 0; }
.prose hr { border: none; border-top: 1px dashed var(--line-strong); margin: 32px 0; }
.prose table { border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 0.87em; }
.prose th, .prose td { border: 1px dashed var(--line-strong); padding: 8px 14px; text-align: left; }
.prose th { color: var(--dim); font-weight: 600; background: var(--card); letter-spacing: 0.04em; }
.prose td { color: var(--fg); }
.prose tr:hover td { background: var(--card); }
.prose img {
  display: block;
  width: auto;
  max-width: min(100%, 560px);
  height: auto;
  margin: 22px auto;
  border: 1px dashed var(--line-strong);
}
.prose img[width][height] {
  max-height: 520px;
  object-fit: contain;
}
.prose img[width][height][width="4450"],
.prose img[width][height][width="4523"],
.prose img[width][height][width="5236"] {
  max-width: 100%;
  max-height: none;
}

/* Standalone image + its alt as a visible caption (built by post.js — see prosImgs loop).
   The figure carries the vertical rhythm so the image hugs its caption. */
.prose-figure { margin: 22px auto; }
.prose-figure img { margin: 0 auto; }
.prose-figure-caption {
  margin-top: 8px;
  color: var(--dim);
  font-size: 13px;
  line-height: 1.45;
  text-align: center;
}

/* Side-by-side image pairs (e.g. device-posture comparisons). Wraps to stacked
   on narrow viewports. */
.prose .img-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
  justify-content: center;
  margin: 22px 0;
}
/* Selector matches [width][height] too, so it out-specifies the .prose
   img[width][height] sizing rule above. max-width keeps both in one row. */
.prose .img-row img,
.prose .img-row img[width][height] {
  margin: 0;
  width: auto;
  height: auto;
  max-width: 48%;
  max-height: 280px;
  object-fit: contain;
}

/* Post cover image */
.post-cover {
  display: block;
  width: 100%;
  height: auto;
  margin: 24px auto 0;
  border: 1px dashed var(--line-strong);
}

/* Post action buttons — copy link, share, bookmark */
.post-actions {
  display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0 0;
}
.post-action-btn {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--line); border: 1px solid var(--line-strong);
  color: var(--fg); font-family: inherit; font-size: 11px;
  letter-spacing: 0.04em; padding: 4px 10px; cursor: pointer; transition: .15s;
}
@media (hover: hover) {
  .post-action-btn:hover { background: var(--card); border-color: var(--accent); color: var(--accent); }
}
.post-action-btn.is-active { background: var(--card); color: var(--accent); border-color: var(--accent); }

/* Reading progress bar */
.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 0%;
  background: var(--accent);
  z-index: 9999;
  pointer-events: none;
  transition: width 80ms linear;
  will-change: width;
}

/* Code language label */
.prose pre[data-lang] { padding-top: 30px; }
.prose pre[data-lang]::before {
  content: attr(data-lang);
  position: absolute;
  top: 8px;
  left: 14px;
  font-size: 10px;
  color: var(--dim);
  opacity: 0.65;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  pointer-events: none;
}
/* Filename label (```lang title="file.kt") — declared after the language label so it
   wins the shared ::before when a block has both a filename and a runtime data-lang. */
.prose pre[data-file] { padding-top: 30px; }
.prose pre[data-file]::before {
  content: attr(data-file);
  position: absolute;
  top: 8px;
  left: 14px;
  font-size: 10px;
  color: var(--accent);
  opacity: 0.9;
  letter-spacing: 0.04em;
  text-transform: none;
  pointer-events: none;
}
/* Highlighted lines (```lang {1,3-5}) — full-width tint that bleeds into the pre padding. */
.prose pre .code-line { display: block; }
.prose pre .code-line-hl {
  margin: 0 -18px;
  padding: 0 18px;
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  border-left: 2px solid var(--accent);
}

/* ── Tabbed code groups (:::tabs … @tab) ─────────────────── */
/* The label row is built by post.js (sanitize strips <button>). Before that runs the
   panels stack with their titles shown (.tabs-ready gates the tab behaviour). */
.tabs { margin: 16px 0; }
.tab-labels {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  border-bottom: 1px dashed var(--line-strong);
  margin-bottom: -1px;
}
.tab-btn {
  background: none;
  border: 1px dashed transparent;
  border-bottom: none;
  color: var(--dim);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 0.04em;
  padding: 5px 12px;
  cursor: pointer;
  transition: color .15s, border-color .15s;
}
.tab-btn:hover { color: var(--accent); }
.tab-btn.is-active {
  color: var(--accent);
  border-color: var(--line-strong);
  background: var(--card);
}
/* The active tab's button visually merges with the panel below it. */
.tabs-ready .tab-btn.is-active { border-bottom: 1px solid var(--card); position: relative; z-index: 1; }
.tab-title { display: block; font-size: .82em; color: var(--dim); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 4px; }
.tabs-ready .tab-panel { display: none; }
.tabs-ready .tab-panel.is-active { display: block; }
/* Collapse the inner block margins so the code sits flush inside the panel. */
.tabs-ready .tab-panel > :first-child { margin-top: 8px; }
.tabs-ready .tab-panel > :last-child  { margin-bottom: 0; }

/* Back to top button */
.back-to-top {
  position: fixed;
  bottom: 28px;
  right: 28px;
  background: var(--card);
  border: 1px dashed var(--line-strong);
  color: var(--dim);
  font-family: inherit;
  font-size: 11px;
  padding: 6px 10px;
  cursor: pointer;
  z-index: 100;
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s, color .15s, border-color .15s;
}
/* On desktop, center the button at the bottom */
@media (min-width: 960px) {
  .back-to-top { right: auto; left: 50%; transform: translateX(-50%); }
}
.back-to-top.visible { opacity: 1; pointer-events: auto; }
.back-to-top:hover { color: var(--accent); border-color: var(--accent); }

/* Code copy button */
.copy-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  background: var(--card);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font-family: inherit;
  font-size: 10px;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  cursor: pointer;
  transition: color .15s, border-color .15s, opacity .15s;
  opacity: 0;
  line-height: 1.6;
}
.code-block:hover .copy-btn,
.copy-btn:focus-visible { opacity: 1; }
.copy-btn:hover { color: var(--accent); border-color: var(--accent); }
@media (hover: none) { .copy-btn { opacity: 1; } }

/* Post layout — article + sticky TOC sidebar */
.post-layout {
  display: grid;
  grid-template-columns: 1fr 240px;
  gap: 0 48px;           /* row-gap 0 (series→content spacing comes from .series-nav margin); col-gap 48px */
  align-items: start;
  margin-top: 28px;
}
/* The series banner spans the full width as a bar across the top; the prose and
   TOC sit beneath it (row 2) with aligned tops — no orphaned gap beside it.
   Non-series posts (no series-nav) lay out exactly as before. */
.series-nav { grid-column: 1 / -1; }
.post-content { grid-column: 1; min-width: 0; }
.prose > :first-child { margin-top: 0; }
.toc-sidebar {
  grid-column: 2;
  position: sticky;
  top: 90px;
}

/* Table of contents */
details.toc {
  border: 1px dashed var(--line-strong);
  background: var(--card);
}
.toc-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  color: var(--dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  list-style: none;
  user-select: none;
}
.toc-head::-webkit-details-marker { display: none; }
.toc-head::before { content: "./"; color: var(--accent); }
/* Hide toggle on desktop — sidebar is always open */
@media (min-width: 960px) {
  .toc-head { cursor: default; pointer-events: none; }
  .toc-head::after { display: none; }
}
/* Show toggle on mobile */
@media (max-width: 959px) {
  .toc-head { cursor: pointer; }
  .toc-head::after { content: "[−]"; color: var(--dim); opacity: 0.6; margin-left: auto; }
  details.toc:not([open]) .toc-head::after { content: "[+]"; }
}
.toc-list {
  list-style: none;
  margin: 0;
  padding: 8px 14px 12px;
  border-top: 1px dashed var(--line);
}
@media (min-width: 960px) {
  .toc-list { max-height: calc(100vh - 160px); overflow-y: auto; }
}
.toc-list li { margin: 5px 0; }
.toc-h2 > a,
.toc-h3 > a {
  display: flex;
  align-items: baseline;
  font-size: 12px;
  transition: color .15s;
}
.toc-h2 > a { color: var(--dim); }
/* h3 stays subordinate via the dimmed tree connector below, not via text
   opacity — keeps the link text itself at the full --dim AA contrast. */
.toc-h3 > a { color: var(--dim); }
.toc-branch {
  flex-shrink: 0;
  white-space: pre;
  color: var(--accent);
}
.toc-h3 .toc-branch { opacity: 0.7; }
.toc-h2 > a:hover,
.toc-h3 > a:hover,
.toc-h2 > a.toc-active,
.toc-h3 > a.toc-active { color: var(--fg); }

/* Buy Me a Coffee widget */
.bmc-widget {
  border: 1px dashed var(--line-strong);
  background: var(--card);
  margin-top: 16px;
}
.bmc-head {
  display: flex;
  align-items: center;
  padding: 10px 14px;
  color: var(--dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  border-bottom: 1px dashed var(--line);
}
.bmc-head::before { content: "./"; color: var(--accent); margin-right: 6px; }
.bmc-body { padding: 10px 14px 12px; }
.bmc-text { font-size: 12px; color: var(--dim); margin: 0 0 10px; line-height: 1.55; }
.bmc-btn {
  display: block;
  text-align: center;
  background: var(--line);
  border: 1px solid var(--line-strong);
  padding: 6px 10px;
  font-size: 11px;
  color: var(--fg);
  letter-spacing: 0.04em;
  transition: color .15s, border-color .15s, background .15s;
}
.bmc-btn:hover { background: var(--card); color: var(--accent); border-color: var(--accent); }
/* Desktop: show in sidebar only */
.toc-sidebar .bmc-widget { display: block; }
.post-content .bmc-widget { display: none; }
/* Mobile: show in content only */
@media (max-width: 959px) {
  .toc-sidebar .bmc-widget { display: none; }
  .post-content .bmc-widget { display: block; margin-top: 32px; }
}

/* Post navigation */
.post-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding: 28px 0; }

/* Related posts */
.related-posts { padding: 28px 0; }
.related-item {
  display: grid;
  grid-template-columns: 120px 1fr auto;
  gap: 18px;
  align-items: baseline;
  padding: 14px 0;
  border-bottom: 1px dashed var(--line);
}
.related-item:first-of-type { border-top: 1px dashed var(--line); }
.related-item .when { color: var(--accent); font-size: 12px; }
.related-item .title { font-weight: 500; }
.related-item .meta { color: var(--dim); font-size: 12px; margin-top: 3px; }
.related-item .trail { color: var(--dim); font-size: 11px; }
.related-item { transition: transform 0.18s ease; }
@media (hover: hover) {
  .related-item:hover .title { color: var(--accent); }
  .related-item:hover { transform: translateX(3px); }
}

@media (max-width: 959px) {
  .post-layout { grid-template-columns: 1fr; gap: 0; }
  /* Single column: series first, then TOC, then content. */
  .series-nav   { grid-column: 1; order: -2; }
  .post-content { grid-column: 1; order: 0; }
  .toc-sidebar  { grid-column: 1; order: -1; position: static; max-height: none; margin-bottom: 28px; }
  /* Let prose fill the full column width in single-column layout (wide phones / tablets),
     matching the behaviour on small screens where 68ch < viewport width anyway. */
  .prose { max-width: 100%; }
}
@media (max-width: 720px) {
  .prose { font-size: var(--base-fs, 14px); line-height: 1.68; }
  .prose pre { padding: 12px 14px; font-size: 12px; }
  .related-item { grid-template-columns: 1fr; gap: 4px; }
  .related-item .when { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
  .related-item .trail { font-size: 11px; color: var(--dim); opacity: 0.7; }
}

/* ── Series nav (multi-part posts) ──────────────────────── */
.series-nav {
  border: 1px dashed var(--line-strong);
  background: var(--card);
  margin-bottom: 28px;
}
.series-head {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 10px 14px;
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--dim);
}
.series-kicker { color: var(--accent); }
.series-kicker::before { content: "~ "; }
.series-name { color: var(--fg); letter-spacing: 0.04em; text-transform: none; font-size: 12px; }
/* Dashed filler fills the gap between the name and the part count, matching the
   .section-head treatment — so the wide banner never reads as empty in the middle. */
.series-rule { flex: 1; min-width: 24px; align-self: center; border-top: 1px dashed var(--line); }
.series-progress { color: var(--dim); }
.series-parts {
  list-style: none; margin: 0; padding: 8px 14px 12px;
  border-top: 1px dashed var(--line);
  counter-reset: none;
}
.series-part {
  display: flex; gap: 10px; align-items: baseline;
  padding: 4px 0; font-size: 13px; line-height: 1.5;
}
.series-num { color: var(--accent); flex-shrink: 0; min-width: 1.2em; }
.series-title { color: var(--dim); transition: color .15s; }
/* Linked parts get the site's dashed-underline affordance so they read as
   tappable; the current part is a plain <span>, so it stays un-underlined. */
a.series-title { border-bottom: 1px dashed var(--line-strong); padding-bottom: 1px; transition: color .15s, border-color .15s; }
.series-part.is-current .series-num { color: var(--fg); }
.series-part.is-current .series-title { color: var(--fg); font-weight: 500; }
@media (hover: hover) { a.series-title:hover { color: var(--accent); border-color: var(--accent); } }

/* ── Image lightbox ─────────────────────────────────────── */
.prose img { cursor: zoom-in; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
.lightbox.open {
  opacity: 1;
  pointer-events: auto;
}
.lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.lightbox-img {
  position: relative;
  z-index: 1;
  max-width: 90vw;
  max-height: 85vh;
  width: auto;
  height: auto;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
  touch-action: none; /* lets JS handle pinch-zoom without browser interference */
  border: 1px dashed var(--line-strong);
  will-change: transform;
}
.lightbox-img.lb-pan  { cursor: grab; }
.lightbox-img.lb-drag { cursor: grabbing; }

/* View Transitions API — shared-element morph for the image (Chrome 111+) */
::view-transition-group(lb-img) {
  animation-duration: 0.35s;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
::view-transition-old(lb-img),
::view-transition-new(lb-img) {
  animation-duration: 0.35s;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
  object-fit: contain;
}
/* Backdrop + controls fade with the root cross-fade at a shorter duration */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.25s;
}
.lightbox-controls {
  position: absolute;
  top: 20px;
  right: 24px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 8px;
}
.lb-zoom-level {
  font-family: inherit;
  font-size: 11px;
  color: var(--dim);
  min-width: 36px;
  text-align: center;
}
.lb-btn {
  background: color-mix(in srgb, var(--card) 92%, transparent);
  backdrop-filter: blur(4px);
  border: 1px dashed var(--line-strong);
  color: var(--dim);
  font-family: inherit;
  font-size: 12px;
  padding: 4px 10px;
  cursor: pointer;
  transition: color .15s, border-color .15s;
}
.lb-btn:hover { color: var(--accent); border-color: var(--accent); }

/* Lightbox carousel nav (shown only in carousel mode; toggled via [hidden] in JS) */
.lb-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 60px;
  background: color-mix(in srgb, var(--card) 90%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: 1px dashed var(--line-strong);
  color: var(--dim);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  transition: color .15s, border-color .15s;
}
.lb-prev { left: 16px; }
.lb-next { right: 16px; }
.lb-nav:hover { color: var(--accent); border-color: var(--accent); }
.lb-nav[hidden] { display: none; }
.lb-count { font-size: 11px; color: var(--dim); margin-right: 4px; }
.lb-count[hidden] { display: none; }

/* Fullscreen carousel stepping slides like the in-page carousel (continuous, not a
   fade): lbStep() overlays the incoming slide (.lb-incoming — a viewport-sized flex
   stage that centres its image exactly over lbImg) offscreen, then translates BOTH the
   current image and the incoming stage by one viewport width so they move together. The
   open/close morph is still handled by the View Transitions API. Only transform animates
   here, so it never fights the inline scale/translate used for zoom & pan. overflow:hidden
   clips the off-stage image during the slide. */
.lightbox { overflow: hidden; }
.lb-slide-anim { transition: transform .35s cubic-bezier(0.2, 0, 0, 1); }
.lb-incoming {
  position: fixed;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.lb-incoming img {
  max-width: 90vw;
  max-height: 85vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border: 1px dashed var(--line-strong);
}

/* ── In-post image carousel ─────────────────────────────── */
/* Pre-JS fallback: slides simply stack (no clipping) until setupCarousel() runs. */
.carousel { margin: 1.75rem 0; }
.carousel > img { width: 100%; height: auto; display: block; margin: 0 0 8px; }

.carousel.carousel-ready {
  border: 1px dashed var(--line-strong);
  background: var(--card);
}
/* The clipped box that holds the sliding track + overlaid controls. The caption sits
   below it (inside the border) so it is not clipped, and is the positioning context
   for the absolutely-placed arrows/counter/dots. */
.carousel-viewport {
  position: relative;
  overflow: hidden;
}
.carousel-track {
  display: flex;
  transition: transform .35s cubic-bezier(0.2, 0, 0, 1);
}
.carousel-slide {
  flex: 0 0 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  height: min(60vh, 460px);
}
.carousel-slide img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  margin: 0;
  cursor: zoom-in;
}

.carousel-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 44px;
  /* Solid-ish background (no backdrop-filter): these controls sit over the image and
     stay on screen while the page scrolls, so a live blur would re-rasterise every
     frame and cause scroll jank. */
  background: color-mix(in srgb, var(--card) 96%, transparent);
  border: 1px dashed var(--line-strong);
  color: var(--dim);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: color .15s, border-color .15s;
}
.carousel-prev { left: 8px; }
.carousel-next { right: 8px; }
.carousel-arrow:hover { color: var(--accent); border-color: var(--accent); }

.carousel-count {
  position: absolute;
  top: 8px;
  right: 10px;
  z-index: 2;
  padding: 2px 6px;
  background: color-mix(in srgb, var(--card) 96%, transparent);
  border: 1px dashed var(--line-strong);
  color: var(--dim);
  font-size: 11px;
}
.carousel-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8px;
  z-index: 2;
  display: flex;
  justify-content: center;
  gap: 6px;
}
.carousel-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.carousel-dot.is-active { background: var(--accent); border-color: var(--accent); }

/* Per-slide caption (the current image's alt). Hidden when the slide has no alt. */
.carousel-caption {
  padding: 8px 12px;
  border-top: 1px dashed var(--line);
  color: var(--dim);
  font-size: 13px;
  line-height: 1.45;
  text-align: center;
}
.carousel-caption:empty { display: none; }

/* Fullscreen caption — centred under the image, over the blurred backdrop. */
.lb-caption {
  position: absolute;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 2;
  max-width: 90vw;
  padding: 4px 12px;
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  color: var(--fg);
  font-size: 13px;
  line-height: 1.45;
  text-align: center;
}
.lb-caption:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
  .carousel-track { transition: none; }
  .lb-slide-anim { transition: none; }
}

/* ── Callouts / admonitions ─────────────────────────────── */
/* Variant accent is set via --c; the tint mixes it into the card so it reads in both
   light and dark. Generated by the ::: <variant> Markdown extension. */
.callout {
  --c: var(--accent);
  margin: 16px 0;
  padding: 10px 14px;
  /* Dashed box to match the site's terminal aesthetic; solid coloured left edge for accent. */
  border: 1px dashed color-mix(in srgb, var(--c) 35%, var(--line));
  border-left: 3px solid var(--c);
  background: color-mix(in srgb, var(--c) 7%, var(--card));
}
/* Scoped above `.prose p` (which is a <p>) so its 12px margins don't apply to the title. */
.callout .callout-title {
  margin: 0 0 2px;
  color: var(--c);
  font-size: .82em;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.callout-title::before { content: var(--icon, "»") " "; }
/* Collapse the prose margins on the body's outer paragraphs so the box stays tight. */
.callout .callout-body > :first-child { margin-top: 0; }
.callout .callout-body > :last-child  { margin-bottom: 0; }

.callout-note      { --c: var(--accent);  --icon: "ℹ"; }
.callout-tip       { --c: #2ea043; --icon: "✓"; }
.callout-info      { --c: #3b82f6; --icon: "ℹ"; }
.callout-important { --c: #a371f7; --icon: "★"; }
.callout-warning   { --c: #d29922; --icon: "⚠"; }
.callout-caution   { --c: #fb8500; --icon: "⚠"; }
.callout-danger    { --c: #f85149; --icon: "✕"; }

/* ── Excalidraw diagrams (rendered to SVG via kroki at build) ── */
/* Diagrams carry author-chosen colours that don't follow the site palette, so they
   sit on a fixed light panel to stay legible in both light and dark themes. The
   [width][height] variant out-specifies the generic .prose img[width][height] sizing. */
.prose img.diagram,
.prose img.diagram[width][height] {
  max-width: 100%;
  max-height: none;
  padding: 12px;
  background: #fff;
  object-fit: contain;
}
.diagram-missing { color: var(--danger); font-size: .9em; }

/* ── Print (post pages) ───────────────────────────────────
   Strip the reading furniture (TOC, share/save actions, related posts, post nav,
   comments, support widget) and let the prose run full width on the page. The
   ink-on-paper palette + global chrome hiding live in base.css. */
@media print {
  .toc-sidebar, .post-actions, .related-posts, .post-nav, .series-nav,
  .giscus-section, .bmc-widget { display: none !important; }
  /* Collapse the two-column post grid so the article fills the page width. */
  .post-layout { display: block !important; }
  .prose { max-width: none !important; font-size: 11pt !important; }
  /* Code, callouts, tabs and diagrams print as bordered ink-on-paper blocks. */
  .prose pre, .callout, .tabs, .prose img.diagram {
    border: 1px solid #999 !important;
    background: #fff !important;
    color: #000 !important;
  }
  /* No interactive switcher in print — show every tab panel stacked with its title. */
  .tabs-ready .tab-panel { display: block !important; }
  .tab-labels { display: none !important; }
  .tab-title { display: block !important; }
  .copy-btn { display: none !important; }
  /* The heading anchor markers (##, ###) are navigation noise on paper. */
  .heading-anchor { display: none !important; }
}

/* ── Footnotes ──────────────────────────────────────────── */
/* Reference marker: a small superscript number; the link drops the dashed prose underline. */
.footnote-ref { font-size: .72em; line-height: 0; }
.prose .footnote-ref a { border-bottom: none; padding: 0 2px; color: var(--accent); }
.footnote-ref:target { background: color-mix(in srgb, var(--accent) 18%, transparent); }

/* Definitions list, separated from the article by a dashed rule. */
.footnotes { margin-top: 40px; font-size: .9em; color: var(--dim); }
.footnotes hr { border: none; border-top: 1px dashed var(--line-strong); margin: 0 0 16px; }
.footnotes ol { padding-left: 20px; }
.footnotes li { margin: 6px 0; }
.footnotes li:target { background: color-mix(in srgb, var(--accent) 12%, transparent); } /* flash the jumped-to note */
.prose .footnote-backref { border-bottom: none; margin-left: 4px; color: var(--accent); }
