/* 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: 'Noto Sans';
  src: url('fonts/noto-sans/noto-sans-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: 'Noto Sans';
  src: url('fonts/noto-sans/noto-sans-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: 'Noto Sans';
  src: url('fonts/noto-sans/noto-sans-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: 'Noto Sans';
  src: url('fonts/noto-sans/noto-sans-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: '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: 'Pixie';
  src: url('fonts/pixie/Pixie.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Smith-Waite Titling';
  src: url('fonts/pixie/Pixie.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 {
  /* ── The ground: the Style's vocabulary, at the Hyperliner preset's values ──
   * A published page has ONE dress, the reading Style's (spec/current/
   * publishing.md §One dress): the sidebar, the header row, the footer and
   * every control read the Style's own furniture — Panels, Panel rules,
   * Panel ink, Signals, the panel type — through the same `--rs-*` names the
   * card's grid and map panels read in the app. applyStyle() writes the
   * active Style's vars inline on this element and removes the rest, so
   * these grounds reach only a key the Style lacks. They are the Hyperliner
   * preset's values, key for key, and test/publisher-parity/viewer-ground
   * .test.ts holds them there: adopting the ground was provably a no-op
   * under the default Style, which is the themes arc's own trick. No `--sc-*`
   * ground, and no `--sc-*` consumption anywhere in this sheet but the
   * outline's palette, which is DATA set by applyPalette, not dress. */
  --rs-text: #000000;
  --rs-text-2: #555555;
  --rs-muted: #555555;
  --rs-meta: #777777;
  --rs-faint: #999999;
  --rs-text-inverse: #ffffff;
  --rs-surface: #ffffff;
  --rs-field: #ffffff;
  --rs-panel-bg: #f6f4ee;
  --rs-paper-old: #f4eee1;
  --rs-divider: #e5e5e5;
  --rs-border: #e5e5e5;
  --rs-rule: #e6ddc8;
  --rs-line: rgba(0, 0, 0, 0.12);
  --rs-line-2: rgba(0, 0, 0, 0.05);
  --rs-accent: #000000;
  --rs-highlight: #b8860b;
  --rs-highlight-soft: #fdf0c8;
  --rs-highlight-strong: #ffd34d;
  --rs-danger: #b3261e;
  --rs-link: #1a5fb4;
  --rs-font-body: 'Work Sans', system-ui, sans-serif;
  --rs-font-heading: 'Playfair Display', Georgia, serif;
  --rs-font-ui: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --rs-font-mono: 'Fragment Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Filled by viewer.js from snapshot.palette. Default = blue. The outline's
   * own colour: data, not dress, which is why these keep their own name. */
  --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'] {
  /* No chrome values of its own any more: a dark Style carries its own
   * furniture (Dark Hyperliner, Dark Tufte, Dark Folio each set the Panels,
   * Panel rules and Panel ink keys), and a second opinion on them here was
   * the one `--sc-*` ground the page kept after the Style took the rest. */
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--rs-panel-bg);
  color: var(--rs-text);
  font-family: var(--rs-font-ui);
  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(--rs-meta);
  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(--rs-panel-bg);
  border-right: 1px solid var(--rs-divider);
  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(--rs-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(--rs-font-ui);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
  flex: 1;
}

/* The author's wordmark — the press's mark at the head of the sidebar, above
 * the pill that names the site (spec/current/publishing.md §Sidebar). One
 * line tall; the stored aspect (inline, validated) gives it its width, and a
 * mark wider than the sidebar is held to it. An INKED mark is a mask in the
 * page's ink — the mechanism the app's own logotype uses — so a monochrome
 * file follows a dark Style with no second file; a mark drawn as itself is
 * an <img>. */
.sb-wordmark {
  display: block;
  height: 28px;
  width: auto;
  max-width: calc(100% - 28px);
  margin: 6px 14px 14px;
}
.sb-wordmark-ink {
  /* The mark's url, written inline by renderSiteWordmark from a validated
   * hash; `none` here so the name is declared where it is read. */
  --wm-src: none;
  background-color: var(--rs-text);
  -webkit-mask-image: var(--wm-src);
  mask-image: var(--wm-src);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: left center;
  mask-position: left center;
}

/* The site's tagline — the outline's subtitle, one muted line under the
 * pill (spec/current/publishing.md §Sidebar). Clipped to its line with the
 * whole text on the title: a tagline is a sentence, and a second line of it
 * under the pill reads as a paragraph. With a tagline the pill gives up most
 * of its bottom margin so the two read as one head; without one nothing
 * moves. */
