/* ============================================================
   LEANDRO'S BLOG — styles.css
   Dark, sober theme. CSS custom properties throughout.
   ============================================================ */

/* ── Reset & Base ─────────────────────────────────────────── */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

@font-face { font-family: 'IBM Plex Mono'; font-weight: 600; font-style: normal; font-display: swap;
             src: url('../fonts/ibm-plex-mono-600.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-weight: 700; font-style: normal; font-display: swap;
             src: url('../fonts/ibm-plex-mono-700.woff2') format('woff2'); }
@font-face { font-family: 'Public Sans'; font-weight: 400; font-style: normal; font-display: swap;
             src: url('../fonts/public-sans-400.woff2') format('woff2'); }
@font-face { font-family: 'Public Sans'; font-weight: 600; font-style: normal; font-display: swap;
             src: url('../fonts/public-sans-600.woff2') format('woff2'); }

:root {
  /* Colour tokens — terminal-black ground + identity accents */
  --ink:           #000000;
  --surface:       #07080a;
  --surface-2:     #0d0f14;
  --surface-3:     #151920;
  --border:        #1c1f27;
  --ash:           #838a94;
  --paper:         #e8ebee;
  --signal:        #4fc3d9;
  --slate:         #7d95ad;

  /* Validated chart ramp (AD-009: literal values, NOT var(--slate) aliases —
     a future tweak to --slate must not silently invalidate this ramp) */
  --chart-1: #7d95ad;
  --chart-2: #556a82;
  --chart-3: #39485a;

  /* Syntax highlighting (AD-012). Dedicated tokens with literal values, for
     the reason AD-009 gives the chart ramp: --signal means "interactive", so a
     string in a code block cannot borrow it, even where the hue coincides. */
  --code-text:      #e8ebee;  /* plain code, operators, punctuation */
  --code-structure: #7d95ad;  /* keywords, keys, tags */
  --code-data:      #4fc3d9;  /* strings, numbers, literals */
  --code-name:      #ffffff;  /* function and class names */
  --code-meta:      #838a94;  /* decorators, directives, shebangs */
  --code-comment:   #5a616b;  /* comments, where comment-dimming put them */
  --code-deletion:  #ff6b6b;  /* removed lines in a diff */

  /* Semantic aliases */
  --bg:            var(--ink);
  --border-focus:  var(--signal);
  --text:          var(--paper);
  --text-muted:    var(--ash);
  --text-faint:    #5a616b;
  --accent:        var(--paper);
  --accent-strong: #ffffff;
  --tag-bg:        var(--surface-2);
  --tag-border:    var(--border);
  --danger:        #ff6b6b;
  --danger-border: #6b2a2a;

  /* Spacing */
  --space-1:  4px;
  --space-2:  8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;

  /* Typography */
  --font-display: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-sans: 'Public Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, "Cascadia Code", "Fira Code", monospace;

  /* Fluid reading-scale interpolation: 0px at 1024px viewport, 1px at 1920px.
     It is already a length, so the multiplier is unitless — `Npx * …` would
     be px × px, invalid, and the whole font-size declaration gets dropped.
     clamp() at each call site pins it flat outside that range — use as
     `calc(MINpx + N * var(--read-scale))` inside a clamp(MINpx, …, MAXpx). */
  --read-scale: calc((100vw - 1024px) / 896);

  /* Type scale: every font-size is `calc(<size> * var(--type-scale))`, so
     this one number sizes all text and nothing else — spacing, icons and
     the 800px column stay put (prose still holds its measure: 52ch grows
     with the type). rem values are left unwrapped: they already scale
     through html's font-size. Dial here, never per call site. */
  --type-scale: 1.25;

  /* Geometry */
  --radius:     4px;
  --radius-sm:  3px;
  --radius-lg:  6px;

  /* Transitions */
  --transition: 120ms ease;

  /* Layout */
  --header-h: 56px;
  --sidebar-w: 220px;
  --content-max: 1160px;
}

/* Phones keep the base size: there the prose column is the screen width,
   not the 52ch measure, and 1.25× text would drop it to ~35 characters a
   line. */
@media (max-width: 768px) {
  :root {
    --type-scale: 1;
  }
}

html {
  font-size: calc(16px * var(--type-scale));
  scroll-behavior: smooth;
}

/* Smooth scrolling is motion, and anchor jumps are the loudest kind.
   Site-wide, not just on posts — the rule above has been overriding
   the reader's stated preference on every page. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  line-height: 1.6;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ── Focus rings ──────────────────────────────────────────── */
:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ── Typography helpers ───────────────────────────────────── */
h1, h2, h3, h4 {
  line-height: 1.25;
  font-weight: 600;
  color: var(--accent-strong);
}

a {
  color: var(--accent);
  text-decoration: none;
}

a:hover {
  color: var(--accent-strong);
  text-decoration: underline;
}

code, kbd, samp, pre {
  font-family: var(--font-mono);
}

/* ── Scrollbar ────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }

/* ============================================================
   HEADER
   ============================================================ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--header-h);
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 var(--space-6);
  gap: var(--space-6);
}

/* Wraps the social links so they move as one unit — e.g. reflowing to their
   own row on small screens (see 09-responsive.css). Pushes them to the right edge: on browse/detail the search field (flex:1)
   fills the space; on home it's hidden, so this keeps the group right-aligned
   there too. */
.header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
  margin-left: auto;
}

/* Contact icons in the header — persistent, always visible (home + browse).
   Icon-only, sitting in the right corner. */
.header-social {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
}

.header-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  transition: color var(--transition), background var(--transition);
}

.header-social a:hover {
  color: var(--text);
  background: var(--surface-2);
  text-decoration: none;
}

.header-social a:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 1px;
}

.header-logo {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  flex-shrink: 0;
}

.header-logo .logo-mark {
  width: 28px;
  height: 28px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: calc(13px * var(--type-scale));
  font-weight: 700;
  color: var(--accent-strong);
  letter-spacing: -0.03em;
}

.header-logo .logo-name {
  font-size: calc(15px * var(--type-scale));
  font-weight: 600;
  color: var(--accent-strong);
  letter-spacing: -0.01em;
}

.header-logo:hover .logo-name {
  text-decoration: none;
}

.header-search-wrap {
  flex: 1;
  max-width: 520px;
  position: relative;
}

.header-search-wrap[hidden] {
  display: none;
}

.header-search-wrap .search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-faint);
  pointer-events: none;
  display: flex;
}

.header-search-input {
  width: 100%;
  height: 36px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: calc(14px * var(--type-scale));
  padding: 0 36px 0 36px;
  transition: border-color var(--transition);
}

.header-search-input::placeholder {
  color: var(--text-faint);
}

.header-search-input:focus {
  outline: none;
  border-color: var(--border-focus);
}

.header-search-kbd {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: calc(11px * var(--type-scale));
  color: var(--text-faint);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  pointer-events: none;
}

.header-nav {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}

