ids theme: rework the references slide layout and auto-fit it

Address four points of criticism on the bibliography slide:
- "Authors (Year):" no longer sits bold on its own line but runs
  inline in a medium weight -- it was overemphasized for text that is
  already darker than the entry, and looked blurred at small sizes.
- Entries get breathing room below the title bar (padding, so the
  vertical-centering wrapper's first-child margin reset cannot eat it).
- Authors running inline saves a line per entry.
- The slide auto-fits: decreasing font sizes are tried and, when even
  the smallest would overflow, trailing entries move onto
  automatically created "References (cont.)" slides (Reveal.sync()
  registers them, so navigation, menu and numbering keep working).
  In print/PDF the scroll box is disabled so long bibliographies flow
  across pages instead of being clipped.

Change-Id: I59897723d9eabe824f7f5ed794cf75c0082e031b
diff --git a/inst/reveal.js-5.2.1/dist/theme/ids.css b/inst/reveal.js-5.2.1/dist/theme/ids.css
index 8d56808..1489184 100644
--- a/inst/reveal.js-5.2.1/dist/theme/ids.css
+++ b/inst/reveal.js-5.2.1/dist/theme/ids.css
@@ -969,7 +969,9 @@
   font-family: "Fira Sans Condensed", sans-serif;
   font-size: 16px;
   text-align: left;
-  margin-top: 10px;
+  /* breathing room below the title bar; padding instead of margin, because
+     the vertical-centering wrapper zeroes its first child's margin */
+  padding-top: 18px;
   max-height: 510px;
   font-weight: 400;
   color: #253b43;
@@ -983,13 +985,22 @@
 
 .references .csl-entry {
   color: #888888;
-  margin-bottom: 0.5em;
+  /* the hanging indent visually separates entries; no extra margin needed */
+  margin-bottom: 0;
 }
 
 .csl-entry .csl-authors-year {
-  display: block;
-  font-weight: bold;
-  color: #4d616b;
+  font-weight: 400;
+  color: #253b43;
+}
+
+/* in print/PDF, let long bibliographies flow onto continuation pages
+   instead of being clipped by the on-screen scroll box */
+@media print {
+  .reveal .references {
+    max-height: none !important;
+    overflow: visible !important;
+  }
 }
 
 .reveal code.sourceCode:last-child + pre {
diff --git a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
index 287082e..5ee7d35 100644
--- a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
+++ b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
@@ -872,14 +872,15 @@
 
 $if(theme-ids)$
 <script>
-  // IDS theme: in the bibliography, "Authors (Year):" should form a bold first
-  // line, followed by a line break. Which part of an entry citeproc wraps in
-  // spans/links depends on the entry type, so a CSS "break before first child
-  // element" hack cannot place the break reliably. Wrap the leading text up to
-  // and including the first "):" explicitly instead; entries without a year
-  // marker are left untouched.
+  // IDS theme: in the bibliography, "Authors (Year):" should be set
+  // darker than the rest of the entry, but in normal weight. Which part
+  // of an entry citeproc wraps in spans/links depends on the entry type,
+  // so a CSS "break before first child element" hack cannot mark it
+  // reliably. Wrap the leading text up to and including the first "):"
+  // explicitly instead; entries without a year marker are left untouched.
   (function () {
     document.querySelectorAll('.csl-entry').forEach(function (entry) {
+      if (entry.querySelector('.csl-authors-year')) return;
       var textNodes = [];
       var accumulated = '';
       var children = Array.prototype.slice.call(entry.childNodes);
@@ -907,6 +908,117 @@
     });
   })();
 </script>
+<script>
+  // IDS theme: bibliography slides are tightly packed, so auto-fit them:
+  // try a window of font sizes and, when even the smallest one overflows,
+  // move trailing entries onto automatically created continuation slides.
+  // Only runs once per references slide (when it is first shown, so its
+  // entries are measurable); printing is handled by CSS, which lets long
+  // bibliographies flow across pages.
+  (function () {
+    var SIZES = [18, 17, 16, 15, 14, 13, 12];
+    var SPLIT_SIZE = 14;
+
+    function footerReserve() {
+      var v = parseFloat(getComputedStyle(document.documentElement)
+        .getPropertyValue('--ids-footer-reserve'));
+      return isNaN(v) ? 48 : v;
+    }
+
+    function availableHeight(ref) {
+      var slide = ref.closest('section');
+      if (!slide || !slide.clientHeight) return 0;
+      return Math.min(
+        slide.clientHeight - ref.offsetTop - footerReserve() - 10,
+        parseFloat(getComputedStyle(ref).maxHeight) || Infinity
+      );
+    }
+
+    function fits(ref) {
+      var avail = availableHeight(ref);
+      return avail > 0 && ref.scrollHeight <= avail + 1;
+    }
+
+    function splitOff(ref) {
+      ref.style.fontSize = SPLIT_SIZE + 'px';
+      if (fits(ref)) return; // readable again at the split size
+
+      var section = ref.closest('section');
+      var heading = section.querySelector('h1,h2');
+      var title = heading ? heading.textContent : 'References';
+      var avail = availableHeight(ref);
+
+      // measure all entries at the split size, keep those that fit on the
+      // original slide and distribute the rest over continuation slides
+      // with the same geometry
+      var entries = Array.prototype.slice.call(ref.querySelectorAll(':scope > .csl-entry'));
+      var heights = entries.map(function (entry) {
+        return entry.getBoundingClientRect().height +
+          parseFloat(getComputedStyle(entry).marginBottom);
+      });
+      var keep = 0;
+      var used = 0;
+      for (var k = 0; k < entries.length; k++) {
+        if (used + heights[k] > avail) break;
+        used += heights[k];
+        keep = k + 1;
+      }
+      if (keep === 0) keep = 1; // at least one entry stays on the slide
+      if (keep >= entries.length) return; // everything fits after all
+      var stash = entries.slice(keep);
+      stash.forEach(function (entry) { entry.remove(); });
+
+      var pages = [[]];
+      used = 0;
+      stash.forEach(function (entry, i) {
+        var height = heights[keep + i];
+        if (used + height > avail && pages[pages.length - 1].length) {
+          pages.push([]);
+          used = 0;
+        }
+        pages[pages.length - 1].push(entry);
+        used += height;
+      });
+
+      var insertAfter = section;
+      pages.forEach(function (pageEntries, i) {
+        if (!pageEntries.length) return;
+        var cont = document.createElement('section');
+        cont.className = section.className;
+        cont.id = (section.id || 'references') + '-cont-' + (i + 1);
+        if (heading) {
+          var h = document.createElement(heading.tagName);
+          h.textContent = title + ' (cont.)';
+          cont.appendChild(h);
+        }
+        var newRef = ref.cloneNode(false);
+        pageEntries.forEach(function (entry) { newRef.appendChild(entry); });
+        cont.appendChild(newRef);
+        insertAfter.parentNode.insertBefore(cont, insertAfter.nextSibling);
+        insertAfter = cont;
+      });
+      if (typeof Reveal !== 'undefined' && Reveal.isReady()) Reveal.sync();
+    }
+
+    function fitBibliographies() {
+      document.querySelectorAll('.reveal .references').forEach(function (ref) {
+        if (ref.dataset.fitDone) return;
+        if (!ref.closest('section').clientHeight) return; // not laid out yet
+        ref.dataset.fitDone = '1';
+        ref.style.fontSize = '';
+        for (var i = 0; i < SIZES.length; i++) {
+          ref.style.fontSize = SIZES[i] + 'px';
+          if (fits(ref)) return;
+        }
+        splitOff(ref);
+      });
+    }
+
+    Reveal.on('ready', fitBibliographies);
+    Reveal.on('slidechanged', function () { requestAnimationFrame(fitBibliographies); });
+    window.addEventListener('load', fitBibliographies);
+  })();
+</script>
 $endif$
 
 $if(theme-ids)$
@@ -1223,6 +1335,9 @@
     function wrapSlideContent(slide) {
       if (slide.dataset.contentWrapped) return;
       slide.dataset.contentWrapped = '1';
+      // bibliographies stay top-aligned: they are packed anyway, and the
+      // fitting/splitting logic below works from the slide top downwards
+      if (slide.querySelector('.references')) return;
       var content = Array.prototype.filter.call(
         Array.prototype.slice.call(slide.children, headingCount(slide)),
         function (el) { return el.tagName !== 'ASIDE'; }