/* Additions on top of site.css. Keep this file small. */

/* ------------------------------------------------------------------ theme
   Flat and low-saturation: near-neutral surfaces, neutral hairlines, and a muted
   clay red (~28% saturation) reserved for links. --accent is just the heading ink, so the timeline
   cursor and active tab stay monochrome.
   Light by default, dark when the visitor's system prefers it.
   site.css sets these variables per mode too, so !important makes ours win. */

/* Light: #f6f5f0 warm off-white page, #ecebe5 stone panels.
   Contrast on page / on panels: ink 13.5 / 12.3, links 6.2 / 5.7, muted 5.3 / 4.8. */
:root {
  --bg: #f6f5f0 !important;
  --background: #f6f5f0 !important;
  --foreground: #1f2a2e !important;
  --text-heading: #1f2a2e !important;
  --text-copy: #1f2a2e !important;
  --text-secondary: #5c686c !important;
  --text-nav: #5c686c !important;
  --text-nav-hover: #1f2a2e !important;
  --text-link: #804e48;
  /* One muted color per hue for the bio's links, in the order they are used. */
  --hue-1: #804e48; /* red */
  --hue-2: #4a6b42; /* green */
  --hue-3: #44607f; /* blue */
  --hue-4: #6b6230; /* yellow */
  --hue-5: #85592f; /* orange */
  --hue-6: #66507f; /* purple */
  --hue-7: #386a66; /* teal */
  --hue-8: #80486a; /* pink */
  --surface-subtle: #ecebe5 !important;
  --line-subtle: rgb(31 42 46 / .14) !important;
  --border: rgb(31 42 46 / .14) !important;
  --ring: #804e48 !important;
  --accent: #1f2a2e;
  --code-bg: #1f2a2e;
  --code-inline: #1f2a2e;
  --selection-bg: #d9dcd8;
  --selection-text: #1f2a2e;
  color-scheme: light;
}

/* Dark: #121517 charcoal page, #1a1e21 panels.
   Contrast on page / on panels: heading 15.9 / 14.5, copy 13.0 / 11.9,
   links 8.0 / 7.4, muted 7.0 / 6.4.
   Comes after the light block, so it wins when the media query matches.
   Screen only, so pages always print in the light palette. */
@media screen and (prefers-color-scheme: dark) {
  :root {
    --bg: #121517 !important;
    --background: #121517 !important;
    --foreground: #d5dada !important;
    --text-heading: #ecefef !important;
    --text-copy: #d5dada !important;
    --text-secondary: #98a2a5 !important;
    --text-nav: #98a2a5 !important;
    --text-nav-hover: #ecefef !important;
    --text-link: #c9a39d;
    --hue-1: #c9a39d;
    --hue-2: #a5c49e;
    --hue-3: #9fb6d1;
    --hue-4: #cdc492;
    --hue-5: #d1b08f;
    --hue-6: #bba8d1;
    --hue-7: #97c4bf;
    --hue-8: #d1a3bd;
    --surface-subtle: #1a1e21 !important;
    --line-subtle: rgb(213 218 218 / .14) !important;
    --border: rgb(213 218 218 / .14) !important;
    --ring: #c9a39d !important;
    --accent: #ecefef;
    --code-bg: #1a1e21;
    --code-inline: #ecefef;
    --selection-bg: #3a4246;
    --selection-text: #ecefef;
    color-scheme: dark;
  }
}