.header-nav a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--transition), background var(--transition);
}

.header-nav a:hover,
.header-nav a.active {
  color: var(--text);
  text-decoration: none;
  background: var(--surface-2);
}

.header-nav a:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 1px;
}

/* ============================================================
   MAIN CONTENT WRAPPER
   ============================================================ */
.site-main {
  min-height: calc(100vh - var(--header-h));
}

/* ============================================================
   VIEW SYSTEM  (each view is hidden/shown via JS)
   ============================================================ */
.view {
  display: none;
}
.view.active {
  display: block;
}

/* ============================================================
   HOME VIEW
   ============================================================ */
.home-hero {
  max-width: calc(640px * var(--type-scale));
  margin: 0 auto;
  padding: var(--space-16) var(--space-6) var(--space-10);
  text-align: center;
}

.home-hero h1 {
  font-family: var(--font-display);
  font-size: clamp(2rem, 5vw, 3rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  margin-bottom: var(--space-4);
  color: var(--accent-strong);
}

.home-hero .hero-sub {
  font-size: calc(17px * var(--type-scale));
  color: var(--text-muted);
  margin-bottom: var(--space-8);
  line-height: 1.55;
}

.home-search-wrap {
  position: relative;
  margin-bottom: var(--space-5);
}

.home-search-wrap .search-icon {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-faint);
  pointer-events: none;
  display: flex;
}

.home-search-input {
  width: 100%;
  height: 54px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: calc(16px * var(--type-scale));
  padding: 0 52px 0 48px;
  transition: border-color var(--transition), background var(--transition);
}

.home-search-input::placeholder {
  color: var(--text-faint);
}

.home-search-input:focus {
  outline: none;
  border-color: var(--border-focus);
  background: var(--surface-2);
}

.home-search-kbd {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: calc(12px * var(--type-scale));
  color: var(--text-faint);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 7px;
  pointer-events: none;
}

/* Inline live-search dropdown (shared by home hero + header search) */
.search-results {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  z-index: 50;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  max-height: 380px;
  overflow-y: auto;
  padding: 6px;
  text-align: left;
}

.search-result {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  cursor: pointer;
  text-align: left;
  color: var(--text);
  font-family: var(--font-sans);
}

.search-result:hover,
.search-result:focus {
  background: var(--surface-2);
  outline: none;
}

.search-result-main {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.search-result-title {
  font-size: calc(14px * var(--type-scale));
  font-weight: 600;
}

.search-result-cat {
  font-size: calc(11px * var(--type-scale));
  color: var(--text-muted);
  font-family: var(--font-mono);
  white-space: nowrap;
}

.search-result-summary {
  display: block;
  margin-top: 2px;
  font-size: calc(12px * var(--type-scale));
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result-all {
  margin-top: 4px;
  border-top: 1px solid var(--border);
  border-radius: 0;
  color: var(--text-muted);
  font-size: calc(13px * var(--type-scale));
  font-weight: 500;
}

.search-results-hint {
  margin: 0;
  padding: 12px;
  font-size: calc(13px * var(--type-scale));
  color: var(--text-muted);
}

/* Partition rule — the post count as a rule + legend, replacing the old
   plain-text count line (VID-05/06/07). */
.partition-rule {
  margin-bottom: var(--space-8);
}

.partition-track {
  display: flex;
  gap: 2px;
  height: 8px;
}

.partition-seg {
  width: var(--w);
  border-radius: 2px;
  animation: partition-fill 750ms cubic-bezier(.2, .8, .2, 1) both;
}

.seg-blog { background: var(--chart-3); }

@keyframes partition-fill {
  from { width: 0; }
  to   { width: var(--w); }
}

@media (prefers-reduced-motion: reduce) {
  .partition-seg {
    animation: none;
  }
}

.partition-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: center;
  margin-top: var(--space-3);
  list-style: none;
}

.partition-legend li {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: calc(11.5px * var(--type-scale));
  color: var(--ash);
}

.legend-value {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--paper);
}

.legend-swatch {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex-shrink: 0;
}

/* The "posts" segment doubles as a shortcut into Browse — same hover
   affordance as the category chips below, but understated (color-only)
   since it sits in a compact stats line rather than as its own control. */
.legend-link {
  display: flex;
  align-items: center;
  gap: 6px;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius-sm);
}

.legend-link:hover,
.legend-link:focus-visible {
  color: var(--accent-strong);
  text-decoration: underline;
}

/* Category chips */
.category-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: center;
  margin-bottom: var(--space-10);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 6px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: calc(13px * var(--type-scale));
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
  white-space: nowrap;
  min-height: 32px;
  text-decoration: none;
}

.chip:hover {
  background: var(--surface-2);
  border-color: var(--border-focus);
  color: var(--text);
  text-decoration: none;
}

.chip.active {
  background: var(--surface-3);
  border-color: var(--text-faint);
  color: var(--accent-strong);
}

.chip-count {
  font-size: calc(11px * var(--type-scale));
  color: var(--text-faint);
}

/* Featured strip — at most 3 cards, so the site's normal content column
   fits them in one row. See the 09-responsive.css breakpoint for the
   3/1-column step that keeps every row fully packed. */
.featured-section {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0 var(--space-6) var(--space-16);
}

.featured-section h2 {
  font-size: calc(13px * var(--type-scale));
  font-weight: 500;
  color: var(--text-faint);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  margin-bottom: var(--space-4);
}

.featured-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
}

/* ============================================================
   BROWSE VIEW
   ============================================================ */
/* Wider than --content-max so the results
   grid can pick up extra columns on large screens instead of capping
   out at 3 no matter how wide the monitor is. */
.browse-layout {
  max-width: calc(1680px * var(--type-scale));
  margin: 0 auto;
  padding: var(--space-6);
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  gap: var(--space-6);
  align-items: start;
}

/* Filter rail */
.filter-rail {
  position: sticky;
  top: calc(var(--header-h) + var(--space-4));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-5);
}

.filter-rail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
}

.filter-rail-header h3 {
  font-size: calc(13px * var(--type-scale));
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.clear-filters-btn {
  font-size: calc(12px * var(--type-scale));
  color: var(--text-faint);
  background: none;
  border: none;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  transition: color var(--transition);
}

.clear-filters-btn:hover {
  color: var(--text);
}

.filter-group {
  margin-bottom: var(--space-5);
}

.filter-group-label {
  font-size: calc(11px * var(--type-scale));
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: var(--space-2);
  display: block;
}

.filter-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.filter-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.filter-item input[type="checkbox"] {
  width: 14px;
  height: 14px;
  border-radius: 3px;
  accent-color: var(--text);
  cursor: pointer;
}

.filter-item label {
  font-size: calc(13px * var(--type-scale));
  color: var(--text-muted);
  cursor: pointer;
  flex: 1;
  transition: color var(--transition);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 3px 0;
  min-height: 28px;
}

.filter-item label:hover {
  color: var(--text);
}

.filter-item input:checked + label {
  color: var(--text);
}

.filter-count {
  font-size: calc(11px * var(--type-scale));
  color: var(--text-faint);
}

/* Results area */
.results-area {
  min-height: 60vh;
}

.results-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

.results-meta {
  font-size: calc(13px * var(--type-scale));
  color: var(--text-muted);
}

.results-meta strong {
  color: var(--text);
  font-weight: 500;
}

.active-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.active-filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: calc(12px * var(--type-scale));
  color: var(--text);
}

