Title slide: align all four corners, make QR codes scalable

qrcode::generate_svg() paints the code into a fixed 300x300 canvas and
emits no viewBox, so the svg could not be resized by CSS: given a smaller
box it kept drawing at full size and only its top left corner stayed
visible.  qr_svg_fit() adds a viewBox cropped to the dark modules, so the
code scales to whatever box it is placed in and its quiet zone becomes
the surrounding layout's business -- the white plate behind the title
slide's code, and padding on a.qrcode elsewhere.

With that in place all four title-slide corners share one 8px inset
measured to the visible ink: IDS (plus partner) logo top right, website
QR top left, funder logo bottom left, WGL membership logo bottom right.
The funder logo previously sat 20px lower than intended, because
reveal.js' default image margin adds itself to the offset of an
absolutely positioned image.

Also point the documented repository url at gitlab rather than gerrit,
and give the ids example deck an Overview slide.

Change-Id: I27447527345b71572a9e952bc21f542eba1ec352
diff --git a/NEWS.md b/NEWS.md
index e4e3de4..8324cf3 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,10 @@
 
 - Title slide branding (ported from posterdown.ids): three new optional yaml entries -- `website` places a linked QR code (generated with the qrcode package) in the top left of the title slide; `partner_logo` shows a second institution's logo next to the IDS logo in the top right of every slide (for talks by authors from two institutions); `funder_logo` puts a funder logo in the bottom left of the title slide, opposite the Leibniz Gemeinschaft membership logo. Local logo files are embedded as data URIs, so they need not be shipped; a linked QR code anywhere on a slide can be produced with the new `qrlink()` function in inline R code. The navigation chrome (arrows, slide number, menu button) is now hidden on the title slide.
 
+- `ids` theme: all four title-slide corners now share one inset (8px to both slide edges), measured to the visible ink: IDS (plus partner) logo top right, website QR top left, funder logo bottom left, WGL membership logo bottom right. The funder logo previously sat 20px lower than intended (reveal.js' default image margin adds itself to the offset of an absolutely positioned image), and the QR sat a white plate's padding away from the corner.
+
+- `qrlink()`: the QR code svg now carries a viewBox and scales to the box it is placed in. Without one, a code sized down by CSS -- as on the title slide -- kept drawing at its full 300px and only its top left corner remained visible. The svg is cropped to the code's dark modules, so its quiet zone comes from the surrounding layout (the white plate behind the title slide's code, and `padding` on `a.qrcode` elsewhere).
+
 - New `df_print` option (default `"kable"`): data frames are now printed as theme-styled HTML tables by default, so raw results on slides look reasonable without extra code. Use `df_print: default` for plain text output (or `tibble` / `paged` as in `rmarkdown::html_document`); `"kable"` prints all rows, so show only `head(x)` on slides with many-rowed results.
 
 - New transcript view, toggled with `r` (or, on touch devices, with a list button next to the speaker notes button): a single scrollable page containing every slide's content and speaker notes. Text-to-speech ("read aloud") browser extensions cannot walk a reveal.js deck (hidden slides are not part of the visible page text), but they can read the transcript from start to finish -- e.g. to have the whole talk including notes read aloud while practicing.
diff --git a/R/utils.R b/R/utils.R
index 4f2c21f..ff4ddc1 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -75,6 +75,46 @@
   sprintf("data:%s;base64,%s", mime, base64enc::base64encode(path))
 }
 
