/* 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;
}

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

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

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

  --sb-width: 320px;

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

* { box-sizing: border-box; }

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

#app { min-height: 100vh; }

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

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

@media (max-width: 720px) {
  .app { grid-template-columns: minmax(0, 1fr); }
  .body-pane { display: none; }
  .app.has-selection .body-pane { display: block; }
  .app.has-selection .sidebar { display: none; }
}

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

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

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

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


/* Outline title row — styled as Hyperliner's outline-row pill: a
 * rounded-rect band filled with the outline's palette colour, ink
 * coloured to match. Read-only — no chevron toggle, no info button,
 * no drag handle. */
.sb-outline-row {
  display: flex;
  align-items: center;
  background: var(--sc-palette-bg);
  color: var(--sc-palette-ink);
  border-radius: 8px;
  padding: 9px 14px;
  margin: 2px 0 16px 0;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

.toggle svg {
  display: block;
}

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

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

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

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

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

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

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

/* Card links ([[uuid]] / [[uuid|label]]) in card bodies — palette-tinted,
   clickable to the target. In preview lines `.preview a` already sets
   pointer-events:none, so there they read as plain text and the row's
   click selects it. An unresolved target (private / Draft / deleted)
   renders muted + non-clickable so it neither 404s nor leaks a target. */
/* Card-link — same-size accent text on a manila rounded-rect (the manila
   paints via `background`, so it never grows the line box). Matches the editor
   chip; accent = the outline palette, harmonised with citations + plain links. */
.body-card .body-content a.card-link {
  color: var(--sc-palette-bg, #0875b9);
  background: var(--card-link-bg, #efe7d2);
  text-decoration: none;
  border-radius: 5px;
  padding: 0.1em 0.5em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  cursor: var(--rs-card-link-cursor, pointer);
  transition: background 0.12s;
}
.body-card .body-content a.card-link:hover {
  background: var(--card-link-bg-hover, #e6d9ba);
}
.body-card .body-content .card-link-unresolved {
  color: var(--sc-muted, #6b6b6b);
  border-bottom: 1px dotted var(--sc-muted, #6b6b6b);
  cursor: default;
}

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

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

.body-pane {
  padding: 56px clamp(28px, 5vw, 64px) 72px;
  background: var(--sc-bg);
  overflow-y: auto;
  min-height: 0;
}

.body-header {
  max-width: 720px;
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-bottom: 28px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--sc-border);
}

/* The card title shares the header row with the Back button + view
 * toggle. flex: 1 lets it claim all the space the controls don't
 * use; ellipsis stops a long title from pushing the toggle off the
 * right edge. The bottom-border separator now lives on .body-header
 * (above), not on the title itself. */
.body-header .body-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
  border-bottom: none;
  font-family: var(--sc-heading-font);
  font-size: clamp(28px, 3.6vw, 42px);
  font-weight: 600;
  /* normal tracking so the title's ligatures (fi/ffl) render — see the
   * note on the body-heading rule. */
  letter-spacing: normal;
  /* Wrap long headlines onto multiple lines. Slightly looser than
   * the single-line value (1.08) so stacked lines don't crowd. */
  line-height: 1.15;
  overflow-wrap: break-word;
}

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

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

/* Inline control cluster at the trailing edge of .body-header:
 * [single|concat]  [spatial]  [Aa style ▾], all on one row.
 * `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;
}

/* single | concat segmented control. Mirrors the web editor's
 * `.view-toggle` (CardEditor.svelte mode bar). `margin-left: auto`
 * is no longer needed because flex: 1 on the title in between
 * already pushes it to the end. */
.view-toggle {
  display: inline-flex;
  border: 1px solid var(--sc-border);
  border-radius: 999px;
  overflow: hidden;
  flex: 0 0 auto;
  background: var(--sc-row-bg);
}

.view-toggle-btn {
  border: none;
  background: transparent;
  color: var(--sc-muted);
  font-family: var(--sc-body-font);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  padding: 0.3rem 0.85rem;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}

.view-toggle-btn.active {
  background: var(--sc-text);
  color: #fff;
}

.view-toggle-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Standalone pill that switches the right pane to the spatial canvas.
 * Visually echoes a single view-toggle-btn so the two controls read
 * as the same family — but it's its own pill, not part of the
 * segmented group, because clicking it changes the *pane*, not the
 * body-pane sub-mode. */
.spatial-toggle-btn {
  border: 1px solid var(--sc-border);
  background: var(--sc-row-bg);
  color: var(--sc-muted);
  font-family: var(--sc-body-font);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  padding: 0.3rem 0.85rem;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}

.spatial-toggle-btn:hover {
  background: var(--sc-text);
  color: #fff;
  border-color: var(--sc-text);
}

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

/* ── Reading-surface Style variables on the body view ─────────────────
 * The active/visitor-chosen style sets --rs-* on :root (applyStyle in
 * viewer.js); the body reading surface consumes them with fallbacks to
 * the existing --sc-* / hardcoded values so an unstyled snapshot is
 * unchanged. */
.body-card { max-width: var(--rs-measure, 720px); }
.body-card .body-content {
  font-family: var(--rs-font-body, var(--sc-body-font));
  font-size: var(--rs-font-size, 1rem);
  line-height: var(--rs-line-height, 1.7);
  color: var(--rs-text, var(--sc-text));
}
.body-card .body-content p {
  text-indent: var(--rs-paragraph-indent, 0);
  margin: 0 0 var(--rs-paragraph-spacing, 1em);
}
.body-card .body-content :is(h1, h2, h3, h4) + p { text-indent: 0; }
.body-card .body-content a {
  color: var(--rs-link, var(--sc-palette-bg, inherit));
  text-decoration: var(--rs-link-decoration, underline);
}
.body-card .body-content blockquote {
  border-left: 3px solid var(--rs-border, var(--sc-border));
  color: var(--rs-muted, var(--sc-muted));
}
/* Tables (GFM) — mirrors the editor's .ProseMirror table rules in
 * CardEditor.svelte; keep the two in step. marked emits align attrs on
 * th/td, which browsers honour natively. Wide tables scroll in place
 * (display:block) instead of breaking the measure. */
.body-card .body-content table {
  display: block;
  overflow-x: auto;
  border-collapse: collapse;
  width: 100%;
  font-size: 0.95em;
  margin: 0.75em 0;
}
.body-card .body-content th,
.body-card .body-content td {
  border: 1px solid var(--rs-border, var(--sc-border));
  padding: 0.3rem 0.55rem;
  vertical-align: top;
  min-width: 3rem;
}
.body-card .body-content th {
  font-weight: 600;
  border-bottom-width: 2px;
  background: color-mix(in srgb, var(--rs-border, var(--sc-border)) 18%, transparent);
}
/* Shared content-class vocabulary (mirrors the editor's). */
.body-content .newthought {
  font-variant: var(--rs-newthought-variant, small-caps);
  font-size: var(--rs-newthought-size, 1.1em);
  font-weight: var(--rs-newthought-weight, inherit);
  letter-spacing: var(--rs-newthought-letter-spacing, 0.03em);
}
/* Optional sigil prefixed to a note (empty by default). The var is
   emitted pre-quoted by viewer.js applyStyle. */
.body-content .sidenote::before { content: var(--rs-sidenote-sigil, ''); }
.body-content .marginnote::before { content: var(--rs-marginnote-sigil, ''); }
.body-content .epigraph { margin: 1em 0 1.5em; font-style: italic; color: var(--rs-muted, var(--sc-muted)); }
.body-content .fullwidth { max-width: none; width: 100%; }
.body-content :is(.marginnote, .sidenote) {
  display: block;
  font-size: var(--rs-marginnote-font-size, 0.85rem);
  color: var(--rs-marginnote-color, var(--sc-muted));
  line-height: 1.4;
  border-left: 2px solid var(--rs-border, var(--sc-border));
  padding-left: 0.6rem;
  margin: 0.5rem 0;
}
[data-rs-marginnote='right'] .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 card title takes the active Style's heading font + ink so it
 * reads as part of the styled surface (e.g. et-book under Tufte). */
.body-header .body-title {
  font-family: var(--rs-font-heading, var(--sc-heading-font));
  color: var(--rs-heading-color, inherit);
  font-variant-ligatures: common-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. */
[data-rs-family='tufte'] .body-title {
  font-weight: 400;
  font-style: normal;
  letter-spacing: normal;
  font-size: clamp(2.6rem, 4vw, 3.4rem);
}
/* Headings: Tufte scale. Weight (400) + italic come from --rs-* via the
 * canonical heading rule; these rules only adjust the per-heading size,
 * leading, and letter-spacing. The `.body-card` qualifier raises
 * specificity above the canonical size rules (.body-card .body-content
 * h2 { font-size: 23px }) further down this file, which would otherwise
 * win on source order. */
[data-rs-family='tufte'] .body-card .body-content :is(h1, h2, h3, h4) {
  letter-spacing: normal;
  line-height: 1.1;
}
[data-rs-family='tufte'] .body-card .body-content h1 { font-size: var(--rs-h1-size, 2.2rem); margin: 2.1rem 0 1.4rem; }
[data-rs-family='tufte'] .body-card .body-content h2 { font-size: var(--rs-h2-size, 1.7rem); margin: 2.1rem 0 1.4rem; }
[data-rs-family='tufte'] .body-card .body-content h3 { font-size: var(--rs-h3-size, 1.4rem); margin: 2rem 0 1.4rem; }
/* First paragraph after a heading / the lede has no top margin so the
 * heading sits tight to its text. */
[data-rs-family='tufte'] .body-card .body-content :is(h1, h2, h3, h4) + p { margin-top: 0.4rem; }
/* Blockquote: no rule, slightly narrower, right-aligned attribution
 * (a trailing cite/footer or the last short line). The `.body-card`
 * qualifier raises specificity above the default blockquote rule that
 * paints a palette-tinted left border further down this file. */
[data-rs-family='tufte'] .body-card .body-content blockquote {
  border-left: none;
  /* Tufte indents the quotation rather than ruling it. */
  padding: 0 0 0 2.5rem;
  margin: 1.4rem 0;
  font-style: normal;
  color: inherit;
}
[data-rs-family='tufte'] .body-card .body-content blockquote :is(footer, cite) {
  display: block;
  text-align: right;
  font-size: 1.1rem;
  font-style: italic;
}
/* Code stays a touch smaller than the larger Tufte body. */
[data-rs-family='tufte'] .body-content :is(code, pre) { font-size: 1rem; }
/* newthought lede — small caps. */
[data-rs-family='tufte'] .body-content .newthought { font-size: var(--rs-newthought-size, 1.2em); }

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

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

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

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

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

/* Citation card = a read-only library index card (mirrors the editor):
   a left margin with the @citekey call-number + a type glyph/label, a red
   margin rule, two faint blue rules (top + under the title), and the main
   column carrying the title heading, the details (author · year, venue), the
   doi/url links, and the abstract (the card body). */
.body-card.citation-index-card {
  max-width: 760px;
  /* The parchment palette — hand-mirror of the app-side definition on
     :global(.editor.citation-index-card) in src/editor/CardEditor.svelte
     (the published page loads no app CSS). Change BOTH or neither. */
  --cite-rule-red: #e2a49b;
  --cite-rule-blue: #bcd4e8;
  --cite-rule-blue-faint: #dfeaf2;
  --cite-sepia: #8a6d3b;
  --cite-sepia-dark: #7a5c1e;
  --cite-ink: #4a4436;
  --cite-ink-strong: #26221a;
  --cite-paper: #f3ecd8;
  --cite-paper-hover: #efe6cd;
  --cite-border: #e4d8b8;
  --cite-border-dashed: #cbb98a;
  --cite-tan: #a8935f;
  --cite-tan-label: #a08b6a;
  --cite-placeholder: #b7a074;
  --cite-dot: #b7ab90;
  --cite-focus: #b89a5e;
  --cite-cream: #faf5e6;
  --cite-control-bg: #fff;
  --cite-warn: #b45309;
  --cite-wash: rgba(122, 92, 30, 0.06);
  --cite-wash-strong: rgba(122, 92, 30, 0.1);
}
.body-card.citation-index-card .cite-idx {
  position: relative;
  display: grid;
  grid-template-columns: 104px 1fr;
  column-gap: 20px;
  padding-top: 12px;
  border-top: 1px solid var(--cite-rule-blue-faint);
}
.body-card.citation-index-card .cite-idx::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 104px;
  width: 1px;
  background: var(--cite-rule-red);
  pointer-events: none;
}
.cite-idx-margin { padding-left: 4px; min-width: 0; }
.cite-idx-callno {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.45;
  color: var(--cite-sepia);
  word-break: break-all;
}
.cite-idx-type {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 72px;
  padding: 8px 4px;
  background: var(--cite-paper);
  border: 1px solid var(--cite-border);
  border-radius: 8px;
  color: var(--cite-sepia-dark);
}
.cite-idx-type-icon { display: flex; }
.cite-idx-type-icon svg { width: 20px; height: 20px; }
.cite-idx-type-label { font-size: 11px; line-height: 1.15; text-align: center; color: var(--cite-sepia-dark); }
.cite-idx-main { min-width: 0; }
.cite-idx-title {
  margin: 0 0 0.5rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--cite-rule-blue);
  font-family: var(--sc-heading-font, 'Playfair Display', Georgia, serif);
  font-size: 1.6rem;
  line-height: 1.2;
  font-weight: 600;
  color: var(--sc-text, #1a1a1a);
}
.cite-idx-details {
  margin: 0.6rem 0 0;
  font-family: var(--sc-heading-font, Georgia, 'Times New Roman', serif);
  font-size: 16.5px;
  line-height: 1.75;
  color: var(--cite-ink-strong);
}
.cite-idx-details em { font-style: italic; }
.cite-idx-links {
  margin-top: 6px;
  font-size: 13px;
  color: var(--sc-muted, #777);
}
.cite-idx-links .citation-link {
  color: var(--sc-palette-bg, #0875b9);
  text-decoration: none;
  border-bottom: 1px solid var(--sc-palette-tint-strong, rgba(8, 117, 185, 0.22));
  overflow-wrap: anywhere;
}
.cite-idx-links .cite-idx-dot { color: var(--cite-dot); }
.body-card.citation-index-card .cite-idx-abstract {
  margin-top: 1.1rem;
}
/* Narrow collapse — mirrors the editor face's ≤600px behaviour at the
   viewer's own single-column breakpoint (720px, same as .app's): the 104px
   margin column ate ~40% of a phone-width line. The margin becomes a TOP
   band (call-number + type chip in a row) carrying the red rule as its
   bottom border; the full-height rule goes away; the grid single-columns. */
@media (max-width: 720px) {
  .body-card.citation-index-card .cite-idx {
    grid-template-columns: 1fr;
  }
  .body-card.citation-index-card .cite-idx::before {
    display: none;
  }
  .cite-idx-margin {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    padding-left: 0;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--cite-rule-red);
  }
  .cite-idx-margin .cite-idx-type {
    margin-top: 0;
  }
}

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

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

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

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

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

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

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

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

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

/* 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: #2563eb;
  text-decoration: none;
  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;
}
.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;
}

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

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

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

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

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

.spatial-canvas {
  position: relative;
}

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

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

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

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

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

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

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

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

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