.active-filter-pill button {
  background: none;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  font-size: calc(14px * var(--type-scale));
  line-height: 1;
  padding: 0;
  display: flex;
  align-items: center;
  transition: color var(--transition);
  min-width: 16px;
  min-height: 16px;
}

.active-filter-pill button:hover {
  color: var(--text);
}

.sort-control {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: calc(13px * var(--type-scale));
  color: var(--text-muted);
}

.sort-select {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-family: var(--font-sans);
  font-size: calc(13px * var(--type-scale));
  padding: 6px 10px;
  cursor: pointer;
  transition: border-color var(--transition);
  min-height: 32px;
}

.sort-select:focus {
  outline: none;
  border-color: var(--border-focus);
}

.sort-select option {
  background: var(--surface);
  color: var(--text);
}

/* ── Skill cards ──────────────────────────────────────────── */
.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(280px * var(--type-scale)), 1fr));
  gap: var(--space-3);
}

.skill-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--slate);
  border-radius: var(--radius);
  padding: var(--space-5);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  text-decoration: none;
  color: inherit;
}

.skill-card:hover {
  border-color: var(--border-focus);
  /* .border-color above is a shorthand and would repaint all four edges —
     including the left one — signal-cyan. Re-declare the left edge so it
     stays the structural slate marker while the rest still cue interactivity. */
  border-left-color: var(--slate);
  background: var(--surface-2);
  text-decoration: none;
  color: inherit;
}

.skill-card:focus-visible {
  border-color: var(--border-focus);
  border-left-color: var(--slate);
}

.card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
}

.card-title {
  font-size: calc(15px * var(--type-scale));
  font-weight: 600;
  color: var(--accent-strong);
  line-height: 1.3;
}

/* Detail-header pill only now (card grid uses plain-text .card-category, VID-10) */
.category-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: calc(11px * var(--type-scale));
  font-weight: 500;
  white-space: nowrap;
  flex-shrink: 0;
  color: color-mix(in srgb, var(--slate) 78%, var(--paper));
  background: color-mix(in srgb, var(--slate) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--slate) 34%, transparent);
}

/* Plain-text category label beside the card meta row (VID-10) */
.card-category {
  color: var(--ash);
  font-size: calc(11px * var(--type-scale));
}

.card-summary {
  font-size: calc(13px * var(--type-scale));
  color: var(--text-muted);
  line-height: 1.55;
  text-align: justify;
  hyphens: auto;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Type badge — the "Post" label on every card */
.type-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: calc(10px * var(--type-scale));
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  flex-shrink: 0;
  border: 1px solid var(--border);
}

.type-badge.type-blog {
  color: var(--ash);
  background: color-mix(in srgb, var(--ash) 12%, transparent);
  border-color: color-mix(in srgb, var(--ash) 34%, transparent);
}

.card-date,
.post-meta {
  font-size: calc(11px * var(--type-scale));
  color: var(--text-faint);
  font-family: var(--font-mono);
  white-space: nowrap;
}

.card-date {
  margin-left: auto;
}

.card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
}

.tag {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  background: var(--tag-bg);
  border: 1px solid var(--tag-border);
  border-radius: var(--radius-sm);
  font-size: calc(11px * var(--type-scale));
  color: var(--text-muted);
  font-family: var(--font-mono);
  white-space: nowrap;
}

.tag-unstable {
  color: var(--danger);
  border-color: var(--danger-border);
}


/* Empty state */
.empty-state {
  padding: var(--space-16) var(--space-6);
  text-align: center;
  color: var(--text-muted);
}

.empty-state h3 {
  font-size: calc(18px * var(--type-scale));
  margin-bottom: var(--space-3);
  color: var(--text);
}

.empty-state p {
  font-size: calc(14px * var(--type-scale));
  color: var(--text-faint);
}

/* Mobile filter drawer trigger */
.filter-drawer-btn {
  display: none;
  align-items: center;
  gap: var(--space-2);
  padding: 8px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: calc(14px * var(--type-scale));
  cursor: pointer;
  min-height: 44px;
  transition: border-color var(--transition);
}

.filter-drawer-btn:hover {
  border-color: var(--border-focus);
  color: var(--text);
}

/* ============================================================
   DETAIL VIEW
   ============================================================ */
.detail-layout {
  max-width: 800px;
  margin: 0 auto;
  padding: var(--space-8) var(--space-6) var(--space-16);
}

.detail-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: calc(13px * var(--type-scale));
  color: var(--text-faint);
  margin-bottom: var(--space-6);
  transition: color var(--transition);
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
}

.detail-back:hover {
  color: var(--text);
}

.detail-header {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--border);
}

.detail-title {
  font-size: clamp(1.5rem, 4vw, 2rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-3);
  color: var(--accent-strong);
}

.detail-summary {
  font-size: calc(clamp(16px, calc(16px + 2 * var(--read-scale)), 18px) * var(--type-scale));
  color: var(--text-muted);
  line-height: 1.6;
  margin-bottom: var(--space-5);
  /* Justify the descriptive prose on post detail headers. */
  text-align: justify;
  hyphens: auto;
}

.detail-meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.detail-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.copy-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: calc(12px * var(--type-scale));
  font-family: var(--font-sans);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
  min-height: 30px;
}

.copy-btn:hover {
  background: var(--surface-3);
  color: var(--text);
  border-color: var(--border-focus);
}

.copy-btn.copied {
  color: #6fba6f;
  border-color: #3a5a3a;
}

.btn-secondary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 8px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: calc(14px * var(--type-scale));
  font-family: var(--font-sans);
  cursor: pointer;
  text-decoration: none;
  transition: border-color var(--transition), color var(--transition), background var(--transition);
  min-height: 44px;
}

.btn-secondary:hover {
  border-color: var(--border-focus);
  color: var(--text);
  background: var(--surface-2);
  text-decoration: none;
}

/* Skill body (rendered markdown) */
.skill-body {
  font-size: calc(clamp(15px, calc(15px + 2 * var(--read-scale)), 17px) * var(--type-scale));
  line-height: 1.7;
  color: var(--text-muted);
  margin-bottom: var(--space-10);
}

/* Lazy-loaded body states (SPLIT-04/05): theme-driven so they stay consistent
   across devices and light/dark. */
.body-loading {
  color: var(--text-faint);
  font-size: calc(14px * var(--type-scale));
  /* Holds a screen's height until the body arrives, so the related section
     below starts off-screen instead of jumping down when it lands (AD-016). */
  min-height: 100vh;
}