.sb-outline-head:has(.sb-outline-sub) .sb-outline-row { margin-bottom: 6px; }
.sb-outline-sub {
  margin: 0 0 16px;
  padding: 0 14px;
  font-family: var(--rs-font-ui);
  font-size: 13px;
  line-height: 1.35;
  color: var(--rs-meta);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ─── 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(--rs-line-2);
  background: var(--rs-panel-bg);
  font-size: 12px;
  color: var(--rs-meta);
  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(--rs-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(--rs-paper-old);
  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(--rs-line);
  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(--rs-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(--rs-font-ui);
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--rs-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(--rs-font-ui);
  font-size: 0.75rem;
  color: var(--rs-meta);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}

/* ─── Card icons — the `icon` dressing (card-icon.js) ────────────────────
 * A small mark at the left of a card's name wherever the name is a
 * signpost: a sidebar row, a Site band link, a shelf card, a turn link.
 * One em square, so it scales with each place's own type; a glyph is that
 * type; a picture as drawn is an <img>; an inked picture is a mask in
 * `currentColor`, so it follows the row's ink — selected, hovered, a dark
 * Style — with no second file (the wordmark's mechanism). */
.card-icon {
  display: inline-block;
  flex: 0 0 auto;
  width: 1em;
  height: 1em;
  margin-right: 0.4em;
  vertical-align: -0.15em;
}
.card-icon-img {
  object-fit: contain;
  border-radius: 2px;
}
.card-icon-ink {
  /* The mark's url, written inline by renderCardIcon from a gated src;
   * `none` here so the name is declared where it is read. */
  --ci-src: none;
  background-color: currentColor;
  -webkit-mask-image: var(--ci-src);
  mask-image: var(--ci-src);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}
.card-icon-glyph {
  width: auto;
  min-width: 1em;
  text-align: center;
  line-height: 1;
  font-style: normal;
}

/* 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, and has no block
   padding, so it is the font's ascender to descender). 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 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(--rs-meta);
  border-bottom: 1px dotted var(--rs-meta);
  cursor: default;
}
/* A pending link — a card its author named and has not written yet. The
   editor's chip (reading-surface.css): purple on a wash of itself, with
   nothing to open here. The palette is not loaded on a published page, so
   the purple is spelled as its fallback. */
.body-card .body-content .card-link-pending {
  color: var(--pal-purple, #7508b9);
  border-bottom: 0;
  background: color-mix(in srgb, var(--pal-purple, #7508b9) 11%, transparent);
  border-radius: 5px;
  padding: 0 0.5em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
:root[data-rs-appearance='dark'] .body-card .body-content .card-link-pending {
  color: color-mix(in srgb, var(--pal-purple, #7508b9) 55%, white);
  background: color-mix(in srgb, var(--pal-purple, #7508b9) 30%, transparent);
}

/* 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(--rs-card-bg, #faf8f0);
}
.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(--rs-meta);
  font-size: 0.82em;
  color: var(--rs-meta);
}
.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 {
  /* Named, because a sticky descendant has to know them: Chrome pins a
     sticky box at the scroller's PADDING edge, not its border edge, so the
     table figure's header pins 56px down and rows show through the band
     above it unless it reaches back by exactly this much (§ Table figure). */
  --pane-pad-top: 56px;
  --pane-pad-x: clamp(28px, 5vw, 64px);
  padding: var(--pane-pad-top) var(--pane-pad-x) 72px;
  background: var(--rs-card-bg, #faf8f0);
  /* ── 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;
}
/* The paper: what a pane holds. A `bleed` figure and a cover head reach
 * the paper's edge by this box's width — the measure is left-aligned
 * inside the pane, so only the pane's own box can say where its edge is,
 * and as an inline-size query container it says it in cqw. The WRAPPER
 * rather than the pane: a scroll container's cqw is read without its
 * scrollbar taken out (Chrome, 2026-10-01), so a figure sized by the pane
 * overshot by the bar's width and the pane grew a second one. */
.body-pane > .paper {
  container-type: inline-size;
}
.body-pane.has-card-bg {
  background-color: var(--rs-card-bg, #faf8f0);
  background-image:
    var(--card-grid-layer, none, none),
    linear-gradient(color-mix(in srgb, var(--rs-card-bg, #faf8f0) var(--card-bg-veil, 65%), transparent) 0 0),
    var(--card-bg-image, none);
  background-size:
    var(--card-grid-pitch, 1.75rem) var(--card-grid-pitch, 1.75rem),
    var(--card-grid-pitch, 1.75rem) var(--card-grid-pitch, 1.75rem),
    cover, cover;
  background-position: center top, center top, center, var(--card-bg-pos, center);
  background-repeat: repeat, repeat, no-repeat, no-repeat;
  background-blend-mode: normal, normal, 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).
 * The image is the FOURTH layer: two ruling slots, the veil, then it. */
.body-pane.has-card-bg[data-bg-blend='multiply'] { background-blend-mode: normal, normal, normal, multiply; }
.body-pane.has-card-bg[data-bg-blend='screen'] { background-blend-mode: normal, normal, normal, screen; }
.body-pane.has-card-bg[data-bg-blend='overlay'] { background-blend-mode: normal, normal, normal, overlay; }
.body-pane.has-card-bg[data-bg-blend='soft-light'] { background-blend-mode: normal, normal, normal, soft-light; }
.body-pane.has-card-bg[data-bg-blend='luminosity'] { background-blend-mode: normal, normal, normal, luminosity; }
:root[data-rs-appearance='dark'] .body-pane.has-card-bg[data-bg-blend='multiply'] { background-blend-mode: normal, normal, normal, screen; }
:root[data-rs-appearance='dark'] .body-pane.has-card-bg[data-bg-blend='screen'] { background-blend-mode: normal, normal, normal, multiply; }
/* ── The ruling — a lattice printed on the paper ────────────────────────
 * Mirrors the editor frame (src/editor/CardEditor.svelte § The ruling): a
 * Style's `card-grid` names a KIND, stamped on the root as data-rs-grid by
 * applyStyle; ink and pitch are its knobs. Two image slots at the top of
 * the pane's background lists, `none, none` unless a kind is named, so the
 * wash's layers after them never shift. The pane is the published card's
 * paper, which is where the card's ruling goes. */
.body-pane {
  background-image: var(--card-grid-layer, none, none);
  background-size: var(--card-grid-pitch, 1.75rem) var(--card-grid-pitch, 1.75rem);
  background-position: center top;
  background-repeat: repeat;
}
[data-rs-grid] .body-pane {
  --card-grid-ink: var(--rs-card-grid-ink, rgba(0, 0, 0, 0.12));
  --card-grid-pitch: var(--rs-card-grid-size, 1.75rem);
}
[data-rs-grid='dots'] .body-pane {
  --card-grid-layer:
    radial-gradient(circle at center,
      var(--card-grid-ink) calc(var(--card-grid-pitch) * 0.045),
      transparent calc(var(--card-grid-pitch) * 0.045 + 0.6px)),
    none;
}
[data-rs-grid='lines'] .body-pane {
  --card-grid-layer:
    linear-gradient(to bottom, transparent calc(100% - 1px), var(--card-grid-ink) calc(100% - 1px)),
    none;
}
[data-rs-grid='squares'] .body-pane {
  --card-grid-layer:
    linear-gradient(to bottom, transparent calc(100% - 1px), var(--card-grid-ink) calc(100% - 1px)),
    linear-gradient(to right, transparent calc(100% - 1px), var(--card-grid-ink) calc(100% - 1px));
}

.body-header {
  max-width: 720px;
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-bottom: 28px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--rs-divider);
  /* 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. A cover's head wears the same
 * declaration, so the Style's title knobs land there too. */
.body-header .body-title,
.cover-head .body-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
  border-bottom: none;
  font-family: var(--rs-font-heading);
  /* 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;
}

/* ── The cover (layout: 'cover') ──────────────────────────────────────
 * The title and the dek over the card's wash at the pane's full width,
 * the prose below at the measure (card-types.md §Layouts). Arranged, not
 * painted: the head is the pane's own ground — the wash where the card
 * wears one, the paper where it does not — reached by the pane's own
 * padding, and the headline wears the Style's Card title knobs through
 * the header's rule above. The editor draws the same head on its title
 * row (CardEditor.svelte § The cover). */
.body-header.body-header-cover {
  max-width: none;
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
  justify-content: space-between;
}
.cover-head {
  margin: calc(-1 * var(--pane-pad-top, 56px)) calc(-1 * var(--pane-pad-x, 28px)) 32px;
  padding: min(40vh, 380px) var(--pane-pad-x, 28px) 28px;
  border-bottom: 1px solid var(--rs-divider);
}
.body-header-cover + .cover-head {
  margin-top: 0;
  padding-top: min(30vh, 300px);
}
.cover-head .body-title {
  margin: 0;
  max-width: 960px;
  font-size: var(--rs-title-size, clamp(34px, 5.2vw, 60px));
  line-height: var(--rs-title-line-height, 1.08);
}
.cover-head .body-sub {
  max-width: var(--rs-measure, 720px);
  margin: 12px 0 0;
  font-size: 1.2rem;
}

.back-btn {
  display: none;
  border: 1px solid var(--rs-divider);
  background: transparent;
  color: var(--rs-text);
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-family: var(--rs-font-ui);
  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(--rs-divider);
  background: var(--rs-surface);
  color: var(--rs-meta);
  border-radius: 999px;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  font-family: var(--rs-font-ui);
  font-size: 0.7rem;
  line-height: 1;
}
.style-pub-trigger:hover { background: var(--rs-text); color: var(--rs-text-inverse); border-color: var(--rs-text); }
.style-pub-trigger svg { display: block; }
.style-pub-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 60;
  min-width: 11rem;
  /* The page's own ground, so a dark reading Style darkens the menu with it
     (:root[data-rs-appearance='dark']) — a fixed #fff left its light text
     on white. */
  background: var(--rs-surface);
  border: 1px solid var(--rs-divider);
  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(--rs-font-ui);
  font-size: 0.82rem;
  color: var(--rs-text);
  cursor: pointer;
  text-align: left;
}
.style-pub-item:hover { background: color-mix(in srgb, var(--rs-text) 8%, transparent); }
.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); }
/* Links in the outline's colour (schema.ts `link-ink`, stamped by applyStyle
 * as data-rs-link-ink): the card re-points --rs-link at the outline's own
 * colour, which on this page is --sc-palette-bg, so every link consumer
 * below follows. Mirrors the editor frame (src/editor/CardEditor.svelte
 * § Links in the outline's colour). */
[data-rs-link-ink='outline'] .body-card {
  --rs-link: var(--sc-palette-bg, var(--rs-accent, inherit));
}
.body-card .body-content {
  font-family: var(--rs-font-body);
  font-size: var(--rs-font-size, 1rem);
  line-height: var(--rs-line-height, 1.7);
  color: var(--rs-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, .clear-break) + p { text-indent: 0; }
/* The clear break (`===`, renderers/clear-break.js): ends every float above
 * it, so what follows starts below the lowest wrapped figure. It draws
 * nothing else — the editor's hairline is the author's handle, not ink. */
.body-content .clear-break { clear: both; }
.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);
  color: var(--rs-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);
  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) 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); }
/* 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);
  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;
}

/* ── The note (notes.md) — `^[…]` and `^[{-} …]` ──────────────────────
 * The editor's atom, drawn by renderers/notes.js: a marker where the thought
 * is and an aside holding the words, ONE DOM, and the destination decides
 * which shows. Mirrors src/editor/reading-surface.css § The numbered note
 * rule for rule; keep the two in step. The foot list is the page run's
 * `ol.citation-footnotes` (renderers/citations.js), a sidenote's row and a
 * displaced citation's in one sequence. */
.body-content .note {
  /* The atom is the marker's width only: the aside is taken out of flow
   * (margin) or hidden (foot), never inline. */
  white-space: normal;
}
.body-content .note-marker {
  font-size: 0.75em;
  line-height: 0;
  color: var(--rs-muted);
}
/* HIDDEN unless the margin is asked for, not shown unless the foot is: the
 * foot is the fail-safe. A style stored before the destination existed, or
 * a typo in a custom one, lands on the footnote — never on an aside
 * stranded in the text flow. */
.body-content .note-aside {
  display: none;
  font-size: var(--rs-marginnote-font-size, 0.85rem);
  color: var(--rs-marginnote-color, var(--rs-muted));
  line-height: 1.4;
  /* A paragraph's first-line indent must not reach into the margin. */
  text-indent: 0;
}
.body-content .note-aside-number {
  font-size: 0.75em;
  vertical-align: super;
  line-height: 0;
  margin-right: 0.25em;
}
/* Margin destination: the aside floats beside its anchor on the formula the
 * unnumbered margin note uses — one column, one width — and the foot list
 * stands down, so one page never prints its notes twice. */
[data-rs-notes='margin'] .body-content .note-aside,
.body-content .note-margin .note-aside {
  display: block;
  float: right;
  clear: right;
  width: var(--rs-marginnote-width, 16rem);
  margin: 0 calc(-1 * var(--rs-marginnote-width, 16rem) - 1.5rem) 0.6rem 0;
}
[data-rs-notes='margin'] .body-content ol.citation-footnotes { display: none; }

/* The UNNUMBERED margin note. No number and no place in the sequence, so it
 * never follows the destination knob: a margin note is in the margin. What it
 * wears instead of a number is the Style's glyph (--rs-marginnote-sigil,
 * blank means none) — at the aside's head, and as its marker, INLINE on the
 * baseline in the note's own ink rather than a superscript: a margin note has
 * no number to raise, and the glyph pairs anchor and note as a number pairs a
 * sidenote and its entry. */
.body-content .note-margin .note-aside-number::before {
  content: var(--rs-marginnote-sigil, '');
}
.body-content .note-margin .note-marker {
  font-size: 1em;
  line-height: inherit;
  vertical-align: baseline;
  color: var(--rs-marginnote-color, var(--rs-muted));
  margin: 0 0.1em;
}
.body-content .note-margin .note-marker::before {
  content: var(--rs-marginnote-sigil, '');
}

/* ── PORTRAIT: the page has no right margin ───────────────────────────
 * `data-rs-portrait` on the root (viewer.js syncPortrait) — set when the
 * viewport is under 720px, under every Style; the app's rule is the card's
 * own width (lib/styles/orientation.ts).
 * Mirrors the editor's `.editor.portrait` rules (src/editor/reading-
 * surface.css § PORTRAIT) for what this page renders: the measure widens
 * to the pane, a full-width block has no column to break into, a numbered
 * note prints at the foot whatever the Style asked for, the margin note
 * folds inline behind its glyph, and the legacy margin-note marks stop
 * floating and set as an indented aside where they stand. Last in the
 * cascade of the note rules, so it wins over the margin destination. */
[data-rs-portrait] .body-card { max-width: none; }
[data-rs-portrait] .body-content .fullwidth { width: auto; }
[data-rs-portrait] .body-content .note-aside { display: none; }
[data-rs-portrait][data-rs-notes='margin'] .body-content ol.citation-footnotes { display: block; }
/* The fold. The glyph is the disclosure: viewer.js reads `--note-fold` off
 * the note to know it is one here (the stylesheet decides, the script asks)
 * and toggles `note-open`. Open, the aside sets as an indented block inside
 * the paragraph under the same glyph, and the marker goes to full ink.
 * Nothing moves and nothing rotates — the reader has to find the same mark
 * in the margin when the page widens again. */
[data-rs-portrait] .body-content .note-margin { --note-fold: 1; }
[data-rs-portrait] .body-content .note-margin .note-marker {
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none; /* Safari reads only the prefixed form */
}
[data-rs-portrait] .body-content .note-margin.note-open .note-marker { color: var(--rs-text); }
[data-rs-portrait] .body-content .note-margin.note-open .note-aside {
  display: block;
  float: none;
  width: auto;
  margin: 0.35em 0 0.5em;
  padding-left: 0.6rem;
  border-left: 2px solid var(--rs-border);
}
[data-rs-portrait] .body-content :is(.marginnote, .sidenote) {
  float: none;
  width: auto;
  margin: 0.5rem 0;
  border-left: 2px solid var(--rs-border);
  padding-left: 0.6rem;
}

/* 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(--rs-divider);
  border-radius: 3px;
}
.body-content .detail-map-sea {
  fill: var(--rs-card-bg, #faf8f0);
}
.body-content .detail-map-grat {
  stroke: var(--rs-divider);
  stroke-width: 1.5;
}
.body-content .detail-map-land {
  fill: var(--rs-divider);
  stroke: var(--rs-meta);
  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(--rs-meta);
  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,
.cover-head .body-title {
  font-family: var(--rs-font-title, var(--rs-font-heading));
  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(--rs-meta);
  font-size: 14px;
  margin-top: 32px;
}

.body-card { max-width: 720px; }
/* THE MARGIN COLUMN, made room for (notes.md §The two columns). A reading
 * surface is two columns — the measure and the margin a note sets in — and
 * the rule above, which outranks the Style's measure by coming later, leaves
 * no room for the second: a margin note floated past a card that fills the
 * paper lands off the pane. So a page holding anything for the margin — a
 * margin note, or under margin placement any note's or footnote's aside —
 * wears the Style's own measure, capped where every page is, and the margin
 * column takes 40% of it beside it, as in the editor. Every card on that page
 * alike, so a read-on stack keeps one column; a page with nothing for the
 * margin is set as before. `:where` keeps this at the rule's own weight, so
 * the figure cards that set no measure (table, map, spread) and the portrait
 * posture's full width still win. */
:where(.paper:has(.note-margin)) .body-card,
:where([data-rs-notes='margin'] .paper:has(.note-aside)) .body-card {
  max-width: min(var(--rs-measure, 720px), 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(--rs-divider);
}
/* 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(--rs-font-ui);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--rs-text);
  background: var(--rs-surface);
  border: 1px solid var(--rs-divider);
  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(--rs-text);
  color: #fff;
  border-color: var(--rs-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(--rs-font-ui);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--rs-meta);
}

.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(--rs-meta);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.body-card .card-fields .field-val {
  margin: 0;
  color: var(--rs-text);
  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(--rs-meta);
  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; }
/* A web citation's preview: the page's poster at the details' right edge,
   a small cover the reader can open; the frame below clears it. */
.cite-idx-preview {
  float: right;
  display: block;
  width: 168px;
  height: 104px;
  margin: 0 0 8px 14px;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.04);
}
.cite-idx-preview-img { display: block; width: 100%; height: 100%; background-repeat: no-repeat; }
.cite-idx-frame { margin: 1rem 0 0; }
/* The frame starts at the text column — the 104px margin plus the 20px gap
   of the .cite-idx grid above it — level with the title and clear of the
   red rule's line, as the editor's footer slot is (2026-09-07). */
.body-card.citation-index-card .cite-idx-frame { margin-left: 124px; }
.cite-idx-frame .media-el-embed { width: 100%; height: 60vh; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 6px; display: block; }
.cite-idx-frame .media-open-link { display: inline-block; margin-top: 0.35rem; font-size: 0.85rem; }
.cite-idx-title {
  margin: 0 0 0.5rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--cite-rule-blue);
  font-family: var(--rs-font-heading);
  font-size: 1.6rem;
  line-height: 1.2;
  font-weight: 600;
  color: var(--rs-text);
}
.cite-idx-details {
  margin: 0.6rem 0 0;
  font-family: var(--rs-font-heading);
  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(--rs-meta);
}
.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;
  }
  .body-card.citation-index-card .cite-idx-frame {
    margin-left: 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);
  --tf-rule-strong: var(--rs-meta);
  --tf-head-bg: var(--rs-surface);
  --tf-gutter-ink: var(--rs-meta);
  --tf-ink: var(--rs-text);
  --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;
  /* The pane's top edge, not its padding edge — see `.body-pane`. Pinned at
     `top: 0` the header floated 56px down with rows scrolling past above it. */
  top: calc(-1 * var(--pane-pad-top, 0px));
  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 {
  background: var(--tf-zebra);
}
/* The row gutter is sticky, so the cells scroll UNDER it — and the stripe is
   translucent (a `transparent` mix). Laid straight on, it made every striped
   row's number a window onto the cell passing beneath. The stripe goes over
   the gutter's own opaque ground instead. */
.table-figure .tf-grid tbody tr:nth-child(even) .tf-rownum {
  background: linear-gradient(var(--tf-zebra), var(--tf-zebra)), var(--tf-head-bg);
}
.table-figure .tf-grid tbody tr:last-child :is(td, th) { border-bottom: none; }
/* Merged cells (a column set to merge repeated values): the run reads as one
   cell. A continuation's ink goes; the rule between two rows of a run goes
   (the upper cell's bottom rule — this grid draws rules below cells); and
   every cell of a run wears its HEAD row's stripe, so the zebra does not cut
   one tall cell into bands. `tr > td` matches the stripe rule's weight. */
.table-figure .tf-grid tbody tr > td.tf-mcont { color: transparent; }
.table-figure .tf-grid tbody tr > td.tf-mnext { border-bottom-color: transparent; }
.table-figure .tf-grid tbody tr > td.tf-mp { background: transparent; }
.table-figure .tf-grid tbody tr > td.tf-mz { background: var(--tf-zebra); }
/* 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;
  /* The pane's left edge, for the same reason the header reaches for its top. */
  left: calc(-1 * var(--pane-pad-x, 0px));
  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; }
/* A value's weight (cell-fills.ts: `bold` beside a colour), as the app draws it. */
.table-figure .tf-grid td.tf-bold { font-weight: 600; }
.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(--rs-text);
  background: var(--rs-surface);
  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(--rs-meta);
  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(--rs-meta);
}
/* 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;
}

/* Search card (spec/refactor/published-search.md): the answer its author
   published — never the live query. The list face reads as the app's search
   card reads (SearchHits.svelte: the kind, the title, the opening of the
   text, the chips that say why), in the reading surface's own ink; the table
   face is the table figure above, its rows opening the cards they name. */
.body-card.search-card {
  --sf-rule: var(--rs-border);
  --sf-muted: var(--rs-meta);
  --sf-wash: color-mix(in srgb, var(--sf-rule) 40%, transparent);
}
.search-figure { margin: 0; }
.search-figure .sf-hits { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--sf-rule); }
.search-figure .sf-hit {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 6px 9px;
  border-bottom: 1px solid var(--sf-rule);
  cursor: pointer;
}
.search-figure .sf-hit:hover { background: var(--sf-wash); }
.search-figure .sf-hit-row { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.search-figure a.sf-title { color: inherit; text-decoration: none; }
.search-figure a.sf-title:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 2px; }
/* The snippet's own links are inert, as in the sidebar's previews: the hit
   is the target. Its code spans read as code. */
.search-figure .sf-snippet a { pointer-events: none; }
.search-figure .sf-snippet code {
  font-size: 0.9em;
  background: var(--sf-wash);
  border-radius: 3px;
  padding: 0 3px;
}
.search-figure .sf-kind {
  flex: none;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--sf-muted);
  border: 1px solid var(--sf-rule);
  border-radius: 4px;
  padding: 0 5px;
}
.search-figure .sf-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A hit in another of the author's outlines (published-search.md §U3) is a
   link to its own page: the outline it is in rides after the title, quieter,
   and the arrow says the click leaves this page. */
.search-figure .sf-outline {
  flex: none;
  font-size: 12px;
  color: var(--sf-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 45%;
}
.search-figure .sf-outline::before { content: '↗ '; }
.search-figure .sf-snippet {
  color: var(--sf-muted);
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
.search-figure .sf-why { display: flex; flex-wrap: wrap; gap: 4px; }
.search-figure .sf-why-chip {
  font-size: 10px;
  color: var(--sf-muted);
  background: var(--sf-wash);
  border-radius: 3px;
  padding: 0 5px;
}
.search-figure .sf-count,
.search-figure .sf-note { font-size: 12px; color: var(--sf-muted); margin: 8px 0 0; }
/* The card already holds the measure (and gives it up in portrait); a list
   does not widen the card the way a grid does, so its caption needs none of
   its own. */
.search-figure .sf-caption { margin-top: 20px; }
/* The reader's box (published-search.md U2): the scope as the finder's own
   chip — × where the author freed it — and the reader's line beside it. One
   bordered field, so the chip reads as part of the question. The input is
   16px at the least: iOS zooms the page into any smaller field it focuses. */
.search-figure .sf-reader {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  margin: 0 0 12px;
  padding: 4px 6px 4px 8px;
  border: 1px solid var(--sf-rule);
  border-radius: 8px;
}
.search-figure .sf-reader:focus-within { border-color: color-mix(in srgb, currentColor 45%, var(--sf-rule)); }
.search-figure .sf-scopes { display: inline-flex; align-items: center; gap: 2px; flex: none; max-width: 100%; }
.search-figure .sf-scope {
  font-size: 12px;
  color: var(--sf-muted);
  background: var(--sf-wash);
  border-radius: 999px;
  padding: 1px 9px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.search-figure .sf-scope + .sf-scope { margin-left: 4px; }
.search-figure a.sf-scope-name { color: inherit; font-weight: 600; text-decoration: none; }
.search-figure a.sf-scope-name:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; border-radius: 2px; }
.search-figure button.sf-scope-off {
  font: inherit;
  font-size: 12px;
  color: var(--sf-muted);
  background: transparent;
  border: 1px dashed var(--sf-rule);
  border-radius: 999px;
  padding: 1px 9px;
  margin-left: 4px;
  white-space: nowrap;
  cursor: pointer;
}
.search-figure button.sf-scope-off:hover { color: inherit; background: var(--sf-wash); }
.search-figure .sf-scope-x,
.search-figure .sf-reader-clear {
  font: inherit;
  color: var(--sf-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 4px;
}
.search-figure .sf-scope-x { font-size: 15px; line-height: 1; }
.search-figure .sf-reader-clear { font-size: 12px; }
.search-figure .sf-scope-x:hover,
.search-figure .sf-reader-clear:hover { color: inherit; background: var(--sf-wash); }
.search-figure .sf-reader-input {
  flex: 1 1 6em;
  min-width: 0;
  font: inherit;
  font-size: max(16px, 1em);
  color: inherit;
  background: transparent;
  border: 0;
  outline: none;
  padding: 4px 2px;
  -webkit-appearance: none;
  appearance: none;
}
.search-figure .sf-reader-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.search-figure .sf-reader-note code { font-size: 0.95em; }
/* A table-faced card's box sits above its grid, outside the figure the grid
   draws. */
.search-figure.sf-lead { margin-bottom: 0; }
/* A search grid's row opens the card it names (renderers/table.js `targets`):
   the whole row takes the click, and its first cell is the link the keyboard
   reaches. */
.table-figure .tf-grid tbody tr.tf-link { cursor: pointer; }
.table-figure .tf-grid tbody tr.tf-link:hover > td { background: color-mix(in srgb, var(--tf-rule) 45%, transparent); }
.table-figure .tf-grid .tf-hit {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 2px;
}

/* 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);
  /* 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; }
/* The frame and the notes panel together — the panel's positioning context. */
.map-figure .mf-stage { position: relative; }
.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(--rs-surface);
}
.map-figure .mf-note {
  margin: 0;
  padding: 12px 16px;
  font-size: 12px;
  color: var(--rs-meta);
  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, #f2eee1); }
.map-figure .mland { fill: var(--rs-map-land, #c7bda0); stroke: var(--rs-map-coast, #5a4f3d); 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, #5a4f3d); 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, #b3301b); 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, #b3301b); 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, #77735f); }
.map-figure .mterminus { fill: var(--rs-map-mark, #1f1d18); }
.map-figure .mport { fill: var(--rs-map-mark, #1f1d18); opacity: 0.5; stroke: var(--rs-map-halo, #f6f2e4); 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, #1f1d18);
  font-family: var(--rs-font-map-label, 'Fragment Mono', ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: calc(var(--rs-map-label-size, 9.5px) * var(--mscale, 1) * var(--mlabel-fit, 1));
  font-weight: var(--rs-map-label-weight, 500);
  letter-spacing: var(--rs-map-label-tracking, 0.06em);
  text-transform: var(--rs-map-label-case, uppercase);
  font-variant-caps: var(--rs-map-label-case, uppercase);
}
/* Place names carry a halo so they stay legible over land or sea. */
.map-figure .mleglabel {
  paint-order: stroke;
  stroke: var(--rs-map-halo, #f6f2e4);
  stroke-width: calc(var(--rs-map-halo-width, 3px) * var(--mscale, 1));
  stroke-linejoin: round;
}
.map-figure .mshipdot { stroke: var(--rs-map-mark-edge, #fbf9f1); 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; }
.map-figure .mshipicon { pointer-events: none; }
.map-figure .mshipicon-hit { pointer-events: auto; cursor: pointer; }
/* 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, #55523f);
  font-family: var(--rs-font-map-water, 'Newsreader', Georgia, 'Times New Roman', Times, serif);
  /* 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, 14px) * var(--mscale, 1) * var(--mlabel-fit, 1));
  font-weight: var(--rs-map-water-weight, 400);
  font-style: var(--rs-map-water-slope, italic);
  letter-spacing: var(--rs-map-water-tracking, 0);
  /* Level with its ship (map-labels.js VOYAGE_OFF = 0; the plan's box follows). */
  dominant-baseline: central;
}

/* Camera controls — zoom in, zoom out, fit. Inside the frame and over the
   scene, where the editor's own sit, because they act ON the picture rather
   than beside it; the transport row below is about TIME and stays outside.

   They exist mainly for the readers who cannot use the gestures: wheel, drag
   and pinch have always worked, but a tablet has no wheel, and a pinch that
   wanders has no way home. Fit is the load-bearing one — the other two make
   the camera discoverable at all.

   `position: absolute` over `.mf-frame`'s `relative`, top-right, clear of the
   labels the scene writes at its left edge. A touch target of 30px: below the
   44px ideal, because this furniture sits ON a map the reader is also dragging
   and a bigger button eats the thing it controls — so they are spaced instead,
   which keeps the mis-tap rate down without taking the corner. */
.map-figure .mf-camera {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  z-index: 2;
}
.map-figure .mf-cam {
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--mf-rule);
  border-radius: 3px;
  /* Not fully opaque: the reader should be able to see what is under the
     corner they are about to zoom into. */
  background: color-mix(in srgb, var(--rs-surface) 88%, transparent);
  color: var(--rs-text);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  /* iOS paints its own grey flash over a tap here, on top of a map. */
  -webkit-tap-highlight-color: transparent;
  /* No double-tap-to-zoom on the buttons: two quick taps on + are two zooms,
     not a page zoom that swallows the second. */
  touch-action: manipulation;
  /* + and − are held to keep zooming: a long press must not select the
     glyph or raise iOS's callout. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* Hover only where there is one: on a phone a tap leaves :hover stuck on. */
@media (hover: hover) {
  .map-figure .mf-cam:hover { border-color: var(--rs-text); }
}
.map-figure .mf-cam:active { background: var(--rs-surface); }
.map-figure .mf-cam:focus-visible {
  outline: 2px solid var(--rs-accent, #b8860b);
  outline-offset: 1px;
}

/* 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(--rs-surface);
  color: var(--rs-text);
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
}
.map-figure .mf-play:hover { border-color: var(--rs-text); }
/* The timeline's ruler — the panel's own (MapPanel `.maxis`): the range input
   is kept for the gesture and the keyboard and drawn as nothing but its thumb,
   over an <svg> the renderer draws — a hairline in the ink cut at every month
   by a break one line-width long, a tick and the year at every January, and
   the run so far under it in gold at three times the line's weight. */
.map-figure .mf-scrub-wrap { flex: 1; position: relative; height: 28px; min-width: 0; margin-top: 7px; }
.map-figure .mf-axis { position: absolute; inset: 0; display: block; overflow: visible; pointer-events: none; }
.map-figure .mf-axis-run { stroke: var(--rs-highlight, #b8860b); stroke-width: 3; shape-rendering: crispEdges; }
.map-figure .mf-axis-line, .map-figure .mf-axis-year { stroke: var(--rs-text); stroke-width: 1; shape-rendering: crispEdges; }
.map-figure .mf-axis-label { fill: var(--rs-text); font-size: 0.62rem; text-anchor: middle; font-variant-numeric: tabular-nums; }
/* The fleet's density under the ruler (MapPanel `.maxis-dens`): bars in the
   ink at low opacity, the most at once named at the right end with a paper
   halo so it reads over the bars it names. The wrap's height is written
   inline by `transportHtml` — the track grows to hold the strip. */
.map-figure .mf-axis-dens rect { fill: var(--rs-text); fill-opacity: 0.18; shape-rendering: crispEdges; }
.map-figure .mf-axis-dens-max {
  fill: var(--rs-meta); font-size: 0.62rem; text-anchor: end; font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: var(--rs-surface); stroke-width: 3px; stroke-linejoin: round;
}
.map-figure .mf-scrub {
  position: absolute; left: 0; top: 0; width: 100%; height: 16px; margin: 0; padding: 0;
  -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer;
}
.map-figure .mf-scrub:focus-visible { outline: none; }
.map-figure .mf-scrub::-webkit-slider-runnable-track { height: 16px; background: transparent; border: 0; }
.map-figure .mf-scrub::-moz-range-track { height: 16px; background: transparent; border: 0; }
.map-figure .mf-scrub::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; margin-top: 2px; border-radius: 50%;
  background: var(--rs-text); border: 2px solid var(--rs-surface);
  box-shadow: 0 0 0 1px var(--rs-text);
}
.map-figure .mf-scrub::-moz-range-thumb {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--rs-text); border: 2px solid var(--rs-surface);
  box-shadow: 0 0 0 1px var(--rs-text);
}
.map-figure .mf-scrub:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 1px var(--rs-text), 0 0 0 4px var(--rs-highlight-soft, #fdf0c8); }
.map-figure .mf-scrub:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 1px var(--rs-text), 0 0 0 4px var(--rs-highlight-soft, #fdf0c8); }
.map-figure .mf-date {
  flex: none; min-width: 5.5rem; text-align: right;
  font-size: 0.8rem; line-height: 2rem; color: var(--rs-text);
  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. It stands clear
   of the camera column (8 + 30 + 8 px) so the buttons stay reachable mid-run. */
.map-figure .mf-hud {
  position: absolute; top: 8px; right: 46px; 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(--rs-meta);
  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(--rs-text);
  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(--rs-text); white-space: pre-wrap;
}
.map-figure .mf-hud-empty {
  margin: 0; font-size: 0.78rem; font-style: italic;
  color: var(--rs-meta);
}
/* The second group — the voyages the calendar cannot place, drawn whole and
   dimmed: a sub-head in the head's face without its ground, and names in the
   muted ink at a lighter weight, the dimmed tracks' own register. */
.map-figure .mf-hud-group {
  margin: 0.7rem 0 0.05rem;
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em;
  font-weight: 600; color: var(--rs-meta);
}
.map-figure .mf-hud-name.mf-undated {
  margin-top: 0.3rem; font-weight: 600;
  color: var(--rs-meta);
}

/* On a phone the panel would cover most of a map already cut to the column's
   width, so it leaves the map and reads under it, full width, its notes whole
   — below the timeline, which belongs to the map. The stage steps aside
   (`display: contents`) so the frame and the panel become the figure's own
   flex items and can be put in that order: the frame, the key and the
   timeline first, then the panel, then everything else. A phone on its side
   is wider than 720 px but no taller than a hand: it takes the same layout. */
@media (max-width: 720px), (pointer: coarse) and (max-height: 540px) {
  .map-figure { display: flex; flex-direction: column; }
  .map-figure .mf-stage { display: contents; }
  .map-figure .mf-hud { order: 1; }
  .map-figure > :not(.mf-stage):not(.mf-legend):not(.mf-transport) { order: 2; }
  /* A phone's map can be as short as its width ÷ 2.6 (≈130 px): the column
     of four tightens to fit it rather than turning into a row. */
  .map-figure .mf-camera { top: 6px; right: 6px; gap: 3px; }
  .map-figure .mf-cam { width: 28px; height: 28px; }
  .map-figure .mf-hud {
    position: static;
    width: auto; max-width: none; max-height: none;
    margin-top: 8px;
    background: var(--rs-surface);
    backdrop-filter: none;
    box-shadow: none;
    border-radius: 3px;
  }
}

/* Full screen: the map and its timeline, nothing else — the browser's own
   full screen where it has one, else `.mf-fs` pins the figure over the page
   (an iPhone gives element full screen to video only) and `.mf-fs-lock` stops
   the page scrolling under it. The map FILLS what the timeline leaves: the
   svg takes the frame's whole box and the camera takes the box's shape
   (map.js `fitFullscreenView`), so there are no sea-coloured bands. The notes
   panel, the colour key and the caption stay on the page; the credits, a
   licence obligation, shrink to one small line under the timeline. No touch
   in here may zoom the page: a pinch is the map's (map.js cancels iOS's own
   gesture events as well, which touch-action alone does not stop). */
.map-figure:fullscreen,
.map-figure.mf-fs {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; flex-direction: column;
  width: 100%; height: 100%;
  box-sizing: border-box;
  padding: env(safe-area-inset-top) max(8px, env(safe-area-inset-right))
           max(4px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
  background: var(--rs-card-bg, #faf8f0);
  overflow: hidden;
  touch-action: none;
}
.map-figure:fullscreen .mf-stage,
.map-figure.mf-fs .mf-stage {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
}
.map-figure:fullscreen .mf-frame,
.map-figure.mf-fs .mf-frame {
  flex: 1; min-height: 0;
  border-radius: 0;
}
.map-figure:fullscreen .mf-svg,
.map-figure.mf-fs .mf-svg {
  width: 100%; height: 100%;
  aspect-ratio: auto !important;   /* the inline ratio is the page's, not the screen's */
}
.map-figure:fullscreen .mf-hud,
.map-figure.mf-fs .mf-hud,
.map-figure:fullscreen .mf-legend,
.map-figure.mf-fs .mf-legend,
.map-figure:fullscreen > :not(.mf-stage):not(.mf-transport):not(.mf-credits),
.map-figure.mf-fs > :not(.mf-stage):not(.mf-transport):not(.mf-credits) { display: none; }
.map-figure:fullscreen .mf-transport,
.map-figure.mf-fs .mf-transport { flex: none; margin-top: 6px; }
.map-figure:fullscreen .mf-credits,
.map-figure.mf-fs .mf-credits { flex: none; }
.map-figure:fullscreen .tf-credits,
.map-figure.mf-fs .tf-credits {
  margin: 0; padding: 0; border: 0;
  font-size: 10px; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.map-figure:fullscreen .tf-credits-roll,
.map-figure.mf-fs .tf-credits-roll { display: inline; margin-right: 8px; }
html.mf-fs-lock, html.mf-fs-lock body { overflow: hidden; }

/* The colour key under the frame (a map coloured by a column): the column's
   name in the HUD head's voice, each value beside its swatch. The same swatch
   rides each HUD row, so a voyage can be found in the key from the panel. */
.map-figure .mf-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  margin-top: 8px;
  font-size: 0.72rem; color: var(--rs-text);
}
.map-figure .mf-legend-label {
  margin-right: 0.1rem;
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em;
  font-weight: 600; color: var(--rs-meta);
}
.map-figure .mf-legend-cat { display: inline-flex; align-items: center; gap: 0.3rem; }
.map-figure .mf-legend-other { color: var(--rs-meta); }
.map-figure .mf-swatch {
  display: inline-block; flex: none; width: 10px; height: 10px; border-radius: 50%;
  border: 1px solid var(--mf-rule); box-sizing: border-box;
}
.map-figure .mf-hud-name .mf-swatch { width: 9px; height: 9px; margin-right: 0.35rem; vertical-align: 0.05em; }

/* 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(--rs-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);
  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);
}
.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(--rs-font-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--rs-text-2);
}

.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(--rs-text-2);
  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); a cropped
 * image's box is the fourth, below. */
/* A CROPPED picture (`data-crop`) is a clipping box — the editor's
 * reading-surface.css twin, built here by applyEmbedLayoutStyles after the
 * markup mounts: the box wears the picture's words and the inline width and
 * takes the place rules below as a fourth host; its aspect is set inline
 * from the kept rectangle once the natural size is known; the <img> inside
 * is absolute and oversized by the crop's reciprocals, and must not clamp
 * itself. Blend still lands on the img, through the forwarding rules. */
.body-card .body-content .picture-crop {
  position: relative;
  display: block;
  overflow: hidden;
  max-width: 100%;
}
.body-card .body-content .picture-crop > img {
  position: absolute;
  display: block;
  max-width: none;
  height: auto;
  margin: 0;
}
.body-card .body-content img[data-place] { display: block; }
.body-card .body-content :is(img, p, .spread-figure, .media-figure, .picture-crop)[data-place='center'] {
  margin-left: auto;
  margin-right: auto;
}
.body-card .body-content :is(img, p, .spread-figure, .media-figure, .picture-crop)[data-place='right'] { margin-left: auto; }
.body-card .body-content :is(img, p, .spread-figure, .media-figure, .picture-crop)[data-place='wrap-left'] {
  float: left;
  margin: 0.25em 1em 0.5em 0;
}
.body-card .body-content :is(img, p, .spread-figure, .media-figure, .picture-crop)[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, .picture-crop)[data-place='wrap-left']:not([data-width]),
.body-card .body-content :is(p, .spread-figure, .picture-crop)[data-place='wrap-right']:not([data-width]) {
  width: 50%;
}
/* `bleed` — the figure reaches the paper's edge, where `center` keeps the
 * measure (the editor's reading-surface.css twin). The pane is the paper
 * and an inline-size query container, so 100cqw is its content width; the
 * first declaration is what a browser without container units gets. */
.body-card .body-content :is(img, p, .spread-figure, .media-figure, .picture-crop)[data-place='bleed'] {
  width: calc(100% + 2 * var(--pane-pad-x, 0px));
  width: calc(100cqw + 2 * var(--pane-pad-x, 0px));
  max-width: none;
  margin-left: calc(-1 * var(--pane-pad-x, 0px));
  margin-right: 0;
}
.body-card .body-content .media-figure[data-place='bleed'] .media-el { width: 100%; max-width: none; }
/* 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, a healed figure or a crop box forwards its data-blend to
 * the img inside. */
.body-card .body-content img[data-blend='multiply'],
.body-card .body-content :is(p, .spread-figure, .picture-crop)[data-blend='multiply'] img { mix-blend-mode: multiply; }
.body-card .body-content img[data-blend='screen'],
.body-card .body-content :is(p, .spread-figure, .picture-crop)[data-blend='screen'] img { mix-blend-mode: screen; }
.body-card .body-content img[data-blend='overlay'],
.body-card .body-content :is(p, .spread-figure, .picture-crop)[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, .picture-crop)[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, .picture-crop)[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, .picture-crop)[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, .picture-crop)[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, .picture-crop)[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, .picture-crop)[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, .picture-crop)[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/public/spatial-layout.js
 * (baked server-side, re-run here on a fold or a phone); 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(--rs-panel-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(--rs-panel-bg);
  border-bottom: 1px solid var(--rs-divider);
}

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

.spatial-canvas {
  position: relative;
}

/* ─── The card plane (publishing.md §The card plane) ───────────────────────
 * The route's front door when the author chose it: the editor's own plane,
 * mounted once (viewer.js `mountPlaneInto`) and moved into each new pane.
 * Its canvas is transparent face up, so the ground it shows is this pane's —
 * the page's panel ground, not a card's paper: the plane is the outline's
 * summary, not a card. No padding and no scroll: the plane pans itself. */
.body-pane.plane-pane {
  /* Nothing here is text: the faces are painted and the head holds one key. */
  user-select: none;
  -webkit-user-select: none;
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
  background: var(--rs-panel-bg);
  background-image: none;
}
.plane-host {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}
.plane-mount {
  position: absolute;
  inset: 0;
}
/* The way to the index, at compact width only (below). */
.plane-head {
  display: none;
  padding: 10px 14px;
  border-bottom: 1px solid var(--rs-divider);
}
.plane-index-btn,
.plane-return-btn {
  border: 1px solid var(--rs-divider);
  background: transparent;
  color: var(--rs-text);
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-family: var(--rs-font-ui);
  font-size: 13px;
}
.plane-return-btn {
  margin: 0 0 12px;
}
/* A card's Back goes to the cards, and the plane is a place the sidebar has
 * no row for — so the key shows at every width, not only on a phone. */
.app.has-plane .back-btn { display: inline-block; }
@media (max-width: 720px) {
  .app.showing-plane .sidebar { display: none; }
  .app.showing-plane .body-pane.plane-pane { display: flex; }
  .app.showing-plane .plane-head { display: block; }
}

.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(--rs-font-ui);
  color: var(--rs-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;
}
/* A cropped body picture (`{data-crop=T,R,B,L}`): not a background image but
 * a face under the scrim — the kept rectangle, covering — drawn by the same
 * three spans the editor's CroppedFace renders (.picture-face below). The
 * card isolates its stacking so the face's negative z-index lands above its
 * own paper and below its title; the scrim moves onto the face's own layer. */
.spatial-card.has-cropped-face {
  background-image: none;
  isolation: isolate;
}
.spatial-card.has-cropped-face .picture-face {
  z-index: -1;
}
.spatial-card.has-cropped-face .picture-face::after {
  content: '';
  position: absolute;
  inset: 0;
  background: 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%
  );
}
.spatial-card.has-bg-image .spatial-card-title {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

/* A FACE showing a cropped picture — the kept rectangle, then covering
 * (markdown.md §The block-layout vocabulary; src/components/CroppedFace.svelte
 * carries these rules word for word). The FRAME lies over its host and clips,
 * and is a size container; the KEPT BOX is centred in it at the kept
 * rectangle's aspect (`--kept-ar`, set by fitCropFaces once the picture has
 * loaded) and at least as wide and as tall as the frame — `cover`, said for a
 * box; the IMG inside is oversized and offset by embedCropGeometry, written
 * inline by cropFaceHtml, so the kept rectangle fills the kept box exactly. */
.picture-face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  container-type: size;
  pointer-events: none;
}
.picture-face-kept {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: calc(max(100cqw, 100cqh * var(--kept-ar, 1)) + 1px);
  aspect-ratio: var(--kept-ar, 1);
  overflow: hidden;
}
.picture-face-kept img {
  position: absolute;
  display: block;
  max-width: none;
  height: auto;
}
.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(--rs-meta);
  letter-spacing: 0.02em;
}

/* The selected card's read key — the editor's full-screen key
 * (CardSprite .card-fullscreen-btn), corner and all, opening the card's
 * page instead of its editor. Flipped to read against an image's scrim. */
.spatial-card-read {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  padding: 3px;
  background: rgba(255, 255, 255, 0.65);
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 4px;
  color: rgba(0, 0, 0, 0.55);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
  z-index: 2;
}
.spatial-card-read:hover {
  background: rgba(255, 255, 255, 0.95);
  color: rgba(0, 0, 0, 0.85);
  border-color: rgba(0, 0, 0, 0.18);
}
.spatial-card-read svg { width: 100%; height: 100%; }
.spatial-card.has-bg-image .spatial-card-read {
  background: rgba(0, 0, 0, 0.45);
  border-color: rgba(255, 255, 255, 0.2);
  color: rgba(255, 255, 255, 0.85);
}
.spatial-card.has-bg-image .spatial-card-read:hover {
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
}
/* A phone tap is a coarse pointer: give the key a finger-sized target
 * without growing the glyph. */
@media (pointer: coarse) {
  .spatial-card-read { width: 32px; height: 32px; padding: 7px; top: 2px; right: 2px; }
}

/* 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);
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--rs-meta);
}
.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);
}
/* 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);
  padding: 0.1rem 0 0.1rem 0.95rem;
}
.body-card .spread-figure-quote-title {
  display: block;
  font-family: var(--rs-font-heading);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rs-meta);
  text-decoration: none;
}
.body-card .spread-figure-quote-title:hover { color: var(--rs-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);
  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));
}
.spread-site-band .site-name {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--rs-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);
  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(--rs-divider);
}
.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(--rs-font-heading);
  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(--rs-text);
}
/* The dek on any card (2026-09-06: every card's slot, not the spread's) —
 * under the header row for an ordinary card, under the index card's own
 * title for a citation. Same voice as the spread's `.spread-sub` below and
 * `.card-header-sub` in the editor: the card's text a step back. */
.body-sub {
  margin: 0.35rem 0 0.9rem;
  font-family: var(--rs-font-heading);
  font-size: 1.05rem;
  line-height: 1.35;
  color: var(--rs-text);
  opacity: 0.75;
}
.cite-idx .body-sub {
  margin: 0.15rem 0 0.5rem;
}
/* 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);
  font-size: 1.05rem;
  line-height: 1.35;
  color: var(--rs-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);
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--rs-meta);
}
.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;
}

/* ─── The Site binding ──────────────────────────────────────────────────
 * One column under a band (spec/current/publishing.md §The bindings). The
 * band carries the author's mark and the signposts, a stack reads as a
 * shelf, every page turns to its neighbours, and the style menu sits in the
 * foot. Every value is the Style's furniture (§One dress); nothing here is a
 * colour of its own, and the outline's palette underlines the current
 * signpost as it fills the Reader's pill. */
.app.binding-site {
  display: block;
  height: auto;
  min-height: 100vh;
  background: var(--rs-card-bg, #faf8f0);
}
.site-band {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 10px 24px;
  background: var(--rs-panel-bg);
  border-bottom: 1px solid var(--rs-divider);
  font-family: var(--rs-font-ui);
}
.site-band-home {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  color: var(--rs-text);
  text-decoration: none;
}
.site-band-home .sb-wordmark { margin: 0; height: 26px; max-width: 240px; }
.site-band-icon {
  width: 24px;
  height: 24px;
  border-radius: 5px;
  box-shadow: 0 0 0 1px var(--rs-line);
}
.site-band-name {
  font-family: var(--rs-font-heading);
  font-size: 20px;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.site-nav { margin-left: auto; min-width: 0; }
.site-nav-summary { display: none; }
.site-nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px 18px;
}
.site-nav-link {
  display: inline-block;
  padding: 4px 0;
  color: var(--rs-text-2);
  text-decoration: none;
  font-size: 14px;
  border-bottom: 2px solid transparent;
}
.site-nav-link:hover { color: var(--rs-text); }
.site-nav-link.current { color: var(--rs-text); border-bottom-color: var(--sc-palette-bg); }

.site-main { max-width: 1040px; margin: 0 auto; padding: 0 24px; }
.site-tagline {
  margin: 0 0 14px;
  font-family: var(--rs-font-ui);
  font-size: 15px;
  color: var(--rs-meta);
}
/* The pane is a column in the page's own scroll, not a scroller of its own. */
.app.binding-site .body-pane {
  height: auto;
  min-height: 0;
  overflow: visible;
  /* The colour only: the shorthand would reset the wash's image layers
     (.body-pane.has-card-bg), and a Site or Book page keeps its wash. */
  background-color: transparent;
}

.site-shelf { margin: 32px 0 8px; }
/* A shelf said outright (`layout: shelf`) in the Reader's pane keeps the
 * measure the prose keeps; under the Site binding the column is the pane. */
.paper > .site-shelf { max-width: var(--rs-measure, 720px); }
.app.binding-site .paper > .site-shelf { max-width: none; }
.site-shelf-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 18px;
}
.site-shelf-card { margin: 0; }
.site-shelf-link {
  display: flex;
  flex-direction: column;
  gap: 8px;
  height: 100%;
  box-sizing: border-box;
  padding: 14px 16px 16px;
  border: 1px solid var(--rs-divider);
  border-radius: 10px;
  background: var(--rs-surface);
  color: var(--rs-text);
  text-decoration: none;
}
.site-shelf-link:hover { border-color: var(--rs-text-2); }
.site-shelf-face {
  display: block;
  aspect-ratio: 5 / 3;
  border-radius: 6px;
  background: var(--rs-paper-old) center / cover no-repeat;
  /* A cropped face is drawn inside the span by .picture-face, which wants a
     positioned, clipping host; harmless for a background-image face. */
  position: relative;
  overflow: hidden;
}
.site-shelf-title { font-family: var(--rs-font-heading); font-size: 18px; line-height: 1.25; }
.site-shelf-line { font-family: var(--rs-font-ui); font-size: 13px; line-height: 1.4; color: var(--rs-meta); }
.site-shelf-line a { pointer-events: none; color: inherit; }
.site-shelf-line img { display: none; }
.site-shelf-count {
  margin-top: auto;
  font-family: var(--rs-font-ui);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rs-faint);
}

.site-turn {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 16px;
  margin: 40px 0 24px;
  padding-top: 16px;
  border-top: 1px solid var(--rs-divider);
  font-family: var(--rs-font-ui);
  font-size: 14px;
}
.site-turn-link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  color: var(--rs-text);
  text-decoration: none;
}
.site-turn-link.next { text-align: right; }
.site-turn-link.up { text-align: center; }
.site-turn-key {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rs-meta);
}
.site-turn-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.site-foot,
.book-foot {
  max-width: 1040px;
  margin: 24px auto 0;
  padding: 14px 24px 28px;
  border-top: 1px solid var(--rs-line-2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.site-foot .sidebar-footer, .book-foot .sidebar-footer { border: 0; background: transparent; padding: 0; }
.site-foot-style { position: relative; }
/* The menu opens upward here: the foot is the page's end. */
.site-foot .style-pub-dropdown, .book-foot .style-pub-dropdown { top: auto; bottom: calc(100% + 6px); }

/* ─── The Book binding ──────────────────────────────────────────────────
 * A bound volume, read in order (spec/current/publishing.md §The bindings):
 * the cover, the contents, every page in reading order down one document
 * scroll, and a slim rail at the left saying where you are. Furniture
 * throughout (§One dress); the pages are `.body-card`s, so the Style's
 * prose rules dress them exactly as they dress a Reader page. */
.app.binding-book {
  display: block;
  height: auto;
  min-height: 100vh;
  padding-left: 180px;
  background: var(--rs-card-bg, #faf8f0);
}
.book-rail {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 180px;
  box-sizing: border-box;
  padding: 22px 18px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  background: var(--rs-panel-bg);
  border-right: 1px solid var(--rs-divider);
  font-family: var(--rs-font-ui);
  font-size: 13px;
  color: var(--rs-meta);
}
.book-rail-home {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  max-width: 100%;
  color: var(--rs-text);
  text-decoration: none;
}
.book-rail-icon {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  box-shadow: 0 0 0 1px var(--rs-line);
}
.book-rail-name {
  font-family: var(--rs-font-heading);
  font-size: 15px;
  line-height: 1.2;
  overflow-wrap: anywhere;
}
.book-rail-here {
  display: block;
  min-width: 0;
  max-width: 100%;
  color: var(--rs-text);
  text-decoration: none;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.book-rail-here:hover { text-decoration: underline; }
.book-rail-turn { display: inline-flex; gap: 6px; }
.book-rail-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 1px solid var(--rs-divider);
  border-radius: 999px;
  background: var(--rs-surface);
  color: var(--rs-text);
  text-decoration: none;
  font-size: 13px;
}
.book-rail-key:hover { background: var(--rs-text); color: var(--rs-text-inverse); border-color: var(--rs-text); }
.book-rail-key.empty { opacity: 0.3; }

.book-main { max-width: 800px; margin: 0 auto; padding: 0 24px; container-type: inline-size; --pane-pad-x: 0px; }
.book-cover { padding: 64px 0 8px; }
.book-cover .sb-wordmark { margin: 0; height: 40px; max-width: 360px; }
.book-title {
  margin: 0;
  font-family: var(--rs-font-heading);
  font-size: 36px;
  line-height: 1.12;
  color: var(--rs-text);
}
.book-tagline {
  margin: 8px 0 0;
  font-family: var(--rs-font-ui);
  font-size: 15px;
  color: var(--rs-meta);
}
/* The one pane is the cover's; the volume is the document's own scroll. */
.app.binding-book .body-pane {
  height: auto;
  min-height: 0;
  overflow: visible;
  /* The colour only: the shorthand would reset the wash's image layers
     (.body-pane.has-card-bg), and a Site or Book page keeps its wash. */
  background-color: transparent;
  /* No side gutter: the front door's page aligns with the cover's title
     above it and the pages below it, which sit in the column directly. The
     pads are restated as the vars a cover head and a bleed figure reach
     the paper's edge by. */
  --pane-pad-top: 28px;
  --pane-pad-x: 0px;
  padding: var(--pane-pad-top) 0 0;
}

.book-contents {
  margin: 44px 0 8px;
  padding-top: 28px;
  border-top: 1px solid var(--rs-divider);
  font-family: var(--rs-font-ui);
}
.book-contents-summary { display: none; }
.book-contents-title {
  margin: 0 0 12px;
  font-family: var(--rs-font-heading);
  font-size: 22px;
  font-weight: 600;
  color: var(--rs-text);
}
.book-contents-list { list-style: none; margin: 0; padding: 0; }
.book-contents-list .book-contents-list { padding-left: 20px; }
.book-contents-link {
  display: inline-block;
  padding: 3px 0;
  color: var(--rs-text);
  text-decoration: none;
  font-size: 15px;
  line-height: 1.35;
}
.book-contents-link:hover { text-decoration: underline; }

.book-page {
  margin-top: 44px;
  padding-top: 40px;
  border-top: 1px solid var(--rs-divider);
  scroll-margin-top: 24px;
}
.book-page .body-content > :first-child { margin-top: 0; }
.book-chapter { margin-top: 80px; padding-top: 56px; }
.book-chapter-title {
  margin: 0 0 14px;
  font-family: var(--rs-font-heading);
  font-size: 30px;
  line-height: 1.15;
  color: var(--rs-text);
}
.book-chapter .body-sub { margin-top: 0; }
.book-chapter .body-tags { margin: 10px 0 16px; }

@media (max-width: 720px) {
  .app.binding-book { padding-left: 0; }
  /* The rail becomes a strip across the top: the mark, where you are, the
     two keys — sticky, so a long chapter keeps its name in view. */
  .book-rail {
    position: sticky;
    top: 0;
    left: auto;
    bottom: auto;
    width: auto;
    z-index: 5;
    flex-direction: row;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    border-right: 0;
    border-bottom: 1px solid var(--rs-divider);
  }
  .book-rail-here { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .book-rail-name { font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }
  .book-rail-icon { width: 24px; height: 24px; }
  .book-main { padding: 0 14px; }
  .book-cover { padding-top: 32px; }
  .book-title { font-size: 28px; }
  .book-page { scroll-margin-top: 52px; }
  .app.binding-book .body-pane { display: block; }
  /* The contents fold behind a key. */
  .book-contents-title { display: none; }
  .book-contents-summary {
    display: inline-block;
    font: inherit;
    font-size: 13px;
    padding: 6px 12px;
    border: 1px solid var(--rs-divider);
    border-radius: 999px;
    background: var(--rs-surface);
    color: var(--rs-text);
    cursor: pointer;
  }
  .book-contents > .book-contents-list { display: none; }
  .book-contents.open > .book-contents-list { display: block; margin-top: 10px; }
  .book-foot { flex-direction: column; align-items: flex-start; }
}

/* ─── The press page — a host's own front door ───────────────────────────
 * publish/press-page.js (spec/current/publishing.md §The press page): the
 * host's name or, on a one-owner host, that owner's wordmark; each route a
 * shelf card — a swatch of its palette, its icon and title, its tagline —
 * dressed in the default Style's furniture, which the server inlines on
 * the root as applyStyle would. Static: no script, no snapshot. */
.app.app-solo.press {
  display: block;
  height: auto;
  min-height: 100vh;
  background: var(--rs-panel-bg);
  font-family: var(--rs-font-ui);
  color: var(--rs-text);
}
.press-head {
  max-width: 1040px;
  margin: 0 auto;
  padding: 56px 24px 8px;
}
.press-head .sb-wordmark { margin: 0; height: 40px; max-width: 360px; }
.press-mark { margin-bottom: 10px; }
.press-host {
  margin: 0;
  font-size: 14px;
  color: var(--rs-meta);
}
.press-name {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
  font-family: var(--rs-font-heading);
  font-size: 32px;
  line-height: 1.15;
  font-weight: 600;
}
.press-icon {
  width: 32px;
  height: 32px;
  border-radius: 7px;
  box-shadow: 0 0 0 1px var(--rs-line);
}
.press-main {
  max-width: 1040px;
  margin: 0 auto;
  padding: 24px 24px 40px;
}
.press-shelf {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 18px;
}
.press-card { margin: 0; }
.press-link {
  display: flex;
  flex-direction: column;
  gap: 8px;
  height: 100%;
  box-sizing: border-box;
  padding: 14px 16px 16px;
  border: 1px solid var(--rs-divider);
  border-radius: 10px;
  background: var(--rs-surface);
  color: var(--rs-text);
  text-decoration: none;
}
.press-link:hover { border-color: var(--rs-text-2); }
/* The route's palette, as the editor's outline row wears it. The colour is
 * the route's own data, validated before it reaches the link's inline
 * `--press-swatch`; the resting value is declared on the LINK, where the
 * inline one outranks it — declared on the swatch itself it would outrank
 * the inherited colour instead (found in the eyeball pass). */
.press-link { --press-swatch: var(--rs-paper-old); }
.press-swatch {
  display: block;
  height: 8px;
  border-radius: 4px;
  background: var(--press-swatch);
}
.press-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--rs-font-heading);
  font-size: 19px;
  line-height: 1.25;
}
.press-card-icon {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--rs-line);
}
.press-line { font-size: 13px; line-height: 1.4; color: var(--rs-meta); }
.press-members {
  margin-top: auto;
  align-self: flex-start;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rs-faint);
}
.press-empty { margin: 0; color: var(--rs-meta); }
.press-foot {
  max-width: 1040px;
  margin: 0 auto;
  padding: 14px 24px 28px;
  border-top: 1px solid var(--rs-line-2);
  font-size: 12px;
  color: var(--rs-meta);
}
/* The one Hyperliner on the page, as quiet as the footer's: the inherited
 * ink, an underline mixed from it, the page's ink on hover. */
.press-foot .live-brand {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
}
.press-foot .live-brand:hover { color: var(--rs-text); }
@media (max-width: 720px) {
  .press-head { padding: 32px 14px 6px; }
  .press-main { padding: 18px 14px 32px; }
  .press-name { font-size: 26px; }
}

/* The Book is the binding that prints: the rail, the fold and the foot
   are the screen's. The Style's own print sheet is a later unit. */
@media print {
  .app.binding-book { padding-left: 0; min-height: 0; }
  .book-rail, .book-foot, .book-contents-summary, .binding-book .load-more { display: none; }
}

@media (max-width: 720px) {
  .site-band { padding: 8px 14px; gap: 10px; flex-wrap: wrap; }
  .site-nav { margin-left: auto; }
  .site-nav-summary {
    display: inline-block;
    font: inherit;
    font-size: 13px;
    padding: 6px 10px;
    border: 1px solid var(--rs-divider);
    border-radius: 999px;
    background: var(--rs-surface);
    color: var(--rs-text);
    cursor: pointer;
  }
  .site-nav-list { display: none; }
  .site-nav.open { flex-basis: 100%; }
  .site-nav.open .site-nav-list { display: flex; flex-direction: column; gap: 0; padding: 6px 0 2px; }
  .site-main { padding: 0 14px; }
  .app.binding-site .body-pane { display: block; }
  .site-foot { flex-direction: column; align-items: flex-start; }
}
