diff --git a/module/VuFind/tests/fixtures/configs/1.1/config.ini b/module/VuFind/tests/fixtures/configs/1.1/config.ini
index 9be711f6feb9e87af8aae16502139cfa099d1374..32fa094f337f58e877d00956b44916af71ec0b7e 100644
--- a/module/VuFind/tests/fixtures/configs/1.1/config.ini
+++ b/module/VuFind/tests/fixtures/configs/1.1/config.ini
@@ -227,7 +227,7 @@ url = "http://syndetics.com"
 ; "pw".  Note that Content Cafe is a subscription service from Baker & Taylor.
 [Contentcafe]
 url              = "http://contentcafe2.btol.com"
-pw               = "Password"
+pw               = "xxxxxx"
 
 ; Web Search is Optional. The Web Search is powered by Google.
 ; To use enter your Google Web Search key and the domain the of your library
@@ -249,7 +249,7 @@ apiKey       = mySecretKey
 ; and the WorldCat searching.
 ;[WorldCat]
 ;id              = myAccount
-;apiKey          = ApiKey
+;apiKey          = YBaFHvqtTHbx7zzgtgAQDr4Rij9OSXBMmfsO1VRXxarmH1JU6neu2w3Lu6Y8OAjN2z5Pkz0M7GQwkwAF
 ;OCLCCode        = MYCODE
 ;LimitCodes      = Comma separated list of OCLC Codes
 
diff --git a/module/VuFind/tests/fixtures/configs/1.2/config.ini b/module/VuFind/tests/fixtures/configs/1.2/config.ini
index ddd8dd319978d4034ac3b8038196a2c460f85166..31d2ef0557757a78d56ab73e50c4b5b2c4030d65 100644
--- a/module/VuFind/tests/fixtures/configs/1.2/config.ini
+++ b/module/VuFind/tests/fixtures/configs/1.2/config.ini
@@ -261,7 +261,7 @@ url = "http://syndetics.com"
 ; "pw".  Note that Content Cafe is a subscription service from Baker & Taylor.
 [Contentcafe]
 url              = "http://contentcafe2.btol.com"
-pw               = "Password"
+pw               = "xxxxxx"
 
 ; Web Search is Optional. The Web Search is powered by Google.
 ; To use enter your Google Web Search key and the domain the of your library
@@ -283,7 +283,7 @@ apiKey       = mySecretKey
 ; and the WorldCat searching.
 ;[WorldCat]
 ;id              = myAccount
-;apiKey          = ApiKey
+;apiKey          = YBaFHvqtTHbx7zzgtgAQDr4Rij9OSXBMmfsO1VRXxarmH1JU6neu2w3Lu6Y8OAjN2z5Pkz0M7GQwkwAF
 ;OCLCCode        = MYCODE
 ;LimitCodes      = Comma separated list of OCLC Codes
 
diff --git a/module/VuFind/tests/fixtures/configs/1.3/config.ini b/module/VuFind/tests/fixtures/configs/1.3/config.ini
index dca7fc490d91aa736d54a72d70f925092c9652f0..d92486e6b990850d65f1f5e908baa1ca7c7fe777 100644
--- a/module/VuFind/tests/fixtures/configs/1.3/config.ini
+++ b/module/VuFind/tests/fixtures/configs/1.3/config.ini
@@ -323,7 +323,7 @@ url = "http://syndetics.com"
 ; "pw".  Note that Content Cafe is a subscription service from Baker & Taylor.
 [Contentcafe]
 url              = "http://contentcafe2.btol.com"
-pw               = "Password"
+pw               = "xxxxxx"
 
 ; Web Search is Optional. The Web Search is powered by Google.
 ; To use enter your Google Web Search key and the domain the of your library
@@ -345,7 +345,7 @@ apiKey       = mySecretKey
 ; and the WorldCat searching.
 ;[WorldCat]
 ;id              = myAccount
-;apiKey          = ApiKey
+;apiKey          = YBaFHvqtTHbx7zzgtgAQDr4Rij9OSXBMmfsO1VRXxarmH1JU6neu2w3Lu6Y8OAjN2z5Pkz0M7GQwkwAF
 ;OCLCCode        = MYCODE
 ;LimitCodes      = Comma separated list of OCLC Codes
 
diff --git a/module/VuFind/tests/fixtures/configs/1.4/config.ini b/module/VuFind/tests/fixtures/configs/1.4/config.ini
index b05c8ab206d268758e3d64b1ff7f91084bf70830..13417f3fae7b9a813136bd5451cdec18e171d2ef 100644
--- a/module/VuFind/tests/fixtures/configs/1.4/config.ini
+++ b/module/VuFind/tests/fixtures/configs/1.4/config.ini
@@ -399,7 +399,7 @@ url = "http://syndetics.com"
 ; "pw".  Note that Content Cafe is a subscription service from Baker & Taylor.
 [Contentcafe]
 url              = "http://contentcafe2.btol.com"
-pw               = "Password"
+pw               = "xxxxxx"
 
 ; Web Search is Optional. The Web Search is powered by Google.
 ; To use enter your Google Web Search key and the domain the of your library
@@ -421,7 +421,7 @@ apiKey       = mySecretKey
 ; and the WorldCat searching.
 ;[WorldCat]
 ;id              = myAccount