.body-error {
  color: var(--text-faint);
  font-size: calc(14px * var(--type-scale));
}

.body-error a {
  color: var(--accent);
}

.skill-body h2 {
  font-size: calc(clamp(17px, calc(17px + 2 * var(--read-scale)), 19px) * var(--type-scale));
  font-weight: 600;
  color: var(--text);
  margin: var(--space-8) 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.skill-body h3 {
  font-size: calc(clamp(15px, calc(15px + 2 * var(--read-scale)), 17px) * var(--type-scale));
  font-weight: 600;
  color: var(--text);
  margin: var(--space-5) 0 var(--space-2);
}

.skill-body p,
.skill-body li {
  /* Justify body prose; auto-hyphenation avoids wide inter-word gaps (rivers). */
  text-align: justify;
  hyphens: auto;
}

.skill-body p {
  margin-bottom: var(--space-4);
}

.skill-body ul,
.skill-body ol {
  margin: 0 0 var(--space-4) var(--space-5);
}

.skill-body li {
  margin-bottom: var(--space-1);
}

/* The build writes each image's pixel size as width/height (AD-016). content-box
   makes that size the bitmap's, as it was before the attributes, so the 1px
   border sits outside it instead of squeezing it; the max-width leaves room for
   the border so a wide image still fits the column exactly. */
.skill-body img {
  display: block;
  box-sizing: content-box;
  max-width: calc(100% - 2px);
  height: auto;
  margin: var(--space-4) auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}

.skill-body pre {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  overflow-x: auto;
  margin-bottom: var(--space-4);
}

.skill-body code {
  font-family: var(--font-mono);
  font-size: calc(13px * var(--type-scale));
  color: var(--accent);
}

.skill-body pre code {
  font-size: calc(13px * var(--type-scale));
  color: var(--accent);
  background: none;
  padding: 0;
}

.skill-body p code,
.skill-body li code {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: calc(12.5px * var(--type-scale));
}

/* Related posts */
.related-section {
  border-top: 1px solid var(--border);
  padding-top: var(--space-8);
}

.related-section h2 {
  font-size: calc(13px * var(--type-scale));
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-bottom: var(--space-4);
}

.related-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(220px * var(--type-scale)), 1fr));
  gap: var(--space-3);
}

/* Connector tree (VID-13/VID-15): the box-drawing list the post's document
   tree is built from (doc-tree.js; 11-post.css adapts it to the rail). */
.tree-list {
  list-style: none;
}

.tree-item {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.tree-item:last-child {
  border-bottom: none;
}

/* Box-drawing glyphs need reliable monospace metrics — the self-hosted Plex
   Mono (--font-display) only ships weights 600/700 reserved for display use,
   so the connector stays on the existing system mono stack (--font-mono,
   VID-04 leaves it untouched). */
.tree-connector {
  flex-shrink: 0;
  font-family: var(--font-mono);
  color: var(--slate);
  user-select: none;
}

.tree-link {
  display: block;
  flex: 1;
  min-width: 0;
  padding: var(--space-3) 0;
}

.tree-link:hover,
.tree-link:focus-visible {
  text-decoration: none;
}

.tree-summary {
  display: block;
  /* Indent via padding, not a stacked "│" glyph — no cross-font
     glyph-alignment dependency between the connector and the summary. */
  padding-left: var(--space-5);
  font-size: calc(13px * var(--type-scale));
  color: var(--text-faint);
  margin-top: var(--space-1);
}

/* ============================================================
   SITE FOOTER
   ============================================================ */
.site-footer {
  border-top: 1px solid var(--border);
  padding: var(--space-8) var(--space-6);
  color: var(--text-faint);
  font-size: calc(13px * var(--type-scale));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
}

/* Contact / social links — icon + label pills, sober and minimal */
.footer-social {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
}

.footer-social .social-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
  font-size: calc(13px * var(--type-scale));
  transition: color var(--transition), border-color var(--transition),
              background var(--transition);
}

.footer-social .social-link:hover {
  color: var(--text);
  border-color: var(--border-focus);
  background: var(--surface-2);
  text-decoration: none;
}

.footer-social .social-link svg {
  flex-shrink: 0;
}

.footer-tagline {
  margin: 0;
  color: var(--text-faint);
}

/* ============================================================
   MOBILE DRAWER OVERLAY
   ============================================================ */
.drawer-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.7);
  z-index: 200;
}

.drawer-overlay.open {
  display: block;
}

.filter-drawer {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: min(320px, 90vw);
  background: var(--surface);
  border-right: 1px solid var(--border);
  z-index: 201;
  padding: var(--space-6);
  overflow-y: auto;
  transform: translateX(-100%);
  transition: transform 200ms ease;
}

.filter-drawer.open {
  transform: translateX(0);
}

.drawer-close-btn {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-bottom: var(--space-4);
  background: none;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  font-size: calc(20px * var(--type-scale));
  width: 100%;
  min-height: 44px;
}

.drawer-close-btn:hover {
  color: var(--text);
}

/* ============================================================
   RESPONSIVE
   ============================================================ */

/* Featured grid — steps down through divisors of the 3-card max (3/1)
   so every row stays fully packed: a 2-column step would strand the third
   card alone. The single column is capped at the hero's width so the
   cards don't stretch across a tablet-wide screen. The step is the 860px
   design × --type-scale (1.25): cards grow with their text, so a row of
   them needs that much more screen. A media query can't read the token —
   re-derive this if it changes. */
@media (max-width: 1075px) {
  .featured-grid {
    grid-template-columns: 1fr;
    max-width: calc(640px * var(--type-scale));
    margin: 0 auto;
  }
}

@media (max-width: 768px) {
  .browse-layout {
    grid-template-columns: 1fr;
  }

  .filter-rail {
    display: none;
  }

  .filter-drawer-btn {
    display: flex;
    order: 1;
  }

  /* Row 1 is always nav (left) + actions (right) — that pair alone fits one
     line at phone widths, so it never wraps. The search field (only shown
     on browse/detail — see router.js showView) is reordered onto its own
     full-width row 2 instead of being left in the middle of row 1, where it
     would force .header-actions onto a second row with empty space under
     the nav icons. On home, where search is [hidden], row 2 never forms and
     the header stays a single line. --header-h grows to match whenever row 2
     is present (see 01-base.css and its consumers). */
  :root:has(.header-search-wrap:not([hidden])) {
    --header-h: 96px;
  }

  .site-header {
    height: auto;
    flex-wrap: wrap;
    padding: var(--space-2) var(--space-4);
    row-gap: var(--space-2);
  }

  .header-search-wrap {
    max-width: none;
    order: 3;
    flex: 1 1 100%;
  }

  .header-logo .logo-name {
    display: none;
  }

  .header-actions {
    flex: 0 1 auto;
    margin-left: auto;
  }

  .home-hero {
    padding: var(--space-10) var(--space-4) var(--space-8);
  }

  .home-hero h1 {
    font-size: 1.9rem;
  }

  .home-search-input {
    height: 50px;
    font-size: calc(15px * var(--type-scale));
  }

  .detail-layout {
    padding: var(--space-5) var(--space-4) var(--space-10);
  }

  /* Filters and Sort share row 1 (edges of the row, via the existing
     space-between) so neither is stranded on the left with empty space
     beside it; the result count/pills wrap to their own full-width row 2. */
  .results-info {
    order: 3;
    flex: 1 1 100%;
  }

  .sort-control {
    order: 2;
  }
}

