/* Hyperliner public viewer — appearance + interactions match the
 * editor's sidebar (web/native), minus every edit affordance. The
 * tree-row anatomy below mirrors src/components/OutlineItem.svelte:
 * recursive nested <ul>, filled-triangle chevron, single-line title
 * with a muted preview line, rounded selection inversion, and an
 * indent-guide pseudo-element on every parent that has expanded
 * children. */

/* Tufte reading face. Glyphs are **XETBook** (Daniel Benjamin Miller's
 * actively-maintained ETBembo successor, SIL OFL — files + COPYING under
 * publish/public/fonts/xetbook/, served at /_/fonts/...). XETBook adds the
 * OpenType data the distributed et-book lacks: real `liga` ligatures
 * (ﬁ ﬂ ﬃ ﬄ), kerning, old-style figures, a true bold-italic. The CSS
 * family name stays `et-book` so the seeded Tufte Style (which references
 * it) upgrades in place. Mirrors src/app.css. Tufte Style only. */
@font-face {
  font-family: 'et-book';
  src: url('fonts/xetbook/XETBook-Regular.woff2') format('woff2');
  font-weight: normal; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'et-book';
  src: url('fonts/xetbook/XETBook-Italic.woff2') format('woff2');
  font-weight: normal; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'et-book';
  src: url('fonts/xetbook/XETBook-Bold.woff2') format('woff2');
  font-weight: bold; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'et-book';
  src: url('fonts/xetbook/XETBook-BoldItalic.woff2') format('woff2');
  font-weight: bold; font-style: italic; font-display: swap;
}

/* Playfair Display + Work Sans — self-hosted (latin subset, normal style;
 * italics synthesize, matching the weights the old Google css2 link served)
 * so the CSP can be font-src 'self' with no fonts.googleapis.com /
 * fonts.gstatic.com reach (hardening publish-1 / publish-2). woff2 files from
 * @fontsource, pinned in-repo under fonts/. */