+# qrcode::generate_svg() paints the code -- quiet zone included -- into a
+# fixed 300x300 canvas and emits no viewBox, so the svg cannot be resized by
+# CSS: given a smaller box it keeps drawing at full size and only its top
+# left corner remains visible. Give it a viewBox cropped to the dark modules
+# instead. The code then scales to whatever box it is placed in, and its
+# quiet zone becomes the surrounding layout's business (on the title slide
+# the white plate behind the code supplies it), which keeps the placement
+# independent of how many modules the encoded url happens to need.
+qr_svg_fit <- function(svg) {
+  rects <- regmatches(svg, gregexpr("<rect[^>]*fill=\"black\"[^>]*>", svg))[[1]]
+  attr_num <- function(name) {
+    suppressWarnings(as.numeric(
+      sub(sprintf(".*[ ]%s=\"([0-9.]+)(px)?\".*", name), "\\1", rects)
+    ))
+  }
+  x <- attr_num("x")
+  y <- attr_num("y")
+  w <- attr_num("width")
+  h <- attr_num("height")
+  if (!length(rects) || anyNA(c(x, y, w, h))) {
+    return(svg)
+  }
+  left <- min(x)
+  top <- min(y)
+  side <- max(max(x + w) - left, max(y + h) - top)
+  vb <- sprintf(
+    'viewBox="%s %s %s %s" width="%s" height="%s"',
+    format(left), format(top), format(side), format(side),
+    format(side), format(side)
+  )
+  root <- regmatches(svg, regexpr("<svg[^>]*>", svg))
+  fitted <- sub(
+    "<svg", paste("<svg", vb),
+    gsub("[ ](width|height)=\"[0-9.]+\"", "", root),
+    fixed = TRUE
+  )
+  # the xml prolog would end up as a stray comment once inlined into html
+  sub("^<[?]xml[^>]*[?]>", "", sub(root, fitted, svg, fixed = TRUE))
+}
+
 #' HTML code for a linked QR code
 #'
 #' Generates a QR code (inline SVG) linking to the given URL and returns the
@@ -111,7 +151,7 @@
     ""
   }
 