@media (max-width: 620px) {
  .cards-grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 480px) {
  .related-grid {
    grid-template-columns: 1fr;
  }

  .home-hero h1 {
    font-size: 1.6rem;
  }
}

/* ============================================================
   LOADING SKELETON (optional progressive enhancement)
   ============================================================ */
.skeleton {
  background: linear-gradient(90deg, var(--surface) 25%, var(--surface-2) 50%, var(--surface) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border-radius: var(--radius-sm);
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ============================================================
   POST READING SURFACE
   Long-form prose on the terminal ground. A post is served as
   prerendered HTML (AD-010) inside the very same 800px
   .detail-layout every other detail page uses — the column never
   moves, only the type inside it changes.

   ⚠ CASCADE ORDER. build-css.mjs concatenates the partials by
   lexical sort, so this file lands AFTER 09-responsive.css: a plain
   rule here would silently beat a responsive rule there. Every post
   media query therefore lives in THIS file, and nothing about posts
   is added to 09-responsive.css.

   Colour is AD-009 throughout: --paper = text, --signal =
   interactive, --slate = structure/nesting, --ash/--text-muted =
   quiet/human, --text-faint = dimmed. No token leaves its meaning.
   ============================================================ */

/* ── The measure, and the breakout ────────────────────────────
   The three-column grid IS the breakout mechanism. Prose sits in the
   centre track, capped at the measure; code, figures and tables opt
   out by spanning all three tracks, up to the full shell.

   Grid rather than `max-width` + `margin-inline: auto` on every
   child: `.skill-body ul` (06-detail.css:222) sets a margin
   *shorthand* at higher specificity, so an auto inline margin would
   lose to it and the lists would hang left of the paragraphs. The
   grid never touches margins, so lists keep their own indent.

   SPEC_DEVIATION — the measure is 52ch, not the 68ch design.md and
   spec.md name. `ch` is the advance of "0", and the two are only
   interchangeable when a font's zero is about as wide as its average
   letter. Public Sans's is not: "0" is 0.613em against an average
   prose character of 0.456em, a ratio of 1.34. Measured in a real
   browser over the real posts, 68ch renders 88 characters per line —
   *wider* than the ~95 this feature was commissioned to fix. The
   binding requirement is the band, not the unit (spec P1 AC2 /
   BR-07: "constrained to a 60–75 character measure (~68ch target)"),
   so the band wins and the unit follows it. 52ch measures 63–72
   characters per line, mean 70, at every viewport — `ch` scales with
   font-size, so the count holds as the type grows. */
/* minmax(0, 1fr), not 1fr: a bare `1fr` is `minmax(auto, 1fr)`, and that
   `auto` floor lets the min-content width of a full-width breakout item
   (the code block's header, the widest row of a table) inflate the side
   tracks until the grid is wider than the shell that holds it. */
.post-prose {
  display: grid;
  grid-template-columns: minmax(0, 1fr) min(52ch, 100%) minmax(0, 1fr);
  color: var(--paper);
  font-size: calc(clamp(16px, calc(16px + 2 * var(--read-scale)), 18px) * var(--type-scale));
  line-height: 1.75;
}

.post-prose > * {
  grid-column: 2;
  min-width: 0; /* a long token must scroll or wrap, never widen the track */
}

/* BR-09 names three breakout kinds: a code block, a table, or an image. An image
   reaches the prose in TWO shapes — a captioned `<figure>`, and, when its alt is
   empty, a bare `<img>` (BR-27 AC2; 36 of the corpus's 58 images). The bare form
   is a direct child of the grid too, so it needs the same breakout as the figure
   it would otherwise be — without this it stays pinned to the 52ch measure. */
.post-prose > .code-block,
.post-prose > figure,
.post-prose > img,
.post-prose > table {
  grid-column: 1 / -1;
}

/* A `<center>` block in the markdown (.md-center) breaks out like the table it
   holds, so the table can center across the full shell. Only the table's box
   moves: th/td keep their own text-align: left. The table is display: block with
   width: max-content, so auto inline margins center it; this selector outranks
   the `margin: … 0` shorthand on `.post-prose table`. */
.post-prose > .md-center {
  grid-column: 1 / -1;
}

.post-prose .md-center > table {
  margin-inline: auto;
}

/* ── Prose ────────────────────────────────────────────────────
   Ragged right. Justification without an H&J engine opens rivers,
   and narrowing the column makes them worse, not better — so this
   deliberately supersedes the justify + hyphens pair that
   `wide-screen-readability` set on .skill-body p/li
   (06-detail.css:211). */
.post-prose p,
.post-prose li {
  text-align: left;
  hyphens: manual;
  overflow-wrap: break-word; /* inherited by inline <code> — see below */
}

.post-prose p {
  margin: 0 0 var(--space-5);
}

.post-prose li {
  margin-bottom: var(--space-2);
}

/* `spark.sql.shuffle.partitions` and friends: an unbreakable token
   longer than the measure breaks rather than pushing the column
   open. Inline code is a pill, so let it break mid-token too. */
.post-prose code {
  overflow-wrap: anywhere;
}

/* ── Links ────────────────────────────────────────────────────
   Visible without hover. AD-009 keeps text as text (--paper) and
   puts the affordance where the token's meaning already lives:
   --signal means "interactive", and a link is interactive. The
   underline carries the signal; the words stay words. */
.post-prose a {
  color: var(--paper);
  text-decoration: underline;
  text-decoration-color: var(--signal);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color var(--transition), text-decoration-color var(--transition);
}

/* Re-declares text-decoration-color because the global `a:hover`
   (01-base.css:123) sets the `text-decoration` shorthand, which would
   otherwise reset the colour back to currentColor. The focus ring is
   the global :focus-visible outline — untouched. */
.post-prose a:hover,
.post-prose a:focus-visible {
  color: var(--accent-strong);
  text-decoration: underline;
  text-decoration-color: var(--signal);
  text-decoration-thickness: 2px;
}

/* ── Headings ─────────────────────────────────────────────────
   The renderer shifts markdown down one level, so `#` → h2, `##` →
   h3, `###` → h4. Ten of the eleven posts open at `##`, which makes
   h3 the workhorse section head and h4 a real sub-level (21 of them
   across the blog, with no CSS at all until now).

   All three are set in --font-display (IBM Plex Mono, the site's
   headline face — 04-home.css), so a post's structure speaks in the
   site's voice while the prose stays in Public Sans for reading. */
.post-prose h2 {
  font-family: var(--font-display);
  font-size: calc(clamp(24px, calc(24px + 4 * var(--read-scale)), 28px) * var(--type-scale));
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--accent-strong);
  margin: var(--space-12) 0 var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}

.post-prose h3 {
  font-family: var(--font-display);
  font-size: calc(clamp(19px, calc(19px + 2 * var(--read-scale)), 21px) * var(--type-scale));
  font-weight: 600;
  line-height: 1.35;
  color: var(--text);
  margin: var(--space-10) 0 var(--space-3);
  padding-bottom: 0;
  border-bottom: none;
}

/* h4 gets a categorically different treatment, not merely a smaller
   size: three levels down, size alone stops reading as hierarchy. An
   uppercase, letter-spaced mono eyebrow is unmistakably not a
   paragraph, and it reuses the site's existing label vocabulary
   (.code-block-label, .related-section h2).

   SPEC_DEVIATION: design.md's type table sizes h4 at clamp(16px, …,
   17px). Rendered, that fails its own purpose — mono capitals are
   optically wider and taller than lowercase sans, so an uppercase
   17px Plex Mono line reads LARGER than the 16–18px Public Sans body
   it is meant to subordinate, and it starts competing with h3.
   Dropped to 14–15px, where the tracking and the caps do the
   hierarchy work and the body text stays the biggest thing on the
   page. Everything else in that row (uppercase, letter-spaced,
   --font-display, --text-muted) is as designed. */
.post-prose h4 {
  font-family: var(--font-display);
  font-size: calc(clamp(14px, calc(14px + 1 * var(--read-scale)), 15px) * var(--type-scale));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  line-height: 1.4;
  color: var(--text-muted);
  margin: var(--space-8) 0 var(--space-2);
}

/* ── Rules ────────────────────────────────────────────────────
   Section heads carry their own spacing, so posts don't put an <hr>
   before them: .post-prose is a grid, margins don't collapse, and a
   rule there doubled the gap to ~125px. A rule is for a real break
   inside a section; keep it hairline-quiet. */
.post-prose hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--space-10) 0;
}