/* Code blocks are dark in both modes, so they always use site.css's dark syntax colors. */
:root, :root.dark, .dark {
  --sh-class: #7aa2f7 !important;
  --sh-sign: #89ddff !important;
  --sh-string: #9ece6a !important;
  --sh-keyword: #bb9af7 !important;
  --sh-comment: #8a93b8 !important;
  --sh-jsxliterals: #7aa2f7 !important;
  --sh-property: #73daca !important;
  --sh-entity: #e0af68 !important;
}
pre { background-color: var(--code-bg) !important; }
pre code { color: #f1faee; }
code:not(pre code), code:not(pre code) span { color: var(--code-inline) !important; }
::selection { background: var(--selection-bg); color: var(--selection-text); }

/* Links in the bio take the hues in turn (numbered by hue_links() in src/helpers.php;
   config.php can pin a link to one hue).
   Contrast on the page is at least 5.5:1 in light mode and 8:1 in dark mode. */
.link-hue-1 { --text-link: var(--hue-1); }
.link-hue-2 { --text-link: var(--hue-2); }
.link-hue-3 { --text-link: var(--hue-3); }
.link-hue-4 { --text-link: var(--hue-4); }
.link-hue-5 { --text-link: var(--hue-5); }
.link-hue-6 { --text-link: var(--hue-6); }
.link-hue-7 { --text-link: var(--hue-7); }
.link-hue-8 { --text-link: var(--hue-8); }
.link-hue-9 { --text-link: var(--text-heading); } /* "white": the brightest text color */

/* Links get their own color; site.css draws them with the muted nav color. */
.content-frame a.content-link, .notes-list a { color: var(--text-link); text-decoration-color: color-mix(in srgb, var(--text-link) 45%, transparent); }
/* Hover: the same hue, a step more saturated and a touch brighter, not white. */
.content-frame a.content-link:hover, .notes-list a:hover { color: var(--text-link); text-decoration-color: currentColor; }
@supports (color: oklch(from red calc(l + .1) c h)) {
  .content-frame a.content-link:hover, .notes-list a:hover { color: oklch(from var(--text-link) calc(l + 0.08) calc(c * 1.6) h); }
}
.content-frame a.content-link.link-hue-9:hover { color: var(--text-heading); }

/* Active bio tab underline. */
/* The tabs are links (real addresses for crawlers); site.css styled them as buttons. */
.bio-toggle a { color: inherit; cursor: pointer; font: inherit; padding: 0; background: none; border: 0; padding-block: .2rem; text-decoration: none; transition: color .16s; position: relative; }
.bio-toggle a:hover, .bio-toggle a[aria-pressed=true] { color: var(--text-copy); }
.bio-toggle a[aria-pressed=true]::after { content: ""; position: absolute; left: 0; right: 0; bottom: -.62rem; height: 1px; background: var(--accent); }

/* With assets/js/bio.js, one line glides between the tabs like the timeline cursor. */
.bio-toggle-options { position: relative; }
.bio-toggle-options.has-indicator a[aria-pressed=true]::after { display: none; }
.bio-toggle-indicator { position: absolute; left: 0; bottom: -.62rem; width: var(--w, 0); height: 1px; background: var(--accent); transform: translateX(var(--x, 0)); transition: transform .52s cubic-bezier(.34, 1.56, .64, 1), width .52s cubic-bezier(.34, 1.56, .64, 1); pointer-events: none; }
.bio-toggle-options.is-instant .bio-toggle-indicator { transition: none; }
@media (prefers-reduced-motion: reduce) { .bio-toggle-indicator { transition: none; } }
/* The reload sequence: the page lands at the top and "Long" takes a tiny hop. As it
   comes back down, the line launches across to "Summary". Skipped if the toggle was
   already on screen when the page was reloaded. */
/* 350ms in all (HOP_MS in bio.js): a quick 50ms rise, about 165ms held at the top, and a
   135ms fall. */
.bio-toggle a.is-hopping { animation: bio-toggle-hop .35s both; }
@keyframes bio-toggle-hop {
  0% { transform: translateY(0); animation-timing-function: cubic-bezier(.2, .8, .4, 1); }
  14% { transform: translateY(-3px); animation-timing-function: linear; }
  61% { transform: translateY(-3px); animation-timing-function: cubic-bezier(.6, 0, .9, .4); }
  100% { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) { .bio-toggle a.is-hopping { animation: none; } }
/* While a reload puts the Long bio back (see the head script in layout.php). */
.is-restoring .home-layout { visibility: hidden; }
.content-blockquote { border-left-color: var(--line-subtle); }
/* A page title with no byline under it keeps the same distance to the text as a post's byline would. */
.content-frame .content-title:not(:has(+ .article-meta)):not(:has(+ .content-subtitle)) { margin-bottom: 1.6rem; }
/* The subtitle between a post's title and byline (front matter `subtitle:`). */
.content-frame .content-subtitle { margin: 0 0 .9rem; font-size: 1.05em; font-style: normal; line-height: 1.4; color: var(--text-secondary); }
.content-frame .content-title:has(+ .content-subtitle) { margin-bottom: .45rem; line-height: 1.1; }
/* The mirror's top spacing keys off title + byline; keep it when a subtitle sits between. */
.content-frame:has(> .content-title + .content-subtitle + .article-meta) { margin-top: 48px; }
ol.content-list { list-style: decimal; }

/* Side-by-side images in a justified row: each item's --ratio (set server-side) gives equal heights. */
.bio-gallery { display: flex; gap: .75rem; align-items: flex-start; }
.bio-gallery-item { display: block; flex: var(--ratio, 1) 1 0; min-width: 0; }
.bio-gallery-item:nth-child(2n) { margin-top: 0; }
/* A row mixing portrait and landscape: the taller portrait overhangs evenly above and below. */
.bio-gallery.is-mixed { align-items: center; }
.bio-gallery-item img, .bio-gallery-item video { display: block; width: 100%; height: auto; }
/* Galleries, quotes and the mini bookshelf sit in from the column edges: 40px, or half
   that on narrow screens. (@gallery inset and @bookshelf inset are now just the default.) */
:root { --inset: 40px; }
@media (max-width: 640px) { :root { --inset: 20px; } }
.bio-gallery { padding-inline: var(--inset); }
.content-blockquote { margin-inline: var(--inset); }
/* A quote's attribution: its last paragraph, when it's just an italic name. */
.content-blockquote > p:last-child:has(> em:only-child) { margin-top: -.9rem; margin-bottom: 0; font-size: .9em; color: var(--text-nav); }
/* A gallery with an indented caption line: small, muted, under the row. */
.content-frame .bio-figure { margin: 0 0 1.45rem; }
.content-frame .bio-figure .bio-gallery { margin-bottom: 0; }
.bio-gallery-caption { margin: .5rem 0 0; padding-inline: var(--inset); color: var(--text-nav); font-family: var(--font-ui); font-size: 12px; line-height: 1.5; }
/* @gallery portrait: a lone tall photo, kept from filling the column. */
.bio-gallery.portrait { max-width: 460px; margin-inline: auto; }

/* Lightbox: a full-screen <dialog>, see assets/js/lightbox.js. */
.lightbox { width: 100vw; height: 100vh; max-width: none; max-height: none; margin: 0; padding: 0; border: 0; background: transparent; overflow: auto; cursor: zoom-out; }
.lightbox::backdrop { background: rgb(0 0 0 / .88); }
.lightbox figure { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100%; margin: 0; padding: 2rem 1rem; box-sizing: border-box; }
.lightbox img { max-width: 100%; max-height: calc(100vh - 6rem); object-fit: contain; border-radius: 3px; cursor: zoom-in; }
.lightbox.is-zoomed figure { display: block; }
.lightbox.is-zoomed img { max-width: none; max-height: none; cursor: zoom-out; }
.lightbox figcaption { margin-top: .75rem; color: #d6d3d1; font-family: var(--font-ui); font-size: 13px; text-align: center; }

/* ------------------------------------------------------------------ timeline
   Markup: templates/partials/timeline.php. Behavior: assets/js/timeline.js.
   --cursor is the active dot's center along the track, set by the script. */

/* Milestone stamps: a muted line where each section of the long bio starts, carrying the
   same title and label as the timeline. A foothold in the text, not a heading. */
.milestone { display: flex; gap: .6rem; align-items: baseline; margin: 2.6rem 0 1rem; scroll-margin-top: 5.5rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 12px; letter-spacing: .06em; text-transform: uppercase; }
.milestone:first-child { margin-top: 0; }
.milestone-label { opacity: .7; letter-spacing: .02em; text-transform: none; }
.milestone-label::before { content: "·"; margin-right: .6rem; }

.timeline { --dot: 9px; --row: 22px; --ease-glide: cubic-bezier(.34, 1.56, .64, 1); font-family: var(--font-ui); }
.timeline-track { position: relative; }
.timeline-list { position: relative; margin: 0; padding: 0; list-style: none; }
.timeline-item a { display: flex; align-items: center; gap: .9rem; min-height: var(--row); color: var(--text-nav); text-decoration: none; }

.timeline-dot { position: relative; flex: none; width: var(--dot); height: var(--dot); border: 1.5px solid color-mix(in srgb, var(--text-nav) 55%, transparent); border-radius: 50%; background: var(--bg); box-sizing: border-box; transition: background-color .3s, border-color .3s; }
.timeline-item.is-passed .timeline-dot { border-color: var(--text-nav); background: var(--text-nav); }

.timeline-rail, .timeline-fill { position: absolute; pointer-events: none; }
.timeline-rail { background: var(--line-subtle); }
.timeline-fill { background: var(--accent, var(--text-nav)); transition: height .52s ease-out, width .52s ease-out; }

.timeline-cursor { position: absolute; top: 0; left: 0; z-index: 1; width: 15px; height: 15px; border-radius: 50%; background: var(--accent, var(--text-heading)); box-shadow: 0 0 0 4px var(--bg); pointer-events: none; transition: transform .52s var(--ease-glide); }

/* Arrival: the checkpoint sends out a ring once the cursor settles on it. */
.timeline-item.is-arrived .timeline-dot::after { content: ""; position: absolute; inset: -1.5px; border: 1.5px solid var(--accent, var(--text-heading)); border-radius: 50%; animation: timeline-ripple .9s ease-out forwards; }
@keyframes timeline-ripple { from { opacity: .7; transform: scale(1); } to { opacity: 0; transform: scale(3.4); } }

.timeline.is-instant .timeline-cursor, .timeline.is-instant .timeline-fill { transition: none; }

/* Vertical (wide screens): bare on the page, in the sticky .home-timeline column. */
.timeline-y { display: flex; flex-direction: column; height: 100%; padding-block: 1.25rem; box-sizing: border-box; }
.timeline-y .timeline-track { flex: 1; min-height: 0; }
/* "Return to top" sits under the timeline, apart from its line: it scrolls up and goes
   back to the Summary bio, the same way a reload does (see assets/js/bio.js). */
.timeline-reset { display: flex; flex-direction: column; align-items: center; align-self: flex-start; gap: .1rem; margin: 1.75rem 0 0; padding: 0; border: 0; background: none; color: var(--text-nav); font-family: var(--font-ui); font-size: 13px; line-height: 1.4; cursor: pointer; transition: color .2s; }
.timeline-reset-arrow { font-size: 15px; line-height: 1; }
/* Centered a third of the way across the entries' text (the column right of the dots). */
.timeline-y .timeline-reset { --text-start: calc(var(--dot) + .85rem); align-self: flex-start; margin-left: calc(var(--text-start) + (100% - var(--text-start)) / 3); transform: translateX(-50%); }
.timeline-reset:hover, .timeline-reset:focus-visible { color: var(--text-heading); }
/* A compact vertical timeline sizes to its entries instead of filling a panel. */
.timeline-y.timeline-compact { height: auto; padding-block: 0; }
.timeline-y.timeline-compact .timeline-track { flex: none; }
.timeline-y.timeline-compact .timeline-list { height: auto; gap: 2rem; justify-content: flex-start; }
.timeline-y.timeline-compact .timeline-item a { align-items: start; }
.timeline-y.timeline-compact .timeline-dot { margin-top: .38rem; }
.timeline-y.timeline-compact .timeline-label { display: flex; flex-wrap: wrap; gap: .05rem .6rem; margin-top: .15rem; }
.timeline-y .timeline-list { display: flex; flex-direction: column; justify-content: space-between; height: 100%; }
.timeline-y { --row: 35px; }
.timeline-y .timeline-rail { top: var(--rail-top, calc(var(--row) / 2)); bottom: var(--rail-bottom, calc(var(--row) / 2)); left: calc(var(--dot) / 2 - .5px); width: 1px; }
.timeline-y .timeline-fill { top: var(--rail-top, calc(var(--row) / 2)); left: calc(var(--dot) / 2 - .5px); width: 1px; height: max(0px, var(--cursor, 0px) - var(--rail-top, var(--row) / 2)); }
.timeline-y .timeline-cursor { left: calc(var(--dot) / 2); transform: translate(-50%, calc(var(--cursor, 17px) - 50%)); }
.timeline-y .timeline-item a { display: grid; grid-template: "dot title" "dot label" / auto minmax(0, 1fr); gap: 0 .85rem; }
.timeline-y .timeline-dot { grid-area: dot; }
.timeline-y .timeline-label { grid-area: label; font-size: 11px; line-height: 1.3; letter-spacing: .01em; font-variant-numeric: tabular-nums; }
.timeline-y .timeline-title { grid-area: title; line-height: 1.3; color: var(--text-secondary); font-family: var(--font-reading); font-size: 15px; transition: color .3s, transform .45s var(--ease-glide);  text-wrap: balance;}
.timeline-y a:hover .timeline-title { color: var(--text-heading); }
.timeline-y .is-active .timeline-label { color: var(--text-copy); }
.timeline-y .is-active .timeline-title { color: var(--text-heading); transform: translateX(4px); }

/* Horizontal bar (mobile): sticks to the top while the long bio scrolls by. */
.timeline-x { position: sticky; top: 0; z-index: 20; margin: 0 0 1.75rem; padding: .75rem 0 .6rem; border-bottom: 1px solid var(--line-subtle); background: var(--bg); }
.timeline-x .timeline-current { display: flex; gap: .6rem; align-items: baseline; margin: 0 0 .35rem; color: var(--text-nav); font-size: 12px; }
.timeline-x .timeline-current .timeline-title { color: var(--text-heading); font-family: var(--font-reading); font-size: 15px; }
.timeline-x .timeline-current.is-changing { animation: timeline-swap .4s ease-out; }
@keyframes timeline-swap { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.timeline-x .timeline-list { display: flex; justify-content: space-between; }
.timeline-x .timeline-item a { justify-content: center; width: 24px; height: 24px; min-height: 0; }
.timeline-x .timeline-rail { top: 50%; left: 12px; right: 12px; height: 1px; }
.timeline-x .timeline-fill { top: 50%; left: 12px; height: 1px; width: max(0px, var(--cursor, 12px) - 12px); }
.timeline-x .timeline-cursor { top: 50%; transform: translate(calc(var(--cursor, 12px) - 50%), -50%); }
@media (min-width: 1400px) { .timeline-x { display: none; } }

@media (prefers-reduced-motion: reduce) {
  .timeline-cursor, .timeline-fill, .timeline-y .timeline-title { transition: none; }
  .timeline-item.is-arrived .timeline-dot::after, .timeline-x .timeline-current.is-changing { animation: none; }
}


/* ------------------------------------------------------------------ text-art portrait
   Home page panel (Summary view). Markup: templates/partials/ascii-portrait.php.
   Behavior: assets/js/ascii.js. Monochrome, in the theme's ink. */
.ascii-portrait { position: absolute; inset: 0; margin: 0; overflow: hidden; cursor: crosshair; }
/* A canvas: its CSS color and font are read by the script to draw the grid. */
/* The canvas is painted opaque (its background-color) to cover the <video> sources behind it. */
.ascii-output { position: absolute; inset: 14px; z-index: 1; width: calc(100% - 28px); height: calc(100% - 28px); background-color: var(--surface-subtle); color: var(--text-copy); font: 11px/1.32 ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
/* Kept a few px inside the canvas: at fractional sizes, layers with matching edges both
   partly cover the same screen pixel, and a sliver of video shows through. */
.ascii-source { position: absolute; inset: 20px; width: calc(100% - 40px); height: calc(100% - 40px); object-fit: cover; pointer-events: none; }
.ascii-stats { position: absolute; z-index: 2; top: 28px; left: 28px; padding: .6rem .85rem; border: 1px solid var(--line-subtle); border-radius: 6px; background: var(--surface-subtle); color: var(--text-nav); font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; line-height: 1.5; }
/* One grid for the stats and the camera row, so their columns line up. */
.ascii-stats dl, .ascii-camera { display: grid; grid-template-columns: 8.5em auto; gap: 0 1rem; margin: 0; white-space: nowrap; }
.ascii-stats dt, .ascii-stats dd { margin: 0; }
.ascii-stats dd, .ascii-camera span + span { color: var(--text-copy); font-variant-numeric: tabular-nums; }
.ascii-camera { width: 100%; margin-top: .35rem; padding: .35rem 0 0; border: 0; border-top: 1px solid var(--line-subtle); background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.ascii-camera:hover span, .ascii-camera:focus-visible span { color: var(--text-heading); }

/* Home layout: the text column tops out at 600px, so cap its track at 720px and give the
   rest to the right-hand panel instead of an empty gap (site.css splits 1.75fr / 1fr). */
@media (min-width: 1100px) {
  .home-layout { grid-template-columns: minmax(0, 720px) minmax(380px, 1fr); }
}

/* Wide screens, Long bio only: a third column between the text and the panel holds the
   timeline. In Summary the slot is empty and hidden, so the panel keeps its full width. */
.home-timeline { display: none; }
@media (min-width: 1400px) {
  .home-layout:has(.home-timeline .timeline) { grid-template-columns: minmax(0, 600px) 210px minmax(380px, 1fr); }
  .home-timeline:has(.timeline) { display: block; position: sticky; top: var(--page-pad); height: calc(100svh - 2 * var(--page-pad)); }
}

/* The way back home, on every page but the home page. With room beside the text it is a
   lone arrow fixed at mid-height just left of the text column (600px wide, centered), like
   a carousel's "previous".
   Without that room it is a plain link above the title. */
.content-frame a.home-arrow { display: inline-block; margin: 0 0 2.5rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 13px; text-decoration: none; transition: color .2s, transform .2s; }
.content-frame a.home-arrow:hover, .content-frame a.home-arrow:focus-visible { color: var(--text-heading); }
@media (min-width: 800px) {
  .content-frame a.home-arrow { position: fixed; z-index: 5; top: 50%; left: max(0px, calc(50% - 300px - 260px)); margin: 0; padding: 1.5rem 1.75rem; font-size: 26px; line-height: 1; transform: translateY(-50%); }
  .content-frame a.home-arrow:hover, .content-frame a.home-arrow:focus-visible { transform: translate(-4px, -50%); }
  /* Still read out by screen readers. */
  .home-arrow-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}

/* Veiled gallery photos (![Alt](photo.jpg "Click to show")): blurred until clicked once. */
.bio-gallery-item { position: relative; }
.bio-gallery-item.is-veiled { cursor: pointer; }
.bio-gallery-item.is-veiled img { filter: blur(10px) brightness(.85); transform: scale(1.06); }
.veil-label { position: absolute; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: center; gap: .2rem; padding: 1rem; color: #fff; font-family: var(--font-ui); font-size: 13px; line-height: 1.4; text-align: center; text-shadow: 0 1px 3px rgb(0 0 0 / .6); }
.bio-gallery-item.is-veiled .veil-label { display: flex; }
/* Once revealed, a small "Hide" control in the bottom left corner blurs it again. */
.veil-hide { position: absolute; left: .5rem; bottom: .5rem; padding: .1rem .55rem; border-radius: 999px; background: rgb(0 0 0 / .55); color: #fff; font-family: var(--font-ui); font-size: 12px; line-height: 1.6; cursor: pointer; }
.veil-hide:hover, .veil-hide:focus-visible { background: rgb(0 0 0 / .8); }
.bio-gallery-item.is-veiled .veil-hide { display: none; }

/* Notes under an opened video, such as a transcript link. */
.video-notes p { margin: 0 0 .5rem; }

/* ------------------------------------------------------------------ library
   /library: templates/library.php, data in content/library.csv. Each book's color (--c)
   is its category's hue. */
.library-search { margin: 0 0 1rem; }
.library-search-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.library-search input { width: 100%; padding: .5rem 0; border: 0; border-bottom: 1px solid var(--line-subtle); border-radius: 0; background: none; color: var(--text-copy); font-family: var(--font-ui); font-size: 15px; outline: none; }
.library-search input:focus { border-bottom-color: var(--text-nav); }
.library-search input::placeholder { color: var(--text-nav); opacity: .8; }

.library-categories { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 2rem; }
.content-frame .library-categories a { display: inline-flex; align-items: center; gap: .4rem; padding: .1rem .6rem; border: 1px solid var(--line-subtle); border-radius: 999px; color: var(--text-nav); font-family: var(--font-ui); font-size: 12px; line-height: 1.7; text-decoration: none; }
.library-categories a[style]::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--c); }
.library-categories a span { opacity: .7; font-variant-numeric: tabular-nums; }
.content-frame .library-categories a:hover, .content-frame .library-categories a[aria-current] { border-color: var(--text-nav); color: var(--text-heading); }

/* The shelves: one thin spine per book, standing on a line. */
/* Swapping the list must not move the page (see embeds.js); no browser scroll anchoring. */
.content-frame:has(#library-body) { overflow-anchor: none; }
/* One bookcase, six shelves, and a caption naming the book under the cursor. */
.content-frame .library-case-caption { min-height: 1.4em; margin: -1.5rem 0 2rem; color: var(--text-heading); font-family: var(--font-ui); font-size: 13px; line-height: 1.4; }
.content-frame .library-case-caption { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }
.library-case-words { min-width: 0; }
/* Both caption lines keep their height when empty, so nothing below moves on hover. */
.library-case-words span { display: block; min-height: 1.4em; transition: opacity .4s; }
/* "Don't." is the one link on the page that means it. It shows while a book's name is in
   the caption (.has-browsed), and fades out a second after the name does, unless hovered. */
.content-frame a.library-dont { flex: none; color: var(--text-nav); text-decoration: none; opacity: 0; pointer-events: none; transition: opacity .6s, color .2s; }
.library-case.has-browsed a.library-dont { opacity: .55; pointer-events: auto; }
.library-case.has-browsed a.library-dont:hover { opacity: 1; color: var(--text-heading); }
/* "Reshelve" is its own thing: always visible while the books are down, hover or not. */
.content-frame a.library-reshelve { flex: none; color: var(--text-nav); text-decoration: none; opacity: .8; transition: opacity .2s, color .2s; }
.content-frame a.library-reshelve:hover { opacity: 1; color: var(--text-heading); }
/* Dominoes: each spine tips onto the next, pivoting on its bottom-right corner. */
.library-spine { transform-origin: 100% 100%; }
.library-spine.is-fallen { transform: rotate(80deg); opacity: .55; transition: transform .28s cubic-bezier(.5, 0, 1, .6), opacity .28s; }
.library-spine.is-fallen:hover { transform: rotate(80deg); }
.library-spine.is-rising { transition: transform .22s cubic-bezier(0, .6, .4, 1), opacity .22s; }
@media (prefers-reduced-motion: reduce) { .library-spine.is-fallen, .library-spine.is-rising { transition: none; } }
.library-case-place, .library-mini-place { display: block; min-height: 1.4em; color: var(--text-nav); opacity: .7; font-size: 12px; }
.library-case-place.is-fading, .library-mini-place.is-fading { opacity: 0; }
.library-shelves { display: grid; gap: .7rem; margin: 2rem 0; }
.library-shelf { display: flex; align-items: flex-end; gap: 2px; height: 52px; border-bottom: 2px solid var(--line-subtle); }
.library-spine { flex: 1 1 0; min-width: 2px; max-width: 14px; height: var(--h); border-radius: 2px 2px 0 0; background: var(--c); opacity: .8; transition: transform .16s, opacity .16s; }
a.library-spine:hover { transform: translateY(-5px); opacity: 1; }
.library-spine.is-dim { opacity: .12; }

.library-bar { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: .25rem 1.5rem; margin: 0 0 .5rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 13px; }
.library-bar p { margin: 0; }
.library-sorts { display: flex; flex-wrap: wrap; gap: .9rem; }
.content-frame .library-bar a { color: var(--text-nav); text-decoration: none; }
.content-frame .library-bar a:hover, .content-frame .library-sorts a[aria-current] { color: var(--text-heading); }
.content-frame .library-sorts a[aria-current] { text-decoration: underline 1px; text-underline-offset: .4em; }

.library-heading { margin: 2.25rem 0 .25rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 12px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
.library-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line-subtle); }
.library-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 1rem; align-items: baseline; padding: .6em 0 .6em .8rem; border-bottom: 1px solid var(--line-subtle); box-shadow: inset 2px 0 0 var(--c); scroll-margin-top: 6rem; transition: background-color .3s; }
.library-row:target, .library-row.is-found { background: var(--surface-subtle); }
.library-title { color: var(--text-copy); }
.library-subtitle { color: var(--text-secondary); }
.library-byline { display: block; color: var(--text-nav); font-family: var(--font-ui); font-size: 13px; line-height: 1.5; }
.library-year { color: var(--text-nav); font-family: var(--font-ui); font-size: 13px; font-variant-numeric: tabular-nums; }
/* Withheld books: a hatched spine, and a row that says only that something was withheld. */
.library-row.is-redacted { --c: var(--text-nav); }
.library-row.is-redacted .library-title { color: var(--text-nav); text-decoration: line-through; }
.library-spine.is-withheld { --c: var(--text-nav); background: repeating-linear-gradient(45deg, var(--c) 0 2px, transparent 2px 4px); }

/* The library's timeline: one entry per shelf, beside the list. It starts where the list
   starts and then stays on screen. Wide screens only; there's no room for it otherwise. */
.library-listing { position: relative; }
.library-timeline { display: none; }
@media (min-width: 1180px) {
  .library-timeline { display: block; position: absolute; top: 2.25rem; bottom: 0; left: calc(100% + 3.5rem); width: 215px; }
  .library-timeline .timeline { position: sticky; top: 2.5rem; }
}
.library-heading { scroll-margin-top: 2.25rem; }
.timeline-category { display: inline-flex; align-items: center; gap: .3rem; white-space: nowrap; }
.timeline-category::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--c); }

/* The mini bookshelf (@bookshelf): the library's bookcase at half height, as one link. */
.content-frame a.library-mini, .library-mini { display: block; margin: 0 0 2rem; color: inherit; text-decoration: none !important; border: 0; }
/* Inset like a gallery. */
.library-mini { padding-inline: var(--inset); }
.library-mini .library-shelves { gap: .6rem; margin: 0 0 .6rem; }
.library-mini .library-shelf { height: 38px; gap: 1px; border-bottom-width: 1px; }
.library-mini .library-spine { border-radius: 1px 1px 0 0; }
.library-mini .library-spine:hover { transform: translateY(-3px); opacity: 1; }
.library-mini-caption { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 13px; line-height: 1.4; }
.library-mini-book { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.library-mini-book > span { display: block; overflow: hidden; text-overflow: ellipsis; transition: color .15s, opacity .4s; }
.library-mini-book .is-fading, .library-case-caption .is-fading { opacity: 0; }
.library-mini-book .is-book { color: var(--text-heading); }
.library-mini-more { flex: none; transition: color .15s; }
.library-mini:hover .library-mini-more, .library-mini:focus-visible .library-mini-more { color: var(--text-heading); }

/* The "Watch ..." disclosure arrow tilts toward open on hover (halfway to the 90° of an
   open one), and settles back when the cursor leaves. */
.content-frame details:not([open]) > summary:hover::before { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) { .content-frame details:not([open]) > summary:hover::before { transform: none; } }

/* Code blocks. Tokens come from src/Highlight.php; the colors are the link hues, so code
   reads as part of the page rather than an editor pasted into it. */
.content-frame pre { position: relative; }
.content-frame pre[data-lang]::before { content: attr(data-lang); position: absolute; top: .55rem; right: .8rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; opacity: .7; }
.content-frame pre code { color: var(--text-copy); }
.hl-c { color: var(--text-nav); font-style: italic; }
.hl-s { color: var(--hue-2); }
.hl-k { color: var(--hue-6); }
.hl-n { color: var(--hue-5); }
.hl-f { color: var(--hue-3); }
.hl-v { color: var(--hue-1); }
.hl-a { color: var(--hue-7); }
.hl-t { color: var(--hue-3); }
.hl-o { color: var(--text-nav); }

/* Inline diagrams (@svg). Their colors resolve here, so one file works in both themes. */
.content-frame .content-diagram {
  /* Wider than the text column when the window allows: up to 900px, centered on the column. */
  --diagram-width: max(100%, min(900px, 100vw - 180px));
  width: var(--diagram-width);
  margin-left: calc((100% - var(--diagram-width)) / 2);
  --diagram-ink: var(--text-heading);
  --diagram-muted: var(--text-nav);
  --diagram-fill: var(--surface-subtle);
  --diagram-box: var(--bg);
  --diagram-line: var(--line-subtle);
  --diagram-ink-fill: var(--text-heading);
  --diagram-panel: color-mix(in srgb, var(--hue-7) 6%, transparent);
  --diagram-green: color-mix(in srgb, var(--hue-2) 22%, var(--bg));
  --diagram-purple: color-mix(in srgb, var(--hue-6) 22%, var(--bg));
  --diagram-red: color-mix(in srgb, var(--hue-1) 22%, var(--bg));
  --diagram-yellow: color-mix(in srgb, var(--hue-4) 22%, var(--bg));
  margin-block: 0 1.45rem;
}
.content-diagram svg { display: block; width: 100%; height: auto; font-family: var(--font-ui); }
/* Text the diagram left uncolored (its titles and notes) takes the ink; CSS beats the attributes, so only unfilled text. */
.content-diagram svg text:not([fill]) { fill: var(--diagram-ink); }
.content-diagram svg text:not([fill]) tspan:not([fill]) { fill: inherit; }
.content-diagram .bio-gallery-caption { padding-inline: 0; max-width: 600px; margin-inline: auto; }


/* Words the books say (dominoes): tiny, over the spine, gone in a second. */
.library-shelf { position: relative; }
.library-word { position: absolute; z-index: 2; color: var(--text-heading); font-family: var(--font-ui); font-size: 10px; line-height: 1; white-space: nowrap; pointer-events: none; }
.library-word.is-foresight { color: var(--text-nav); font-style: italic; }


/* The track deck (assets/js/deck.js), in the portrait panel's bottom-right corner: a plate
   with note, title, open-on-YouTube arrow and play/pause. While playing, the YouTube player
   sits directly on top of the plate as one piece, 16:9 so there are no black bars, and the
   title scrolls like a car deck's display. */
.home-deck { position: absolute; z-index: 2; right: 28px; bottom: 28px; display: flex; flex-direction: column; align-items: stretch; max-width: calc(100% - 56px); border: 1px solid var(--line-subtle); border-radius: 6px; background: var(--surface-subtle); overflow: hidden; transition: border-color .2s; }
.home-deck:hover { border-color: var(--text-nav); }
/* The player takes the plate's width, but never less than YouTube's 200px minimum height
   (356px wide at 16:9): the deck grows to that while playing. */
.home-deck.is-playing { min-width: 356px; }
.home-deck-player { width: 100%; min-height: 200px; aspect-ratio: 16 / 9; border-bottom: 1px solid var(--line-subtle); background: #000; }
/* Paused: collapsed to nothing but still in layout (see deck.js). */
.home-deck-player.is-away { min-height: 0; height: 0; aspect-ratio: auto; border-bottom: 0; overflow: hidden; visibility: hidden; }
/* Colors mostly muted, toward the portrait beside it. (A CSS filter on the frame; the player itself is untouched.) */
.home-deck-player iframe { display: block; width: 100%; height: 100%; filter: saturate(.3); }
.home-listening { display: flex; align-items: center; gap: .55rem; min-width: 0; padding: .45rem .55rem .45rem .75rem; color: var(--text-nav); font-family: var(--font-ui); font-size: 12px; line-height: 1; }
/* The note and the title together open the playlist; the glyph on the right plays and pauses. */
.home-listening-menu { display: flex; flex: 1 1 auto; align-items: center; gap: .55rem; min-width: 0; margin: 0; padding: 0; border: 0; background: none; color: inherit; font: inherit; line-height: 1; cursor: pointer; text-align: left; }
.home-listening-menu:hover, .home-listening-menu[aria-expanded="true"] { color: var(--text-heading); }
/* The note becomes a hamburger when the playlist control is hovered or open. */
.home-listening-note { position: relative; width: 14px; height: 14px; font-size: 14px; }
.home-listening-glyph, .home-listening-bars { position: absolute; inset: 0; display: block; transition: opacity .15s; }
.home-listening-bars { display: flex; flex-direction: column; justify-content: center; gap: 3px; padding: 1px 1px; opacity: 0; }
.home-listening-bars i { display: block; height: 1.5px; border-radius: 1px; background: currentColor; }
.home-listening-menu:hover .home-listening-glyph, .home-listening-menu[aria-expanded="true"] .home-listening-glyph { opacity: 0; }
.home-listening-menu:hover .home-listening-bars, .home-listening-menu[aria-expanded="true"] .home-listening-bars { opacity: 1; }
.home-listening-play { flex: none; margin: 0; padding: 0 .15rem; border: 0; background: none; color: var(--text-heading); font: inherit; font-size: 13px; line-height: 1; cursor: pointer; opacity: .85; }
.home-listening-play:hover { opacity: 1; }
.home-listening-play:disabled { opacity: .45; cursor: default; }
/* The playlist: a plain list on top of the plate, in the same box. */
.home-deck-list { margin: 0; padding: .35rem 0; list-style: none; border-bottom: 1px solid var(--line-subtle); font-family: var(--font-ui); font-size: 12px; }
.home-deck.is-open { width: max-content; }
.home-deck-list button { display: block; width: 100%; margin: 0; padding: .35rem .75rem; border: 0; background: none; color: var(--text-nav); font: inherit; line-height: 1.3; text-align: left; cursor: pointer; white-space: nowrap; }
.home-deck-list button:hover { color: var(--text-heading); }
.home-deck-list button[aria-current] { color: var(--text-heading); }
.home-deck-list button[aria-current]::before { content: "▸ "; }
.home-listening-strip { display: block; flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; }
/* The deck keeps one width, playing or not, so pressing play changes nothing but the height:
   deck.js measures the plate once, with the whole title showing, and pins it. */
.home-listening-title { display: inline-block; }
.home-deck.is-playing .home-listening-strip { -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.home-deck.is-playing .home-listening-title { padding-left: 100%; animation: deck-marquee 14s linear infinite; }
@keyframes deck-marquee { to { transform: translateX(-100%); } }
@media (prefers-reduced-motion: reduce) { .home-deck.is-playing .home-listening-title { animation: none; padding-left: 0; } }

/* /flash-freezing: three.js diagrams (assets/js/freezer-diagrams.js). The figure takes the diagram width; the canvas is 16:9. */
.content-frame .freezer-3d { margin-block: 1.5rem 2rem; }
.content-frame .freezer-3d canvas { display: block; width: 100%; border-radius: 10px; background: var(--surface-subtle); }
.content-frame .freezer-3d figcaption { font-family: var(--font-ui); font-size: 0.85rem; color: var(--text-nav); margin-top: 0.6rem; max-width: 600px; margin-inline: auto; text-align: center; }
.content-frame .freezer-3d-failed { display: none; }
.content-frame .freezer-3d-boot { display: none; }

/* Posts marked coming_soon: a small tag in the lists and on the placeholder page. */
/* A coming-soon post's row: set like a date, the two words taking turns changing (assets/js/coming-soon.js). */
.blog-row-soon { color: var(--text-nav); font-family: var(--font-ui); font-size: 14px; white-space: nowrap; }
@media (max-width: 639px) { .blog-row-soon { font-size: 13px; } }
.blog-row-soon { display: inline-block; }
.blog-row-soon span { display: inline-block; }
.blog-row-soon.is-soft, .blog-row-soon span.is-soft { animation: soon-soft .45s ease-out; }
.blog-row-soon.is-pull, .blog-row-soon span.is-pull { animation: soon-pull .38s cubic-bezier(.2,.9,.3,1.2); }
@keyframes soon-pull { from { opacity: 0; transform: translateY(var(--take, 6px)); } to { opacity: 1; transform: none; } }
@keyframes soon-soft { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
.soon { position: relative; margin-top: 2rem; }
/* The stand-in: blurred, and masked to a soft oval so no edge of the text block reads as a line. */
.soon-ghost { filter: blur(5px); opacity: .45; user-select: none; pointer-events: none; mask-image: radial-gradient(ellipse 72% 68% at 50% 44%, #000 30%, transparent 96%); -webkit-mask-image: radial-gradient(ellipse 72% 68% at 50% 44%, #000 30%, transparent 96%); }
.soon-ghost h2 { margin: 1.6rem 0 .6rem; font-size: 1.2em; }
.soon-card { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 1rem; }
/* Two lines, left-aligned inside a centered block, so the changing word moves nothing else. */
.soon-line { margin: 0; font-size: 1.3em; line-height: 1.5; text-align: left; color: var(--text-heading); text-shadow: 0 0 6px var(--bg), 0 0 14px var(--bg), 0 0 28px var(--bg), 0 1px 2px var(--bg); }
.soon-word { display: inline-block; font-style: italic; text-align: left; white-space: nowrap; }
.soon-word.is-in { animation: soon-ghost-up .26s ease-out; }
@keyframes soon-ghost-up { from { opacity: 0; transform: translateY(.45em); filter: blur(2px); } to { opacity: 1; transform: none; filter: none; } }
@media (prefers-reduced-motion: reduce) { .soon-word.is-in { animation: none; } }

/* Arrow-key reading (assets/js/keys.js): one caret, placed by transform from the frame's top-left, its center
   on the spot it stands over. The color settles to the link's underline tone after a landing. Hidden where there is no keyboard. */
.key-caret { position: absolute; top: 0; left: 0; z-index: 3; width: 9px; height: 6px; margin: -3px 0 0 -4.5px; pointer-events: none; color: var(--text-heading); transform-origin: 50% 50%; transition: color .5s ease; will-change: transform; }
.key-caret svg { display: block; }
a[data-key-link]:focus-visible { outline: none; }
/* unpublished, listed anyway: struck through (a coming_soon post or note) */
.blog-row.is-soon > span:first-child, .notes-list > li.is-soon > a { text-decoration: line-through; text-decoration-color: color-mix(in srgb, currentColor 55%, transparent); color: var(--text-secondary); }
a.home-arrow[data-key-link]:focus-visible { outline: 1px solid var(--text-nav); outline-offset: 4px; border-radius: 4px; }
@media (hover: none) and (pointer: coarse), (max-width: 640px) { .key-caret { display: none; } }

/* The bio to Miscellany, and Miscellany, Blog, Press: closer together than site.css's 3.5rem now that there are three sections. */
.writing-index, .writing-index > section + section { margin-top: 2.4rem; }

/* A Press row (templates/home.php): the title links out to the coverage; the right side is the press kit link and the month. */
.press-row > a:first-child, .content-frame .press-row > a:first-child:hover { color: inherit; text-decoration: none; }
.press-row-meta { color: var(--text-nav); font-family: var(--font-ui); font-size: 14px; white-space: nowrap; }
.press-row-meta a { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 45%, transparent); text-underline-offset: 3px; }
.press-row-meta a:hover { color: var(--text-copy); text-decoration-color: currentColor; }
@media (max-width: 639px) { .press-row-meta { font-size: 13px; } }

/* The fisherman on the Blog rule (assets/js/fisherman.js): a canvas laid over the section, drawn at 1x. */
.writing-index, .writing-index > section { position: relative; }
canvas.fisherman { position: absolute; left: 0; z-index: 2; pointer-events: none; image-rendering: pixelated; }
@media (max-width: 640px) { canvas.fisherman { display: none; } }