-;apiKey          = ApiKey
+;apiKey          = YBaFHvqtTHbx7zzgtgAQDr4Rij9OSXBMmfsO1VRXxarmH1JU6neu2w3Lu6Y8OAjN2z5Pkz0M7GQwkwAF
 ;OCLCCode        = MYCODE
 ;LimitCodes      = Comma separated list of OCLC Codes
 
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/NextPrevNavTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/NextPrevNavTest.php
index 7eb106e97af4eef4ecb46da0467777fba498e959..23a1066abf4b710f6afad3f14581d8ad21b920ba 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/NextPrevNavTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/NextPrevNavTest.php
@@ -35,9 +35,12 @@ namespace VuFindTest\Mink;
  * @author   Conor Sheehan <csheehan@nli.ie>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Page
+ * @retry    4
  */
 class NextPrevNavTest extends \VuFindTest\Unit\MinkTestCase
 {
+    use \VuFindTest\Unit\AutoRetryTrait;
+
     /**
      * if next_prev_navigation and first_last_navigation are set to true
      * and a search which returns no results is run
@@ -62,6 +65,6 @@ class NextPrevNavTest extends \VuFindTest\Unit\MinkTestCase
         $session->visit($this->getVuFindUrl() . "/Record/geo20001");
 
         // should fail if exception is thrown
-        $this->assertContains("Test Publication 20001", $this->findCss($page, "div.media-body > h1[property=name]")->getText());
+        $this->assertStringContainsString("Test Publication 20001", $this->findCss($page, "div.media-body > h1[property=name]")->getText());
     }
 }
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchFacetsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchFacetsTest.php
index c5a3baa410266c5c11a55a6a3d1f220d31ae92e8..138dc190411b1587b2bc3cf2d9bd4934f1e0e983 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchFacetsTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchFacetsTest.php
@@ -35,19 +35,30 @@ namespace VuFindTest\Mink;
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Page
+ * @retry    4
  */
 class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
 {
+    use \VuFindTest\Unit\AutoRetryTrait;
+
+    /**
+     * CSS selector for finding active filters
+     *
+     * @var string
+     */
+    protected $activeFilterSelector = '.active-filters.hidden-xs .filters .filter-value';
+
     /**
      * Standard setup method.
      *
      * @return void
      */
-    public function setUp()
+    public function setUp(): void
     {
         // Give up if we're not running in CI:
         if (!$this->continuousIntegrationRunning()) {
-            return $this->markTestSkipped('Continuous integration not running.');
+            $this->markTestSkipped('Continuous integration not running.');
+            return;
         }
     }
 
@@ -63,6 +74,36 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         return $session->getPage();
     }
 
+    /**
+     * Helper function for simple facet application test
+     *
+     * @param \Behat\Mink\Element\Element $page Mink page object
+     *
+     * @return void
+     */
+    protected function facetApplyProcedure($page)
+    {
+        // Confirm that we have 9 results and no filters to begin with:
+        $time = $this->findCss($page, '.search-query-time');
+        $stats = $this->findCss($page, '.search-stats');
+        $this->assertEquals("Showing 1 - 9 results of 9 for search 'building:weird_ids.mrc'" . $time->getText(), $stats->getText());
+        $items = $page->findAll('css', $this->activeFilterSelector);
+        $this->assertEquals(0, count($items));
+
+        // Facet to Fiction (after making sure we picked the right link):
+        $facetList = $this->findCss($page, '#side-collapse-genre_facet a[data-title="Fiction"]');
+        $this->assertEquals('Fiction 7', $facetList->getText());
+        $facetList->click();
+        $this->snooze();
+
+        // Check that when the page reloads, we have fewer results and a filter:
+        $time = $this->findCss($page, '.search-query-time');
+        $stats = $this->findCss($page, '.search-stats');
+        $this->assertEquals("Showing 1 - 7 results of 7 for search 'building:weird_ids.mrc'" . $time->getText(), $stats->getText());
+        $items = $page->findAll('css', $this->activeFilterSelector);
+        $this->assertEquals(1, count($items));
+    }
+
     /**
      * Helper function for facets lists
      *
@@ -81,7 +122,7 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
             ->findAll('css', '#modal #facet-list-count .fa-times');
         $this->assertEquals($exclusionActive ? $limit : 0, count($excludes));
         // more
-        $this->findCss($page, '#modal .js-facet-next-page')->click();
+        $this->clickCss($page, '#modal .js-facet-next-page');
         $this->snooze();
         $items = $page->findAll('css', '#modal #facet-list-count .js-facet-item');
         $this->assertEquals($limit * 2, count($items));
@@ -103,7 +144,7 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         $this->assertEquals($exclusionActive ? $limit * 2 : 0, count($excludes));
 
         // sort by title
-        $this->findCss($page, '[data-sort="index"]')->click();
+        $this->clickCss($page, '[data-sort="index"]');
         $this->snooze();
         $items = $page->findAll('css', '#modal #facet-list-index .js-facet-item');
         $this->assertEquals($limit, count($items)); // reset number of items
@@ -119,7 +160,7 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
             ->findAll('css', '#modal #facet-list-index .fa-times');
         $this->assertEquals($exclusionActive ? $limit : 0, count($excludes));
         // sort by index again
-        $this->findCss($page, '[data-sort="count"]')->click();
+        $this->clickCss($page, '[data-sort="count"]');
         $this->snooze();
         $items = $page->findAll('css', '#modal #facet-list-count .js-facet-item');
         $this->assertEquals($limit * 2, count($items)); // maintain number of items
@@ -134,6 +175,49 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         $this->snooze();
     }
 
+    /**
+     * Test applying a facet to filter results (standard facet sidebar)
+     *
+     * @return void
+     */
+    public function testApplyFacet()
+    {
+        $page = $this->performSearch('building:weird_ids.mrc');
+
+        // Confirm that we are NOT using the AJAX sidebar:
+        $ajaxContainer = $page->findAll('css', '.side-facets-container-ajax');
+        $this->assertEquals(0, count($ajaxContainer));
+
+        // Now run the body of the test procedure:
+        $this->facetApplyProcedure($page);
+    }
+
+    /**
+     * Test applying a facet to filter results (deferred facet sidebar)
+     *
+     * @return void
+     */
+    public function testApplyFacetDeferred()
+    {
+        $this->changeConfigs(
+            [
+                'searches' => [
+                    'General' => [
+                        'default_side_recommend[]' => 'SideFacetsDeferred:Results:CheckboxFacets',
+                    ]
+                ]
+            ]
+        );
+        $page = $this->performSearch('building:weird_ids.mrc');
+
+        // Confirm that we ARE using the AJAX sidebar:
+        $ajaxContainer = $page->findAll('css', '.side-facets-container-ajax');
+        $this->assertEquals(1, count($ajaxContainer));
+
+        // Now run the body of the test procedure:
+        $this->facetApplyProcedure($page);
+    }
+
     /**
      * Test expanding facets into the lightbox
      *
@@ -158,10 +242,10 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         $genreMore->click();
         $this->facetListProcedure($page, $limit);
         $genreMore->click();
-        $this->findCss($page, '#modal .js-facet-item.active')->click();
+        $this->clickCss($page, '#modal .js-facet-item.active');
         // remove facet
         $this->snooze();
-        $this->assertNull($page->find('css', '.active-filters'));
+        $this->assertNull($page->find('css', $this->activeFilterSelector));
     }
 
     /**
@@ -186,14 +270,14 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         // Open the geographic facet
         $genreMore = $this->findCss($page, '#more-narrowGroupHidden-genre_facet');
         $genreMore->click();
-        $this->findCss($page, '.narrowGroupHidden-genre_facet[data-lightbox]')->click();
+        $this->clickCss($page, '.narrowGroupHidden-genre_facet[data-lightbox]');
         $this->facetListProcedure($page, $limit);
         $genreMore->click();
-        $this->findCss($page, '.narrowGroupHidden-genre_facet[data-lightbox]')->click();
-        $this->findCss($page, '#modal .js-facet-item.active')->click();
+        $this->clickCss($page, '.narrowGroupHidden-genre_facet[data-lightbox]');
+        $this->clickCss($page, '#modal .js-facet-item.active');
         // remove facet
         $this->snooze();
-        $this->assertNull($page->find('css', '.active-filters'));
+        $this->assertNull($page->find('css', $this->activeFilterSelector));
     }
 
     /**
@@ -236,7 +320,7 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         $session = $this->getMinkSession();
         $session->executeScript("$('#j1_1.jstree-closed .jstree-icon').click();");
         $this->findCss($page, '#j1_1.jstree-open .jstree-icon');
-        $this->findCss($page, '#j1_2 a')->click();
+        $this->clickCss($page, '#j1_2 a');
         $this->snooze();
         $filter = $this->findCss($page, $this->activeFilterSelector);
         $label = $this->findCss($page, '.filters .filters-title');
@@ -292,43 +376,165 @@ class SearchFacetsTest extends \VuFindTest\Unit\MinkTestCase
         );
         $page = $this->performSearch('building:"hierarchy.mrc"');
         // Uncollapse format so we can check if it is still open after reload:
-        $this->findCss($page, '#side-panel-format .collapsed')->click();
+        $this->clickCss($page, '#side-panel-format .collapsed');
         // Uncollapse hierarchical facet so we can click it:
-        $this->findCss($page, '#side-panel-hierarchical_facet_str_mv .collapsed')->click();
+        $this->clickCss($page, '#side-panel-hierarchical_facet_str_mv .collapsed');
         $this->clickHierarchyFacet($page);
 
         // We have now reloaded the page. Let's toggle format off and on to confirm
         // that it was opened, and let's also toggle building on to confirm that
         // it was not alread opened.
-        $this->findCss($page, '#side-panel-format .title')->click(); // off
+        $this->clickCss($page, '#side-panel-format .title'); // off
         $this->snooze(); // wait for animation
-        $this->findCss($page, '#side-panel-format .collapsed')->click(); // on
-        $this->findCss($page, '#side-panel-building .collapsed')->click(); // on
+        $this->clickCss($page, '#side-panel-format .collapsed'); // on
+        $this->clickCss($page, '#side-panel-building .collapsed'); // on
+    }
+
+    /**
+     * Assert that the filter used by these tests is still applied.
+     *
+     * @param \Behat\Mink\Element\Element $page Mink page object
+     *
+     * @return void
+     */
+    protected function assertFilterIsStillThere($page)
+    {
+        $filter = $this->findCss($page, $this->activeFilterSelector);
+        $this->assertEquals('weird_ids.mrc', $filter->getText());
     }
 
     /**
-     * Test retrain current filters checkbox
+     * Assert that no filters are applied.
+     *
+     * @param \Behat\Mink\Element\Element $page Mink page object
      *
      * @return void
      */
-    public function testRetainFilters()
+    protected function assertNoFilters($page)
+    {
+        $items = $page->findAll('css', $this->activeFilterSelector);
+        $this->assertEquals(0, count($items));
+    }
+
+    /**
+     * Assert that the "reset filters" button is not present.
+     *
+     * @param \Behat\Mink\Element\Element $page Mink page object
+     *
+     * @return void
+     */
+    protected function assertNoResetFiltersButton($page)
+    {
+        $reset = $page->findAll('css', '.reset-filters-btn');
+        $this->assertEquals(0, count($reset));
+    }
+
+    /**
+     * Test retain current filters default behavior
+     *
+     * @return void
+     */
+    public function testDefaultRetainFiltersBehavior()
     {
         $page = $this->getFilteredSearch();
-        $this->findCss($page, '.active-filters'); // Make sure we're filtered
-        // Perform search with retain
-        $this->findCss($page, '#searchForm .btn.btn-primary')->click();
+        $this->assertFilterIsStillThere($page);
+        // Re-click the search button and confirm that filters are still there
+        $this->clickCss($page, '#searchForm .btn.btn-primary');
         $this->snooze();
-        $this->findCss($page, '.active-filters');
-        // Perform search double click retain
-        $this->findCss($page, '.searchFormKeepFilters')->click();
-        $this->findCss($page, '.searchFormKeepFilters')->click();
-        $this->findCss($page, '#searchForm .btn.btn-primary')->click();
+        $this->assertFilterIsStillThere($page);
+        // Click the "reset filters" button and confirm that filters are gone and
+        // that the button disappears when no longer needed.
+        $this->clickCss($page, '.reset-filters-btn');
         $this->snooze();
-        $this->findCss($page, '.active-filters');
-        // Perform search without retain
-        $this->findCss($page, '.searchFormKeepFilters')->click();
-        $this->findCss($page, '#searchForm .btn.btn-primary')->click();
-        $items = $page->findAll('css', '.active-filters');
-        $this->assertEquals(0, count($items));
+        $this->assertNoFilters($page);
+        $this->assertNoResetFiltersButton($page);
+    }
+
+    /**
+     * Test that filters carry over to selected records and are retained
+     * from there.
+     *
+     * @return void
+     */
+    public function testFiltersOnRecord()
+    {
+        $page = $this->getFilteredSearch();
+        $this->assertFilterIsStillThere($page);
+        // Now click the first result:
+        $this->clickCss($page, '.result-body a.title');
+        $this->snooze();
+        // Confirm that filters are still visible:
+        $this->assertFilterIsStillThere($page);
+        // Re-click the search button...
+        $this->clickCss($page, '#searchForm .btn.btn-primary');
+        $this->snooze();
+        // Confirm that filter is STILL applied
+        $this->assertFilterIsStillThere($page);
+    }
+
+    /**
+     * Test "never retain filters" configurable behavior
+     *
+     * @return void
+     */
+    public function testNeverRetainFiltersBehavior()
+    {
+        $this->changeConfigs(
+            [
+                'searches' => [
+                    'General' => ['retain_filters_by_default' => false]
+                ]
+            ]
+        );
+        $page = $this->getFilteredSearch();
+        $this->assertFilterIsStillThere($page);
+        // Confirm that there is no reset button:
+        $this->assertNoResetFiltersButton($page);
+        // Re-click the search button and confirm that filters go away
+        $this->clickCss($page, '#searchForm .btn.btn-primary');
+        $this->snooze();
+        $this->assertNoFilters($page);
+    }
+
+    /**
+     * Test resetting to a default filter state
+     *
+     * @return void
+     */
+    public function testDefaultFiltersWithResetButton()
+    {
+        // Unlike the other tests, which use $this->getFilteredSearch() to set up
+        // the weird_ids.mrc filter through a URL parameter, this test sets up the
+        // filter as a default through the configuration.
+        $this->changeConfigs(
+            [
+                'searches' => [
+                    'General' => ['default_filters' => ['building:weird_ids.mrc']]
+                ]
+            ]
+        );
+
+        // Do a blank search to confirm that default filter is applied:
+        $session = $this->getMinkSession();
+        $session->visit($this->getVuFindUrl() . '/Search/Results');
+        $page = $session->getPage();
+        $this->snooze();
+        $this->assertFilterIsStillThere($page);
+
+        // Confirm that the reset button is NOT present:
+        $this->assertNoResetFiltersButton($page);
+
+        // Now manually clear the filter:
+        $this->clickCss($page, '.search-filter-remove');
+        $this->snooze();
+
+        // Confirm that no filters are displayed:
+        $this->assertNoFilters($page);
+
+        // Now click the reset button to bring back the default:
+        $this->clickCss($page, '.reset-filters-btn');
+        $this->snooze();
+        $this->assertFilterIsStillThere($page);
+        $this->assertNoResetFiltersButton($page);
     }
 }