/* ── Blockquote ───────────────────────────────────────────────
   Used across the corpus as an author's aside ("Note: the code is at
   …"). --text-muted is exactly AD-009's "quiet/human"; --border keeps
   the rail as chrome rather than borrowing a token that means
   something else. */
.post-prose blockquote {
  margin: var(--space-6) 0;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border-left: 2px solid var(--border);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--text-muted);
  font-size: calc(15px * var(--type-scale));
}

.post-prose blockquote p:last-child {
  margin-bottom: 0;
}

/* ── Tables ───────────────────────────────────────────────────
   The tables in these posts ARE query result sets, so the cells are
   set in mono — the data reads as data. The header bar borrows the
   .code-block-label vocabulary rather than inventing a second one.

   `display: block` + `width: max-content` + `max-width: 100%` makes
   the <table> its own horizontal scroll container: it sizes to its
   content, is capped at the shell, and scrolls inside its own frame.
   The page body never scrolls sideways.

   SPEC_DEVIATION: design.md's breakout snippet targets a
   `.post-prose > .table-wrap` wrapper, but the renderer emits a bare
   <table> (no build task adds a wrapper, and CSS is the only surface
   this phase may touch). The table is made its own scroll container
   instead, which satisfies BR-04 / the spec's "scroll horizontally
   inside its own container" without changing the emitted HTML. */
.post-prose table {
  display: block;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin: var(--space-6) 0;
}

.post-prose th,
.post-prose td {
  padding: var(--space-2) var(--space-4);
  text-align: left;
  border-bottom: 1px solid var(--border);
}

.post-prose th {
  background: var(--surface);
  font-family: var(--font-display);
  font-size: calc(12px * var(--type-scale));
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-faint);
  white-space: nowrap;
}

.post-prose td {
  font-family: var(--font-mono);
  font-size: calc(13px * var(--type-scale));
  color: var(--paper);
  white-space: nowrap;
}

.post-prose tbody tr:last-child td {
  border-bottom: none;
}

/* ── Figures ──────────────────────────────────────────────────
   58 images across the corpus carry alt text that was invisible
   until the renderer started promoting them to figures. The caption
   is secondary by construction: smaller than body, and --text-muted. */
.post-prose figure {
  margin: var(--space-8) 0;
}

.post-prose figure img {
  margin: 0 auto;
}

.post-prose figcaption {
  margin-top: var(--space-3);
  text-align: center;
  font-family: var(--font-sans);
  font-size: calc(13px * var(--type-scale));
  line-height: 1.5;
  color: var(--text-muted);
}

/* A `::: figure` caption is markdown, so it arrives as paragraphs; they keep
   the caption's size and centring, not the prose paragraph's. */
.post-prose figcaption p {
  margin: 0;
  text-align: inherit;
}

.post-prose figcaption p + p {
  margin-top: var(--space-2);
}

/* ── Key idea ─────────────────────────────────────────────────
   `::: key-idea`: the one sentence a section turns on, pulled out of the
   paragraph around it. Not a quote, so not the blockquote's quiet surface:
   it stays in --paper at full strength, and the rail is --slate, AD-009's
   structure token, marking it as set apart rather than as interactive. */
.post-prose .key-idea {
  margin: var(--space-2) 0 var(--space-6);
  padding: var(--space-1) 0 var(--space-1) var(--space-5);
  border-left: 2px solid var(--slate);
  color: var(--paper);
  font-weight: 600;
}

.post-prose .key-idea p:last-child {
  margin-bottom: 0;
}

/* ── Citations ────────────────────────────────────────────────
   `[@id]` renders a numbered link to its entry in `::: references`. A
   marker, not a phrase: set small in mono, in --signal because it is a
   link, with the underline kept for hover so a run of citations doesn't
   stripe the paragraph. nowrap keeps "[3, ch. 4]" on one line. */
.post-prose a.cite {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--signal);
  text-decoration: none;
  white-space: nowrap;
}

/* The entry a citation jumped to is marked, so the eye lands on it in the
   list. scroll-margin keeps it clear of the top edge. */
.post-prose .references li {
  scroll-margin-top: var(--space-12);
}

.post-prose .references li:target {
  background: color-mix(in srgb, var(--signal) 15%, transparent);
  border-radius: var(--radius);
}

/* ── Math ─────────────────────────────────────────────────────
   KaTeX's own rules (katex.css, loaded only by posts with `math: true`,
   AD-019) set the glyphs; these place them in the column. KaTeX draws in
   currentColor, so math takes the prose's --paper. A display equation
   wider than the column scrolls inside its own box, as a table does
   (BR-04), instead of widening the page on a phone.

   An equation has no frame to show it is cut off, so the clipped side
   carries a shadow: the --bg covers scroll with the content (`local`)
   and hide the fixed edge tints only when that end is in view. */