@font-face {
  font-family: 'Playfair Display';
  src: url('fonts/playfair-display-latin-400-normal.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('fonts/playfair-display-latin-500-normal.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('fonts/playfair-display-latin-600-normal.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('fonts/playfair-display-latin-700-normal.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('fonts/work-sans-latin-400-normal.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('fonts/work-sans-latin-500-normal.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('fonts/work-sans-latin-600-normal.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}

/* ═══ HL-HOUSE-FONTS (generated) ═══
 * The house typeface library, mirrored from the app: woff2 under
 * fonts/<family>/ (copies of public/fonts/<family>/, licences alongside)
 * and this block generated from src/lib/fonts/house.ts via
 * fontFaceCss() — do not hand-edit between the markers.
 * test/publisher-parity/font-presets.test.ts regenerates and compares;
 * when it fails, re-run the generation (see spec/refactor/typefaces.md)
 * and re-copy any new family dir. Incumbents (Playfair, Work Sans,
 * et-book) keep their own blocks above. */
@font-face {
  font-family: 'Lexend';
  src: url('fonts/lexend/lexend-latin-wght-normal.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Lexend';
  src: url('fonts/lexend/lexend-latin-ext-wght-normal.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Lexend Peta';
  src: url('fonts/lexend-peta/lexend-peta-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Lexend Peta';
  src: url('fonts/lexend-peta/lexend-peta-latin-ext-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-ext-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-ext-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-700-normal.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-ext-700-normal.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-700-italic.woff2') format('woff2');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  src: url('fonts/atkinson-hyperlegible/atkinson-hyperlegible-latin-ext-700-italic.woff2') format('woff2');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Literata';
  src: url('fonts/literata/literata-latin-wght-normal.woff2') format('woff2');
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Literata';
  src: url('fonts/literata/literata-latin-ext-wght-normal.woff2') format('woff2');
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Literata';
  src: url('fonts/literata/literata-latin-wght-italic.woff2') format('woff2');
  font-weight: 200 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Literata';
  src: url('fonts/literata/literata-latin-ext-wght-italic.woff2') format('woff2');
  font-weight: 200 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Newsreader';
  src: url('fonts/newsreader/newsreader-latin-wght-normal.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  src: url('fonts/newsreader/newsreader-latin-ext-wght-normal.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Newsreader';
  src: url('fonts/newsreader/newsreader-latin-wght-italic.woff2') format('woff2');
  font-weight: 200 800;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  src: url('fonts/newsreader/newsreader-latin-ext-wght-italic.woff2') format('woff2');
  font-weight: 200 800;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'EB Garamond';
  src: url('fonts/eb-garamond/eb-garamond-latin-wght-normal.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'EB Garamond';
  src: url('fonts/eb-garamond/eb-garamond-latin-ext-wght-normal.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'EB Garamond';
  src: url('fonts/eb-garamond/eb-garamond-latin-wght-italic.woff2') format('woff2');
  font-weight: 400 800;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'EB Garamond';
  src: url('fonts/eb-garamond/eb-garamond-latin-ext-wght-italic.woff2') format('woff2');
  font-weight: 400 800;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-ext-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-ext-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-600-normal.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-ext-600-normal.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-600-italic.woff2') format('woff2');
  font-weight: 600;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-ext-600-italic.woff2') format('woff2');
  font-weight: 600;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-700-normal.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Spectral';
  src: url('fonts/spectral/spectral-latin-ext-700-normal.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Fraunces';
  src: url('fonts/fraunces/fraunces-latin-wght-normal.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Fraunces';
  src: url('fonts/fraunces/fraunces-latin-ext-wght-normal.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Fraunces';
  src: url('fonts/fraunces/fraunces-latin-wght-italic.woff2') format('woff2');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Fraunces';
  src: url('fonts/fraunces/fraunces-latin-ext-wght-italic.woff2') format('woff2');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Smith-Waite Titling';
  src: url('fonts/smith-waite-titling/SmithWaiteTitling.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Fragment Mono';
  src: url('fonts/fragment-mono/fragment-mono-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Fragment Mono';
  src: url('fonts/fragment-mono/fragment-mono-latin-ext-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Fragment Mono';
  src: url('fonts/fragment-mono/fragment-mono-latin-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Fragment Mono';
  src: url('fonts/fragment-mono/fragment-mono-latin-ext-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant';
  src: url('fonts/cormorant/cormorant-latin-wght-normal.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cormorant';
  src: url('fonts/cormorant/cormorant-latin-ext-wght-normal.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant';
  src: url('fonts/cormorant/cormorant-latin-wght-italic.woff2') format('woff2');
  font-weight: 300 700;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cormorant';
  src: url('fonts/cormorant/cormorant-latin-ext-wght-italic.woff2') format('woff2');
  font-weight: 300 700;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant SC';
  src: url('fonts/cormorant-sc/cormorant-sc-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cormorant SC';
  src: url('fonts/cormorant-sc/cormorant-sc-latin-ext-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant SC';
  src: url('fonts/cormorant-sc/cormorant-sc-latin-500-normal.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cormorant SC';
  src: url('fonts/cormorant-sc/cormorant-sc-latin-ext-500-normal.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Barlow Condensed';
  src: url('fonts/barlow-condensed/barlow-condensed-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Barlow Condensed';
  src: url('fonts/barlow-condensed/barlow-condensed-latin-ext-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Barlow Condensed';
  src: url('fonts/barlow-condensed/barlow-condensed-latin-500-normal.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Barlow Condensed';
  src: url('fonts/barlow-condensed/barlow-condensed-latin-ext-500-normal.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
/* ═══ /HL-HOUSE-FONTS ═══ */

:root {
  --sc-bg: #f5f5f0;
  --sc-row-bg: #ffffff;
  --sc-card-body-bg: #f4f4ed;
  --sc-text: #1a1a1a;
  --sc-muted: #6b6b6b;
  --sc-muted-strong: #4a4a4a;
  --sc-border: #e5e5e0;
  --sc-border-soft: #ececea;
  --sc-guide: rgba(0, 0, 0, 0.35);

  --sc-heading-font: 'Playfair Display', Georgia, 'Times New Roman', Times, serif;
  --sc-body-font: 'Work Sans', -apple-system, BlinkMacSystemFont, 'SF Pro', Helvetica, Arial, sans-serif;

  /* Filled by viewer.js from snapshot.palette. Default = blue. */
  --sc-palette-bg: #0875B9;
  --sc-palette-ink: #FFFFFF;
  /* Translucent palette tints for selection/accent surfaces. Set
   * alongside --sc-palette-bg in viewer.js so they stay in sync. */
  --sc-palette-tint: rgba(8, 117, 185, 0.10);
  --sc-palette-tint-strong: rgba(8, 117, 185, 0.18);

  --sb-width: 320px;

  /* Indent + disclosure measurements — match the editor's Outline
   * sidebar so the indent-guide column aligns the same way. The
   * editor uses a 32 px disclosure zone with chevron at 16 px (per
   * src/sidebar/styles.css) and renders its indent
   * lines at `depth * --indent + 16 - 0.5` px; we keep the
   * publisher's lighter 18 px chevron column and reconstruct the
   * same relationship from --row-pad-x + --toggle-w. */
  --indent: 24px;
  --row-pad-x: 0.55rem;
  --toggle-w: 18px;
}

/* ── The published page after dark ──────────────────────────────────────
   Set by viewer.js from the reading Style's `appearance` (applyStyle), so
   a visitor choosing Dark Hyperliner or Dark Tufte from the style menu
   takes the whole page with them, not just the prose colour.

   THE PUBLISHER HAS NO THEME AXIS, and this is not one arriving. A
   published page is the reading surface end to end — its header, outline
   nav and footer exist to serve the body, and there is no card frame to
   draw a seam round. So the one signal the Style already carries governs
   the page, where in the app the same attribute governs only what is
   inside a card (spec/refactor/themes.md § 1a).

   Nine tokens, and no more: this is the publisher's own ground, re-stated
   for a dark page, not the app's --sc-* vocabulary arriving here. Values
   are the card-side reading of the app's Tokyo Night ground so the two
   surfaces recognise each other. */
:root[data-rs-appearance='dark'] {
  color-scheme: dark;
  --sc-bg: #14120e;
  --sc-row-bg: #1c1a14;
  --sc-card-body-bg: #1c1a14;
  --sc-text: #f2efe6;
  --sc-muted: #9d9789;
  --sc-muted-strong: #cbc5b6;
  --sc-border: #33302a;
  --sc-border-soft: #262320;
  --sc-guide: rgba(255, 255, 255, 0.3);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--sc-bg);
  color: var(--sc-text);
  font-family: var(--sc-body-font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

#app { min-height: 100vh; }

/* ─── App layout ──────────────────────────────────────────────────────── */

.app {
  display: grid;
  /* `minmax(0, 1fr)` is load-bearing: it lets the grid track shrink
   * below its content's min-content. Without it, a single very long
   * unbreakable token in the tree (e.g. a `HYPERLINER_REFERENCE_V1:…`
   * preview line with `white-space: nowrap`) expands the track —
   * `overflow: hidden` clips paint but doesn't constrain layout —
   * and on a 375 px viewport the sidebar can end up ~1500 px wide. */
  grid-template-columns: var(--sb-width) minmax(0, 1fr);
  height: 100vh;
}

/* The two states that replace the whole viewer and draw no sidebar — the
 * holding page and the audience door. Without this they render their single
 * .body-pane into the SIDEBAR's track and the message reads in a ~360 px
 * column against an empty screen. */
.app.app-solo {
  grid-template-columns: minmax(0, 1fr);
}

@media (max-width: 720px) {
  .app { grid-template-columns: minmax(0, 1fr); }
  .body-pane { display: none; }
  .app.has-selection .body-pane { display: block; }
  /* A solo state's body pane is the whole page, and there is no selection to
     make: without this the holding page and the door render as a blank screen
     on a phone, which is what they did until 2026-08-11. */
  .app.app-solo .body-pane { display: block; }
  .app.has-selection .sidebar { display: none; }
  /* The tree pane is a whole view, not a thing below the sidebar: it
   * takes the screen the way the body pane does, and carries its own
   * back bar (the sidebar's outline title, which opened it, is now
   * off-screen). */
  .app.showing-tree .sidebar { display: none; }
  .app.showing-tree .spatial-header { display: block; }
}

.loading,
.error {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  color: var(--sc-muted);
  font-size: 14px;
}
.error { color: #b91c1c; }

/* ─── Sidebar ─────────────────────────────────────────────────────────── */
/* Two-row flex column so the live-status footer stays pinned to the
 * bottom of the sidebar regardless of how far the tree scrolls. */

.sidebar {
  background: var(--sc-bg);
  border-right: 1px solid var(--sc-border);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Pair with the parent grid's `minmax(0, …)` so the sidebar can't
   * be pushed wider than its track by a long unbreakable token in
   * the tree. */
  min-width: 0;
}

.sidebar-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 22px 14px 16px;
}


/* Outline title row — styled as Hyperliner's outline-row pill: a
 * rounded-rect band filled with the outline's palette colour, ink
 * coloured to match. No chevron toggle, no info button, no drag
 * handle. When the snapshot carries a baked tree it is a <button>
 * (the way into the tree view) and otherwise a <span>; both wear
 * the same pill, so the rule is written once for either element. */
.sb-outline-heading { margin: 0; }

.sb-outline-row {
  display: flex;
  align-items: center;
  width: 100%;
  border: 0;
  font: inherit;
  text-align: left;
  background: var(--sc-palette-bg);
  color: var(--sc-palette-ink);
  border-radius: 8px;
  padding: 9px 14px;
  margin: 2px 0 16px 0;
}

/* Active = black outline, the house rule: the halo says "this pill is
 * what the right-hand pane is showing". Hover is the pill's own colour
 * bled outward, which reads on the page's ground without competing
 * with the selection inversion further down the tree. */
button.sb-outline-row {
  cursor: pointer;
  transition: box-shadow 0.12s;
}
button.sb-outline-row:hover {
  box-shadow: 0 0 0 3px var(--sc-palette-tint);
}
button.sb-outline-row[aria-pressed='true'] {
  box-shadow: 0 0 0 2px var(--sc-text);
}

/* The author's site icon, inside the title pill. Sized to the pill's own
 * text rather than to a nominal icon size, so it sits on the cap-height of
 * the title beside it and doesn't grow the pill. Rounded on the platforms'
 * own terms; the hairline is what stops a white-ground mark dissolving into
 * a pale palette. */
.sb-site-icon {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-right: 9px;
  border-radius: 4px;
  object-fit: cover;
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.12);
}

.sb-outline-title {
  font-family: var(--sc-body-font);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
  flex: 1;
}

/* ─── Sidebar footer (live indicator) ─────────────────────────────────── */

.sidebar-footer {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px 14px;
  border-top: 1px solid var(--sc-border-soft);
  background: var(--sc-bg);
  font-size: 12px;
  color: var(--sc-muted);
  letter-spacing: 0.01em;
}

.sidebar-footer .live-dot {
  flex: 0 0 auto;
}

.sidebar-footer .live-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The one Hyperliner on someone else's page. Inherits the footer's muted
 * size and colour — it is a credit, not a call to action — and only earns
 * the page's ink on hover, which is enough to say it is a link without
 * making the footer a second thing to read.
 *
 * The underline is mixed from `currentColor`, not from a border token: the
 * first cut used --sc-border-soft, which resolved to #ececea against the
 * #f4f5f0 footer and was invisible on the page. Subtle is the brief, but a
 * link nobody can see is not a subtle link. Mixing from the inherited colour
 * also survives every reading style the visitor can pick, dark ones included,
 * which a fixed token would not. */
.sidebar-footer .live-brand {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-underline-offset: 2px;
}

.sidebar-footer .live-brand:hover {
  color: var(--sc-text);
  text-decoration-color: currentColor;
}

/* ─── Tree (recursive) ────────────────────────────────────────────────── */

.outline-root,
.children {
  list-style: none;
  margin: 0;
  padding: 0;
}

.children {
  /* One indent step per nesting level. Matches the editor's
   * `--indent` (24 px). Increases the horizontal gap between a
   * parent's indent-guide line and its children's chevrons. */
  padding-left: var(--indent);
}

.outline-item {
  position: relative;
  list-style: none;
}

/* Active-subtree shading: when an ancestor of this item is selected,
 * the row paints a subtle warm-grey across the full width past the
 * indent gutter. The web build uses a `::before` painted from
 * `-1.25rem` to the right edge for the same effect; we do the same
 * but at 55% opacity per the spec's "subtle" rule. */
.outline-item.in-active-subtree::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -1.25rem;
  right: 0;
  background: var(--sc-card-body-bg);
  opacity: 0.55;
  z-index: 0;
  pointer-events: none;
}

/* Indent guide — a thin vertical line drawn on every parent that has
 * its disclosure open, starting just under the parent's chevron and
 * descending through all its children. The line's x lives at the
 * chevron's mid-point (row padding-left + half the toggle column),
 * minus 0.5 px to centre the 1 px-wide line on that mid-point.
 * Mirrors the editor's `seg.x - 0.5` formula in
 * src/sidebar/interactions/outline-backdrop.ts. */
.outline-item.has-expanded-children::after {
  content: '';
  position: absolute;
  left: calc(var(--row-pad-x) + var(--toggle-w) / 2 - 0.5px);
  top: 1.75rem;
  bottom: 0;
  width: 1px;
  background: var(--sc-guide);
  z-index: 2;
  pointer-events: none;
}

/* ─── Tree row ────────────────────────────────────────────────────────── */

.row {
  display: flex;
  align-items: flex-start;
  gap: 0.45rem;
  padding: 0.35rem var(--row-pad-x);
  cursor: pointer;
  border-radius: 8px;
  position: relative;
  background: transparent;
  z-index: 1;
  transition: background-color 0.12s ease;
}

@media (hover: hover) {
  .row:hover {
    background: rgba(0, 0, 0, 0.045);
  }
}

/* Descendant rows of the selected node share a transparent background
 * — the `.outline-item.in-active-subtree::before` overlay is what
 * paints the warm-grey. */
.row.in-active-subtree {
  background: transparent;
  border-radius: 0;
}

@media (hover: hover) {
  .row.in-active-subtree:hover {
    background: rgba(0, 0, 0, 0.04);
  }
}

/* Selected row picks up the outline's palette colour — selection
 * inherits the outline's identity rather than reverting to a flat
 * black inversion. The palette pill at the top of the sidebar and
 * the selected row now read as the same visual material. */
.row.selected {
  background: var(--sc-palette-bg);
  color: var(--sc-palette-ink);
  border-radius: 8px;
}

.row.selected .title { color: var(--sc-palette-ink); font-weight: 600; }
.row.selected .preview { color: var(--sc-palette-ink); opacity: 0.78; }
.row.selected .toggle { color: var(--sc-palette-ink); opacity: 1; }

/* ─── Toggle (chevron) ────────────────────────────────────────────────── */

.toggle {
  width: var(--toggle-w);
  flex: 0 0 var(--toggle-w);
  height: calc(0.9rem * 1.35);
  margin-top: 1px;
  border: none;
  background: none;
  padding: 0;
  color: var(--sc-text);
  cursor: pointer;
  /* Web reference uses 0.3 here, but the user wants the disclosure
     indicators to read as deliberate black glyphs — full opacity. */
  opacity: 1;
  user-select: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.toggle.leaf {
  cursor: default;
  opacity: 0.3;
  font-weight: 700;
}

.toggle svg {
  display: block;
}

/* ─── Title + preview ─────────────────────────────────────────────────── */

.text {
  flex: 1;
  min-width: 0;
  padding-top: 1px;
}

.title {
  font-family: var(--sc-body-font);
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--sc-text);
  line-height: 1.35;
  /* Wrap long card titles across multiple lines rather than
   * truncating with an ellipsis. `break-word` keeps normal word
   * boundaries and only breaks inside a word as a last resort —
   * `anywhere` is too eager and splits short words mid-character. */
  overflow-wrap: break-word;
}

.title-ref-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-right: 0.3em;
  vertical-align: middle;
  opacity: 0.65;
}

.preview {
  font-family: var(--sc-body-font);
  font-size: 0.75rem;
  color: var(--sc-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}

/* Inline marks rendered into `.preview` by `marked.parseInline`.
 * Inherit colour + size; bold/italic just shape the run, code
 * gets the editor's translucent-black pill, links don't get the
 * default blue (they should read as muted prose, not as
 * actionable). The visitor's tap on the row selects the card —
 * an actual `<a href>` would steal the click target. */
.preview strong { font-weight: 600; }
.preview em     { font-style: italic; }
/* A leading body image (`![](url)`) gets rendered into the preview line
 * by marked.parseInline. Clamp it to a small inline thumbnail — without
 * this the source image paints at full natural size and blows the row
 * open. Square-ish, ride the baseline next to the preview text. */
.preview img {
  height: 22px;
  width: auto;
  max-width: 36px;
  object-fit: cover;
  border-radius: 3px;
  vertical-align: middle;
  margin-right: 5px;
}
.preview code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.95em;
  background: rgba(0, 0, 0, 0.06);
  padding: 0 0.25em;
  border-radius: 2px;
}
.preview a {
  color: inherit;
  text-decoration: none;
  pointer-events: none;
}

.row.selected .preview code {
  background: rgba(255, 255, 255, 0.22);
  color: var(--sc-palette-ink);
}

/* Card links ([[uuid]] / [[uuid|label]]) in card bodies — palette-tinted,
   clickable to the target. In preview lines `.preview a` already sets
   pointer-events:none, so there they read as plain text and the row's
   click selects it. An unresolved target (private / Draft / deleted)
   renders muted + non-clickable so it neither 404s nor leaks a target. */
/* Card-link — same-size accent text on a manila rounded-rect (the manila
   paints via `background`, so it never grows the line box). Matches the editor
   chip; accent = the outline palette, harmonised with citations + plain links. */
.body-card .body-content a.card-link {
  color: var(--sc-palette-bg, #0875b9);
  background: var(--card-link-bg, #efe7d2);
  text-decoration: none;
  border-radius: 5px;
  padding: 0.1em 0.5em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  cursor: var(--rs-card-link-cursor, pointer);
  transition: background 0.12s;
}
.body-card .body-content a.card-link:hover {
  background: var(--card-link-bg-hover, #e6d9ba);
}
/* The manila chip after dark. Its fallback is the value in force — no
   Style has ever set --card-link-bg — so without this a cream rect sits
   lit on every dark page. Same paper, aged down; mirrors the editor. */
:root[data-rs-appearance='dark'] .body-card .body-content a.card-link {
  background: var(--card-link-bg, #3a3324);
}
:root[data-rs-appearance='dark'] .body-card .body-content a.card-link:hover {
  background: var(--card-link-bg-hover, #473e2b);
}
.body-card .body-content .card-link-unresolved {
  color: var(--sc-muted, #6b6b6b);
  border-bottom: 1px dotted var(--sc-muted, #6b6b6b);
  cursor: default;
}

/* Citations (cite-4): baked author-year reads as prose; the live-wire signal is
   a Tufte-style descender-aware underline UNDER THE INNER ITEMS ONLY (brackets
   and `;` stay plain). Rest = body ink; :hover lifts to the palette accent, the
   rule following via currentColor. Unmapped tokens never reach here. */
.body-card .body-content .citation {
  white-space: nowrap;
  --cite-uline-bg: var(--sc-bg, #f5f5f0);
}
.body-card .body-content a.citation-link {
  color: inherit;
  text-decoration: none;
  cursor: var(--rs-card-link-cursor, pointer);
  background:
    linear-gradient(var(--cite-uline-bg), var(--cite-uline-bg)),
    linear-gradient(var(--cite-uline-bg), var(--cite-uline-bg)),
    linear-gradient(currentColor, currentColor);
  background-size: 0.06em 1px, 0.06em 1px, 1px 1px;
  background-repeat: no-repeat, no-repeat, repeat-x;
  background-position: 0 0.92em, 100% 0.92em, 0 0.92em;
  text-shadow:
    0.03em 0 var(--cite-uline-bg), -0.03em 0 var(--cite-uline-bg),
    0.06em 0 var(--cite-uline-bg), -0.06em 0 var(--cite-uline-bg),
    0.09em 0 var(--cite-uline-bg), -0.09em 0 var(--cite-uline-bg),
    0.12em 0 var(--cite-uline-bg), -0.12em 0 var(--cite-uline-bg),
    0.15em 0 var(--cite-uline-bg), -0.15em 0 var(--cite-uline-bg);
}
.body-card .body-content a.citation-link:hover {
  color: var(--sc-palette-bg, #0875b9);
}

/* Footnote citations (composed-cards U4): under citation_style: footnote a
   token is a superscript number and the references collect at the foot of
   the body — both derived at render, markers CLASSED never styled inline
   (the sanitize pass strips style attrs). Body ink at rest, accent on
   hover; no underline machinery on a one-glyph marker. Zero new colour
   literals — the ratchet's inherit rule. */
.body-card .body-content .citation-footnote {
  font-size: 0.72em;
  line-height: 0;
}
.body-card .body-content .citation-footnote a.citation-footnote-marker {
  color: inherit;
  text-decoration: none;
  cursor: var(--rs-card-link-cursor, pointer);
}
.body-card .body-content .citation-footnote a.citation-footnote-marker:hover {
  color: var(--sc-palette-bg, #0875b9);
}
.body-card .body-content ol.citation-footnotes {
  margin: 1.6em 0 0;
  padding: 0.7em 0 0 1.8em;
  border-top: 1px solid var(--sc-muted, currentColor);
  font-size: 0.82em;
  color: var(--sc-muted, inherit);
}
.body-card .body-content ol.citation-footnotes .citation-footnote-entry {
  margin: 0.2em 0;
}
/* The entry's link reads as a reference, not a Tufte-underlined inline cite:
   plain ink, accent on hover (the underline knockout assumes body-run text). */
.body-card .body-content ol.citation-footnotes a.citation-link {
  background: none;
  text-shadow: none;
}

/* ─── Body pane ───────────────────────────────────────────────────────── */

.body-pane {
  padding: 56px clamp(28px, 5vw, 64px) 72px;
  background: var(--sc-bg);
  /* ── The card's background wash (card-background.js) ──────────────────
   * The published page has no card frame — this pane IS the card's paper —
   * so the `background` dressing paints here: the image blended into the
   * pane's own ground per --card-bg-blend, with a veil of that ground
   * mixed over it for the opacity control. The pane is the scroll
   * container, so the wash holds the viewport while the prose scrolls. */
  /* Both axes — the one real scroller, mirroring the editor pane (the app's
     card-types.md § The grid viewport): a wide table widens its card and the
     pane scrolls to it natively. One scroller means no wheel remap, sticky
     headers with zero JS, and compositor-threaded scrolling. */
  overflow: auto;
  min-height: 0;
}
.body-pane.has-card-bg {
  background-color: var(--sc-bg);
  background-image:
    linear-gradient(color-mix(in srgb, var(--sc-bg) var(--card-bg-veil, 65%), transparent) 0 0),
    var(--card-bg-image, none);
  background-size: cover;
  background-position: center, var(--card-bg-pos, center);
  background-repeat: no-repeat;
  background-blend-mode: normal, normal;
}
/* The wash's transfer mode, enumerated off the data-bg-blend attribute so
 * the dark pairing can remap it (a selector cannot read a var's value). */
.body-pane.has-card-bg[data-bg-blend='multiply'] { background-blend-mode: normal, multiply; }
.body-pane.has-card-bg[data-bg-blend='screen'] { background-blend-mode: normal, screen; }
.body-pane.has-card-bg[data-bg-blend='overlay'] { background-blend-mode: normal, overlay; }
.body-pane.has-card-bg[data-bg-blend='soft-light'] { background-blend-mode: normal, soft-light; }
.body-pane.has-card-bg[data-bg-blend='luminosity'] { background-blend-mode: normal, luminosity; }
:root[data-rs-appearance='dark'] .body-pane.has-card-bg[data-bg-blend='multiply'] { background-blend-mode: normal, screen; }
:root[data-rs-appearance='dark'] .body-pane.has-card-bg[data-bg-blend='screen'] { background-blend-mode: normal, multiply; }

.body-header {
  max-width: 720px;
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-bottom: 28px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--sc-border);
  /* What the title inherits when --rs-title-case names a value one of its
     two properties cannot parse — see the .body-title rule below. Mirrors
     .title-row in the editor's CardEditor.svelte. */
  text-transform: none;
  font-variant-caps: normal;
}

/* The card title shares the header row with the Back button + view
 * toggle. flex: 1 lets it claim all the space the controls don't
 * use; ellipsis stops a long title from pushing the toggle off the
 * right edge. The bottom-border separator now lives on .body-header
 * (above), not on the title itself. */
.body-header .body-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
  border-bottom: none;
  font-family: var(--sc-heading-font);
  /* The published headline is bigger than the editor's and fluid with the
   * viewport, so `title-size` falls back to THIS surface's value, not to a
   * shared one — same per-surface rule the h1–h4 sizes follow. A Style that
   * names a size gives up the clamp, which is the trade it is asking for. */
  font-size: var(--rs-title-size, clamp(28px, 3.6vw, 42px));
  font-weight: var(--rs-title-weight, var(--rs-heading-weight, 600));
  font-style: var(--rs-title-style, var(--rs-heading-style, normal));
  text-transform: var(--rs-title-case, var(--rs-heading-case, none));
  font-variant-caps: var(--rs-title-case, var(--rs-heading-case, normal));
  /* normal tracking so the title's ligatures (fi/ffl) render — see the
   * note on the body-heading rule. */
  letter-spacing: var(--rs-title-tracking, normal);
  /* Wrap long headlines onto multiple lines. Slightly looser than
   * the single-line value (1.08) so stacked lines don't crowd. */
  line-height: var(--rs-title-line-height, 1.15);
  overflow-wrap: break-word;
}

.back-btn {
  display: none;
  border: 1px solid var(--sc-border);
  background: transparent;
  color: var(--sc-text);
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-family: var(--sc-body-font);
  font-size: 13px;
}
@media (max-width: 720px) {
  .app.has-selection .back-btn { display: inline-block; }

  /* On mobile the title gets its own full-width row below Back +
   * view-toggle. Without this the title flexes down to ~150px and
   * wraps one short word per line. */
  .body-header {
    flex-wrap: wrap;
    row-gap: 14px;
  }
  .body-header .body-title {
    flex: 1 0 100%;
    order: 2;
  }
  .view-controls { margin-left: auto; }
}

/* Inline control cluster at the trailing edge of .body-header — just
 * the [Aa style ▾] menu now that the body has one view rather than
 * two. `align-self: flex-start` overrides the header's baseline so the
 * cluster hangs from the top of the title row. */
.view-controls {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  align-self: flex-start;
}

/* ── Reading style menu (visitor-facing) ──────────────────────────────
 * Small icon button alongside the title; opens a list of styles to
 * read in (no variable editor — that's editor-only). The visitor's
 * choice is per-device (localStorage); the author's active style is
 * the default. */
.style-menu-pub { position: relative; flex: 0 0 auto; }
.style-pub-trigger {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid var(--sc-border);
  background: var(--sc-row-bg);
  color: var(--sc-muted);
  border-radius: 999px;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  font-family: var(--sc-body-font);
  font-size: 0.7rem;
  line-height: 1;
}
.style-pub-trigger:hover { background: var(--sc-text); color: #fff; border-color: var(--sc-text); }
.style-pub-trigger svg { display: block; }
.style-pub-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 60;
  min-width: 11rem;
  background: #fff;
  border: 1px solid var(--sc-border);
  border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
  padding: 0.3rem;
}
.style-pub-item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  border: none;
  background: none;
  border-radius: 5px;
  padding: 0.4rem 0.45rem;
  font-family: var(--sc-body-font);
  font-size: 0.82rem;
  color: var(--sc-text);
  cursor: pointer;
  text-align: left;
}
.style-pub-item:hover { background: rgba(0, 0, 0, 0.05); }
.style-pub-item.active { font-weight: 600; }
.style-pub-tick { flex: 0 0 14px; display: inline-flex; justify-content: center; color: var(--sc-palette-ink, #1a5fb4); }

/* ── Reading-surface Style variables on the body view ─────────────────
 * The active/visitor-chosen style sets --rs-* on :root (applyStyle in
 * viewer.js); the body reading surface consumes them with fallbacks to
 * the existing --sc-* / hardcoded values so an unstyled snapshot is
 * unchanged. */
.body-card { max-width: var(--rs-measure, 720px); }
.body-card .body-content {
  font-family: var(--rs-font-body, var(--sc-body-font));
  font-size: var(--rs-font-size, 1rem);
  line-height: var(--rs-line-height, 1.7);
  color: var(--rs-text, var(--sc-text));
  /* What a heading inherits when --rs-heading-case names a value one of its
     two properties cannot parse — the .body-header guard, for prose. */
  text-transform: none;
  font-variant-caps: normal;
}
.body-card .body-content p {
  text-indent: var(--rs-paragraph-indent, 0);
  margin: 0 0 var(--rs-paragraph-spacing, 1em);
}
.body-card .body-content :is(h1, h2, h3, h4) + p { text-indent: 0; }
.body-card .body-content a {
  color: var(--rs-link, var(--sc-palette-bg, inherit));
  text-decoration: var(--rs-link-decoration, underline);
}
.body-card .body-content blockquote {
  border-left: 3px solid var(--rs-border, var(--sc-border));
  color: var(--rs-muted, var(--sc-muted));
}
/* Tables (GFM) — mirrors the editor's .ProseMirror table rules in
 * CardEditor.svelte; keep the two in step. marked emits align attrs on
 * th/td, which browsers honour natively. Wide tables scroll in place
 * (display:block) instead of breaking the measure. */
.body-card .body-content table {
  display: block;
  overflow-x: auto;
  border-collapse: collapse;
  width: 100%;
  font-size: 0.95em;
  margin: 0.75em 0;
}
.body-card .body-content th,
.body-card .body-content td {
  border: 1px solid var(--rs-border, var(--sc-border));
  padding: 0.3rem 0.55rem;
  vertical-align: top;
  min-width: 3rem;
}
.body-card .body-content th {
  font-weight: 600;
  border-bottom-width: 2px;
  background: color-mix(in srgb, var(--rs-border, var(--sc-border)) 18%, transparent);
}
/* Shared content-class vocabulary (mirrors the editor's). */
.body-content .newthought {
  font-variant: var(--rs-newthought-variant, small-caps);
  font-size: var(--rs-newthought-size, 1.1em);
  font-weight: var(--rs-newthought-weight, inherit);
  letter-spacing: var(--rs-newthought-letter-spacing, 0.03em);
}
/* Optional sigil prefixed to a note (empty by default). The var is
   emitted pre-quoted by viewer.js applyStyle. */
.body-content .sidenote::before { content: var(--rs-marginnote-sigil, ''); }
.body-content .marginnote::before { content: var(--rs-marginnote-sigil, ''); }
.body-content .epigraph { margin: 1em 0 1.5em; font-style: italic; color: var(--rs-muted, var(--sc-muted)); }
/* Spans text column + margin column + gutter — the margin-note negative-
 * margin arithmetic in reverse (same var, same fallback, same right edge).
 * `width: 100%` was a no-op: the measure is a max-width on .body-card,
 * and a child's 100% cannot break past its containing block. Mirrors the
 * editor face (src/editor/reading-surface.css). */
.body-content .fullwidth {
  max-width: none;
  width: calc(100% + var(--rs-marginnote-width, 16rem) + 1.5rem);
}
.body-content :is(.marginnote, .sidenote) {
  display: block;
  font-size: var(--rs-marginnote-font-size, 0.85rem);
  color: var(--rs-marginnote-color, var(--sc-muted));
  line-height: 1.4;
}
/* The unnumbered margin note sits in the margin under every Style —
   it has no marker to pair with and so no foot to fall to (notes.md).
   Mirrors src/editor/reading-surface.css. */
.body-content :is(.marginnote, .sidenote) {
  float: right;
  clear: right;
  width: var(--rs-marginnote-width, 16rem);
  border-left: none;
  padding-left: 0;
  margin: 0 calc(-1 * var(--rs-marginnote-width, 16rem) - 1.5rem) 0.6rem 0;
}

/* Inline detail-map figure (data-detectors §9) — mirrors the editor's block in
 * src/editor/reading-surface.css. Square at the margin note's own width so it
 * drops into the margin column exactly like the note it sits inside, and the
 * caption keeps the coordinate, because a map with no numbers on it is not a
 * citation. Before mount — and wherever the chart has no cover — the element
 * is simply its fallback text, which is the datum itself. */
.body-content .detail-map {
  display: inline-block;
  width: var(--rs-marginnote-width, 16rem);
  max-width: 100%;
  vertical-align: top;
}
.body-content .detail-map-svg {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--sc-border, #ccc);
  border-radius: 3px;
}
.body-content .detail-map-sea {
  fill: var(--sc-card-body-bg, #f4f4ed);
}
.body-content .detail-map-grat {
  stroke: var(--sc-border, #e5e5e0);
  stroke-width: 1.5;
}
.body-content .detail-map-land {
  fill: var(--sc-border, #e5e5e0);
  stroke: var(--sc-muted, #6b6b6b);
  stroke-width: 1.5;
}
.body-content .detail-map-cross {
  /* The published surface has no --pal-gold (its tokens are the --sc-* set),
   * so the literal IS the material here, not a missing token. Same gold as
   * src/app.css --pal-gold — the editor and the page must mark our coordinate
   * in the same colour. */
  stroke: #cb8a08;
  stroke-width: 6;
}
.body-content .detail-map-caption {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem;
  color: var(--sc-muted, #777);
  margin-top: 0.2rem;
}

/* The card title takes the active Style's Card title group, each knob
 * falling back to its Headings counterpart so a Style that has set none of
 * them reads as part of the styled surface exactly as before (e.g. et-book
 * under Tufte). Mirrors `.title-input` in src/editor/CardEditor.svelte —
 * test/publisher-parity/card-title.test.ts holds the two in step.
 *
 * Weight and slope arrived with the Card title group and are a CHANGE to
 * published pages, not a no-op: this headline hardcoded `font-weight: 600`
 * while the editor's title had read `--rs-heading-weight` since the Style
 * system shipped, so Hyperliner published at semibold what it edited at
 * regular. The intent above ("takes the active Style's heading font + ink")
 * was always for the two to agree; weight and slope were simply missed. */
.body-header .body-title {
  font-family: var(--rs-font-title, var(--rs-font-heading, var(--sc-heading-font)));
  color: var(--rs-title-color, var(--rs-heading-color, inherit));
  font-variant-ligatures: common-ligatures var(--rs-heading-ligatures, discretionary-ligatures);
}

/* ── Tufte family layout ───────────────────────────────────────────────
 * Structural shape that the flat --rs-* vars can't express. Keyed on
 * data-rs-family="tufte" (set by applyStyle). Mirrors tufte-css. */
/* Tufte's roman title at its own scale — each value the FALLBACK of the
   matching Card title knob, never a literal: this selector outranks
   `.body-header .body-title`, so hardcoding it would leave the knobs live
   under Hyperliner and dead under Tufte. Mirrors the same rule in
   src/editor/reading-surface.css. */
[data-rs-family='tufte'] .body-title {
  font-weight: var(--rs-title-weight, 400);
  font-style: var(--rs-title-style, normal);
  letter-spacing: var(--rs-title-tracking, normal);
  font-size: var(--rs-title-size, clamp(2.6rem, 4vw, 3.4rem));
}
/* Headings: Tufte scale. Weight (400) + italic come from --rs-* via the
 * canonical heading rule; these rules only adjust the per-heading size,
 * leading, and letter-spacing. The `.body-card` qualifier raises
 * specificity above the canonical size rules (.body-card .body-content
 * h2 { font-size: 23px }) further down this file, which would otherwise
 * win on source order. */
[data-rs-family='tufte'] .body-card .body-content :is(h1, h2, h3, h4) {
  letter-spacing: normal;
  line-height: 1.1;
}
[data-rs-family='tufte'] .body-card .body-content h1 { font-size: var(--rs-h1-size, 2.2rem); margin: 2.1rem 0 1.4rem; }
[data-rs-family='tufte'] .body-card .body-content h2 { font-size: var(--rs-h2-size, 1.7rem); margin: 2.1rem 0 1.4rem; }
[data-rs-family='tufte'] .body-card .body-content h3 { font-size: var(--rs-h3-size, 1.4rem); margin: 2rem 0 1.4rem; }
/* First paragraph after a heading / the lede has no top margin so the
 * heading sits tight to its text. */
[data-rs-family='tufte'] .body-card .body-content :is(h1, h2, h3, h4) + p { margin-top: 0.4rem; }
/* Blockquote: no rule, slightly narrower, right-aligned attribution
 * (a trailing cite/footer or the last short line). The `.body-card`
 * qualifier raises specificity above the default blockquote rule that
 * paints a palette-tinted left border further down this file. */
[data-rs-family='tufte'] .body-card .body-content blockquote {
  border-left: none;
  /* Tufte indents the quotation rather than ruling it. */
  padding: 0 0 0 2.5rem;
  margin: 1.4rem 0;
  font-style: normal;
  color: inherit;
}
[data-rs-family='tufte'] .body-card .body-content blockquote :is(footer, cite) {
  display: block;
  text-align: right;
  font-size: 1.1rem;
  font-style: italic;
}
/* Code stays a touch smaller than the larger Tufte body. */
[data-rs-family='tufte'] .body-content :is(code, pre) { font-size: 1rem; }
/* newthought lede — small caps. */
[data-rs-family='tufte'] .body-content .newthought { font-size: var(--rs-newthought-size, 1.2em); }

.body-empty {
  color: var(--sc-muted);
  font-size: 14px;
  margin-top: 32px;
}

.body-card { max-width: 720px; }

/* ─── Reading on into the subtree ─────────────────────────────────────
 * Each card unrolled by "Load more" is its own <article> under a
 * hairline, so the page reads as a sequence of cards rather than one
 * undifferentiated column — the seam is the fact that you crossed from
 * one card to the next. The rule is the same 1px --sc-border the body
 * header hangs under, at the reading measure, not the pane's width. */
.body-card-more {
  margin-top: 44px;
  padding-top: 40px;
  border-top: 1px solid var(--sc-border);
}
/* The section's own H2 opens the card, so it needs no lead-in space
 * of its own on top of the padding above. */
.body-card-more .body-content > :first-child { margin-top: 0; }

/* Foot-of-page control: keeps the reading measure and centres in it,
 * so it sits under the prose rather than out at the pane's edge. */
.load-more {
  max-width: 720px;
  display: flex;
  justify-content: center;
  margin: 48px 0 8px;
}

.load-more-btn {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  font-family: var(--sc-body-font);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--sc-text);
  background: var(--sc-row-bg, #fff);
  border: 1px solid var(--sc-border);
  border-radius: 999px;
  padding: 9px 22px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.load-more-btn:hover {
  background: var(--sc-text);
  color: #fff;
  border-color: var(--sc-text);
}
/* How far through the subtree this click takes you. Dimmed against the
 * label, and it has to survive the inverted hover, so the muted token
 * gives way to a plain alpha. */
.load-more-count {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  opacity: 0.62;
}

/* Nothing left to load. A quiet full stop rather than a disabled
 * button, which would look like a control that had failed. */
.load-more-end {
  font-family: var(--sc-body-font);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--sc-muted);
}

.body-card .body-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 12px 0 0 0;
}

.body-card .body-tag {
  background: var(--sc-palette-tint);
  color: var(--sc-palette-bg);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
}

/* Typed fields (card-as-typed-object) — a definition list above the body. */
.body-card .card-fields {
  margin: 16px 0 0 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 16px;
  font-size: 14px;
}
.body-card .card-fields .field-item {
  display: contents;
}
.body-card .card-fields .field-key {
  color: var(--sc-muted, #777);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.body-card .card-fields .field-val {
  margin: 0;
  color: var(--sc-text, #111);
  overflow-wrap: anywhere;
}
.body-card .card-fields .field-val a {
  color: var(--sc-palette-tint, #000);
}
.body-card .card-fields .field-chip {
  display: inline-block;
  background: var(--sc-palette-tint);
  color: var(--sc-palette-bg);
  font-size: 12px;
  padding: 1px 8px;
  border-radius: 999px;
  margin: 0 2px 2px 0;
}
/* A date claim's provenance ("as written"), rendered beside the canonical
   value — never a hover secret (spec/refactor/dates.md §Visible derivation). */
.body-card .card-fields .field-raw {
  color: var(--sc-palette-muted, #8a8a86);
  font-size: 12px;
}

/* Citation card = a read-only library index card (mirrors the editor):
   a left margin with the @citekey call-number + a type glyph/label, a red
   margin rule, two faint blue rules (top + under the title), and the main
   column carrying the title heading, the details (author · year, venue), the
   doi/url links, and the abstract (the card body). */
.body-card.citation-index-card {
  max-width: 760px;
  /* The parchment palette — hand-mirror of the app-side definition on
     :global(.editor.citation-index-card) in src/editor/CardEditor.svelte
     (the published page loads no app CSS). Change BOTH or neither. */
  --cite-rule-red: #e2a49b;
  --cite-rule-blue: #bcd4e8;
  --cite-rule-blue-faint: #dfeaf2;
  --cite-sepia: #8a6d3b;
  --cite-sepia-dark: #7a5c1e;
  --cite-ink: #4a4436;
  --cite-ink-strong: #26221a;
  --cite-paper: #f3ecd8;
  --cite-paper-hover: #efe6cd;
  --cite-border: #e4d8b8;
  --cite-border-dashed: #cbb98a;
  --cite-tan: #a8935f;
  --cite-tan-label: #a08b6a;
  --cite-placeholder: #b7a074;
  --cite-dot: #b7ab90;
  --cite-focus: #b89a5e;
  --cite-cream: #faf5e6;
  --cite-control-bg: #fff;
  --cite-warn: #b45309;
  --cite-wash: rgba(122, 92, 30, 0.06);
  --cite-wash-strong: rgba(122, 92, 30, 0.1);
}
/* The parchment after dark — hand-mirror of the app-side dark block on
   :global(:root[data-rs-appearance='dark']) .editor.citation-index-card.
   Change BOTH or neither. These 21 tokens are tuned as PAPER, so on a dark
   page they are restated rather than inherited or the card reads as a
   lightbox burning a hole in the page: the paper darkens, the sepia and
   tan inks lift to hold contrast, and the ruled lines keep their red/blue
   identity at lower saturation so it still reads as ruled stock. */
:root[data-rs-appearance='dark'] .body-card.citation-index-card {
  --cite-rule-red: #7d4a44;
  --cite-rule-blue: #47606f;
  --cite-rule-blue-faint: #2f3d47;
  --cite-sepia: #c9a86b;
  --cite-sepia-dark: #d8b978;
  --cite-ink: #ddd5c2;
  --cite-ink-strong: #f1ebdc;
  --cite-paper: #262218;
  --cite-paper-hover: #2d281d;
  --cite-border: #443c29;
  --cite-border-dashed: #574c33;
  --cite-tan: #a8935f;
  --cite-tan-label: #b09b76;
  --cite-placeholder: #8a7a58;
  --cite-dot: #6f6752;
  --cite-focus: #c8a765;
  --cite-cream: #201d15;
  --cite-control-bg: #322d24;
  --cite-warn: #e0a052;
  --cite-wash: rgba(216, 185, 120, 0.08);
  --cite-wash-strong: rgba(216, 185, 120, 0.14);
}
.body-card.citation-index-card .cite-idx {
  position: relative;
  display: grid;
  grid-template-columns: 104px 1fr;
  column-gap: 20px;
  padding-top: 12px;
  border-top: 1px solid var(--cite-rule-blue-faint);
}
.body-card.citation-index-card .cite-idx::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 104px;
  width: 1px;
  background: var(--cite-rule-red);
  pointer-events: none;
}
.cite-idx-margin { padding-left: 4px; min-width: 0; }
.cite-idx-callno {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.45;
  color: var(--cite-sepia);
  word-break: break-all;
}
.cite-idx-type {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 72px;
  padding: 8px 4px;
  background: var(--cite-paper);
  border: 1px solid var(--cite-border);
  border-radius: 8px;
  color: var(--cite-sepia-dark);
}
.cite-idx-type-icon { display: flex; }
.cite-idx-type-icon svg { width: 20px; height: 20px; }
.cite-idx-type-label { font-size: 11px; line-height: 1.15; text-align: center; color: var(--cite-sepia-dark); }
.cite-idx-main { min-width: 0; }
.cite-idx-title {
  margin: 0 0 0.5rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--cite-rule-blue);
  font-family: var(--sc-heading-font, 'Playfair Display', Georgia, serif);
  font-size: 1.6rem;
  line-height: 1.2;
  font-weight: 600;
  color: var(--sc-text, #1a1a1a);
}
.cite-idx-details {
  margin: 0.6rem 0 0;
  font-family: var(--sc-heading-font, Georgia, 'Times New Roman', serif);
  font-size: 16.5px;
  line-height: 1.75;
  color: var(--cite-ink-strong);
}
.cite-idx-details em { font-style: italic; }
.cite-idx-links {
  margin-top: 6px;
  font-size: 13px;
  color: var(--sc-muted, #777);
}
.cite-idx-links .citation-link {
  color: var(--sc-palette-bg, #0875b9);
  text-decoration: none;
  border-bottom: 1px solid var(--sc-palette-tint-strong, rgba(8, 117, 185, 0.22));
  overflow-wrap: anywhere;
}
.cite-idx-links .cite-idx-dot { color: var(--cite-dot); }
.body-card.citation-index-card .cite-idx-abstract {
  margin-top: 1.1rem;
}
/* Narrow collapse — mirrors the editor face's ≤600px behaviour at the
   viewer's own single-column breakpoint (720px, same as .app's): the 104px
   margin column ate ~40% of a phone-width line. The margin becomes a TOP
   band (call-number + type chip in a row) carrying the red rule as its
   bottom border; the full-height rule goes away; the grid single-columns. */
@media (max-width: 720px) {
  .body-card.citation-index-card .cite-idx {
    grid-template-columns: 1fr;
  }
  .body-card.citation-index-card .cite-idx::before {
    display: none;
  }
  .cite-idx-margin {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    padding-left: 0;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--cite-rule-red);
  }
  .cite-idx-margin .cite-idx-type {
    margin-top: 0;
  }
}

/* Table card = a scholarly FIGURE (spec/refactor/published-tables-maps.md
   §Published table appearance): the grid, the card body below it as the
   caption, a quiet provenance footer under that. Its own scoped section with
   its own custom properties, on the citation card's precedent — nothing here
   leaks into a GFM table inside ordinary prose (which keeps the .body-content
   rules above).

   The grid is its own scroll box with a sticky header, for the reason the
   editor's is (card-types.md §The grid viewport): past ~30 rows the column
   names are gone and you are reading values you can no longer name. A figure
   is also the one card that earns more than the reading measure, so the card
   widens to the pane. */
.body-card.table-card {
  max-width: none;
  --tf-rule: var(--rs-border, var(--sc-border, #e5e5e0));
  --tf-rule-strong: var(--sc-muted, #6b6b6b);
  --tf-head-bg: var(--sc-row-bg, #ffffff);
  --tf-gutter-ink: var(--sc-muted, #6b6b6b);
  --tf-ink: var(--rs-text, var(--sc-text, #1a1a1a));
  --tf-zebra: color-mix(in srgb, var(--tf-rule) 22%, transparent);
}
.table-figure { margin: 0; }
/* NOT a scroll container, on either axis — the grid takes the width and
   height its sheet needs, the card widens to hold it (the `:has` rule on
   `.body-card` below), and the body pane is the one scroller. Two rounds of
   JS (a floated header, a hand-split wheel) existed only to serve a nested
   scroll box; with one scroller, `position: sticky` pins the header and the
   row gutter natively and the JS is gone. */
.table-figure .tf-scroll {
  /* Hug a narrow sheet. A full-width box around a four-column table left the
     header rule and the zebra stripes ending in mid-air. */
  width: max-content;
  /* A stacking context: the sticky cells' z-indexes stay sealed in one layer
     below anything the page pins above the grid. */
  position: relative;
  z-index: 0;
  border: 1px solid var(--tf-rule);
  border-radius: 3px;
  background: var(--tf-head-bg);
}
/* The reading measure is right for prose and a straitjacket for a wide sheet:
   a card holding a table grows to it, and the pane scrolls. */
.body-card:has(.table-figure) { max-width: none; width: fit-content; min-width: min(720px, 100%); }
.table-figure .tf-grid {
  border-collapse: separate;   /* `collapse` drops a sticky header's borders */
  border-spacing: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--tf-ink);
  font-variant-numeric: tabular-nums;
}
.table-figure .tf-grid th,
.table-figure .tf-grid td {
  padding: 4px 10px;
  text-align: left;
  font-weight: 400;
  white-space: pre;
  max-width: 32rem;
  overflow: hidden;
  text-overflow: ellipsis;
  border-bottom: 1px solid var(--tf-rule);
}
.table-figure .tf-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  /* Against the BODY PANE — the figure's own box is not a scroll container,
     so the pane's scrollport is the one sticky resolves against. Native. */
  background: var(--tf-head-bg);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  /* The rule is drawn on the floating box itself so the rows passing under it
     have a line to disappear under. */
  box-shadow: inset 0 -2px 0 var(--tf-rule-strong);
  border-bottom: none;
}
.table-figure .tf-grid tbody tr:nth-child(even) td,
.table-figure .tf-grid tbody tr:nth-child(even) .tf-rownum {
  background: var(--tf-zebra);
}
.table-figure .tf-grid tbody tr:last-child :is(td, th) { border-bottom: none; }
/* The row number is the sheet's coordinate system — the thing you quote to
   someone else — so it keeps meaning the stored position. Sticky on its own
   axis, and opaque, or the scrolled columns show through it. */
.table-figure .tf-rownum {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--tf-head-bg);
  color: var(--tf-gutter-ink);
  font-size: 11px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  user-select: none;
}
.table-figure .tf-corner { z-index: 3; }
/* Written at the same selector depth as the th/td rule above, or it loses to
   it and every number sits left. */
.table-figure .tf-grid :is(th, td).tf-num { text-align: right; }
.table-figure .tf-sr {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.table-figure .tf-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
}
.table-figure .tf-btn {
  font: inherit;
  font-size: 12px;
  color: var(--sc-text);
  background: var(--sc-row-bg, #fff);
  border: 1px solid var(--tf-rule);
  border-radius: 999px;
  padding: 3px 12px;
  cursor: pointer;
}
.table-figure .tf-btn:hover { border-color: var(--tf-rule-strong); }
.table-figure .tf-count,
.table-figure .tf-note {
  font-size: 12px;
  color: var(--sc-muted, #6b6b6b);
  margin: 8px 0 0;
}
.table-figure .tf-note-error { color: var(--cite-warn, #b45309); }
/* The card widens to the pane for the GRID's sake; the caption is prose and
   keeps the reading measure, or a 12-column sheet gives its own caption a
   140-character line. */
.table-figure .tf-caption,
.table-figure .tf-credits,
.map-figure .tf-credits {
  margin-top: 20px;
  max-width: var(--rs-measure, 720px);
}
/* Provenance, rendered by default and never as a hover secret. Quiet enough
   to read as apparatus, present enough to be found. The map figure wears the
   same footer — its column roll is the same roll, over the columns the scene
   reads, and its basemap lines join it rather than starting a second
   apparatus somewhere else on the card. */
.table-figure .tf-credits,
.map-figure .tf-credits {
  padding-top: 8px;
  border-top: 1px solid var(--tf-rule);
  font-size: 11px;
  line-height: 1.6;
  color: var(--sc-muted, #6b6b6b);
}
/* One roll per line — the renamed-headers note and the derivation note answer
   different questions, and running them together reads as one long sentence
   whose halves don't agree. */
.table-figure .tf-credits-roll,
.map-figure .tf-credits-roll { display: block; }
.table-figure .tf-credits-label,
.map-figure .tf-credits-label { margin-right: 6px; }
.table-figure .tf-credit + .tf-credit::before,
.map-figure .tf-credit + .tf-credit::before { content: ' · '; }
.table-figure .tf-credits code,
.map-figure .tf-credits code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.95em;
}

/* Map card = the same figure posture as the table (spec/refactor/
   published-tables-maps.md §Published map appearance): the drawn scene, the
   card body below it as the caption. Its own scoped section, and the mark
   colours are the panel's own (MapPanel.svelte's <style>) rather than reading
   surface tokens — a coastline that changed colour with the reading style
   would stop being the same map the author drew. */
.body-card.map-card {
  max-width: none;
  --mf-rule: var(--rs-border, var(--sc-border, #e5e5e0));
  /* The shared credits footer reads its rule from the table figure's token;
     without this it would fall back to currentColor and draw far too dark. */
  --tf-rule: var(--mf-rule);
}
.map-figure { margin: 0; }
.map-figure .mf-frame {
  position: relative;
  border: 1px solid var(--mf-rule);
  border-radius: 3px;
  overflow: hidden;
  background: #dde7f3;              /* the sea, so the frame never flashes white */
}
/* The placeholder + the states that have no scene yet: the map's frame, at a
   readable height, carrying one sentence. Never the coordinates. */
.map-figure .mf-frame-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 220px;
  background: var(--sc-row-bg, #fff);
}
.map-figure .mf-note {
  margin: 0;
  padding: 12px 16px;
  font-size: 12px;
  color: var(--sc-muted, #6b6b6b);
  text-align: center;
}
.map-figure .mf-note-error { color: var(--cite-warn, #b45309); }
.map-figure .mf-svg {
  display: block;
  width: 100%;
  height: auto;
  /* Every touch belongs to the map: one finger pans, two pinch. */
  touch-action: none;
  cursor: grab;
  -webkit-user-select: none;
  user-select: none;
  /* What the label classes inherit when --rs-map-label-case names a value a
     property cannot parse — see the Cartography block below. */
  text-transform: none;
  font-variant-caps: normal;
}
.map-figure .mf-svg.mf-grabbing { cursor: grabbing; }
/* Scene marks — the panel's palette, kept in the panel's order. Strokes hold a
   constant on-screen width under the viewBox camera via non-scaling-stroke;
   dot radii and label sizes are counter-scaled in the markup.

   The ground and the marks are Style vars now, as the lettering below already
   was: the sea, land and coastline are DRAWN from vector data rather than laid
   on as a picture, so a dark Style takes them with it. The fallbacks repeat the
   --rs-* ground because a published snapshot predating these keys still has to
   draw its chart. */
.map-figure .msea { fill: var(--rs-map-sea, #dde7f3); }
.map-figure .mland { fill: var(--rs-map-land, #e9e4d6); stroke: var(--rs-map-coast, #cdbf9c); stroke-width: 0.6; vector-effect: non-scaling-stroke; }
/* The self-hosted coast (engraved-chart.md unit 2): the ring FILLS land and
   carries the source's 1° cut edges, which are not shore, so the fill drops
   its stroke and .mcoast is the line. The bundled clip and the coast are the
   same ground drawn twice — .mf-land-off is how exactly one of them is up. */
.map-figure .mlandfill { stroke: none; }
/* The relief (engraved-chart unit 4): a MATERIAL, not a picture. The tiles are
   written re-centred so mid-grey is flat ground, which is the pivot `overlay`
   needs; the land's colour stays whatever --rs-map-land says. Mirrors
   `.mhillshade` in MapPanel.svelte — the group carries the blend, never the
   individual images, or every relief-gated tile darkens twice. */
.map-figure .mhillshade { mix-blend-mode: overlay; pointer-events: none; }
.map-figure .mcoast { fill: none; stroke: var(--rs-map-coast, #cdbf9c); stroke-width: 0.6; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.map-figure .mf-static.mf-land-off .mland { display: none; }
.map-figure .mroute { fill: none; stroke: var(--rs-map-route, #9bb0d0); stroke-width: 1; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
/* A leg the router hasn't settled (or couldn't): tight dots, no body — reads
   as pencil, not ink. Distinct from .mroute's 3 3 planned dash. */
.map-figure .mprovroute { fill: none; stroke: var(--rs-map-route, #9bb0d0); stroke-width: 1; stroke-dasharray: 1 3; opacity: 0.55; vector-effect: non-scaling-stroke; pointer-events: none; }
.map-figure .mtravelled { fill: none; stroke: var(--rs-accent, #b8860b); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.map-figure .mtrack { fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.map-figure .mtrack.mdone { stroke-width: 1.4; }
.map-figure .mdot { fill: var(--rs-map-dot, #6b7f9e); }
.map-figure .mterminus { fill: var(--rs-map-mark, #2f3e57); }
.map-figure .mport { fill: var(--rs-map-mark, #2f3e57); opacity: 0.5; stroke: var(--rs-map-halo, #fff); stroke-width: 0.5; vector-effect: non-scaling-stroke; }
/* The chart's lettering — the mirror of MapPanel.svelte's Cartography block.
   Every measure is a Style var, and the sizes are ON-SCREEN sizes: `--mscale`
   (the camera's counter-scale, written onto the <svg> by `applyCamera` in
   renderers/map.js) multiplies them, which is what holds a label at its size
   through a zoom. The fallbacks repeat the --rs-* ground because a published
   page whose snapshot predates these keys still has to letter correctly.

   `--mlabel-fit` is label fit's SHRINK remedy (spec/refactor/map-label-fit.md):
   a crowding-derived factor `applyCamera` writes beside `--mscale`, on the same
   never-one-without-the-other rule. Absent — which is every page whose author
   has not asked for the remedy — it is 1 and this calc is the old one.

   `-case` feeds text-transform AND font-variant-caps from one var; each drops
   what it cannot parse (invalid-at-computed-value-time → unset → inherit),
   which is deterministic because .mf-svg above declares the neutral pair. */
.map-figure .mlabeltxt,
.map-figure .mleglabel {
  fill: var(--rs-map-label-ink, #24313f);
  font-family: var(--rs-font-map-label, inherit);
  font-size: calc(var(--rs-map-label-size, 11px) * var(--mscale, 1) * var(--mlabel-fit, 1));
  font-weight: var(--rs-map-label-weight, 600);
  letter-spacing: var(--rs-map-label-tracking, 0);
  text-transform: var(--rs-map-label-case, none);
  font-variant-caps: var(--rs-map-label-case, normal);
}
/* Place names carry a halo so they stay legible over land or sea. */
.map-figure .mleglabel {
  paint-order: stroke;
  stroke: var(--rs-map-halo, #fff);
  stroke-width: calc(var(--rs-map-halo-width, 3px) * var(--mscale, 1));
  stroke-linejoin: round;
}
.map-figure .mshipdot { stroke: var(--rs-map-mark-edge, #24313f); stroke-width: 1; vector-effect: non-scaling-stroke; }
.map-figure .mship { fill: var(--rs-accent, #b8860b); }
.map-figure .mshiphalo { fill: var(--rs-accent, #b8860b); opacity: 0.18; }
/* A voyage's own name at its ship: worn over .mleglabel, so it keeps that halo
   and adds the maritime voice — the chartmaker's distinction between a place
   and a vessel. Must stay AFTER .mleglabel: equal specificity, so source order
   is what makes it win. */
.map-figure .mvoyagelabel {
  fill: var(--rs-map-water-ink, #24313f);
  font-family: var(--rs-font-map-water, inherit);
  /* Shrink is a WHOLE-CHART remedy — the maritime voice steps down with the
     place names. (The halo width deliberately does not follow.) */
  font-size: calc(var(--rs-map-water-size, 13px) * var(--mscale, 1) * var(--mlabel-fit, 1));
  font-weight: var(--rs-map-water-weight, 500);
  font-style: var(--rs-map-water-slope, italic);
  letter-spacing: var(--rs-map-water-tracking, 0);
}

/* Transport controls (unit 5) — play/pause, the scrubber over the shared
   calendar, the date readout. DOM, outside the SVG: a real button and a real
   range input, so the timeline is keyboard-reachable while the scene itself
   stays role="img" and out of the tab order. The panel's own proportions. */
.map-figure .mf-transport {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-top: 10px;
}
.map-figure .mf-play {
  flex: none;
  width: 2rem; height: 2rem;
  border-radius: 50%;
  border: 1px solid var(--mf-rule);
  background: var(--sc-row-bg, #fff);
  color: var(--sc-text, #1a1a1a);
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
}
.map-figure .mf-play:hover { border-color: var(--sc-text, #1a1a1a); }
.map-figure .mf-scrub-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.map-figure .mf-scrub { width: 100%; margin: 0; accent-color: var(--rs-accent, #b8860b); }
.map-figure .mf-axis { position: relative; height: 1rem; margin-top: 2px; }
.map-figure .mf-tick {
  position: absolute; transform: translateX(-50%); white-space: nowrap;
  font-size: 0.62rem; color: var(--sc-muted, #6b6b6b);
  font-variant-numeric: tabular-nums;
}
.map-figure .mf-date {
  flex: none; min-width: 5.5rem; text-align: right;
  font-size: 0.8rem; color: var(--sc-text, #1a1a1a);
  font-variant-numeric: tabular-nums;
}

/* The "Voyages under way" panel: a panel floats above the labels, which are
   above the underlays (the layers panel's own taxonomy). Fixed at the frame's
   top-right, where the app defaults it — not draggable, since a published map
   carries the author's configuration and no new affordances. */
.map-figure .mf-hud {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 220px; max-width: 60%; max-height: 78%;
  display: flex; flex-direction: column;
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(2px);
  border: 1px solid var(--mf-rule);
  border-radius: 7px;
  box-shadow: 0 3px 14px rgba(0, 0, 0, 0.16);
  overflow: hidden;
  pointer-events: none;             /* it reports; it is not a control */
}
.map-figure .mf-hud-head {
  flex: none;
  padding: 0.3rem 0.5rem;
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em;
  font-weight: 600; color: var(--sc-muted, #6b6b6b);
  background: rgba(0, 0, 0, 0.04);
  border-bottom: 1px solid var(--mf-rule);
}
.map-figure .mf-hud-body { flex: 1; overflow: auto; padding: 0.4rem 0.6rem 0.6rem; }
.map-figure .mf-hud-name {
  margin: 0.5rem 0 0.15rem;
  font-size: 0.9rem; font-weight: 700;
  color: var(--sc-text, #1a1a1a);
  font-family: var(--rs-font-heading, inherit);
}
.map-figure .mf-hud-name:first-child { margin-top: 0; }
.map-figure .mf-hud-notes {
  margin: 0; font-size: 0.78rem; line-height: 1.35;
  color: var(--sc-text, #1a1a1a); white-space: pre-wrap;
}
.map-figure .mf-hud-empty {
  margin: 0; font-size: 0.78rem; font-style: italic;
  color: var(--sc-muted, #6b6b6b);
}

/* The card widens to the pane for the SCENE's sake; the caption is prose and
   keeps the reading measure. */
.map-figure .mf-caption {
  margin-top: 20px;
  max-width: var(--rs-measure, 720px);
}

.body-card .body-content {
  margin-top: 28px;
  font-size: 17px;
  line-height: 1.7;
  color: var(--sc-text);
}

.body-card .body-content h1,
.body-card .body-content h2,
.body-card .body-content h3,
.body-card .body-content h4,
.body-card .body-content h5,
.body-card .body-content h6 {
  /* Heading font/weight/style/colour come from the active Style's vars
   * (et-book light-italic under Tufte, Playfair semibold under
   * Hyperliner). This is the canonical heading rule; it must read --rs-*
   * itself rather than relying on an earlier rule, since it would
   * otherwise win on source order. */
  font-family: var(--rs-font-heading, var(--sc-heading-font));
  font-weight: var(--rs-heading-weight, 600);
  font-style: var(--rs-heading-style, normal);
  /* Discretionary + common ligatures (Playfair Display's fi/ffl/etc.;
   * et-book's too). letter-spacing MUST stay `normal` — any tracking
   * value makes the browser insert space between every glyph and
   * disables ligatures, so a tighter heading would lose its fi/ffl. */
  font-variant-ligatures: common-ligatures var(--rs-heading-ligatures, discretionary-ligatures);
  text-transform: var(--rs-heading-case, none);
  font-variant-caps: var(--rs-heading-case, normal);
  letter-spacing: normal;
  line-height: 1.2;
  margin: 1.6em 0 0.5em;
  color: var(--rs-heading-color, var(--sc-text));
}
.body-card .body-content h1 { font-size: var(--rs-h1-size, 28px); }
.body-card .body-content h2 { font-size: var(--rs-h2-size, 23px); }
.body-card .body-content h3 { font-size: var(--rs-h3-size, 20px); }
.body-card .body-content h4 { font-size: var(--rs-h4-size, 18px); letter-spacing: 0; }
.body-card .body-content h5 { font-size: 17px; letter-spacing: 0; }
.body-card .body-content h6 {
  font-size: 14px;
  font-family: var(--sc-body-font);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sc-muted-strong);
}

.body-card .body-content > *:first-child { margin-top: 0; }

.body-card .body-content p { margin: 0 0 0.85em; }

.body-card .body-content ul,
.body-card .body-content ol {
  padding-left: 1.4rem;
  margin: 0.4em 0 0.8em;
}

.body-card .body-content blockquote {
  border-left: 3px solid var(--sc-palette-bg);
  padding: 0.1em 0 0.1em 1rem;
  margin: 1em 0;
  color: var(--sc-muted-strong);
  font-style: italic;
}
.body-card .body-content blockquote p:last-child { margin-bottom: 0; }

.body-card .body-content code {
  background: rgba(0, 0, 0, 0.05);
  font-size: 0.9em;
  padding: 0.1em 0.3em;
  border-radius: 3px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.body-card .body-content pre {
  background: rgba(0, 0, 0, 0.05);
  padding: 0.6rem 0.8rem;
  border-radius: 4px;
  overflow-x: auto;
}

.body-card .body-content pre code {
  background: transparent;
  padding: 0;
}

/* The code tint after dark — a black wash darkens what it crosses on cream
   and must LIFT on a dark page, or the tint reads as a hole in the line.
   Mirrors the same correction in src/editor/reading-surface.css. */
:root[data-rs-appearance='dark'] .body-card .body-content :is(code, pre) {
  background: rgba(255, 255, 255, 0.07);
}
:root[data-rs-appearance='dark'] .body-card .body-content pre code {
  background: transparent;
}

/* Authoritative body-link rule (wins on source order over the
   reading-surface rule above). Honours the active Style's link vars:
   colour, underline on/off, underline colour (with independent opacity
   via color-mix), thickness, and hover cursor. Falls back to the
   palette colour + a soft underline so an unstyled outline still reads. */
.body-card .body-content a {
  color: var(--rs-link, var(--sc-palette-bg));
  text-decoration-line: var(--rs-link-decoration, underline);
  text-decoration-color: color-mix(in srgb, var(--rs-link-underline-color, currentColor) calc(var(--rs-link-underline-opacity, 1) * 100%), transparent);
  text-decoration-thickness: var(--rs-link-underline-width, 1px);
  text-underline-offset: 2px;
  cursor: var(--rs-link-cursor, pointer);
}

/* ─── Media cards ─────────────────────────────────────────────────────── */
/* Rendered above the caption by renderMedia(). The clamp matches the
 * editor's so a tall photo doesn't push the rest of the page out of
 * view; the <audio> player takes the full content width. */
.body-card .body-content .media-figure {
  margin: 0 0 1rem 0;
  padding: 0;
  display: block;
}
.body-card .body-content .media-el {
  display: block;
  max-width: 100%;
  max-height: 70vh;
  height: auto;
  margin: 0 auto;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.04);
}
.body-card .body-content .media-el-audio { width: 100%; }
.body-card .body-content .media-el-embed {
  /* iframes have no intrinsic height; the .media-el `height: auto` would
     collapse them, so pin an explicit reading height. */
  width: 100%;
  height: 70vh;
  border: 1px solid rgba(0, 0, 0, 0.12);
}
.body-card .body-content .media-open-link {
  display: inline-block;
  margin-top: 0.4rem;
  color: #000;
  text-decoration: underline;
  font-weight: 600;
}
.body-card .body-content .media-open-link:hover { text-decoration: underline; }
/* Inline body images from markdown (`![](url)`) — clamp to the content
 * column so a large source image (the imported blog content runs up to
 * 2000px wide) doesn't overflow the 720px text measure. `.media-el`
 * above is more specific and keeps its own (taller) clamp. */
.body-card .body-content img {
  max-width: 100%;
  height: auto;
}
/* ── Block layout (embed-layout.js vocabulary) ──────────────────────────
 * The editor's reading-surface.css twin: the enumerated half is pure CSS
 * on the data-* attributes applyAttrs lands; the continuous half
 * (data-width / data-opacity) is written inline by applyEmbedLayoutStyles
 * after the sanitized markup mounts. Three hosts: an image carries its own
 * attrs; a prose paragraph carries its trailing-{…} ones; an embed figure
 * inherits its host paragraph's via healEmbedParagraphLayout (the HTML
 * parser auto-closes <p> before a block figure, so the editor's
 * paragraph-hosted layout lands here on the figure itself). */
.body-card .body-content img[data-place] { display: block; }
.body-card .body-content :is(img, p, .spread-figure, .media-figure)[data-place='center'] {
  margin-left: auto;
  margin-right: auto;
}
.body-card .body-content :is(img, p, .spread-figure, .media-figure)[data-place='right'] { margin-left: auto; }
.body-card .body-content :is(img, p, .spread-figure, .media-figure)[data-place='wrap-left'] {
  float: left;
  margin: 0.25em 1em 0.5em 0;
}
.body-card .body-content :is(img, p, .spread-figure, .media-figure)[data-place='wrap-right'] {
  float: right;
  margin: 0.25em 0 0.5em 1em;
}
.body-card .body-content img[data-place='wrap-left']:not([data-width]),
.body-card .body-content img[data-place='wrap-right']:not([data-width]) {
  max-width: 50%;
}
.body-card .body-content :is(p, .spread-figure)[data-place='wrap-left']:not([data-width]),
.body-card .body-content :is(p, .spread-figure)[data-place='wrap-right']:not([data-width]) {
  width: 50%;
}
/* An image CARD's figure carries the same two words, read out of
 * `media.opts` by renderMedia — a float and a stated width have to move the
 * picture and (in the editor, where it is drawn) its caption as ONE block,
 * and the figure is what holds both. The picture's own half — data-blend
 * and data-opacity — rides the <img>, so the rules above and
 * applyEmbedLayoutStyles already cover it with nothing new written here.
 *
 * Absence is the centred figure this surface has always drawn, which is why
 * `left` needs a rule of its own here where in prose it needs none. */
.body-card .body-content .media-figure[data-width]:not([data-place]) {
  margin-left: auto;
  margin-right: auto;
}
.body-card .body-content .media-figure[data-width] .media-el { width: 100%; }
.body-card .body-content .media-figure[data-place='left'] .media-el {
  margin-left: 0;
  margin-right: auto;
}
.body-card .body-content .media-figure[data-place='right'] .media-el {
  margin-left: auto;
  margin-right: 0;
}
/* A float with no stated width would take the whole measure and wrap
 * nothing; hugging the picture is the sensible resting shape, capped at
 * half the measure so the prose beside it still has a column. */
.body-card .body-content .media-figure[data-place^='wrap']:not([data-width]) {
  width: fit-content;
  max-width: 50%;
}
/* Blend lands on the IMAGE, never on a container — the editor's rule
 * (reading-surface.css): Safari would not composite container-level
 * mix-blend in a real layer tree while every reduced test passed. A
 * paragraph host or a healed figure forwards its data-blend to the img
 * inside. */
.body-card .body-content img[data-blend='multiply'],
.body-card .body-content :is(p, .spread-figure)[data-blend='multiply'] img { mix-blend-mode: multiply; }
.body-card .body-content img[data-blend='screen'],
.body-card .body-content :is(p, .spread-figure)[data-blend='screen'] img { mix-blend-mode: screen; }
.body-card .body-content img[data-blend='overlay'],
.body-card .body-content :is(p, .spread-figure)[data-blend='overlay'] img { mix-blend-mode: overlay; }
.body-card .body-content img[data-blend='soft-light'],
.body-card .body-content :is(p, .spread-figure)[data-blend='soft-light'] img { mix-blend-mode: soft-light; }
.body-card .body-content img[data-blend='luminosity'],
.body-card .body-content :is(p, .spread-figure)[data-blend='luminosity'] img { mix-blend-mode: luminosity; }
/* Ink — the editor's rule (reading-surface.css): a declared line-art
 * treatment, monochrome always; multiply on light paper. */
.body-card .body-content img[data-blend='ink'],
.body-card .body-content :is(p, .spread-figure)[data-blend='ink'] img {
  mix-blend-mode: multiply;
  filter: grayscale(1);
}
/* The dark pairing — the editor's rule (reading-surface.css): multiply and
 * screen are formal duals about the ground and swap under a dark Style so
 * the stored intent survives; the other three modes self-adapt. */
:root[data-rs-appearance='dark'] .body-card .body-content img[data-blend='multiply'],
:root[data-rs-appearance='dark'] .body-card .body-content :is(p, .spread-figure)[data-blend='multiply'] img { mix-blend-mode: screen; }
:root[data-rs-appearance='dark'] .body-card .body-content img[data-blend='screen'],
:root[data-rs-appearance='dark'] .body-card .body-content :is(p, .spread-figure)[data-blend='screen'] img { mix-blend-mode: multiply; }
/* Ink's dark half — the editor's rule (reading-surface.css): invert turns
 * dark-ink-on-light into light-ink-on-dark, then screen drops the ground. */
:root[data-rs-appearance='dark'] .body-card .body-content img[data-blend='ink'],
:root[data-rs-appearance='dark'] .body-card .body-content :is(p, .spread-figure)[data-blend='ink'] img {
  mix-blend-mode: screen;
  filter: grayscale(1) invert(1);
}
/* Pre-promote blended images to their own composited layer — the editor's
 * rule (reading-surface.css): Safari's large-image promotion under memory
 * pressure drops mix-blend-mode card-wide once a second large image lands;
 * an explicit transform takes the wired blend path from the start. */
.body-card .body-content img[data-blend],
.body-card .body-content :is(p, .spread-figure)[data-blend] img {
  transform: translateZ(0);
}
/* Floats clear before the card's own footer chrome. */
.body-card .body-content::after {
  content: '';
  display: block;
  clear: both;
}
.body-card .body-content .media-placeholder {
  padding: 1.5rem 1rem;
  text-align: center;
  color: #666;
  font-size: 0.9rem;
  background: rgba(0, 0, 0, 0.04);
  border-radius: 4px;
}

/* ─── "Not yet published" empty state ─────────────────────────────────── */
/* Replaces the whole viewer when the outline's route is configured but
 * its in-CRDT publish flag is off. Holding page, not an error page. */
.body-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  text-align: center;
  padding: 2rem 1rem;
}
.body-empty-title {
  font-family: var(--serif, Georgia, serif);
  font-size: 32px;
  font-weight: 600;
  margin: 0 0 0.4em;
  color: #1c1c1c;
}
.body-empty-prose {
  font-size: 16px;
  color: #666;
  margin: 0;
}

/* ─── The door (audience gate) ────────────────────────────────────────── */
/* Shown instead of the page when this reader isn't in its audience
 * (spec/refactor/prepublish.md). Same empty-state frame as the holding
 * page above — the difference between them is what they say, not how
 * loudly: neither is an error. */
.gate-actions {
  margin: 1.6rem 0 0;
}
/* The house control material: black outline for the deliberate action,
 * the field's white on the page's own ground, gold on hover. No blue —
 * the visual language has none (spec/current/visual-system.md). */
.gate-signin {
  display: inline-block;
  padding: 0.5em 1.3em;
  border: 1px solid #1c1c1c;
  border-radius: 3px;
  background: #ffffff;
  color: #1c1c1c;
  font-size: 15px;
  text-decoration: none;
  transition: background-color 0.15s, box-shadow 0.15s;
}
.gate-signin:hover,
.gate-signin:focus-visible {
  background: var(--gold, #cb8a08);
  color: #ffffff;
  box-shadow: 0 0 0 3px rgba(203, 138, 8, 0.24);
}

/* ─── Live indicator ──────────────────────────────────────────────────── */
/* Lives in .sidebar-footer; sized to read as a status dot at the foot
 * of the tree, paired with the "Live from Hyperliner" label. */

.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #cccccc;
  transition: background-color 0.2s, box-shadow 0.2s;
  flex-shrink: 0;
}

.live-dot.live {
  background: #34c759;
  box-shadow: 0 0 0 3px rgba(52, 199, 89, 0.18);
}
.live-dot.connecting {
  background: #ff9500;
  box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.18);
}

/* ─── Spatial pane (baked-layout cards + SVG) ─────────────────────── */
/* When snapshot.spatial is present, the right pane swaps from the
 * body-card view to this — a scrollable canvas filled with absolute-
 * positioned cards plus an SVG layer beneath them for the connector
 * lines and curly braces. Positions come from publish/spatial-layout.js
 * (server-side); CSS variables on :root carry the card-padding /
 * border-radius / rubber-band geometry so a settings change on the
 * server flows through without a viewer code change. */

.spatial-pane {
  background: var(--sc-bg);
  overflow: auto;
  min-height: 0;
  /* min/max defaults so the canvas can scroll in any direction; the
   * canvas itself carries its computed pixel width/height inline. */
  position: relative;
}

/* The way out of the tree pane at compact width, where the sidebar —
 * and with it the outline title that opened this pane — is off-screen.
 * At two-pane width the title is right there, so this bar isn't.
 * `position: sticky` keeps it reachable from deep inside a tall
 * canvas without a scroll back to the top. */
.spatial-header {
  display: none;
  position: sticky;
  top: 0;
  left: 0;
  z-index: 2;
  padding: 10px 14px;
  background: var(--sc-bg);
  border-bottom: 1px solid var(--sc-border);
}

.tree-back-btn {
  border: 1px solid var(--sc-border);
  background: transparent;
  color: var(--sc-text);
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-family: var(--sc-body-font);
  font-size: 13px;
}

.spatial-canvas {
  position: relative;
}

.spatial-lines {
  /* (0, 0) origin matches the cards' baked-coord origin so a path
   * `M x y …` lands at the same spot as a card at `left: x; top: y`. */
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  overflow: visible;
}

.spatial-card {
  position: absolute;
  box-sizing: border-box;
  background: #faf8f0;
  border: 1px solid rgba(0, 0, 0, 0.08);
  /* Fallback mirrors the client's 4 %-of-width rule (CARD_WIDTH * 0.04 = 8);
     applyPalette normally sets --sp-card-radius from the baked settings. */
  border-radius: var(--sp-card-radius, 8px);
  padding: var(--sp-card-pad-y, 6px) var(--sp-card-pad-x, 17px);
  font-family: var(--sc-body-font);
  color: var(--sc-text);
  box-shadow:
    0 1px 1px rgba(0, 0, 0, 0.05),
    0 2px 6px rgba(0, 0, 0, 0.06);
  overflow: hidden;
  cursor: pointer;
  transition: box-shadow 120ms ease, border-color 120ms ease;
}

.spatial-card:hover {
  border-color: rgba(0, 0, 0, 0.2);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.08),
    0 4px 12px rgba(0, 0, 0, 0.10);
}

.spatial-card.selected {
  border-color: rgba(0, 0, 0, 0.5);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.3),
    0 1px 2px rgba(0, 0, 0, 0.08),
    0 4px 12px rgba(0, 0, 0, 0.12);
}

.spatial-card.is-stack {
  /* Title gutter for the rubber-band strap on the left edge. Tracks
   * the rubber-band knobs so width changes flow through. */
  padding-left: calc(var(--sp-rubber-left, 9px) + var(--sp-rubber-width, 7px) + 6px);
}

/* Image media cards — the node's image fills the spatial card as a
 * background, with a top-down dark scrim so the title stays legible
 * against any image. Mirrors the editor's SpatialView .card.has-bg-image.
 * The image URL is injected inline via --spatial-card-bg-image. */
.spatial-card.has-bg-image {
  background-image:
    linear-gradient(
      180deg,
      rgba(0, 0, 0, 0.60) 0%,
      rgba(0, 0, 0, 0.32) 45%,
      rgba(0, 0, 0, 0.08) 80%,
      rgba(0, 0, 0, 0.00) 100%
    ),
    var(--spatial-card-bg-image);
  /* Image cards fill (cover); URL-card posters honour the author's crop/zoom
     via --spatial-card-bg-size / -pos (default to cover/center). The scrim
     layer always stays cover/center. */
  background-size: cover, var(--spatial-card-bg-size, cover);
  background-position: center, var(--spatial-card-bg-pos, center);
  background-repeat: no-repeat, no-repeat;
  color: #fff;
}
.spatial-card.has-bg-image .spatial-card-title {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.spatial-card.has-bg-image .spatial-card-meta {
  color: rgba(255, 255, 255, 0.88);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

.spatial-card-title {
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.spatial-card-meta {
  margin-top: 6px;
  font-size: 11px;
  color: var(--sc-muted, #8a8a8a);
  letter-spacing: 0.02em;
}

/* Vertical rubber band — same shape as the editor's. Tinted with
 * the outline palette via --sc-palette-bg (set by applyPalette). */
.spatial-rubber {
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: var(--sp-rubber-left, 9px);
  width: var(--sp-rubber-width, 7px);
  background: var(--sc-palette-bg, #c7c7b2);
  transform: rotate(var(--sp-rubber-angle, 1deg));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
  pointer-events: none;
}

/* ══ Placed embeds — `![[…]]` drawn as figures ═══════════════════════════════
 *
 * The same figure shapes everywhere a token draws one: routed into a spread's
 * left column, in place in a plain card's prose, in a continuation section.
 * These are the GENERIC rules; the spread block below keeps its own scoped
 * overrides (same specificity, later in the file, so inside a spread the
 * column's geometry wins). */
.body-card .spread-figure { margin: 1.25rem 0; }
.body-card .spread-figure .media-figure { margin: 0; }
.body-card .spread-figure img { max-width: 100%; height: auto; }
.body-card .spread-figure-credit {
  margin-top: 0.35rem;
  font-family: var(--rs-font-body, var(--sc-body-font));
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--rs-meta, var(--sc-muted));
}
.body-card .spread-figure-credit p { margin: 0; }
/* A gallery names each of its figures; a lone figure's name would only repeat
 * what the prose already says. */
.body-card .spread-figure-title {
  display: block;
  font-weight: 600;
  color: var(--rs-text, var(--sc-text));
}
/* The figure shell already spaces the gallery; the grid inside it must not
 * add the left column's own figure margin on top. */
.body-card .spread-figure .table-figure { margin: 0; }

/* THE CARD FACE — another card, quoted where it lands.
 *
 * A left rule instead of a box, because that is what a quotation looks like in
 * every book this page is imitating: a box says "figure", and this is not a
 * figure, it is somebody else's prose being read here. Mirrored in the editor
 * (src/editor/reading-surface.css, `.card-embed[data-kind='card']`) under the
 * standing rule — a divergence is the editor lying about the public artifact.
 *
 * The title prints whether or not this is one of several, unlike a figure's:
 * a picture is self-evidently a picture OF something, where a paragraph of
 * another card's prose with no attribution simply reads as this page's own. */
.body-card .spread-figure-card {
  display: block;
  border-left: 2px solid var(--rs-divider, var(--sc-border));
  padding: 0.1rem 0 0.1rem 0.95rem;
}
.body-card .spread-figure-quote-title {
  display: block;
  font-family: var(--rs-font-heading, var(--sc-body-font));
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rs-meta, var(--sc-muted));
  text-decoration: none;
}
.body-card .spread-figure-quote-title:hover { color: var(--rs-text, var(--sc-text)); }
.body-card .spread-figure-quote { margin-top: 0.35rem; }
/* The quoted body's own outer blocks must not push the inset open: a
 * transcluded paragraph is a paragraph, and its margins belong to the card it
 * came from, not to the seam it is being read through. */
.body-card .spread-figure-quote > :first-child { margin-top: 0; }
.body-card .spread-figure-quote > :last-child { margin-bottom: 0; }
.body-card .spread-figure-empty {
  color: var(--rs-meta, var(--sc-muted));
  font-style: italic;
}

/* ══ Spread cards — a page in two even columns ═══════════════════════════════
 *
 * spec/current/publishing.md §Published spreads (design in
 * spec/refactor/composed-cards.md). The card widens past the reading measure
 * the way a table figure does — it has two columns to hold, and each of them
 * wants a measure of its own.
 *
 * These rules are MIRRORED in the editor (src/editor/CardEditor.svelte's
 * `.two-column` block and its dek rules, plus SpreadPanel's own styles). The
 * two copies are the citation card's precedent and live under its rule: a
 * divergence is the editor lying about the public artifact.
 */

/* The site band. Site chrome by nature, and spread-only — promoting it to
 * every published page is a deliberate act, not a side effect. */
.spread-site-band {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.7rem 1.25rem;
  margin: 0 0 0.75rem;
  background: var(--rs-panel-bg, #f2ede0);
  border-bottom: 1px solid var(--rs-divider, rgba(0, 0, 0, 0.12));
  font-family: var(--rs-font-title, var(--rs-font-heading, var(--sc-heading-font)));
}
.spread-site-band .site-name {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--rs-text, var(--sc-text));
}
.spread-site-band .site-sep {
  font-size: 0.95rem;
  opacity: 0.55;
}
.spread-site-band .site-section {
  font-size: 1.1rem;
  color: var(--rs-text, var(--sc-text));
  opacity: 0.8;
}

.body-card.spread-card {
  max-width: none;
  padding: 0;
}

/* The header. NOT a band — an earlier cut painted this block in the card's own
 * ink with inverse type, and it was a second styling system answering to
 * nothing the reading-style menu controls, on the surface where the READER
 * picks the dressing. A layout arranges; the Style paints. So the headline is
 * the headline this page always draws, and the rule under it is the one
 * `.body-header` draws for every other card: the spread's contribution is that
 * the title and its dek travel together, above two columns instead of one. */
.spread-card .spread-header {
  display: flex;
  align-items: flex-start;
  gap: 1.25rem;
  padding: 1.1rem 1.25rem 18px;
  border-bottom: 1px solid var(--sc-border);
}
.spread-card .spread-header-text {
  flex: 1 1 auto;
  min-width: 0;
}
/* The same declaration `.body-header .body-title` makes, fallbacks included —
 * a spread's headline is that headline, so a Style that names a title weight,
 * a case or a family must not land differently here. */
.spread-card .spread-title {
  margin: 0;
  font-family: var(--sc-heading-font);
  font-size: var(--rs-title-size, clamp(28px, 3.6vw, 42px));
  font-weight: var(--rs-title-weight, var(--rs-heading-weight, 600));
  font-style: var(--rs-title-style, var(--rs-heading-style, normal));
  text-transform: var(--rs-title-case, var(--rs-heading-case, none));
  font-variant-caps: var(--rs-title-case, var(--rs-heading-case, normal));
  letter-spacing: var(--rs-title-tracking, normal);
  line-height: var(--rs-title-line-height, 1.15);
  color: var(--sc-text);
}
/* Secondary to the headline without being meta — a dek is read, not skipped.
 * Mirrors `.card-header-sub` in the editor. */
.spread-card .spread-sub {
  margin: 0.3rem 0 0;
  font-family: var(--rs-font-heading, var(--sc-heading-font));
  font-size: 1.05rem;
  line-height: 1.35;
  color: var(--sc-text);
  opacity: 0.75;
}
/* Even columns, not body + margin: the figures are the other half of the
 * evidence, not commentary on the prose. Below the breakpoint they stack in
 * the order the page already reads — figures, then prose.
 *
 * The left column's figure rules below outlive the v1 slots that filled it:
 * the same shapes (a figure with a credit line, a grid in a scroll box) come
 * back as placed transclusions, into this same column. */
.spread-card .spread-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 2rem;
  padding: 1.25rem;
  align-items: start;
}
@media (max-width: 900px) {
  .spread-card .spread-cols {
    grid-template-columns: 1fr;
    gap: 1.5rem 0;
  }
}
.spread-card .spread-col { min-width: 0; }
.spread-card .spread-col-right .body-content {
  /* Each column keeps a measure of its own — the card widened for the grid's
     sake, not so prose could run the width of a screen. */
  max-width: var(--rs-measure, 720px);
}

.spread-card .spread-figure { margin: 0 0 1.25rem; }
.spread-card .spread-figure .media-figure { margin: 0; }
.spread-card .spread-figure img,
.spread-card .spread-figure video { max-width: 100%; height: auto; }
/* The image card's own body, read as what it is: a credit line. */
.spread-card .spread-figure-credit {
  margin-top: 0.35rem;
  font-family: var(--rs-font-body, var(--sc-body-font));
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--rs-meta, var(--sc-muted));
}
.spread-card .spread-figure-credit p { margin: 0; }

/* A placed grid arrives inside its `.spread-figure` shell, which owns the
 * gallery spacing — the figure itself adds none, or the gap doubles. */
.spread-card .spread-col-left .table-figure { margin: 0; }
/* The grid is already in a scroll box on both axes; inside half a card it
   needs to be told it cannot claim more than the column it sits in. */
.spread-card .spread-col-left .tf-scroll { max-width: 100%; }

.spread-card .spread-col-left .card-fields { margin: 0; }

/* A spread's headline lives in the card's own header, so the page header keeps
 * only the Back button and the style menu. Without this it still drew its rule
 * at the reading measure — a line ending in mid-air above a full-width card,
 * which is the same "header rule ending nowhere" the table figure's scroll box
 * was widened to avoid. The rule is not lost, only moved: the card's header
 * draws the same one, at the width the card actually occupies. */
.body-header.body-header-spread {
  max-width: none;
  border-bottom: none;
  padding-bottom: 0;
  margin-bottom: 0;
}