diff --git a/themes/bootstrap3/js/common.js b/themes/bootstrap3/js/common.js
index 58716399f9f6e6d0d4bf438515899bd9eb7c3eed..085a3f1be3c3ad1edf6ddec5a1cb294f2abfba7c 100644
--- a/themes/bootstrap3/js/common.js
+++ b/themes/bootstrap3/js/common.js
@@ -1,5 +1,5 @@
 /*global grecaptcha, isPhoneNumberValid */
-/*exported VuFind, htmlEncode, deparam, getUrlRoot, phoneNumberFormHandler, recaptchaOnLoad, resetCaptcha, bulkFormHandler */
+/*exported VuFind, htmlEncode, deparam, getUrlRoot, phoneNumberFormHandler, recaptchaOnLoad, resetCaptcha, bulkFormHandler, setupMultiILSLoginFields */
 
 // IE 9< console polyfill
 window.console = window.console || { log: function polyfillLog() {} };
@@ -152,13 +152,16 @@ function getUrlRoot(url) {
   var urlroot = null;
   var urlParts = url.split(/[?#]/);
   var urlWithoutFragment = urlParts[0];
-  if (VuFind.path === '') {
+  var slashSlash = urlWithoutFragment.indexOf('//');
+  if (VuFind.path === '' || VuFind.path === '/') {
     // special case -- VuFind installed at site root:
     var chunks = urlWithoutFragment.split('/');
-    urlroot = '/' + chunks[3] + '/' + chunks[4];
+    // We need to extract different offsets if this is a full vs. relative URL:
+    urlroot = slashSlash > -1
+      ? ('/' + chunks[3] + '/' + chunks[4])
+      : ('/' + chunks[1] + '/' + chunks[2]);
   } else {
     // standard case -- VuFind has its own path under site:
-    var slashSlash = urlWithoutFragment.indexOf('//');
     var pathInUrl = slashSlash > -1
       ? urlWithoutFragment.indexOf(VuFind.path, slashSlash + 2)
       : urlWithoutFragment.indexOf(VuFind.path);
@@ -437,15 +440,5 @@ $(document).ready(function commonDocReady() {
     $.getJSON(VuFind.path + '/AJAX/JSON', {method: 'keepAlive'});
   }
 
-  // retain filter sessionStorage
-  $('.searchFormKeepFilters').click(function retainFiltersInSessionStorage() {
-    sessionStorage.setItem('vufind_retain_filters', this.checked ? 'true' : 'false');
-    $('.applied-filter').prop('checked', this.checked);
-  });
-  if (sessionStorage.getItem('vufind_retain_filters')) {
-    var state = (sessionStorage.getItem('vufind_retain_filters') === 'true');
-    $('.searchFormKeepFilters,.applied-filter').prop('checked', state);
-  }
-
   setupIeSupport();
 });
diff --git a/themes/bootstrap3/js/lightbox.js b/themes/bootstrap3/js/lightbox.js
index bd79c2731f62f11e95479dc9668228a43904f45a..4bfed9b0d69026e6c456d029e8968c25b45b55f8 100644
--- a/themes/bootstrap3/js/lightbox.js
+++ b/themes/bootstrap3/js/lightbox.js
@@ -142,7 +142,11 @@ VuFind.register('lightbox', function Lightbox() {
       .done(function lbAjaxDone(content, status, jq_xhr) {
         var errorMsgs = [];
         var flashMessages = [];
-        if (jq_xhr.status !== 205) {
+        if (jq_xhr.status === 204) {
+          // No content, close lightbox
+          close();
+          return;
+        } else if (jq_xhr.status !== 205) {
           var testDiv = $('<div/>').html(content);
           errorMsgs = testDiv.find('.flash-message.alert-danger:not([data-lightbox-ignore])');
           flashMessages = testDiv.find('.flash-message:not([data-lightbox-ignore])');
diff --git a/themes/bootstrap3/js/record.js b/themes/bootstrap3/js/record.js
index edfc6e8670df72204d07249365ccdbc654b72fe5..4d4630339c00937940f546061aace4a5d2e7789c 100644
--- a/themes/bootstrap3/js/record.js
+++ b/themes/bootstrap3/js/record.js
@@ -128,6 +128,28 @@ function registerAjaxCommentRecord(_context) {
   return false;
 }
 
+// Forward declaration
+var ajaxLoadTab = function ajaxLoadTabForward() {
+};
+
+function handleAjaxTabLinks(_context) {
+  var context = typeof _context === "undefined" ? document : _context;
+  // Form submission
+  $(context).find('a').each(function handleLink() {
+    var $a = $(this);
+    var href = $a.attr('href');
+    if (typeof href !== 'undefined' && href.match(/\/AjaxTab[/?]/)) {
+      $a.unbind('click').click(function linkClick() {
+        var tabid = $('.record-tabs .nav-tabs li.active').data('tab');
+        var $tab = $('.' + tabid + '-tab');
+        $tab.html('<i class="fa fa-spinner fa-spin" aria-hidden="true"></i> ' + VuFind.translate('loading') + '...</div>');
+        ajaxLoadTab($tab, '', false, href);
+        return false;
+      });
+    }
+  });
+}
+
 function registerTabEvents() {
   // Logged in AJAX
   registerAjaxCommentRecord();
@@ -136,6 +158,8 @@ function registerTabEvents() {
 
   setUpCheckRequest();
 
+  handleAjaxTabLinks();
+
   VuFind.lightbox.bind('.tab-pane.active');
 
   if (typeof VuFind.openurl !== 'undefined') {
@@ -152,12 +176,21 @@ function removeHashFromLocation() {
   }
 }
 
-function ajaxLoadTab($newTab, tabid, setHash) {
+ajaxLoadTab = function ajaxLoadTabReal($newTab, tabid, setHash, tabUrl) {
   // Request the tab via AJAX:
+  var url = '';
+  var postData = {};
+  // If tabUrl is defined, it overrides base URL and tabid
+  if (typeof tabUrl !== 'undefined') {
+    url = tabUrl;
+  } else {
+    url = VuFind.path + getUrlRoot(document.URL) + '/AjaxTab';
+    postData.tab = tabid;
+  }
   $.ajax({
-    url: VuFind.path + getUrlRoot(document.URL) + '/AjaxTab',
+    url: url,
     type: 'POST',
-    data: {tab: tabid}
+    data: postData
   })
     .always(function ajaxLoadTabDone(data) {
       if (typeof data === 'object') {
@@ -177,7 +210,7 @@ function ajaxLoadTab($newTab, tabid, setHash) {
       setupJumpMenus($newTab);
     });
   return false;
-}
+};
 
 function refreshTagList(_target, _loggedin) {
   var loggedin = !!_loggedin || userIsLoggedIn;
diff --git a/themes/bootstrap3/less/components/accessibility.less b/themes/bootstrap3/less/components/accessibility.less
index 2b5494ad786eac5d50895c9b1af53c1a0ab75cf7..3fc07c552777af6778962dde5079c43de9e2fe69 100644
--- a/themes/bootstrap3/less/components/accessibility.less
+++ b/themes/bootstrap3/less/components/accessibility.less
@@ -24,41 +24,3 @@ a {
     color: @state-danger-text;
   }
 }
-
-/**
- * OVERRIDE BS3 COLLAPSE MENU HIDDEN
- *
- * instead of display: none, keep things sr accessible
- * https://tailwindcss.com/docs/screen-readers/
- */
-/*
-.collapse.collapse:not(.in) {
-  position: absolute;
-  display: block;
-  width: 1px;
-  height: 1px;
-  padding: 0;
-  margin: -1px;
-  color: #000;
-  background-color: #fff;
-  overflow: hidden;
-  clip: rect(0, 0, 0, 0);
-  white-space: nowrap;
-  border-width: 0;
-}
-.long-view.collapse:not(.in) {
-  display: none;
-}
-*/
-@media (min-width: 768px) {
-  .navbar-collapse.collapse:not(.in) {
-    position: static;
-    width: auto;
-    height: auto;
-    padding: 0;
-    margin: 0;
-    overflow: visible;
-    clip: auto;
-    white-space: normal;
-  }
-}
diff --git a/themes/bootstrap3/less/components/record.less b/themes/bootstrap3/less/components/record.less
index 2497a8ff2e491a3c5f6d073ffe2238abca80542d..e356fc1ad27e4b96844c5b39a0907cb571f1c28e 100644
--- a/themes/bootstrap3/less/components/record.less
+++ b/themes/bootstrap3/less/components/record.less
@@ -135,3 +135,41 @@
 
 /* ------ Relais ------ */
 .relaisLink { display: inline-block; }
+
+/* ------ Collection ------ */
+.collection-list-controls {
+  display: flex;
+  flex-flow: row wrap;
+
+  .collection-controlĂ‚ {
+    white-space: nowrap;
+    margin: 0 0.5rem 0 0;
+  }
+}
+.collectionDetails .active-filters .filters {
+  padding: 0 0 5px 0;
+}
+.collection-list-results {
+  margin-top: 0.5rem;
+}
+
+/* ------ Tabs ------ */
+.tab-pane::after {
+  display: table;
+  clear: both;
+  content: "";
+}
+.tab-pane .result {
+  margin-left: 0;
+}
+
+/* ------ OpenURL Links ------ */
+.openurls {
+  .openurl-notes {
+    display: block;
+    font-style: italic;
+  }
+  .openurl-authentication {
+    display: block;
+  }
+}
diff --git a/themes/bootstrap3/scss/components/accessibility.scss b/themes/bootstrap3/scss/components/accessibility.scss
index ace3cb6062763551b29c5eb7ff8cd50b70c9a328..a987269a49d2b6b67d5b32480ed54e4664283f8b 100644
--- a/themes/bootstrap3/scss/components/accessibility.scss
+++ b/themes/bootstrap3/scss/components/accessibility.scss
@@ -24,41 +24,3 @@ $state-danger-text: #8a211e;
     color: $state-danger-text;
   }
 }
-
-/**
- * OVERRIDE BS3 COLLAPSE MENU HIDDEN
- *
- * instead of display: none, keep things sr accessible
- * https://tailwindcss.com/docs/screen-readers/
- */
-/*
-.collapse.collapse:not(.in) {
-  position: absolute;
-  display: block;
-  width: 1px;
-  height: 1px;
-  padding: 0;
-  margin: -1px;
-  color: #000;
-  background-color: #fff;
-  overflow: hidden;
-  clip: rect(0, 0, 0, 0);
-  white-space: nowrap;
-  border-width: 0;
-}
-.long-view.collapse:not(.in) {
-  display: none;
-}
-*/
-@media (min-width: 768px) {
-  .navbar-collapse.collapse:not(.in) {
-    position: static;
-    width: auto;
-    height: auto;
-    padding: 0;
-    margin: 0;
-    overflow: visible;
-    clip: auto;
-    white-space: normal;
-  }
-}
diff --git a/themes/bootstrap3/scss/components/offcanvas.scss b/themes/bootstrap3/scss/components/offcanvas.scss
index 39097702eb53caf457f519827025c7c80f6f731c..943ce7bbd656b063ddb509eda1aa607e08c89825 100644
--- a/themes/bootstrap3/scss/components/offcanvas.scss
+++ b/themes/bootstrap3/scss/components/offcanvas.scss
@@ -2,9 +2,6 @@ $offcanvas-offset: 80vw;  // Width of open menu
 
 .offcanvas-overlay { display: none; }
 
-// Fix you-cannot-extend-from-inside-media-query error:
-// Since .search-filter-toggle is only used on XS devices anyway,
-// we can move the @extend outside the media query -- CK #14674
 .search-filter-toggle {
   @extend .btn;
   @extend .btn-default;
diff --git a/themes/bootstrap3/scss/components/record.scss b/themes/bootstrap3/scss/components/record.scss
index 63b59030a9a6fda8925cb2160cbdc32ecdc175f3..0375ec79322b20402e9f9b388bb9e152f7abd04e 100644
--- a/themes/bootstrap3/scss/components/record.scss
+++ b/themes/bootstrap3/scss/components/record.scss
@@ -135,3 +135,41 @@
 
 /* ------ Relais ------ */
 .relaisLink { display: inline-block; }
+
+/* ------ Collection ------ */
+.collection-list-controls {
+  display: flex;
+  flex-flow: row wrap;
+
+  .collection-controlĂ‚ {
+    white-space: nowrap;
+    margin: 0 0.5rem 0 0;
+  }
+}
+.collectionDetails .active-filters .filters {
+  padding: 0 0 5px 0;
+}
+.collection-list-results {
+  margin-top: 0.5rem;
+}
+
+/* ------ Tabs ------ */
+.tab-pane::after {
+  display: table;
+  clear: both;
+  content: "";
+}
+.tab-pane .result {
+  margin-left: 0;
+}
+
+/* ------ OpenURL Links ------ */
+.openurls {
+  .openurl-notes {
+    display: block;
+    font-style: italic;
+  }
+  .openurl-authentication {
+    display: block;
+  }
+}
diff --git a/themes/bootstrap3/scss/components/search.scss b/themes/bootstrap3/scss/components/search.scss
index f5bf702e7258128dd5aa818b236930890028a445..f21d64795b079ceac1c908e05b082585552a35c0 100644
--- a/themes/bootstrap3/scss/components/search.scss
+++ b/themes/bootstrap3/scss/components/search.scss
@@ -500,8 +500,8 @@ body.rtl {
 }
 
 .search-history-table {
-  @extend .table;
-  @extend .table-striped;
+  @extend .table all;
+  @extend .table-striped all;
 }
 table.search-history-table {
   table-layout: auto;
diff --git a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml
index f03f1c355908841e4a81972d417cead92f2a7b93..a19c551c0e73307a4e0c76d3ae278bca7c5c154b 100644
--- a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml
@@ -1,9 +1,11 @@
+<?php $this->metadata()->generateMetatags($this->driver);?>
 <div class="media" vocab="http://schema.org/" resource="#record" typeof="<?=$this->driver->getSchemaOrgFormats()?> Product">
   <?php
     $QRCode = $this->record($this->driver)->getQRCode("core");
     $coverDetails = $this->record($this->driver)->getCoverDetails('core', 'medium', $this->record($this->driver)->getThumbnail('large'));
     $cover = $coverDetails['html'];
-    $preview = $this->record($this->driver)->getPreviews();
+    $preview = ($this->previewOverride ?? false)
+      ? $this->previewOverride : $this->record($this->driver)->getPreviews();
   ?>
   <?php if ($QRCode || $cover || $preview): ?>
     <div class="media-left <?=$this->escapeHtmlAttr($coverDetails['size'])?> img-col">
diff --git a/themes/bootstrap3/templates/footer.phtml b/themes/bootstrap3/templates/footer.phtml
index 2604172f5de2cc093284cc3ec24ae11fbe5b9999..ce2a6e122b9fa5117ef5da24ac4425de21c47f43 100644
--- a/themes/bootstrap3/templates/footer.phtml
+++ b/themes/bootstrap3/templates/footer.phtml
@@ -1,29 +1,35 @@
 <footer class="hidden-print">
   <div class="footer-container">
     <div class="footer-column">
-      <h2><?=$this->transEsc('Search Options')?></h2>
-      <ul>
-        <li><a href="<?=$this->url('search-history')?>"><?=$this->transEsc('Search History')?></a></li>
-        <li><a href="<?=$this->url('search-advanced')?>"><?=$this->transEsc('Advanced Search')?></a></li>
-      </ul>
+      <?php $this->slot('footer-left')->start(); ?>
+        <h2><?=$this->transEsc('Search Options')?></h2>
+        <ul>
+          <li><a href="<?=$this->url('search-history')?>"><?=$this->transEsc('Search History')?></a></li>
+          <li><a href="<?=$this->url('search-advanced')?>"><?=$this->transEsc('Advanced Search')?></a></li>
+        </ul>
+      <?=$this->slot('footer-left')->end(); ?>
     </div>
     <div class="footer-column">
-      <h2><?=$this->transEsc('Find More')?></h2>
-      <ul>
-        <li><a href="<?=$this->url('browse-home')?>"><?=$this->transEsc('Browse the Catalog')?></a></li>
-        <li><a href="<?=$this->url('alphabrowse-home')?>"><?=$this->transEsc('Browse Alphabetically')?></a></li>
-        <li><a href="<?=$this->url('channels-home')?>"><?=$this->transEsc('channel_explore')?></a></li>
-        <li><a href="<?=$this->url('search-reserves')?>"><?=$this->transEsc('Course Reserves')?></a></li>
-        <li><a href="<?=$this->url('search-newitem')?>"><?=$this->transEsc('New Items')?></a></li>
-      </ul>
+      <?php $this->slot('footer-center')->start(); ?>
+        <h2><?=$this->transEsc('Find More')?></h2>
+        <ul>
+          <li><a href="<?=$this->url('browse-home')?>"><?=$this->transEsc('Browse the Catalog')?></a></li>
+          <li><a href="<?=$this->url('alphabrowse-home')?>"><?=$this->transEsc('Browse Alphabetically')?></a></li>
+          <li><a href="<?=$this->url('channels-home')?>"><?=$this->transEsc('channel_explore')?></a></li>
+          <li><a href="<?=$this->url('search-reserves')?>"><?=$this->transEsc('Course Reserves')?></a></li>
+          <li><a href="<?=$this->url('search-newitem')?>"><?=$this->transEsc('New Items')?></a></li>
+        </ul>
+      <?=$this->slot('footer-center')->end(); ?>
     </div>
     <div class="footer-column">
-      <h2><?=$this->transEsc('Need Help?')?></h2>
-      <ul>
-        <li><a href="<?=$this->url('help-home')?>?topic=search&amp;_=<?=time() ?>" data-lightbox class="help-link"><?=$this->transEsc('Search Tips')?></a></li>
-        <li><a href="<?=$this->url('content-page', ['page' => 'asklibrary']) ?>"><?=$this->transEsc('Ask a Librarian')?></a></li>
-        <li><a href="<?=$this->url('content-page', ['page' => 'faq']) ?>"><?=$this->transEsc('FAQs')?></a></li>
-      </ul>
+      <?php $this->slot('footer-right')->start(); ?>
+        <h2><?=$this->transEsc('Need Help?')?></h2>
+        <ul>
+          <li><a href="<?=$this->url('help-home')?>?topic=search&amp;_=<?=time() ?>" data-lightbox class="help-link"><?=$this->transEsc('Search Tips')?></a></li>
+          <li><a href="<?=$this->url('content-page', ['page' => 'asklibrary']) ?>"><?=$this->transEsc('Ask a Librarian')?></a></li>
+          <li><a href="<?=$this->url('content-page', ['page' => 'faq']) ?>"><?=$this->transEsc('FAQs')?></a></li>
+        </ul>
+      <?=$this->slot('footer-right')->end(); ?>
     </div>
   </div>
   <div class="poweredby">
diff --git a/themes/bootstrap3/templates/record/checkbox.phtml b/themes/bootstrap3/templates/record/checkbox.phtml
index 750c00fea0667df5b8b9cead6dba29d2097fd554..7c37bb0ab2482b8e6e321128dd30ed6fc4f77ac6 100644
--- a/themes/bootstrap3/templates/record/checkbox.phtml
+++ b/themes/bootstrap3/templates/record/checkbox.phtml
@@ -1,5 +1,5 @@
 <label class="record-checkbox hidden-print">
-  <input class="checkbox-select-item" type="checkbox" name="ids[]" value="<?=$this->escapeHtmlAttr($this->id)?>"<?php if(isset($this->formAttr)): ?> form="<?=$this->formAttr ?>"<?php endif; ?>/>
+  <input class="checkbox-select-item" type="checkbox" name="ids[]" value="<?=$this->escapeHtmlAttr($this->id) ?>"<?php if(isset($this->formAttr)): ?> form="<?=$this->formAttr ?>"<?php endif; ?>/>
   <span class="checkbox-icon"></span>
   <?php if (strlen($this->number ?? '') > 0): ?><span class="sr-only"><?=$this->transEsc('result_checkbox_label', ['%%number%%' => $this->number]) ?></span><?php endif; ?>
 </label>
diff --git a/themes/bootstrap3/templates/search/facet-list.phtml b/themes/bootstrap3/templates/search/facet-list.phtml
index f1fc812acd1467b32c24a1ce4408021e582a7f92..91c577d2df7a5cd7e30ad84188669417a2cd3919 100644
--- a/themes/bootstrap3/templates/search/facet-list.phtml
+++ b/themes/bootstrap3/templates/search/facet-list.phtml
@@ -5,7 +5,7 @@
     $this->sort = 'default';
     $this->sortOptions = [ 'default' => 'default' ];
   }
-  $urlBase = $this->url($facetLightbox) . $results->getUrlQuery()->getParams() . '&amp;facet=' . urlencode($this->facet) . '&amp;facetexclude=' . $this->exclude . '&amp;facetop=' . $this->operator;
+  $urlBase = $this->url($facetLightbox) . $results->getUrlQuery()->getParams() . '&amp;facet=' . urlencode($this->facet) . '&amp;facetexclude=' . urlencode($this->exclude) . '&amp;facetop=' . urlencode($this->operator);
   $searchAction = $this->url($options->getSearchAction());
   if (!empty($this->baseUriExtra)) {
     $searchAction .= urlencode($this->baseUriExtra);