.post-prose .math-display {
  margin: var(--space-6) 0;
  overflow-x: auto;
  overflow-y: hidden;
  background:
    linear-gradient(to right, var(--bg) 40%, transparent) left / 32px 100% no-repeat local,
    linear-gradient(to left, var(--bg) 40%, transparent) right / 32px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, color-mix(in srgb, var(--ash) 45%, transparent), transparent) left / 12px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--ash) 45%, transparent), transparent) right / 12px 100% no-repeat scroll;
}

.post-prose .math-display .katex-display {
  margin: 0;
  padding: var(--space-1) 0;
}

/* ── Code blocks ──────────────────────────────────────────────
   A frame with a header bar: an uppercase letter-spaced language label
   on the left and the shared .copy-btn (06-detail.css) on the right. */
.code-block {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin: var(--space-6) 0;
  overflow: hidden;
}

/* flex-end + `margin-right: auto` on the label, rather than
   space-between: a bare fence emits a header with no label at all
   (only the copy button), and space-between would strand the button
   on the left. */
.code-block-header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--border);
}

.code-block-label {
  margin-right: auto;
  font-size: calc(12px * var(--type-scale));
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

/* The frame is the .code-block's; the <pre> is just the scroll port,
   so it drops the box .skill-body pre (06-detail.css:240) gives it.

   Size and leading belong on the <pre>, not only on the inline <code>:
   the line box's strut comes from the block's own font, so a <pre> left
   at the prose's 17px/1.75 leads its code at ~30px however small the
   <code> inside it is set. */
.code-block pre {
  margin: 0;
  padding: var(--space-4) var(--space-5);
  background: none;
  border: none;
  border-radius: 0;
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: calc(13.5px * var(--type-scale));
  line-height: 1.6;
}

.code-block pre code {
  font-size: inherit;
  line-height: inherit;
  color: var(--code-text);
}

/* Syntax highlighting (AD-012). highlight.js runs at build time and
   leaves only `hljs-*` spans in the HTML; this is the whole theme. Each
   colour is a --code-* token (01-base.css), never a UI accent (AD-009):
   structure (keywords, keys, tags) recedes, the data a snippet moves
   around (strings, numbers, literals) carries the colour, and comments
   stay where comment-dimming (BR-19) put them. Names a reader scans for
   (functions, classes) are the one bold thing. Unlisted classes
   (operators, params, punctuation, an embedded language's wrapper) fall
   through to --code-text. */
.code-block .hljs-comment,
.code-block .hljs-quote {
  color: var(--code-comment);
}

.code-block .hljs-keyword,
.code-block .hljs-type,
.code-block .hljs-selector-tag,
.code-block .hljs-section,
.code-block .hljs-name,
.code-block .hljs-tag,
.code-block .hljs-attr,
.code-block .hljs-attribute,
.code-block .hljs-property,
.code-block .hljs-variable,
.code-block .hljs-template-variable {
  color: var(--code-structure);
}

.code-block .hljs-string,
.code-block .hljs-regexp,
.code-block .hljs-number,
.code-block .hljs-literal,
.code-block .hljs-symbol,
.code-block .hljs-bullet,
.code-block .hljs-link,
.code-block .hljs-addition {
  color: var(--code-data);
}

.code-block .hljs-title,
.code-block .hljs-title.function_,
.code-block .hljs-title.class_ {
  color: var(--code-name);
  font-weight: 700;
}

.code-block .hljs-meta {
  color: var(--code-meta);
}

.code-block .hljs-subst {
  color: var(--code-text);
}

.code-block .hljs-deletion {
  color: var(--code-deletion);
}

.code-block .hljs-strong {
  font-weight: 700;
}

/* ── The document tree ────────────────────────────────────────
   The signature element, and the one place this page spends any
   boldness: a post's headings printed in the margin like the output
   of `tree`, in the box-drawing connectors of the shared connector tree
   (06-detail.css). --slate is AD-009's
   "structure/nesting", which is precisely what a connector is.

   Below the gutter breakpoint it is what it is in the HTML: an
   in-flow, closed <details> above the post. The `open` attribute is
   set by JS from the SAME matchMedia query as the breakpoint below
   (doc-tree.js:88) — if one value changes, both must. */
.doc-tree {
  margin-bottom: var(--space-8);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.doc-tree-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
  font-size: calc(12px * var(--type-scale));
  font-weight: 600;
  line-height: 20px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-faint);
  list-style: none;
  transition: color var(--transition);
}

.doc-tree-summary::-webkit-details-marker {
  display: none;
}

/* The disclosure state is drawn, not animated — a glyph swap has no
   motion to reduce. */
.doc-tree-summary::before {
  content: '▸';
  font-family: var(--font-mono);
  color: var(--slate);
}

.doc-tree[open] > .doc-tree-summary::before {
  content: '▾';
}

.doc-tree-summary:hover {
  color: var(--text);
}

/* .skill-body ul (06-detail.css:222) hangs a 20px left margin on every
   list in a body. The tree draws its own indentation — connectors, then
   padding — and that inherited margin would push each nested list 20px
   past its parent's right edge, clipping the labels against the rail. */
.doc-tree .tree-list {
  margin: 0;
}

.doc-tree > .tree-list {
  padding: 0 var(--space-5) var(--space-4);
}

/* A child list is emitted INSIDE its parent <li>, after the link
   (doc-tree.js:41). The item therefore wraps: connector + link on the
   first row, the child list on a full-width second row, indented
   under it. Also unsets the row divider .tree-item carries by default —
   this is a dense contents list, not a list of cards. */
/* text-align/hyphens: the tree's entries are <li>s inside .skill-body, so
   `.skill-body li` (06-detail.css:211) justifies and auto-hyphenates them
   too — in a 220px rail that means rivers and words snapped in half
   (`change_-` / `column_names`). The tree is a list of labels, not prose:
   ragged right, no hyphenation. Same correction as the body, different
   selector, because the tree sits outside .post-prose. */
.doc-tree .tree-item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  border-bottom: none;
  text-align: left;
  hyphens: manual;
}

.doc-tree .tree-item > .tree-list {
  flex: 1 0 100%;
  padding: 0 0 0 var(--space-4);
}

.doc-tree .tree-connector {
  font-size: calc(12px * var(--type-scale));
  color: var(--slate);
}

.doc-tree .tree-link {
  padding: var(--space-1) 0;
  font-size: calc(13px * var(--type-scale));
  line-height: 1.5;
  color: var(--text-muted);
  overflow-wrap: break-word;
  transition: color var(--transition);
}

.doc-tree .tree-link:hover,
.doc-tree .tree-link:focus-visible {
  color: var(--text);
}

/* The section the reader is in. --signal means "interactive / active
   nav" under AD-009 — this is that, exactly. */