-  svg <- gsub("\r?\n|\r", "", readChar(tmp, 1e7))
+  svg <- qr_svg_fit(gsub("\r?\n|\r", "", readChar(tmp, 1e7)))
   qr <- sprintf('<a class="qrcode" href="%s">%s</a>', url, svg)
   if (nchar(cap_html) > 0) {
     paste0('<div class="qrcode-container">', qr, cap_html, "</div>")
diff --git a/README.Rmd b/README.Rmd
index 39d0246..f3eae50 100644
--- a/README.Rmd
+++ b/README.Rmd
@@ -261,7 +261,7 @@
 
 ``` yaml
 title: "Habits"
-website: "https://korap.ids-mannheim.de/gerrit/IDS-Mannheim/revealjs"
+website: "https://gitlab.ids-mannheim.de/ids/revealjs"
 partner_logo: "dnb-logo.svg"
 funder_logo: "textplus-logo.svg"
 output: revealjs.ids::revealjs_presentation
diff --git a/README.md b/README.md
index 52cabf4..ae1cb90 100644
--- a/README.md
+++ b/README.md
@@ -335,7 +335,7 @@
 
 ``` yaml
 title: "Habits"
-website: "https://korap.ids-mannheim.de/gerrit/IDS-Mannheim/revealjs"
+website: "https://gitlab.ids-mannheim.de/ids/revealjs"
 partner_logo: "dnb-logo.svg"
 funder_logo: "textplus-logo.svg"
 output: revealjs.ids::revealjs_presentation
diff --git a/examples/ids.Rmd b/examples/ids.Rmd
index d657904..d303aff 100644
--- a/examples/ids.Rmd
+++ b/examples/ids.Rmd
@@ -6,7 +6,7 @@
   - name: John Doe
 institute: "IDS Mannheim"
 date: "`r paste0('Mannheim, ', Sys.Date())`"
-website: "https://korap.ids-mannheim.de/gerrit/IDS-Mannheim/revealjs"
+website: "https://gitlab.ids-mannheim.de/ids/revealjs"
 partner_logo: "logos/clarin-logo.svg"
 funder_logo: "logos/text-plus-logo.svg"
 output: 
@@ -35,6 +35,8 @@
 library(DT)
 #source("https://gitlab.ids-mannheim.de/ICC/2023-07-20-ICC-ICLC10/-/raw/master/R/common.R")
 ```
+## Overview
+
 # Introduction
 
 ## R Markdown
diff --git a/inst/reveal.js-5.2.1/css/theme/source/ids.scss b/inst/reveal.js-5.2.1/css/theme/source/ids.scss
index c55b95f..4691a64 100644
--- a/inst/reveal.js-5.2.1/css/theme/source/ids.scss
+++ b/inst/reveal.js-5.2.1/css/theme/source/ids.scss
@@ -224,10 +224,12 @@
 
 // The IDS logo stays a slide-wide watermark (top right of every slide,
 // including the title page). The SVG carries no internal padding of its own,
-// so it needs an explicit inset from the slide's right edge; the top offset is
-// chosen so the logo's cap line meets the top of the heading text.
-$idsLogoTop: 6px;
-$idsLogoRight: 8px;
+// so it needs an explicit inset from the slide's edges. That inset is shared
+// by everything that sits in a corner of the slide -- IDS (+ partner) logo
+// top right, website QR top left, funder logo bottom left, WGL membership
+// logo bottom right -- and is used for both edges of each corner, so all
+// four keep exactly the same distance to the slide's borders.
+$idsLogoInset: 8px;
 
 .reveal .slides {
   // Second layer: the optional partner institution logo (--partner-logo is
@@ -236,8 +238,8 @@
   background-image: url('https://corpora.ids-mannheim.de/slides/reveal.js.ids/images/IDS-Logo-2019.svg'),
     var(--partner-logo, none);
   background-repeat: no-repeat;
-  background-position: top #{$idsLogoTop} right #{$idsLogoRight},
-    top #{$idsLogoTop} right calc(#{$idsLogoRight} + var(--slide-height) * #{$idsLogoWidthRatio} + #{$idsPartnerGap});
+  background-position: top #{$idsLogoInset} right #{$idsLogoInset},
+    top #{$idsLogoInset} right calc(#{$idsLogoInset} + var(--slide-height) * #{$idsLogoWidthRatio} + #{$idsPartnerGap});
   background-size: calc(var(--slide-height) * #{$idsLogoWidthRatio}) auto,
     auto calc(var(--slide-height) * #{$idsPartnerHeightRatio});
 }
@@ -558,47 +560,51 @@
 // page in print view and cannot be hidden per page, position:fixed). Lift the
 // logo above the footer in print only; the screen layout stays put.
 html.reveal-print .reveal .slides section.title-frame {
-  background-position: right 8px bottom 55px;
+  background-position: right #{$idsLogoInset} bottom 55px;
 }
 
 // Like the orange bar/IDS logo above, this is painted on the slide itself
 // (rather than on .slide-background-content, which spans the raw,
 // unscaled viewport) so it stays pinned to the title slide's true
 // bottom-right corner instead of the browser window's. Its margin mirrors
-// the IDS logo's (top 6px / right 8px of the slide), so the two sit on
-// one shared inset.
+// the IDS logo's, so the two sit on one shared inset.
 .reveal .slides section.title-frame {
   background-image: url("https://corpora.ids-mannheim.de/slides/reveal.js.ids/images/Mitglied_WGL_transparent.svg");
   background-repeat: no-repeat;
-  background-position: right 8px bottom 10px;
+  background-position: right #{$idsLogoInset} bottom #{$idsLogoInset};
   background-size: calc(var(--slide-height) * 0.1) auto;
   /* full slide height, so "bottom" really is the slide's bottom edge */
   height: 100%;
 }
 
+// qrlink() codes placed anywhere else in a deck: qr_svg_fit() crops the svg
+// to the dark modules, so the quiet zone the code needs to stay scannable is
+// this padding (which also keeps a code readable on a dark background).
+.reveal a.qrcode {
+  display: inline-block;
+  padding: 0.5em;
+  background: #fff;
+}
+
 // Website QR code and funder logo on the title slide (document options
-// `website` and `funder_logo`): top-left and bottom-left corners. All
-// logos on the title slide keep the same distance to their respective
-// margins: top 6px / left 8px (QR), top 6px / right 8px (IDS [+ partner]
-// logo), bottom 10px / left 8px (funder), bottom 10px / right 8px (WGL
-// membership logo), so the four corners share one inset vocabulary. The QR
-// is sized generously (a quarter of the slide height) so it stays scannable
-// in all circumstances. Both are DOM elements injected into the title frame
-// by the pandoc template (the QR itself is generated in R by qrlink());
-// absolute positioning sticks to the slide, because reveal.js positions the
-// sections themselves.
+// `website` and `funder_logo`): top-left and bottom-left corners. Like the
+// two logos on the right, they keep $idsLogoInset to both slide edges --
+// measured to the visible ink, so that all four corners look alike. Both are
+// DOM elements injected into the title frame by the pandoc template (the QR
+// itself is generated in R by qrlink()); absolute positioning sticks to the
+// slide, because reveal.js positions the sections themselves.
+//
+// qrlink() crops the svg to the dark modules, so the quiet zone the code
+// needs to stay scannable is this white plate's padding -- which is also
+// what puts the modules themselves at $idsLogoInset from the slide's edges,
+// the plate sitting flush in the corner (invisible on the white slide).
 .reveal .slides section.title-frame .title-qr {
   position: absolute;
-  top: #{$idsLogoTop};
-  left: #{$idsLogoRight};
-  // The generated QR matrix has no quiet zone of its own (dark modules reach
-  // the svg edges), which makes it unscannable; the white plate's padding of
-  // ~4 modules supplies it. The plate's edge is also the element's visual
-  // boundary for the uniform corner insets, hence an outer size of 28% of
-  // the slide height minus the padding on two sides (~23% of pattern).
+  top: 0;
+  left: 0;
   box-sizing: border-box;
   width: calc(var(--slide-height) * 0.28);
-  padding: calc(var(--slide-height) * 0.025);
+  padding: #{$idsLogoInset};
   background: #fff;
 }
 
@@ -608,13 +614,18 @@
   width: 100%;
   height: auto;
   margin: 0;
+  // the quiet zone comes from the plate around it here
+  padding: 0;
 }
 
 .reveal .slides section.title-frame img.funder-logo {
   position: absolute;
-  left: #{$idsLogoRight};
-  bottom: 10px;
+  left: #{$idsLogoInset};
+  bottom: #{$idsLogoInset};
   height: calc(var(--slide-height) * 0.07);
+  // reveal.js gives images a vertical margin, which for an absolutely
+  // positioned one would add itself to the inset above.
+  margin: 0;
   width: auto;
 }
 
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 982e0aa..daf37a5 100644
--- a/inst/reveal.js-5.2.1/dist/theme/ids.css
+++ b/inst/reveal.js-5.2.1/dist/theme/ids.css
@@ -434,7 +434,7 @@
      supplied by the template from the document's partner_logo yaml entry) */
   background-image: url("https://corpora.ids-mannheim.de/slides/reveal.js.ids/images/IDS-Logo-2019.svg"), var(--partner-logo, none);
   background-repeat: no-repeat;
-  background-position: top 6px right 8px, top 6px right calc(8px + var(--slide-height) * 0.25 + 16px);
+  background-position: top 8px right 8px, top 8px right calc(8px + var(--slide-height) * 0.25 + 16px);
   background-size: calc(var(--slide-height) * 0.25) auto, auto calc(var(--slide-height) * 0.085);
 }
 
@@ -670,21 +670,32 @@
 .reveal .slides section.title-frame {
   background-image: url("https://corpora.ids-mannheim.de/slides/reveal.js.ids/images/Mitglied_WGL_transparent.svg");
   background-repeat: no-repeat;
-  background-position: right 8px bottom 10px;
+  background-position: right 8px bottom 8px;
   background-size: calc(var(--slide-height) * 0.1) auto;
   /* full slide height, so "bottom" really is the slide's bottom edge */
   height: 100%;
 }
 
+/* qrlink() codes placed anywhere else in a deck: qr_svg_fit() crops the svg
+   to the dark modules, so the quiet zone the code needs to stay scannable is
+   this padding (which also keeps a code readable on a dark background). */
+.reveal a.qrcode {
+  display: inline-block;
+  padding: 0.5em;
+  background: #fff;
+}
+
 .reveal .slides section.title-frame .title-qr {
   position: absolute;
-  top: 6px;
-  left: 8px;
-  /* The generated QR matrix has no quiet zone of its own (dark modules
-     reach the svg edges); the white plate's padding supplies it. */
+  top: 0;
+  left: 0;
+  /* qrlink() crops the svg to the dark modules, so the quiet zone the code
+     needs to stay scannable is this white plate's padding -- which is also
+     what puts the modules themselves 8px from the slide's edges, like every
+     other corner logo, the plate sitting flush in the corner. */
   box-sizing: border-box;
   width: calc(var(--slide-height) * 0.28);
-  padding: calc(var(--slide-height) * 0.025);
+  padding: 8px;
   background: #fff;
 }
 
@@ -694,13 +705,18 @@
   width: 100%;
   height: auto;
   margin: 0;
+  /* the quiet zone comes from the plate around it here */
+  padding: 0;
 }
 
 .reveal .slides section.title-frame img.funder-logo {
   position: absolute;
   left: 8px;
-  bottom: 10px;
+  bottom: 8px;
   height: calc(var(--slide-height) * 0.07);
+  /* reveal.js gives images a vertical margin, which for an absolutely
+     positioned one would add itself to the inset above. */
+  margin: 0;
   width: auto;
 }