.doc-tree .tree-link.is-active {
  color: var(--signal);
}

/* ── The rail ─────────────────────────────────────────────────
   1300px is where an 800px centred shell finally leaves room for a
   220px rail plus a gutter. Keep this value identical to
   GUTTER_QUERY in doc-tree.js:88 — that is what sets <details open>,
   and a disagreement here shows up as a closed tree in an open rail. */
@media (min-width: 1300px) {
  /* The rail's containing block. .detail-layout is untouched: it
     stays 800px and centred, so a post title still sits at the same
     x as a skill title, and the rail hangs in the margin OUTSIDE the
     column rather than taking a slice of it. Nothing on this path may
     set `overflow` — the rail lives outside its containing block and
     would be clipped away. */
  .skill-body {
    position: relative;

    --rail-top: calc(var(--header-h) + var(--space-6));
    --rail-summary-h: 32px; /* 20px line-height + 12px padding-bottom */
  }

  /* `position: absolute` and `position: sticky` cannot combine on one
     element. So the <details> is the absolute box — top: 0/bottom: 0
     makes it exactly as tall as the post body, which is the scroll
     distance the tree is allowed to travel — and its two children are
     the sticky riders inside it. */
  .doc-tree {
    position: absolute;
    left: -260px;
    top: 0;
    bottom: 0;
    width: 220px;
    margin: 0;
    background: none;
    border: none;
    border-radius: 0;
  }

  /* A <details> does not hand its children straight to the element box:
     the browser wraps everything after the <summary> in a ::details-content
     box, and THAT is the containing block the sticky list is clamped to. It
     shrink-wraps the list, leaving zero travel — the rail scrolls away
     instead of sticking. Stretching it to the absolute box's height gives
     the list its scroll distance back.

     Its own rule on purpose: a browser that does not know the pseudo-element
     drops this rule whole (and needs it least — with no ::details-content
     box, the list's containing block is already the <details> itself). */
  .doc-tree::details-content {
    block-size: 100%;
  }

  /* The <summary> is NOT inside ::details-content — it is a direct child of
     the absolute box, so it sticks on its own. */
  .doc-tree-summary {
    position: sticky;
    top: var(--rail-top);
    padding: 0 0 var(--space-3);
  }

  /* Sticks directly below the summary, and scrolls within itself if a
     post has more headings than the viewport has room for. */
  .doc-tree > .tree-list {
    position: sticky;
    top: calc(var(--rail-top) + var(--rail-summary-h));
    max-height: calc(100vh - var(--rail-top) - var(--rail-summary-h) - var(--space-8));
    overflow-y: auto;
    padding: 0;
  }
}

/* ── Share row ────────────────────────────────────────────────
   Rendered three times on a post (share.js): as icon buttons on the
   date line (above 768px), as a share menu on the ← Browse row (768px
   and below), and labelled after the body. The controls are the site's
   .btn-secondary; hover and focus already resolve to --border-focus =
   --signal, which is "interactive" under AD-009, so the row adds no
   colour of its own. */
.share-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* On the date line the icons sit opposite the byline, at its right
   edge. At 768px and below that line is too narrow for both, so the
   query further down swaps this row for the toolbar's. */
.detail-meta-row .share-row {
  margin-left: auto;
}

/* The ← Browse row doubles as a toolbar on phones: back on the left,
   share on the right, the native-app arrangement. The back button's
   own bottom margin moves to the row, so on desktop, where the share
   side is hidden, the spacing is unchanged. */
.post-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.post-toolbar .detail-back {
  margin-bottom: 0;
}

.post-toolbar .share-menu {
  display: none;
  position: relative;
}

/* While its menu is open the toggle holds the hover look, so the open
   state shows without a pointer resting on it (touch has none). */
.share-menu-toggle[aria-expanded="true"] {
  border-color: var(--border-focus);
  color: var(--text);
  background: var(--surface-2);
}

/* The menu's panel: the same floating surface as the search results
   (.search-results, 04-home.css), opening down from the toggle and
   aligned to its right edge, since the toggle sits at the right. */
.share-menu-panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: 50;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  padding: 6px;
}

/* Items stack full-width, quiet until hovered or focused, when the
   .btn-secondary hover (--signal border) marks the one in play. */
.share-row-menu {
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
}

.share-row-menu .share-btn {
  justify-content: flex-start;
  border-color: transparent;
  background: none;
}

/* Same specificity as .btn-secondary:hover and later in the cascade, so
   the quiet rule above would swallow the hover; restore it here. */
.share-row-menu .share-btn:hover {
  border-color: var(--border-focus);
  background: var(--surface-2);
}

/* The menu has the room the phone row lacks: keep "Share on". */
.share-row-menu .share-btn-prefix {
  display: inline;
}

/* Square, label-less: the accessible name is the aria-label and the
   tooltip is the title. 44px keeps the site's touch target. */
.share-btn.share-btn-icon {
  min-width: 44px;
  padding: 0;
  justify-content: center;
}

/* "Copied!" needs room the icon alone did not. */
.share-btn.share-btn-icon.copied {
  padding: 0 var(--space-3);
}

.share-btn {
  font-size: calc(13px * var(--type-scale));
  padding: 6px 14px;
}

.share-btn:focus-visible {
  border-color: var(--border-focus);
  color: var(--text);
}

/* The copy confirmation, matching .copy-btn.copied (06-detail.css) so
   "Copied!" reads the same here as on a code block. */
.share-btn.copied {
  color: #6fba6f;
  border-color: #3a5a3a;
}

/* Announces "Link copied" to screen readers; never seen. */
.share-status {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The end-of-post copy: a quiet label in the site's eyebrow style
   (.related-section h2), then the same row. */
.post-share {
  margin-bottom: var(--space-10);
}

.post-share-label {
  font-size: calc(13px * var(--type-scale));
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-bottom: var(--space-3);
}

/* The toolbar's control is 44px tall against the back link's ~20px,
   so the row tightens its bottom margin; the title still sits ~12px
   lower than under a bare back link, which keeps the button clear of
   the POST badge. */
@media (max-width: 768px) {
  .detail-meta-row .share-row {
    display: none;
  }

  .post-toolbar {
    margin-bottom: var(--space-3);
  }

  .post-toolbar .share-menu {
    display: block;
  }
}

/* On a phone, "Share on LinkedIn" pushes "Share…" onto a second line.
   The visible label drops to "LinkedIn"; the accessible name is the
   aria-label, so a screen reader still hears the whole action. */
@media (max-width: 480px) {
  .share-btn-prefix {
    display: none;
  }
}

/* BR-26: the active entry changes instantly. (The smooth-scroll half
   of reduced motion is fixed site-wide in 01-base.css — the global
   `scroll-behavior: smooth` was ignoring the preference on every
   page, not just this one.) */
@media (prefers-reduced-motion: reduce) {
  .doc-tree .tree-link {
    transition: none;
  }
}
