« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static
diff options
context:
space:
mode:
Diffstat (limited to 'src/static')
-rw-r--r--src/static/css/site.css1142
-rw-r--r--src/static/js/client-util.js17
-rw-r--r--src/static/js/client/additional-names-box.js69
-rw-r--r--src/static/js/client/art-tag-gallery-filter.js151
-rw-r--r--src/static/js/client/art-tag-network.js147
-rw-r--r--src/static/js/client/css-compatibility-assistant.js26
-rw-r--r--src/static/js/client/expandable-gallery-section.js77
-rw-r--r--src/static/js/client/hoverable-tooltip.js53
-rw-r--r--src/static/js/client/image-overlay.js52
-rw-r--r--src/static/js/client/index.js6
-rw-r--r--src/static/js/client/quick-description.js2
-rw-r--r--src/static/js/client/sidebar-search.js333
-rw-r--r--src/static/js/client/sticky-heading.js132
-rw-r--r--src/static/js/rectangles.js42
-rw-r--r--src/static/js/search-worker.js180
-rw-r--r--src/static/misc/image.svg11
16 files changed, 2134 insertions, 306 deletions
diff --git a/src/static/css/site.css b/src/static/css/site.css
index 4d4d6416..a9ed90c6 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -61,7 +61,7 @@ body::before, .wallpaper-part {
 
 #page-container {
   max-width: 1100px;
-  margin: 0 auto 40px;
+  margin: 0 auto 38px;
   padding: 15px 0;
 }
 
@@ -76,10 +76,25 @@ body::before, .wallpaper-part {
   height: unset;
 }
 
+@property --banner-shine {
+  syntax: '<percentage>';
+  initial-value: 0%;
+  inherits: false;
+}
+
 #banner {
   margin: 10px 0;
   width: 100%;
   position: relative;
+
+  --banner-shine: 4%;
+  -webkit-box-reflect: below -12px linear-gradient(transparent, color-mix(in srgb, transparent, var(--banner-shine) white));
+  transition: --banner-shine 0.8s;
+}
+
+#banner:hover {
+  --banner-shine: 35%;
+  transition-delay: 0.3s;
 }
 
 #banner::after {
@@ -161,10 +176,9 @@ body::before, .wallpaper-part {
 }
 
 .sidebar-column {
-  flex: 1 1 20%;
+  flex: 1 1 35%;
   min-width: 150px;
   max-width: 250px;
-  flex-basis: 250px;
   align-self: flex-start;
 }
 
@@ -179,6 +193,16 @@ body::before, .wallpaper-part {
   display: none;
 }
 
+.sidebar-column.always-content-column {
+  /* duplicated in thin & medium media query */
+  position: static !important;
+  max-width: unset !important;
+  flex-basis: unset !important;
+  margin-right: 0 !important;
+  margin-left: 0 !important;
+  width: 100%;
+}
+
 .sidebar-multiple {
   display: flex;
   flex-direction: column;
@@ -237,7 +261,10 @@ body {
 }
 
 body::before {
-  background-image: url("../../media/bg.jpg");
+  /* This is where the basic background-image rule
+   * gets applied... but the path *to* that media file
+   * isn't part of the CSS itself anymore!
+   */
 }
 
 body::before, .wallpaper-part {
@@ -249,7 +276,11 @@ body::before, .wallpaper-part {
 #page-container {
   background-color: var(--bg-color, rgba(35, 35, 35, 0.8));
   color: #ffffff;
-  box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
+  border-bottom: 2px solid #fff1;
+  box-shadow:
+    0 0 40px #0008,
+    0 2px 15px -3px #2221,
+    0 2px 6px 2px #1113;
 }
 
 #skippers > * {
@@ -339,6 +370,11 @@ body::before, .wallpaper-part {
   margin: 0;
 }
 
+.sidebar h2:first-child {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+
 .sidebar h3 {
   font-size: 1.1em;
   font-style: oblique;
@@ -390,6 +426,42 @@ body::before, .wallpaper-part {
   padding-left: 10px;
 }
 
+.sidebar details.has-tree-list[open] summary {
+  font-weight: 800;
+}
+
+.sidebar dl.tree-list {
+  margin-top: 0.25em;
+  line-height: 1.25em;
+  padding-left: 15px;
+}
+
+.sidebar dl.tree-list dt {
+  display: list-item;
+  list-style-type: disc;
+  padding-left: 0;
+  margin-left: 20px;
+}
+
+.sidebar dl.tree-list dl {
+  padding-left: 15px;
+}
+
+.sidebar dl.tree-list dd {
+  margin-left: 0;
+}
+
+.sidebar dl.tree-list dt.current a {
+  font-weight: 800;
+  border-bottom: 1px solid;
+}
+
+.sidebar .times-used {
+  opacity: 0.7;
+  font-size: 0.9em;
+  cursor: default;
+}
+
 .sidebar li.current {
   font-weight: 800;
 }
@@ -504,6 +576,40 @@ summary.underline-white > span:hover a:not(:hover) {
   margin-top: 0 !important;
 }
 
+.track-release-sidebar-box {
+  --content-padding: 3px;
+}
+
+.track-release-sidebar-box h1 {
+  margin: 0;
+  font-weight: normal;
+  font-size: 0.9em;
+  font-style: oblique;
+}
+
+.track-release-sidebar-box + .track-release-sidebar-box,
+.track-release-sidebar-box + .track-list-sidebar-box,
+.track-list-sidebar-box + .track-release-sidebar-box {
+  margin-top: 5px !important;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.track-release-sidebar-box:has(+ .track-list-sidebar-box),
+.track-list-sidebar-box:has(+ .track-release-sidebar-box) {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.track-list-sidebar-box summary {
+  padding-left: 20px !important;
+  text-indent: -15px !important;
+}
+
+.track-list-sidebar-box .track-section-range {
+  white-space: nowrap;
+}
+
 .wiki-search-sidebar-box {
   padding: 1px 0 0 0;
 
@@ -655,6 +761,96 @@ summary.underline-white > span:hover a:not(:hover) {
   cursor: default;
 }
 
+.wiki-search-filter-container {
+  padding: 4px;
+}
+
+.wiki-search-filter-link {
+  display: inline-block;
+  margin: 2px;
+  padding: 2px 4px;
+  border: 2px solid transparent;
+  border-radius: 4px;
+}
+
+.wiki-search-filter-link:where(.active.shown) {
+  animation:
+    0.15s ease   0.00s forwards normal    show-filter,
+    0.60s linear 0.15s infinite alternate blink-filter;
+}
+
+.wiki-search-filter-link:where(.active:not(.shown)) {
+  animation:
+    0.00s linear 0.00s forwards normal    show-filter,
+    0.60s linear 0.00s infinite alternate blink-filter;
+}
+
+.wiki-search-filter-link:where(:not(.active).hidden) {
+  /* We can't just reverse the show-filter animation,
+   * because that won't actually start it over again.
+   */
+  animation:
+    0.15s ease   0.00s forwards reverse   show-filter-the-sequel;
+}
+
+.wiki-search-filter-link.active-from-query {
+  background: var(--primary-color);
+  border-color: var(--primary-color);
+  color: #000a;
+  animation: none;
+}
+
+.wiki-search-filter-link.active-from-query::after {
+  content: "I";
+  color: black;
+  font-family: monospace;
+  font-weight: 800;
+  font-size: 1.2em;
+  margin-left: 0.5ch;
+  vertical-align: middle;
+  animation: 1s steps(2, jump-none) 0.6s infinite blink-caret;
+}
+
+@keyframes show-filter {
+  from {
+    background: transparent;
+    border-color: transparent;
+    color: var(--primary-color);
+  }
+
+  to {
+    background: var(--primary-color);
+    border-color: var(--primary-color);
+    color: black;
+  }
+}
+
+/* Exactly the same as show-filter above. */
+@keyframes show-filter-the-sequel {
+  from {
+    background: transparent;
+    border-color: transparent;
+    color: var(--primary-color);
+  }
+
+  to {
+    background: var(--primary-color);
+    border-color: var(--primary-color);
+    color: black;
+  }
+}
+
+@keyframes blink-filter {
+  to {
+    background: color-mix(in srgb, var(--primary-color) 90%, transparent);
+  }
+}
+
+@keyframes blink-caret {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
 .wiki-search-result {
   position: relative;
   display: flex;
@@ -818,6 +1014,10 @@ a:not([href]):hover {
   text-decoration: none;
 }
 
+a .normal-content {
+  color: white;
+}
+
 .external-link:not(.from-content) {
   white-space: nowrap;
 }
@@ -831,8 +1031,26 @@ a:not([href]):hover {
   color: white;
 }
 
-.external-link .normal-content {
-  color: white;
+.image-media-link::after {
+  content: '';
+  display: inline-block;
+  width: 22px;
+  height: 1em;
+
+  background-color: var(--primary-color);
+
+  /* mask-image is set in content JavaScript,
+   * because we can't identify the correct nor
+   * absolute path to the file from CSS.
+   */
+
+  mask-repeat: no-repeat;
+  mask-position: calc(100% - 2px);
+  vertical-align: text-bottom;
+}
+
+.image-media-link:hover::after {
+  background-color: white;
 }
 
 .nav-link {
@@ -853,29 +1071,46 @@ a:not([href]):hover {
   font-weight: 800;
 }
 
-.nav-links-hierarchical .nav-link:not(:first-child)::before {
+.nav-links-hierarchical .nav-link + .nav-link::before,
+.nav-links-hierarchical .nav-link + .blockwrap .nav-link::before {
   content: "\0020/\0020";
 }
 
-.series-nav-link {
+.series-nav-links {
   display: inline-block;
 }
 
-.series-nav-link:not(:first-child)::before {
+.series-nav-links:not(:first-child)::before {
   content: "\00a0»\00a0";
   font-weight: normal;
 }
 
-.series-nav-link:not(:last-child)::after {
+.series-nav-links:not(:last-child)::after {
   content: ",\00a0";
 }
 
-.series-nav-link + .series-nav-link::before {
+.series-nav-links + .series-nav-links::before {
   content: "";
 }
 
+.dot-switcher > span:not(:first-child) {
+  display: inline-block;
+  white-space: nowrap;
+}
+
+/* Yeah, all this stuff only applies to elements of the dot switcher
+ * besides the first, which will necessarily have a bullet point at left.
+ */
+.dot-switcher *:where(.dot-switcher > span:not(:first-child) > *) {
+  display: inline-block;
+  white-space: wrap;
+  text-align: left;
+  vertical-align: top;
+}
+
 .dot-switcher > span:not(:first-child)::before {
   content: "\0020\00b7\0020";
+  white-space: pre;
   font-weight: 800;
 }
 
@@ -902,7 +1137,7 @@ a:not([href]):hover {
   display: block;
 }
 
-#secondary-nav.album-secondary-nav.with-previous-next {
+#secondary-nav.album-secondary-nav {
   display: flex;
   justify-content: space-around;
   padding-left: 7.5% !important;
@@ -920,7 +1155,8 @@ a:not([href]):hover {
   margin-right: 5px;
 }
 
-#secondary-nav.album-secondary-nav .dot-switcher {
+#secondary-nav.album-secondary-nav .group-nav-links .dot-switcher,
+#secondary-nav.album-secondary-nav .series-nav-links .dot-switcher {
   white-space: nowrap;
 }
 
@@ -953,7 +1189,9 @@ a:not([href]):hover {
 .text-with-tooltip.datetimestamp .text-with-tooltip-interaction-cue,
 .text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue,
 .text-with-tooltip.commentary-date .text-with-tooltip-interaction-cue,
-.text-with-tooltip.wiki-edits .text-with-tooltip-interaction-cue {
+.text-with-tooltip.wiki-edits .text-with-tooltip-interaction-cue,
+.text-with-tooltip.rerelease .text-with-tooltip-interaction-cue,
+.text-with-tooltip.first-release .text-with-tooltip-interaction-cue {
   cursor: default;
 }
 
@@ -970,7 +1208,17 @@ a:not([href]):hover {
   text-decoration: none !important;
 }
 
+.text-with-tooltip.wiki-edits > .hoverable {
+  white-space: nowrap;
+}
+
+.isolate-tooltip-z-indexing > * {
+  position: relative;
+  z-index: -1;
+}
+
 .tooltip {
+  font-size: 1rem;
   position: absolute;
   z-index: 3;
   left: -10px;
@@ -978,7 +1226,12 @@ a:not([href]):hover {
   display: none;
 }
 
-li:not(:first-child:last-child) .tooltip,
+.cover-artwork .tooltip,
+#sidebar .tooltip {
+  font-size: 0.9rem;
+}
+
+li:not(:first-child:last-child) .tooltip:where(:not(.cover-artwork .tooltip)),
 .offset-tooltips > :not(:first-child:last-child) .tooltip {
   left: 14px;
 }
@@ -1010,7 +1263,10 @@ li:not(:first-child:last-child) .tooltip,
 
 .datetimestamp-tooltip,
 .missing-duration-tooltip,
-.commentary-date-tooltip {
+.commentary-date-tooltip,
+.rerelease-tooltip,
+.first-release-tooltip,
+.content-tooltip {
   padding: 3px 4px 2px 2px;
   left: -10px;
 }
@@ -1018,18 +1274,23 @@ li:not(:first-child:last-child) .tooltip,
 .thing-name-tooltip,
 .wiki-edits-tooltip {
   padding: 3px 4px 2px 2px;
-  left: -6px !important;
+  left: -6px;
 }
 
-.wiki-edits-tooltip {
+.thing-name-tooltip .tooltip-content,
+.wiki-edits-tooltip .tooltip-content {
   font-size: 0.85em;
 }
 
-/* Terrifying?
- * https://stackoverflow.com/a/64424759/4633828
- */
-.thing-name-tooltip { margin-right: -120px; }
-.wiki-edits-tooltip { margin-right: -200px; }
+.thing-name-tooltip .tooltip-content {
+  width: max-content;
+  max-width: 120px;
+}
+
+.wiki-edits-tooltip .tooltip-content {
+  width: max-content;
+  max-width: 200px;
+}
 
 .contribution-tooltip .tooltip-content {
   padding: 6px 2px 2px 2px;
@@ -1105,11 +1366,16 @@ li:not(:first-child:last-child) .tooltip,
   font-size: 0.85em;
 }
 
-.contribution-tooltip .tooltip-divider {
+.contribution-tooltip .tooltip-divider,
+.tooltip-content hr.cute {
   grid-column-start: icon-start;
   grid-column-end: platform-end;
-
   border-top: 1px dotted var(--primary-color);
+}
+
+/* Don't mind me... */
+.tooltip-content .tooltip-divider,
+.tooltip-content hr.cute {
   margin-top: 3px;
   margin-bottom: 4px;
 }
@@ -1175,6 +1441,37 @@ li:not(:first-child:last-child) .tooltip,
   padding: 3px 4.5px;
 }
 
+.rerelease-tooltip .tooltip-content,
+.first-release-tooltip .tooltip-content {
+  padding: 3px 4.5px;
+  width: 260px;
+  font-size: 0.9em;
+}
+
+.content-tooltip-guy .hoverable a {
+  text-decoration-color: transparent;
+  text-decoration-style: dotted;
+}
+
+.content-tooltip-guy {
+  display: inline-block;
+}
+
+.content-tooltip-guy.has-link .text-with-tooltip-interaction-cue {
+  text-decoration-color: var(--primary-color);
+}
+
+.content-tooltip .tooltip-content {
+  padding: 3px 4.5px;
+  width: 240px;
+}
+
+.cover-artwork .content-tooltip {
+  font-size: 0.85rem;
+  padding: 2px 3px;
+  width: 220px;
+}
+
 .external-icon {
   display: inline-block;
   padding: 0 3px;
@@ -1191,8 +1488,8 @@ li:not(:first-child:last-child) .tooltip,
   fill: var(--primary-color);
 }
 
-.rerelease,
-.other-group-accent {
+.other-group-accent,
+.rerelease-line {
   opacity: 0.7;
   font-style: oblique;
 }
@@ -1225,6 +1522,38 @@ s.spoiler::-moz-selection {
   background: white;
 }
 
+span.path, code.filename {
+  font-size: 0.95em;
+  font-family: "courier new", monospace;
+  font-weight: 800;
+  background: #ccc3;
+
+  padding: 0.05em 0.5ch;
+  border: 1px solid #ccce;
+  border-radius: 2px;
+  line-height: 1.4;
+}
+
+.image-details code.filename {
+  margin-left: -0.4ch;
+  opacity: 0.8;
+}
+
+.image-details code.filename:hover {
+  opacity: 1;
+  cursor: text;
+}
+
+span.path i {
+  display: inline-block;
+  font-style: normal;
+}
+
+span.path i::before {
+  content: "\0020/\0020";
+  color: #ccc;
+}
+
 progress {
   accent-color: var(--primary-color);
 }
@@ -1246,12 +1575,16 @@ p .current {
   font-weight: 800;
 }
 
-#cover-art-container {
+hr.cute,
+#content hr.cute,
+.sidebar hr.cute {
+  border-color: var(--primary-color);
+  border-style: none none dotted none;
+}
+
+.cover-artwork {
   font-size: 0.8em;
   border: 2px solid var(--primary-color);
-  box-shadow:
-    0 2px 14px -6px var(--primary-color),
-    0 0 12px 12px #00000080;
 
   border-radius: 0 0 4px 4px;
   background: var(--bg-black-color);
@@ -1260,37 +1593,64 @@ p .current {
           backdrop-filter: blur(3px);
 }
 
-#cover-art-container:has(.image-details),
-#cover-art-container.has-image-details {
+.cover-artwork:has(.image-details),
+.cover-artwork.has-image-details {
   border-radius: 0 0 6px 6px;
 }
 
-#cover-art-container:not(:has(.image-details)),
-#cover-art-container:not(.has-image-details) {
+.cover-artwork:not(:has(.image-details)),
+.cover-artwork:not(.has-image-details) {
   /* Hacky: `overflow: hidden` hides tag tooltips, so it can't be applied
    * if we've got tags/details visible. But it's okay, because we only
    * need to apply it if it *doesn't* - that's when the rounded border
-   * of #cover-art-container needs to cut off its child image-container
+   * of the .cover-artwork needs to cut off its child .image-container
    * (which has a background that otherwise causes sharp corners).
    */
   overflow: hidden;
 }
 
-#cover-art-container .image-container {
-  /* Border is handled on the cover-art-container. */
+#artwork-column .cover-artwork {
+  box-shadow:
+    0 2px 14px -6px var(--primary-color),
+    0 0 12px 12px #00000080;
+}
+
+#artwork-column .cover-artwork:not(:first-child),
+#artwork-column .cover-artwork-joiner {
+  margin-left: 30px;
+  margin-right: 5px;
+}
+
+#artwork-column .cover-artwork:first-child + .cover-artwork-joiner,
+#artwork-column .cover-artwork.attached-artwork-is-main-artwork,
+#artwork-column .cover-artwork.attached-artwork-is-main-artwork + .cover-artwork-joiner {
+  margin-left: 17.5px;
+  margin-right: 17.5px;
+}
+
+.cover-artwork:where(#artwork-column .cover-artwork:not(:first-child)) {
+  margin-top: 20px;
+}
+
+#artwork-column .cover-artwork:last-child:not(:first-child) {
+  margin-bottom: 25px;
+}
+
+.cover-artwork .image-container {
+  /* Border is handled on the .cover-artwork. */
   border: none;
-  border-radius: 0;
+  border-radius: 0 !important;
 }
 
-#cover-art-container .image-details {
+.cover-artwork .image-details {
   border-top-color: var(--deep-color);
 }
 
-#cover-art-container .image-details + .image-details {
+.cover-artwork .image-details + .image-details {
   border-top-color: var(--primary-color);
 }
 
-#cover-art-container .image {
+.cover-artwork .image {
   display: block;
   width: 100%;
   height: 100%;
@@ -1333,6 +1693,10 @@ p .current {
   margin-bottom: 2px;
 }
 
+ul.image-details.art-tag-details {
+  padding-bottom: 0;
+}
+
 ul.image-details.art-tag-details li {
   display: inline-block;
 }
@@ -1341,35 +1705,64 @@ ul.image-details.art-tag-details li:not(:last-child)::after {
   content: " \00b7 ";
 }
 
-.image-details.non-unique-details {
-  font-style: oblique;
-}
-
 p.image-details.illustrator-details {
   text-align: center;
   font-style: oblique;
 }
 
-#artist-commentary.first-entry-is-dated {
-  clear: right;
+p.image-details.origin-details {
+  margin-bottom: 2px;
 }
 
-.commentary-entry-heading {
-  display: flex;
-  flex-direction: row;
+p.image-details.origin-details .origin-details {
+  display: block;
+  margin-top: 0.25em;
+}
 
-  margin-left: 15px;
-  padding-left: 5px;
-  max-width: 625px;
-  padding-bottom: 0.2em;
+.cover-artwork-joiner {
+  z-index: -2;
+}
 
-  border-bottom: 1px solid var(--dim-color);
+.cover-artwork-joiner::after {
+  content: "";
+  display: block;
+  width: 0;
+  height: 15px;
+  margin-left: auto;
+  margin-right: auto;
+  border-right: 3px solid var(--primary-color);
 }
 
-.commentary-entry-heading-text {
-  flex-grow: 1;
-  padding-left: 1.25ch;
+.cover-artwork-joiner + .cover-artwork {
+  margin-top: 0 !important;
+}
+
+.album-art-info {
+  font-size: 0.8em;
+  border: 2px solid var(--deep-color);
+
+  margin: 10px min(15px, 1vw) 15px;
+
+  background: var(--bg-black-color);
+  padding: 6px;
+  border-radius: 5px;
+
+  -webkit-backdrop-filter: blur(3px);
+          backdrop-filter: blur(3px);
+}
+
+.album-art-info p {
+  margin: 0;
+}
+
+.commentary-entry-heading {
+  margin-left: 15px;
+  padding-left: calc(5px + 1.25ch);
   text-indent: -1.25ch;
+  margin-right: min(calc(8vw - 35px), 45px);
+  padding-bottom: 0.2em;
+
+  border-bottom: 1px solid var(--dim-color);
 }
 
 .commentary-entry-accent {
@@ -1377,13 +1770,12 @@ p.image-details.illustrator-details {
 }
 
 .commentary-entry-heading .commentary-date {
-  flex-shrink: 0;
-
-  margin-left: 0.75ch;
-  align-self: flex-end;
+  display: inline-block;
+  text-indent: 0;
+}
 
-  padding-left: 0.5ch;
-  padding-right: 0.25ch;
+.commentary-entry-heading.dated .commentary-entry-heading-text {
+  margin-right: 0.75ch;
 }
 
 .commentary-entry-heading .hoverable {
@@ -1398,6 +1790,15 @@ p.image-details.illustrator-details {
   color: var(--primary-color);
 }
 
+.inherited-commentary-section {
+  clear: right;
+  margin-top: 1em;
+  margin-right: min(4vw, 60px);
+  border: 2px solid var(--deep-color);
+  border-radius: 4px;
+  background: #ffffff07;
+}
+
 .commentary-art {
   float: right;
   width: 30%;
@@ -1412,6 +1813,33 @@ p.image-details.illustrator-details {
   box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important;
 }
 
+.lyrics-switcher {
+  padding-left: 20px;
+}
+
+.lyrics-switcher > span:not(:first-child)::before {
+  content: "\0020\00b7\0020";
+  font-weight: 800;
+}
+
+.lyrics-entry {
+  padding-left: 40px;
+}
+
+.lyrics-entry .lyrics-details,
+.lyrics-entry .origin-details {
+  font-size: 0.9em;
+  font-style: oblique;
+}
+
+.lyrics-entry .lyrics-details {
+  margin-bottom: 0;
+}
+
+.lyrics-entry .origin-details {
+  margin-top: 0.25em;
+}
+
 .js-hide,
 .js-show-once-data,
 .js-hide-once-data {
@@ -1419,24 +1847,32 @@ p.image-details.illustrator-details {
 }
 
 .content-image-container,
-.content-video-container {
+.content-video-container,
+.content-audio-container {
   margin-top: 1em;
   margin-bottom: 1em;
 }
 
-.content-image-container.align-center,
-.content-video-container.align-center {
+.content-image-container.align-center {
   text-align: center;
   margin-top: 1.5em;
   margin-bottom: 1.5em;
 }
 
-a.align-center, img.align-center {
+.content-image-container.align-full {
+  width: 100%;
+}
+
+a.align-center, img.align-center, audio.align-center, video.align-center {
   display: block;
   margin-left: auto;
   margin-right: auto;
 }
 
+a.align-full, img.align-full, video.align-full {
+  width: 100%;
+}
+
 center {
   margin-top: 1em;
   margin-bottom: 1em;
@@ -1493,11 +1929,6 @@ h1 {
   font-size: 2em;
 }
 
-html[data-url-key="localized.home"] #content h1 {
-  text-align: center;
-  font-size: 2.5em;
-}
-
 #content.flash-index h2 {
   text-align: center;
   font-size: 2.5em;
@@ -1552,6 +1983,54 @@ ul.quick-info li:not(:last-child)::after {
   margin-top: 25px;
 }
 
+.gallery-set-switcher {
+  text-align: center;
+}
+
+.gallery-view-switcher {
+  margin-left: auto;
+  margin-right: auto;
+  text-align: center;
+  line-height: 1.4;
+}
+
+#content.top-index section {
+  margin-bottom: 1.5em;
+}
+
+.expandable-gallery-section .section-expando {
+  margin-top: 1em;
+  margin-bottom: 2em;
+
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+}
+
+.expandable-gallery-section .section-expando-content {
+  text-align: center;
+  line-height: 1.5;
+}
+
+.expandable-gallery-section .section-expando-toggle {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+.expandable-gallery-section.expanded .section-content-below-cut {
+  animation: expand-gallery-section 0.8s forwards;
+}
+
+@keyframes expand-gallery-section {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
 .quick-description:not(.has-external-links-only) {
   --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06);
   margin-left: auto;
@@ -1582,6 +2061,7 @@ ul.quick-info li:not(:last-child)::after {
 
 .quick-description > blockquote {
   margin-left: 0 !important;
+  margin-right: 0 !important;
 }
 
 .quick-description .description-content.long hr ~ p {
@@ -1629,7 +2109,6 @@ ul.quick-info li:not(:last-child)::after {
 
 li .by {
   font-style: oblique;
-  max-width: 600px;
 }
 
 li .by a {
@@ -1645,8 +2124,8 @@ p code {
 
 #content blockquote {
   margin-left: 40px;
-  max-width: 600px;
-  margin-right: 0;
+  margin-right: min(8vw, 75px);
+  width: auto;
 }
 
 #content blockquote blockquote {
@@ -1693,7 +2172,6 @@ main.long-content > h1 {
 
 dl dt {
   padding-left: 40px;
-  max-width: 600px;
 }
 
 dl dt {
@@ -1703,6 +2181,10 @@ dl dt {
   margin-bottom: 2px;
 }
 
+dl dt[id]:not(.content-heading) {
+  --custom-scroll-offset: calc(2.5em - 2px);
+}
+
 dl dd {
   margin-bottom: 1em;
 }
@@ -1718,6 +2200,13 @@ ul > li.has-details {
   margin-left: -17px;
 }
 
+li .origin-details {
+  display: block;
+  margin-left: 2ch;
+  font-size: 0.9em;
+  font-style: oblique;
+}
+
 .album-group-list dt,
 .group-series-list dt {
   font-style: oblique;
@@ -1729,6 +2218,15 @@ ul > li.has-details {
   margin-left: 0;
 }
 
+.album-group-list li {
+  padding-left: 1.5ch;
+  text-indent: -1.5ch;
+}
+
+.album-group-list li > * {
+  text-indent: 0;
+}
+
 .album-group-list blockquote {
   max-width: 540px;
   margin-bottom: 9px;
@@ -1759,31 +2257,54 @@ ul > li.has-details {
 
 #content hr {
   border: 1px inset #808080;
-  width: 100%;
+}
+
+#content hr.split {
+  color: #808080;
 }
 
 #content hr.split::before {
   content: "(split)";
-  color: #808080;
 }
 
-#content hr.split {
+#content hr.main-separator {
+  color: var(--dim-color);
+  clear: none;
+  margin-top: -0.25em;
+  margin-bottom: 1.75em;
+}
+
+#content hr.main-separator::before {
+  content: "♦";
+  font-size: 1.2em;
+}
+
+#content hr.split,
+#content hr.main-separator {
   position: relative;
   overflow: hidden;
   border: none;
 }
 
-#content hr.split::after {
+#content hr.split::after,
+#content hr.main-separator::after {
   display: inline-block;
   content: "";
-  border: 1px inset #808080;
-  width: 100%;
+  width: calc(100% - min(calc(8vw - 35px), 45px));
   position: absolute;
   top: 50%;
-  margin-top: -2px;
   margin-left: 10px;
 }
 
+#content hr.split::after {
+  border: 1px inset currentColor;
+  margin-top: -2px;
+}
+
+#content hr.main-separator::after {
+  border-bottom: 1px solid currentColor;
+}
+
 li > ul {
   margin-top: 5px;
 }
@@ -1898,19 +2419,9 @@ h1 a[href="#additional-names-box"]:hover {
   --custom-scroll-offset: calc(0.5em - 2px);
 
   margin: 1em 0 1em -10px;
-  padding: 15px 20px 10px 20px;
-  width: max-content;
   max-width: min(60vw, 600px);
 
-  border: 1px dotted var(--primary-color);
-  border-radius: 6px;
-
-  background:
-    linear-gradient(var(--bg-color), var(--bg-color)),
-    linear-gradient(#000000bb, #000000bb),
-    var(--primary-color);
-
-  box-shadow: 0 -2px 6px -1px var(--dim-color) inset;
+  padding: 15px 20px 10px 20px;
 
   display: none;
 }
@@ -1947,6 +2458,203 @@ h1 a[href="#additional-names-box"]:hover {
   vertical-align: text-bottom;
 }
 
+#content.top-index #additional-names-box {
+  margin-left: auto;
+  margin-right: auto;
+  margin-bottom: 2em;
+}
+
+#content.top-index #additional-names-box {
+  text-align: center;
+  margin-bottom: 0.75em;
+}
+
+/* Specific pages - homepage */
+
+html[data-url-key="localized.home"] #content h1 {
+  text-align: center;
+  font-size: 2.5em;
+}
+
+html[data-language-code="preview-en"][data-url-key="localized.home"] #content h1::after {
+  font-family: cursive;
+  display: block;
+  content: "(Preview Build)";
+  font-size: 0.8em;
+}
+
+/* Specific pages - art tag gallery */
+
+html[data-url-key="localized.artTagGallery"] #descends-from-line {
+  margin-bottom: 0.25em;
+}
+
+html[data-url-key="localized.artTagGallery"] #descendants-line {
+  margin-top: 0.25em;
+}
+
+html[data-url-key="localized.artTagGallery"] #descends-from-line a,
+html[data-url-key="localized.artTagGallery"] #descendants-line a {
+  display: inline-block;
+}
+
+
+html[data-url-key="localized.artTagGallery"] #featured-direct-line,
+html[data-url-key="localized.artTagGallery"] #featured-indirect-line,
+html[data-url-key="localized.artTagGallery"] #showing-direct-line,
+html[data-url-key="localized.artTagGallery"] #showing-indirect-line {
+  display: none;
+}
+
+html[data-url-key="localized.artTagGallery"] #showing-all-line a,
+html[data-url-key="localized.artTagGallery"] #showing-direct-line a,
+html[data-url-key="localized.artTagGallery"] #showing-indirect-line a {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+/* Specific pages - "Art Tag Network" listing */
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd) {
+  margin-left: 20px;
+  margin-bottom: 0;
+  padding-left: 10px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd):not(:last-child) {
+  padding-bottom: 20px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-stat-line {
+  padding-left: 10px;
+  margin-left: 20px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-stat-line a {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] .network-tag-stat {
+  display: inline-block;
+  text-align: right;
+  min-width: 5ch;
+  margin-right: 2px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-top-dl > dt:has(.network-tag.with-stat:not([style*="display: none"])) {
+  padding-left: 20px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dt + dt:has(+ dd) {
+  padding-top: 20px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dt:has(+ dd) .network-tag-stat {
+  text-align: center;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt {
+  padding-left: 10px;
+  margin-left: 20px;
+  margin-bottom: 0;
+  padding-bottom: 2px;
+  max-width: unset;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd).even,
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:not(#network-top-dl > dt).even {
+  border-left: 1px solid #eaeaea;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd).odd,
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:not(#network-top-dl > dt).odd {
+  border-left: 1px solid #7b7b7b;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd,
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt {
+  position: relative;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:last-child:not(#network-top-dl > dd).odd::after,
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:last-child:not(#network-top-dl > dt).odd::after {
+  content: "";
+  display: block;
+  width: 7px;
+  height: 7px;
+  background: #7b7b7b;
+  position: absolute;
+  bottom: -4px;
+  left: -4px;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:last-child:not(#network-top-dl > dd).even::after,
+html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:last-child:not(#network-top-dl > dt).even::after {
+  content: "";
+  display: block;
+  width: 6px;
+  height: 6px;
+  background: #eaeaea;
+  position: absolute;
+  bottom: -3px;
+  left: -3px;
+  border-bottom-right-radius: 6px;
+  border-top-left-radius: 3px;
+}
+
+/* "Drops" */
+
+.drop {
+  padding: 15px 20px;
+  width: max-content;
+  max-width: min(60vw, 600px);
+
+  border: 1px dotted var(--primary-color);
+  border-radius: 6px;
+
+  background:
+    linear-gradient(var(--bg-color), var(--bg-color)),
+    linear-gradient(#000000bb, #000000bb),
+    var(--primary-color);
+
+  --drop-shadow: 0 -2px 6px -1px var(--dim-color) inset;
+  box-shadow: var(--drop-shadow);
+}
+
+.drop.shiny {
+  cursor: default;
+}
+
+@supports (box-shadow: 1px 1px 1px color-mix(in srgb, blue, 40% red)) {
+  @property --drop-shine {
+    syntax: '<percentage>';
+    initial-value: 0%;
+    inherits: false;
+  }
+
+  .drop.shiny {
+    cursor: default;
+    transition: --drop-shine 0.2s;
+  }
+
+  .drop.shiny:hover {
+    --drop-shine: 100%;
+
+    box-shadow:
+      var(--drop-shadow),
+      0 2px 4px -0.5px color-mix(in srgb, var(--primary-color), calc(100% - var(--drop-shine)) transparent);
+  }
+}
+
+.commentary-drop {
+  margin-top: 25px;
+  margin-bottom: 15px;
+  margin-left: 20px;
+  padding: 10px 20px;
+  max-width: min(60vw, 300px);
+}
+
 /* Images */
 
 .image-container {
@@ -1965,15 +2673,46 @@ h1 a[href="#additional-names-box"]:hover {
   color: white;
 }
 
-/* Videos (in content) get a lite version of image-container. */
-.content-video-container {
-  width: min-content;
+/* Videos and audios (in content) get a lite version of image-container. */
+.content-video-container,
+.content-audio-container {
+  width: fit-content;
+  max-width: 100%;
   background-color: var(--dark-color);
   border: 2px solid var(--primary-color);
   border-radius: 2.5px 2.5px 3px 3px;
   padding: 5px;
 }
 
+.content-video-container video,
+.content-audio-container audio {
+  display: block;
+  max-width: 100%;
+}
+
+.content-video-container.align-center,
+.content-audio-container.align-center {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.content-video-container.align-full,
+.content-audio-container.align-full {
+  width: 100%;
+}
+
+.content-audio-container .filename {
+  color: white;
+  font-family: monospace;
+  display: block;
+  font-size: 0.9em;
+  padding-left: 1ch;
+  padding-right: 1ch;
+  padding-bottom: 0.25em;
+  margin-bottom: 0.5em;
+  border-bottom: 1px solid #fff4;
+}
+
 .image-text-area {
   position: absolute;
   top: 0;
@@ -2028,6 +2767,12 @@ img {
   object-fit: cover;
 }
 
+p > img {
+  max-width: 100%;
+  object-fit: contain;
+  height: auto;
+}
+
 .image-inner-area::after {
   content: "";
   display: block;
@@ -2258,6 +3003,10 @@ video.pixelate, .pixelate video {
   max-width: 200px;
 }
 
+.grid-name-marker {
+  color: white;
+}
+
 .grid-actions {
   display: flex;
   flex-direction: row;
@@ -2616,14 +3365,32 @@ h3.content-heading {
   );
 }
 
+.content-sticky-heading-root {
+  width: calc(100% + 2 * var(--content-padding));
+  margin: calc(-1 * var(--content-padding));
+  margin-bottom: 0;
+}
+
+.content-sticky-heading-anchor,
 .content-sticky-heading-container {
+  width: 100%;
+}
+
+.content-sticky-heading-root:not([inert]) {
   position: sticky;
   top: 0;
+}
 
-  margin: calc(-1 * var(--content-padding));
-  margin-bottom: calc(0.5 * var(--content-padding));
+.content-sticky-heading-anchor:not(:where(.content-sticky-heading-root[inert]) *) {
+  position: relative;
+}
 
-  transform: translateY(-5px);
+.content-sticky-heading-container:not(:where(.content-sticky-heading-root[inert]) *) {
+  position: absolute;
+}
+
+.content-sticky-heading-root[inert] {
+  visibility: hidden;
 }
 
 main.long-content .content-sticky-heading-container {
@@ -2663,9 +3430,60 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   grid-template-columns: 1fr min(40%, 400px);
 }
 
+.content-sticky-heading-container.cover-visible .content-sticky-heading-row {
+  grid-template-columns: 1fr min(40%, 90px);
+}
+
+.content-sticky-heading-root.has-cover {
+  padding-right: min(40%, 400px);
+}
+
 .content-sticky-heading-row h1 {
+  position: relative;
   margin: 0;
   padding-right: 20px;
+  line-height: 1.4;
+}
+
+.content-sticky-heading-row h1 .reference-collapsed-heading {
+  position: absolute;
+  white-space: nowrap;
+  visibility: hidden;
+}
+
+.content-sticky-heading-container.collapse h1 {
+  white-space: nowrap;
+  overflow-wrap: normal;
+
+  animation: collapse-sticky-heading 0.35s forwards;
+  text-overflow: ellipsis;
+  overflow-x: hidden;
+}
+
+@keyframes collapse-sticky-heading {
+  from {
+    height: var(--uncollapsed-heading-height);
+  }
+
+  99.9% {
+    height: var(--collapsed-heading-height);
+  }
+
+  to {
+    height: auto;
+  }
+}
+
+.content-sticky-heading-container h1 a {
+  transition: text-decoration-color 0.35s;
+}
+
+.content-sticky-heading-container h1 a:not([href]) {
+  color: inherit;
+  cursor: text;
+  text-decoration: underline;
+  text-decoration-style: dotted;
+  text-decoration-color: transparent;
 }
 
 .content-sticky-heading-cover-container {
@@ -2693,7 +3511,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   transition: transform 0.35s, opacity 0.30s;
 }
 
-.content-sticky-heading-cover .image-container {
+.content-sticky-heading-cover .cover-artwork {
   border-width: 1px;
   border-radius: 1.25px;
   box-shadow: none;
@@ -2887,31 +3705,34 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
           backdrop-filter: blur(3px);
 }
 
-#image-overlay-image-container {
+#image-overlay-image-area {
   display: block;
-  position: relative;
   overflow: hidden;
   width: 80vmin;
-  height: 80vmin;
   margin-left: auto;
   margin-right: auto;
 }
 
+#image-overlay-image-layout {
+  display: block;
+  position: relative;
+  margin: 4px 3px;
+  background: rgba(0, 0, 0, 0.65);
+}
+
 #image-overlay-image,
 #image-overlay-image-thumb {
-  display: inline-block;
-  object-fit: contain;
+  display: block;
   width: 100%;
-  height: 100%;
-  background: rgba(0, 0, 0, 0.65);
+  height: auto;
 }
 
 #image-overlay-image {
   position: absolute;
-  top: 3px;
-  left: 3px;
-  width: calc(100% - 6px);
-  height: calc(100% - 4px);
+}
+
+#image-overlay-container.no-thumb #image-overlay-image {
+  position: static;
 }
 
 #image-overlay-image-thumb {
@@ -2925,7 +3746,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   transition: opacity 0.25s;
 }
 
-#image-overlay-image-container::after {
+#image-overlay-image-area::after {
   content: "";
   display: block;
   position: absolute;
@@ -2938,18 +3759,18 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   transition: 0.25s;
 }
 
-#image-overlay-container.loaded #image-overlay-image-container::after {
+#image-overlay-container.loaded #image-overlay-image-area::after {
   width: 100%;
   background: white;
   opacity: 0;
 }
 
-#image-overlay-container.errored #image-overlay-image-container::after {
+#image-overlay-container.errored #image-overlay-image-area::after {
   width: 100%;
   background: red;
 }
 
-#image-overlay-container:not(.visible) #image-overlay-image-container::after {
+#image-overlay-container:not(.visible) #image-overlay-image-area::after {
   width: 0 !important;
 }
 
@@ -2977,21 +3798,11 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   font-size: 0.9em;
 }
 
-/* important easter egg mode */
-
-html[data-language-code="preview-en"][data-url-key="localized.home"] #content
-  h1::after {
-  font-family: cursive;
-  display: block;
-  content: "(Preview Build)";
-  font-size: 0.8em;
-}
-
 /* Layout - Wide (most computers) */
 
-@media (min-width: 900px) {
-  #page-container.showing-sidebar-left #secondary-nav:not(.always-visible),
-  #page-container.showing-sidebar-right #secondary-nav:not(.always-visible) {
+@media (min-width: 850px) {
+  #page-container.showing-sidebar-left:not(.sidebars-in-content-column) #secondary-nav:not(.always-visible),
+  #page-container.showing-sidebar-right:not(.sidebars-in-content-column) #secondary-nav:not(.always-visible) {
     display: none;
   }
 }
@@ -3003,7 +3814,7 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
  * if so desired.
  */
 
-@media (min-width: 600px) and (max-width: 899.98px) {
+@media (min-width: 600px) and (max-width: 849.98px) {
   /* Medium layout is mainly defined (to the user) by hiding the sidebar, so
    * don't apply the similar layout change of widening the long-content area
    * if this page doesn't have a sidebar to hide in the first place.
@@ -3017,7 +3828,7 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
 /* Layout - Wide or Medium */
 
 @media (min-width: 600px) {
-  .content-sticky-heading-container {
+  .content-sticky-heading-root {
     /* Safari doesn't always play nicely with position: sticky,
      * this seems to fix images sometimes displaying above the
      * position: absolute subheading (h2) child
@@ -3031,10 +3842,11 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
   /* Cover art floats to the right. It's positioned in HTML beneath the
    * heading, so pull it up a little to "float" on top.
    */
-  #cover-art-container {
+  #artwork-column {
     float: right;
     width: 40%;
-    max-width: 400px;
+    min-width: 220px;
+    max-width: 280px;
     margin: -60px 0 10px 20px;
 
     position: relative;
@@ -3044,7 +3856,7 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
   /* ...Except on top-indexes, where cover art is displayed prominently
    * between the heading and subheading.
    */
-  #content.top-index #cover-art-container {
+  #content.top-index #artwork-column {
     float: none;
     margin: 2em auto 2.5em auto;
     max-width: 375px;
@@ -3063,13 +3875,15 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
 
 /* Layout - Medium or Thin */
 
-@media (max-width: 899.98px) {
+@media (max-width: 849.98px) {
   .sidebar.collapsible,
   .sidebar-box-joiner.collapsible,
   .sidebar-column.all-boxes-collapsible {
     display: none;
   }
 
+  /* Duplicated for "sidebars in content column" */
+
   .layout-columns {
     flex-direction: column;
   }
@@ -3091,11 +3905,53 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
     display: none;
   }
 
+  .wiki-search-sidebar-box {
+    max-height: max(245px, 60vh, calc(100vh - 205px));
+  }
+
+  /* End duplicated for "sidebars in content column" */
+
   .grid-listing > .grid-item {
     flex-basis: 40%;
   }
 }
 
+/* Layout - "sidebars in content column"
+ * This is the same code as immediately above, for medium and
+ * thin layouts, but can be opted into by the page itself
+ * instead of through a media query.
+ */
+
+#page-container.sidebars-in-content-column
+.layout-columns {
+  flex-direction: column;
+}
+
+#page-container.sidebars-in-content-column
+.layout-columns > *:not(:last-child) {
+  margin-bottom: 10px;
+}
+
+#page-container.sidebars-in-content-column
+.sidebar-column {
+  position: static !important;
+  max-width: unset !important;
+  flex-basis: unset !important;
+  margin-right: 0 !important;
+  margin-left: 0 !important;
+  width: 100%;
+}
+
+#page-container.sidebars-in-content-column
+.sidebar .news-entry:not(.first-news-entry) {
+  display: none;
+}
+
+#page-container.sidebars-in-content-column
+.wiki-search-sidebar-box {
+  max-height: max(245px, 60vh, calc(100vh - 205px));
+}
+
 /* Layout - Thin (phones) */
 
 @media (max-width: 600px) {
@@ -3107,12 +3963,18 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
     --responsive-padding-ratio: 0.02;
   }
 
-  #cover-art-container {
+  #artwork-column {
     margin: 25px 0 5px 0;
     width: 100%;
     max-width: unset;
   }
 
+  #artwork-column .cover-artwork:not(:first-child),
+  #artwork-column .cover-artwork-joiner {
+    margin-left: 30px;
+    margin-right: 30px;
+  }
+
   #additional-names-box {
     width: unset;
     max-width: unset;
@@ -3124,7 +3986,7 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
 
   /* Show sticky heading above cover art */
 
-  .content-sticky-heading-container {
+  .content-sticky-heading-root {
     z-index: 2;
   }
 
diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js
index f06b707a..71112313 100644
--- a/src/static/js/client-util.js
+++ b/src/static/js/client-util.js
@@ -1,12 +1,19 @@
 /* eslint-env browser */
 
 export function rebase(href, rebaseKey = 'rebaseLocalized') {
-  const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/';
-  if (relative) {
-    return relative + href;
-  } else {
-    return href;
+  let result = document.documentElement.dataset[rebaseKey] || './';
+
+  if (!result.endsWith('/')) {
+    result += '/';
+  }
+
+  if (href.startsWith('/')) {
+    href = href.slice(1);
   }
+
+  result += href;
+
+  return result;
 }
 
 export function cssProp(el, ...args) {
diff --git a/src/static/js/client/additional-names-box.js b/src/static/js/client/additional-names-box.js
index 558ef06f..195ba25d 100644
--- a/src/static/js/client/additional-names-box.js
+++ b/src/static/js/client/additional-names-box.js
@@ -3,12 +3,17 @@
 import {cssProp} from '../client-util.js';
 
 import {info as hashLinkInfo} from './hash-link.js';
+import {info as stickyHeadingInfo} from './sticky-heading.js';
 
 export const info = {
   id: 'additionalNamesBoxInfo',
 
   box: null,
+
   links: null,
+  stickyHeadingLink: null,
+
+  contentContainer: null,
   mainContentContainer: null,
 
   state: {
@@ -23,6 +28,16 @@ export function getPageReferences() {
   info.links =
     document.querySelectorAll('a[href="#additional-names-box"]');
 
+  info.stickyHeadingLink =
+    document.querySelector(
+      '.content-sticky-heading-container' +
+      ' ' +
+      'a[href="#additional-names-box"]' +
+      ':not(:where([inert] *))');
+
+  info.contentContainer =
+    document.querySelector('#content');
+
   info.mainContentContainer =
     document.querySelector('#content .main-content-container');
 }
@@ -33,6 +48,34 @@ export function addInternalListeners() {
       return false;
     }
   });
+
+  stickyHeadingInfo.event.whenStuckStatusChanges.push((index, stuck) => {
+    const {state} = info;
+
+    if (!info.stickyHeadingLink) return;
+
+    const container = stickyHeadingInfo.contentContainers[index];
+    if (container !== info.contentContainer) return;
+
+    if (stuck) {
+      if (!state.visible) {
+        info.stickyHeadingLink.removeAttribute('href');
+
+        if (info.stickyHeadingLink.hasAttribute('title')) {
+          info.stickyHeadingLink.dataset.restoreTitle = info.stickyHeadingLink.getAttribute('title');
+          info.stickyHeadingLink.removeAttribute('title');
+        }
+      }
+    } else {
+      info.stickyHeadingLink.setAttribute('href', '#additional-names-box');
+
+      const {restoreTitle} = info.stickyHeadingLink.dataset;
+      if (restoreTitle) {
+        info.stickyHeadingLink.setAttribute('title', restoreTitle);
+        delete info.stickyHeadingLink.dataset.restoreTitle;
+      }
+    }
+  });
 }
 
 export function addPageListeners() {
@@ -48,6 +91,7 @@ function handleAdditionalNamesBoxLinkClicked(domEvent) {
 
   domEvent.preventDefault();
 
+  if (!domEvent.target.hasAttribute('href')) return;
   if (!info.box || !info.mainContentContainer) return;
 
   const margin =
@@ -58,7 +102,30 @@ function handleAdditionalNamesBoxLinkClicked(domEvent) {
       ? info.box.getBoundingClientRect()
       : info.mainContentContainer.getBoundingClientRect());
 
-  if (top + 20 < margin || top > 0.4 * window.innerHeight) {
+  const {bottom, height} =
+    (state.visible
+      ? info.box.getBoundingClientRect()
+      : {bottom: null});
+
+  const boxFitsInFrame =
+    (height
+      ? height < window.innerHeight - margin - 60
+      : null);
+
+  const worthScrolling =
+    top + 20 < margin ||
+
+    (height && boxFitsInFrame
+      ? top > 0.7 * window.innerHeight
+   : height && !boxFitsInFrame
+      ? top > 0.4 * window.innerHeight
+      : top > 0.5 * window.innerHeight) ||
+
+    (bottom && boxFitsInFrame
+      ? bottom > window.innerHeight - 20
+      : false);
+
+  if (worthScrolling) {
     if (!state.visible) {
       toggleAdditionalNamesBox();
     }
diff --git a/src/static/js/client/art-tag-gallery-filter.js b/src/static/js/client/art-tag-gallery-filter.js
new file mode 100644
index 00000000..fd40d1a2
--- /dev/null
+++ b/src/static/js/client/art-tag-gallery-filter.js
@@ -0,0 +1,151 @@
+/* eslint-env browser */
+
+export const info = {
+  id: 'artTagGalleryFilterInfo',
+
+  featuredAllLine: null,
+  showingAllLine: null,
+  showingAllLink: null,
+
+  featuredDirectLine: null,
+  showingDirectLine: null,
+  showingDirectLink: null,
+
+  featuredIndirectLine: null,
+  showingIndirectLine: null,
+  showingIndirectLink: null,
+
+  gridItems: null,
+  gridItemsOnlyFeaturedIndirectly: null,
+  gridItemsFeaturedDirectly: null,
+};
+
+export function getPageReferences() {
+  if (document.documentElement.dataset.urlKey !== 'localized.artTagGallery') {
+    return;
+  }
+
+  info.featuredAllLine =
+    document.getElementById('featured-all-line');
+
+  info.featuredDirectLine =
+    document.getElementById('featured-direct-line');
+
+  info.featuredIndirectLine =
+    document.getElementById('featured-indirect-line');
+
+  info.showingAllLine =
+    document.getElementById('showing-all-line');
+
+  info.showingDirectLine =
+    document.getElementById('showing-direct-line');
+
+  info.showingIndirectLine =
+    document.getElementById('showing-indirect-line');
+
+  info.showingAllLink =
+    info.showingAllLine?.querySelector('a') ?? null;
+
+  info.showingDirectLink =
+    info.showingDirectLine?.querySelector('a') ?? null;
+
+  info.showingIndirectLink =
+    info.showingIndirectLine?.querySelector('a') ?? null;
+
+  info.gridItems =
+    Array.from(
+      document.querySelectorAll('#content .grid-listing .grid-item'));
+
+  info.gridItemsOnlyFeaturedIndirectly =
+    info.gridItems
+      .filter(gridItem => gridItem.classList.contains('featured-indirectly'));
+
+  info.gridItemsFeaturedDirectly =
+    info.gridItems
+      .filter(gridItem => !gridItem.classList.contains('featured-indirectly'));
+}
+
+function filterArtTagGallery(showing) {
+  let gridItemsToShow;
+
+  switch (showing) {
+    case 'all':
+      gridItemsToShow = info.gridItems;
+      break;
+
+    case 'direct':
+      gridItemsToShow = info.gridItemsFeaturedDirectly;
+      break;
+
+    case 'indirect':
+      gridItemsToShow = info.gridItemsOnlyFeaturedIndirectly;
+      break;
+  }
+
+  for (const gridItem of info.gridItems) {
+    if (gridItemsToShow.includes(gridItem)) {
+      gridItem.style.removeProperty('display');
+    } else {
+      gridItem.style.display = 'none';
+    }
+  }
+}
+
+export function addPageListeners() {
+  const orderShowing = [
+    'all',
+    'direct',
+    'indirect',
+  ];
+
+  const orderFeaturedLines = [
+    info.featuredAllLine,
+    info.featuredDirectLine,
+    info.featuredIndirectLine,
+  ];
+
+  const orderShowingLines = [
+    info.showingAllLine,
+    info.showingDirectLine,
+    info.showingIndirectLine,
+  ];
+
+  const orderShowingLinks = [
+    info.showingAllLink,
+    info.showingDirectLink,
+    info.showingIndirectLink,
+  ];
+
+  for (let index = 0; index < orderShowing.length; index++) {
+    if (!orderShowingLines[index]) continue;
+
+    let nextIndex = index;
+    do {
+      if (nextIndex === orderShowing.length) {
+        nextIndex = 0;
+      } else {
+        nextIndex++;
+      }
+    } while (!orderShowingLinks[nextIndex]);
+
+    const currentFeaturedLine = orderFeaturedLines[index];
+    const currentShowingLine = orderShowingLines[index];
+    const currentShowingLink = orderShowingLinks[index];
+
+    const nextFeaturedLine = orderFeaturedLines[nextIndex];
+    const nextShowingLine = orderShowingLines[nextIndex];
+    const nextShowing = orderShowing[nextIndex];
+
+    currentShowingLink.addEventListener('click', event => {
+      event.preventDefault();
+
+      currentFeaturedLine.style.display = 'none';
+      currentShowingLine.style.display = 'none';
+
+      nextFeaturedLine.style.display = 'block';
+      nextShowingLine.style.display = 'block';
+
+      filterArtTagGallery(nextShowing);
+    });
+  }
+}
diff --git a/src/static/js/client/art-tag-network.js b/src/static/js/client/art-tag-network.js
new file mode 100644
index 00000000..44e10c11
--- /dev/null
+++ b/src/static/js/client/art-tag-network.js
@@ -0,0 +1,147 @@
+/* eslint-env browser */
+
+import {cssProp} from '../client-util.js';
+
+import {atOffset, stitchArrays} from '../../shared-util/sugar.js';
+
+export const info = {
+  id: 'artTagNetworkInfo',
+
+  noneStatLink: null,
+  totalUsesStatLink: null,
+  directUsesStatLink: null,
+  descendantsStatLink: null,
+  leavesStatLink: null,
+
+  tagsWithoutStats: null,
+  tagsWithStats: null,
+
+  totalUsesStats: null,
+  directUsesStats: null,
+  descendantsStats: null,
+  leavesStats: null,
+};
+
+export function getPageReferences() {
+  if (
+    document.documentElement.dataset.urlKey !== 'localized.listing' ||
+    document.documentElement.dataset.urlValue0 !== 'tags/network'
+  ) {
+    return;
+  }
+
+  info.noneStatLink =
+    document.getElementById('network-stat-none');
+
+  info.totalUsesStatLink =
+    document.getElementById('network-stat-total-uses');
+
+  info.directUsesStatLink =
+    document.getElementById('network-stat-direct-uses');
+
+  info.descendantsStatLink =
+    document.getElementById('network-stat-descendants');
+
+  info.leavesStatLink =
+    document.getElementById('network-stat-leaves');
+
+  info.tagsWithoutStats =
+    document.querySelectorAll('.network-tag:not(.with-stat)');
+
+  info.tagsWithStats =
+    document.querySelectorAll('.network-tag.with-stat');
+
+  info.totalUsesStats =
+    Array.from(document.getElementsByClassName('network-tag-total-uses-stat'));
+
+  info.directUsesStats =
+    Array.from(document.getElementsByClassName('network-tag-direct-uses-stat'));
+
+  info.descendantsStats =
+    Array.from(document.getElementsByClassName('network-tag-descendants-stat'));
+
+  info.leavesStats =
+    Array.from(document.getElementsByClassName('network-tag-leaves-stat'));
+}
+
+export function addPageListeners() {
+  if (!info.noneStatLink) return;
+
+  const linkOrder = [
+    info.noneStatLink,
+    info.totalUsesStatLink,
+    info.directUsesStatLink,
+    info.descendantsStatLink,
+    info.leavesStatLink,
+  ];
+
+  const statsOrder = [
+    null,
+    info.totalUsesStats,
+    info.directUsesStats,
+    info.descendantsStats,
+    info.leavesStats,
+  ];
+
+  const stitched =
+    stitchArrays({
+      link: linkOrder,
+      stats: statsOrder,
+    });
+
+  for (const [index, {link}] of stitched.entries()) {
+    const next = atOffset(stitched, index, +1, {wrap: true});
+
+    link.addEventListener('click', domEvent => {
+      domEvent.preventDefault();
+
+      cssProp(link, 'display', 'none');
+      cssProp(next.link, 'display', null);
+
+      if (next.stats === null) {
+        hideArtTagNetworkStats();
+      } else {
+        showArtTagNetworkStats(next.stats);
+      }
+    });
+  }
+}
+
+function showArtTagNetworkStats(stats) {
+  for (const tagElement of info.tagsWithoutStats) {
+    cssProp(tagElement, 'display', 'none');
+  }
+
+  for (const tagElement of info.tagsWithStats) {
+    cssProp(tagElement, 'display', null);
+  }
+
+  const allStats = [
+    ...info.totalUsesStats,
+    ...info.directUsesStats,
+    ...info.descendantsStats,
+    ...info.leavesStats,
+  ];
+
+  const otherStats =
+    allStats
+      .filter(stat => !stats.includes(stat));
+
+  for (const statElement of otherStats) {
+    cssProp(statElement, 'display', 'none');
+  }
+
+  for (const statElement of stats) {
+    cssProp(statElement, 'display', null);
+  }
+}
+
+function hideArtTagNetworkStats() {
+  for (const tagElement of info.tagsWithoutStats) {
+    cssProp(tagElement, 'display', null);
+  }
+
+  for (const tagElement of info.tagsWithStats) {
+    cssProp(tagElement, 'display', 'none');
+  }
+}
diff --git a/src/static/js/client/css-compatibility-assistant.js b/src/static/js/client/css-compatibility-assistant.js
index 6e7b15b5..aa637cc4 100644
--- a/src/static/js/client/css-compatibility-assistant.js
+++ b/src/static/js/client/css-compatibility-assistant.js
@@ -1,22 +1,30 @@
 /* eslint-env browser */
 
+import {stitchArrays} from '../../shared-util/sugar.js';
+
 export const info = {
   id: 'cssCompatibilityAssistantInfo',
 
-  coverArtContainer: null,
-  coverArtImageDetails: null,
+  coverArtworks: null,
+  coverArtworkImageDetails: null,
 };
 
 export function getPageReferences() {
-  info.coverArtContainer =
-    document.getElementById('cover-art-container');
+  info.coverArtworks =
+    Array.from(document.querySelectorAll('.cover-artwork'));
 
-  info.coverArtImageDetails =
-    info.coverArtContainer?.querySelector('.image-details');
+  info.coverArtworkImageDetails =
+    info.coverArtworks
+      .map(artwork => artwork.querySelector('.image-details'));
 }
 
 export function mutatePageContent() {
-  if (info.coverArtImageDetails) {
-    info.coverArtContainer.classList.add('has-image-details');
-  }
+  stitchArrays({
+    coverArtwork: info.coverArtworks,
+    imageDetails: info.coverArtworkImageDetails,
+  }).forEach(({coverArtwork, imageDetails}) => {
+      if (imageDetails) {
+        coverArtwork.classList.add('has-image-details');
+      }
+    });
 }
diff --git a/src/static/js/client/expandable-gallery-section.js b/src/static/js/client/expandable-gallery-section.js
new file mode 100644
index 00000000..dc83e8b7
--- /dev/null
+++ b/src/static/js/client/expandable-gallery-section.js
@@ -0,0 +1,77 @@
+/* eslint-env browser */
+
+// TODO: Combine this and quick-description.js
+
+import {cssProp} from '../client-util.js';
+
+import {stitchArrays} from '../../shared-util/sugar.js';
+
+export const info = {
+  id: 'expandableGallerySectionInfo',
+
+  sections: null,
+
+  sectionContentBelowCut: null,
+
+  sectionExpandoToggles: null,
+
+  sectionExpandCues: null,
+  sectionCollapseCues: null,
+};
+
+export function getPageReferences() {
+  info.sections =
+    Array.from(document.querySelectorAll('.expandable-gallery-section'))
+      .filter(section => section.querySelector('.section-expando-toggle'));
+
+  info.sectionContentBelowCut =
+    info.sections
+      .map(section => section.querySelector('.section-content-below-cut'));
+
+  info.sectionExpandoToggles =
+    info.sections
+      .map(section => section.querySelector('.section-expando-toggle'));
+
+  info.sectionExpandCues =
+    info.sections
+      .map(section => section.querySelector('.section-expand-cue'));
+
+  info.sectionCollapseCues =
+    info.sections
+      .map(section => section.querySelector('.section-collapse-cue'));
+}
+
+export function addPageListeners() {
+  for (const {
+    section,
+    contentBelowCut,
+    expandoToggle,
+    expandCue,
+    collapseCue,
+  } of stitchArrays({
+    section: info.sections,
+    contentBelowCut: info.sectionContentBelowCut,
+    expandoToggle: info.sectionExpandoToggles,
+    expandCue: info.sectionExpandCues,
+    collapseCue: info.sectionCollapseCues,
+  })) {
+    expandoToggle.addEventListener('click', domEvent => {
+      domEvent.preventDefault();
+
+      const collapsed =
+        cssProp(contentBelowCut, 'display') === 'none';
+
+      if (collapsed) {
+        section.classList.add('expanded');
+        cssProp(contentBelowCut, 'display', null);
+        cssProp(expandCue, 'display', 'none');
+        cssProp(collapseCue, 'display', null);
+      } else {
+        section.classList.remove('expanded');
+        cssProp(contentBelowCut, 'display', 'none');
+        cssProp(expandCue, 'display', null);
+        cssProp(collapseCue, 'display', 'none');
+      }
+    });
+  }
+}
diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js
index 484f2ab0..89119a47 100644
--- a/src/static/js/client/hoverable-tooltip.js
+++ b/src/static/js/client/hoverable-tooltip.js
@@ -118,17 +118,17 @@ export function registerTooltipElement(tooltip) {
     handleTooltipMouseLeft(tooltip);
   });
 
-  tooltip.addEventListener('focusin', event => {
-    handleTooltipReceivedFocus(tooltip, event.relatedTarget);
+  tooltip.addEventListener('focusin', domEvent => {
+    handleTooltipReceivedFocus(tooltip, domEvent.relatedTarget);
   });
 
-  tooltip.addEventListener('focusout', event => {
+  tooltip.addEventListener('focusout', domEvent => {
     // This event gets activated for tabbing *between* links inside the
     // tooltip, which is no good and certainly doesn't represent the focus
     // leaving the tooltip.
-    if (currentlyShownTooltipHasFocus(event.relatedTarget)) return;
+    if (currentlyShownTooltipHasFocus(domEvent.relatedTarget)) return;
 
-    handleTooltipLostFocus(tooltip, event.relatedTarget);
+    handleTooltipLostFocus(tooltip, domEvent.relatedTarget);
   });
 }
 
@@ -158,20 +158,20 @@ export function registerTooltipHoverableElement(hoverable, tooltip) {
     handleTooltipHoverableMouseLeft(hoverable);
   });
 
-  hoverable.addEventListener('focusin', event => {
-    handleTooltipHoverableReceivedFocus(hoverable, event);
+  hoverable.addEventListener('focusin', domEvent => {
+    handleTooltipHoverableReceivedFocus(hoverable, domEvent);
   });
 
-  hoverable.addEventListener('focusout', event => {
-    handleTooltipHoverableLostFocus(hoverable, event);
+  hoverable.addEventListener('focusout', domEvent => {
+    handleTooltipHoverableLostFocus(hoverable, domEvent);
   });
 
-  hoverable.addEventListener('touchend', event => {
-    handleTooltipHoverableTouchEnded(hoverable, event);
+  hoverable.addEventListener('touchend', domEvent => {
+    handleTooltipHoverableTouchEnded(hoverable, domEvent);
   });
 
-  hoverable.addEventListener('click', event => {
-    handleTooltipHoverableClicked(hoverable, event);
+  hoverable.addEventListener('click', domEvent => {
+    handleTooltipHoverableClicked(hoverable, domEvent);
   });
 }
 
@@ -416,7 +416,7 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) {
     }, 1200);
 }
 
-function handleTooltipHoverableClicked(hoverable) {
+function handleTooltipHoverableClicked(hoverable, domEvent) {
   const {state} = info;
 
   // Don't navigate away from the page if the this hoverable was recently
@@ -426,7 +426,7 @@ function handleTooltipHoverableClicked(hoverable) {
     state.currentlyActiveHoverable === hoverable &&
     state.hoverableWasRecentlyTouched
   ) {
-    event.preventDefault();
+    domEvent.preventDefault();
   }
 }
 
@@ -576,6 +576,17 @@ export function showTooltipFromHoverable(hoverable) {
 
   hoverable.classList.add('has-visible-tooltip');
 
+  const isolator =
+    hoverable.closest('.isolate-tooltip-z-indexing > *');
+
+  if (isolator) {
+    for (const child of isolator.parentElement.children) {
+      cssProp(child, 'z-index', null);
+    }
+
+    cssProp(isolator, 'z-index', '1');
+  }
+
   positionTooltipFromHoverableWithBrains(hoverable);
 
   cssProp(tooltip, 'display', 'block');
@@ -667,12 +678,12 @@ export function positionTooltipFromHoverableWithBrains(hoverable) {
 
     for (let i = 0; i < numBaselineRects; i++) {
       for (const [dir1, dir2] of [
+        ['down', 'right'],
+        ['down', 'left'],
         ['right', 'down'],
         ['left', 'down'],
         ['right', 'up'],
         ['left', 'up'],
-        ['down', 'right'],
-        ['down', 'left'],
         ['up', 'right'],
         ['up', 'left'],
       ]) {
@@ -995,6 +1006,14 @@ export function getTooltipBaselineOpportunityAreas(tooltip) {
   return results;
 }
 
+export function mutatePageContent() {
+  for (const isolatorRoot of document.querySelectorAll('.isolate-tooltip-z-indexing')) {
+    if (isolatorRoot.firstElementChild) {
+      cssProp(isolatorRoot.firstElementChild, 'z-index', '1');
+    }
+  }
+}
+
 export function addPageListeners() {
   const {state} = info;
 
diff --git a/src/static/js/client/image-overlay.js b/src/static/js/client/image-overlay.js
index b51d57a4..e9e2708d 100644
--- a/src/static/js/client/image-overlay.js
+++ b/src/static/js/client/image-overlay.js
@@ -66,8 +66,13 @@ export function getPageReferences() {
   info.fileSizeWarning =
     document.getElementById('image-overlay-file-size-warning');
 
+  const linkQuery = [
+    '.image-link',
+    '.image-media-link',
+  ].join(', ');
+
   info.links =
-    Array.from(document.querySelectorAll('.image-link'))
+    Array.from(document.querySelectorAll(linkQuery))
       .filter(link => !link.closest('.no-image-preview'));
 }
 
@@ -88,10 +93,13 @@ function handleContainerClicked(evt) {
     return;
   }
 
-  // If you clicked anything close to or beneath the action bar, don't hide
-  // the image overlay.
+  // If you clicked anything near the action bar, don't hide the
+  // image overlay.
   const rect = info.actionContainer.getBoundingClientRect();
-  if (evt.clientY >= rect.top - 40) {
+  if (
+    evt.clientY >= rect.top - 40 && evt.clientY <= rect.bottom + 40 &&
+    evt.clientX >= rect.left + 20 && evt.clientX <= rect.right - 20
+  ) {
     return;
   }
 
@@ -141,13 +149,23 @@ function getImageLinkDetails(imageLink) {
       a.href,
 
     embeddedSrc:
-      img.src,
+      img?.src ??
+      a.dataset.embedSrc,
 
     originalFileSize:
-      img.dataset.originalSize,
+      img?.dataset.originalSize ??
+      a.dataset.originalSize ??
+      null,
 
     availableThumbList:
-      img.dataset.thumbs,
+      img?.dataset.thumbs ??
+      a.dataset.thumbs ??
+      null,
+
+    dimensions:
+      img?.dataset.dimensions?.split('x') ??
+      a.dataset.dimensions?.split('x') ??
+      null,
 
     color:
       cssProp(imageLink, '--primary-color'),
@@ -170,7 +188,7 @@ function getImageSources(details) {
     };
   } else {
     return {
-      mainSrc: originalSrc,
+      mainSrc: details.originalSrc,
       thumbSrc: null,
       mainThumb: '',
       thumbThumb: '',
@@ -211,15 +229,31 @@ async function loadOverlayImage(details) {
   if (details.thumbSrc) {
     info.thumbImage.src = details.thumbSrc;
     info.thumbImage.style.display = null;
+    info.container.classList.remove('no-thumb');
   } else {
     info.thumbImage.src = '';
     info.thumbImage.style.display = 'none';
+    info.container.classList.add('no-thumb');
   }
 
   // Show the thumbnail size on each <img> element's data attributes.
   // Y'know, just for debugging convenience.
   info.mainImage.dataset.displayingThumb = details.mainThumb;
-  info.thumbImage.dataset.displayingThumb = details.thumbThubm;
+  info.thumbImage.dataset.displayingThumb = details.thumbThumb;
+
+  if (details.dimensions) {
+    info.mainImage.width = details.dimensions[0];
+    info.mainImage.height = details.dimensions[1];
+    info.thumbImage.width = details.dimensions[0];
+    info.thumbImage.height = details.dimensions[1];
+    cssProp(info.thumbImage, 'aspect-ratio', details.dimensions.join('/'));
+  } else {
+    info.mainImage.removeAttribute('width');
+    info.mainImage.removeAttribute('height');
+    info.thumbImage.removeAttribute('width');
+    info.thumbImage.removeAttribute('height');
+    cssProp(info.thumbImage, 'aspect-ratio', null);
+  }
 
   info.mainImage.addEventListener('load', handleMainImageLoaded);
   info.mainImage.addEventListener('error', handleMainImageErrored);
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
index 52d2afd6..aeb9264a 100644
--- a/src/static/js/client/index.js
+++ b/src/static/js/client/index.js
@@ -4,10 +4,13 @@ import '../group-contributions-table.js';
 
 import * as additionalNamesBoxModule from './additional-names-box.js';
 import * as albumCommentarySidebarModule from './album-commentary-sidebar.js';
+import * as artTagGalleryFilterModule from './art-tag-gallery-filter.js';
+import * as artTagNetworkModule from './art-tag-network.js';
 import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip.js';
 import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js';
 import * as datetimestampTooltipModule from './datetimestamp-tooltip.js';
 import * as draggedLinkModule from './dragged-link.js';
+import * as expandableGallerySectionModule from './expandable-gallery-section.js';
 import * as hashLinkModule from './hash-link.js';
 import * as hoverableTooltipModule from './hoverable-tooltip.js';
 import * as imageOverlayModule from './image-overlay.js';
@@ -24,10 +27,13 @@ import * as wikiSearchModule from './wiki-search.js';
 export const modules = [
   additionalNamesBoxModule,
   albumCommentarySidebarModule,
+  artTagGalleryFilterModule,
+  artTagNetworkModule,
   artistExternalLinkTooltipModule,
   cssCompatibilityAssistantModule,
   datetimestampTooltipModule,
   draggedLinkModule,
+  expandableGallerySectionModule,
   hashLinkModule,
   hoverableTooltipModule,
   imageOverlayModule,
diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js
index cff82252..6a7a6023 100644
--- a/src/static/js/client/quick-description.js
+++ b/src/static/js/client/quick-description.js
@@ -1,5 +1,7 @@
 /* eslint-env browser */
 
+// TODO: Combine this and expandable-gallery-section.js
+
 import {stitchArrays} from '../../shared-util/sugar.js';
 
 export const info = {
diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js
index c79fb837..eae1e74e 100644
--- a/src/static/js/client/sidebar-search.js
+++ b/src/static/js/client/sidebar-search.js
@@ -1,7 +1,7 @@
 /* eslint-env browser */
 
 import {getColors} from '../../shared-util/colors.js';
-import {accumulateSum, empty} from '../../shared-util/sugar.js';
+import {accumulateSum, empty, unique} from '../../shared-util/sugar.js';
 
 import {
   cssProp,
@@ -41,6 +41,14 @@ export const info = {
   failedRule: null,
   failedContainer: null,
 
+  filterContainer: null,
+  albumFilterLink: null,
+  artistFilterLink: null,
+  flashFilterLink: null,
+  groupFilterLink: null,
+  tagFilterLink: null,
+  trackFilterLink: null,
+
   resultsRule: null,
   resultsContainer: null,
   results: null,
@@ -49,6 +57,8 @@ export const info = {
   endSearchLine: null,
   endSearchLink: null,
 
+  standbyInputPlaceholder: null,
+
   preparingString: null,
   loadingDataString: null,
   searchingString: null,
@@ -63,12 +73,22 @@ export const info = {
   groupResultKindString: null,
   tagResultKindString: null,
 
+  albumResultFilterString: null,
+  artistResultFilterString: null,
+  flashResultFilterString: null,
+  groupResultFilterString: null,
+  tagResultFilterString: null,
+  trackResultFilterString: null,
+
   state: {
     sidebarColumnShownForSearch: null,
 
     tidiedSidebar: null,
     collapsedDetailsForTidiness: null,
 
+    recallingRecentSearch: null,
+    recallingRecentSearchFromMouse: null,
+
     currentValue: null,
 
     workerStatus: null,
@@ -92,6 +112,10 @@ export const info = {
       maxLength: settings => settings.maxActiveResultsStorage,
     },
 
+    activeFilterType: {
+      type: 'string',
+    },
+
     repeatQueryOnReload: {
       type: 'boolean',
       default: false,
@@ -133,6 +157,9 @@ export function getPageReferences() {
   info.searchSidebarColumn =
     info.searchBox.closest('.sidebar-column');
 
+  info.standbyInputPlaceholder =
+    info.searchInput.placeholder;
+
   const findString = classPart =>
     info.searchBox.querySelector(`.wiki-search-${classPart}-string`);
 
@@ -168,6 +195,24 @@ export function getPageReferences() {
 
   info.tagResultKindString =
     findString('tag-result-kind');
+
+  info.albumResultFilterString =
+    findString('album-result-filter');
+
+  info.artistResultFilterString =
+    findString('artist-result-filter');
+
+  info.flashResultFilterString =
+    findString('flash-result-filter');
+
+  info.groupResultFilterString =
+    findString('group-result-filter');
+
+  info.tagResultFilterString =
+    findString('tag-result-filter');
+
+  info.trackResultFilterString =
+    findString('track-result-filter');
 }
 
 export function addInternalListeners() {
@@ -257,6 +302,38 @@ export function mutatePageContent() {
   info.searchBox.appendChild(info.failedRule);
   info.searchBox.appendChild(info.failedContainer);
 
+  // Filter section
+
+  info.filterContainer =
+    document.createElement('div');
+
+  info.filterContainer.classList.add('wiki-search-filter-container');
+
+  cssProp(info.filterContainer, 'display', 'none');
+
+  forEachFilter((type, _filterLink) => {
+    // TODO: It's probably a sin to access `session` during this step LOL
+    const {session} = info;
+
+    const filterLink = document.createElement('a');
+
+    filterLink.href = '#';
+    filterLink.classList.add('wiki-search-filter-link');
+
+    if (session.activeFilterType === type) {
+      filterLink.classList.add('active');
+    }
+
+    const string = info[type + 'ResultFilterString'];
+    filterLink.appendChild(templateContent(string));
+
+    info[type + 'FilterLink'] = filterLink;
+
+    info.filterContainer.appendChild(filterLink);
+  });
+
+  info.searchBox.appendChild(info.filterContainer);
+
   // Results section
 
   info.resultsRule =
@@ -310,6 +387,43 @@ export function mutatePageContent() {
 export function addPageListeners() {
   if (!info.searchInput) return;
 
+  info.searchInput.addEventListener('mousedown', _domEvent => {
+    const {state} = info;
+
+    if (state.recallingRecentSearch) {
+      state.recallingRecentSearchFromMouse = true;
+    }
+  });
+
+  info.searchInput.addEventListener('focus', _domEvent => {
+    const {session, state} = info;
+
+    if (state.recallingRecentSearch) {
+      info.searchInput.value = session.activeQuery;
+      info.searchInput.placeholder = info.standbyInputPlaceholder;
+      showSidebarSearchResults(session.activeQueryResults);
+      state.recallingRecentSearch = false;
+    }
+  });
+
+  info.searchLabel.addEventListener('click', domEvent => {
+    const {state} = info;
+
+    if (state.recallingRecentSearchFromMouse) {
+      if (info.searchInput.selectionStart === info.searchInput.selectionEnd) {
+        info.searchInput.select();
+      }
+
+      state.recallingRecentSearchFromMouse = false;
+      return;
+    }
+
+    const inputRect = info.searchInput.getBoundingClientRect();
+    if (domEvent.clientX < inputRect.left - 3) {
+      info.searchInput.select();
+    }
+  });
+
   info.searchInput.addEventListener('change', _domEvent => {
     const {state} = info;
 
@@ -326,7 +440,7 @@ export function addPageListeners() {
     const {settings, state} = info;
 
     if (!info.searchInput.value) {
-      clearSidebarSearch();
+      clearSidebarSearch(); // ...but don't clear filter
       return;
     }
 
@@ -388,10 +502,18 @@ export function addPageListeners() {
   info.endSearchLink.addEventListener('click', domEvent => {
     domEvent.preventDefault();
     clearSidebarSearch();
+    clearSidebarFilter();
     possiblyHideSearchSidebarColumn();
     restoreSidebarSearchColumn();
   });
 
+  forEachFilter((type, filterLink) => {
+    filterLink.addEventListener('click', domEvent => {
+      domEvent.preventDefault();
+      toggleSidebarSearchFilter(type);
+    });
+  });
+
   info.resultsContainer.addEventListener('scroll', () => {
     const {settings, state} = info;
 
@@ -412,11 +534,11 @@ export function initializeState() {
   if (!info.searchInput) return;
 
   if (session.activeQuery) {
-    info.searchInput.value = session.activeQuery;
     if (session.repeatQueryOnReload) {
+      info.searchInput.value = session.activeQuery;
       activateSidebarSearch(session.activeQuery);
     } else if (session.activeQueryResults) {
-      showSidebarSearchResults(session.activeQueryResults);
+      considerRecallingRecentSidebarSearch();
     }
   }
 }
@@ -473,9 +595,28 @@ function trackSidebarSearchDownloadEnds(event) {
   }
 }
 
+function forEachFilter(callback) {
+  const filterOrder = [
+    'track',
+    'album',
+    'artist',
+    'group',
+    'flash',
+    'tag',
+  ];
+
+  for (const type of filterOrder) {
+    callback(type, info[type + 'FilterLink']);
+  }
+}
+
 async function activateSidebarSearch(query) {
   const {session, state} = info;
 
+  if (!query) {
+    return;
+  }
+
   if (state.stoppedTypingTimeout) {
     clearTimeout(state.stoppedTypingTimeout);
     state.stoppedTypingTimeout = null;
@@ -535,6 +676,16 @@ function clearSidebarSearch() {
   hideSidebarSearchResults();
 }
 
+function clearSidebarFilter() {
+  const {session} = info;
+
+  toggleSidebarSearchFilter(session.activeFilterType);
+
+  forEachFilter((_type, filterLink) => {
+    filterLink.classList.remove('shown', 'hidden');
+  });
+}
+
 function updateSidebarSearchStatus() {
   const {state} = info;
 
@@ -621,54 +772,122 @@ function showSidebarSearchFailed() {
 }
 
 function showSidebarSearchResults(results) {
-  console.debug(`Showing search results:`, results);
+  const {session} = info;
 
-  showSearchSidebarColumn();
+  console.debug(`Showing search results:`, tidyResults(results));
 
-  const flatResults =
-    Object.entries(results)
-      .filter(([index]) => index === 'generic')
-      .flatMap(([index, results]) => results
-        .flatMap(({doc, id}) => ({
-          index,
-          reference: id ?? null,
-          referenceType: (id ? id.split(':')[0] : null),
-          directory: (id ? id.split(':')[1] : null),
-          data: doc,
-        })));
+  showSearchSidebarColumn();
 
   info.searchBox.classList.add('showing-results');
   info.searchSidebarColumn.classList.add('search-showing-results');
 
-  while (info.results.firstChild) {
-    info.results.firstChild.remove();
+  let filterType = session.activeFilterType;
+  let shownAnyResults =
+    fillResultElements(results, {filterType: session.activeFilterType});
+
+  showFilterElements(results);
+
+  if (!shownAnyResults) {
+    shownAnyResults = toggleSidebarSearchFilter(filterType);
+    filterType = null;
   }
 
-  cssProp(info.resultsRule, 'display', 'block');
-  cssProp(info.resultsContainer, 'display', 'block');
+  if (shownAnyResults) {
+    cssProp(info.endSearchRule, 'display', 'block');
+    cssProp(info.endSearchLine, 'display', 'block');
 
-  if (empty(flatResults)) {
+    tidySidebarSearchColumn();
+  } else {
     const p = document.createElement('p');
     p.classList.add('wiki-search-no-results');
     p.appendChild(templateContent(info.noResultsString));
     info.results.appendChild(p);
   }
 
-  for (const result of flatResults) {
+  restoreSidebarSearchResultsScrollOffset();
+}
+
+function tidyResults(results) {
+  const tidiedResults =
+    results.results.map(({doc, id}) => ({
+      reference: id ?? null,
+      referenceType: (id ? id.split(':')[0] : null),
+      directory: (id ? id.split(':')[1] : null),
+      data: doc,
+    }));
+
+  return tidiedResults;
+}
+
+function fillResultElements(results, {
+  filterType = null,
+} = {}) {
+  const tidiedResults = tidyResults(results);
+
+  const filteredResults =
+    (filterType
+      ? tidiedResults.filter(result => result.referenceType === filterType)
+      : tidiedResults);
+
+  while (info.results.firstChild) {
+    info.results.firstChild.remove();
+  }
+
+  cssProp(info.resultsRule, 'display', 'block');
+  cssProp(info.resultsContainer, 'display', 'block');
+
+  if (empty(filteredResults)) {
+    return false;
+  }
+
+  for (const result of filteredResults) {
     const el = generateSidebarSearchResult(result);
     if (!el) continue;
 
     info.results.appendChild(el);
   }
 
-  if (!empty(flatResults)) {
-    cssProp(info.endSearchRule, 'display', 'block');
-    cssProp(info.endSearchLine, 'display', 'block');
+  return true;
+}
 
-    tidySidebarSearchColumn();
-  }
+function showFilterElements(results) {
+  const {queriedKind} = results;
 
-  restoreSidebarSearchResultsScrollOffset();
+  const tidiedResults = tidyResults(results);
+
+  const allReferenceTypes =
+    unique(tidiedResults.map(result => result.referenceType));
+
+  let shownAny = false;
+
+  forEachFilter((type, filterLink) => {
+    filterLink.classList.remove('shown', 'hidden');
+
+    if (allReferenceTypes.includes(type)) {
+      shownAny = true;
+      cssProp(filterLink, 'display', null);
+
+      if (queriedKind) {
+        filterLink.setAttribute('inert', 'inert');
+      } else {
+        filterLink.removeAttribute('inert');
+      }
+
+      if (type === queriedKind) {
+        filterLink.classList.add('active-from-query');
+      } else {
+        filterLink.classList.remove('active-from-query');
+      }
+    } else {
+      cssProp(filterLink, 'display', 'none');
+    }
+  });
+
+  if (shownAny) {
+    cssProp(info.filterContainer, 'display', null);
+  } else {
+    cssProp(info.filterContainer, 'display', 'none');
+  }
 }
 
 function generateSidebarSearchResult(result) {
@@ -859,6 +1078,8 @@ function generateSidebarSearchResultTemplate(slots) {
 }
 
 function hideSidebarSearchResults() {
+  cssProp(info.filterContainer, 'display', 'none');
+
   cssProp(info.resultsRule, 'display', 'none');
   cssProp(info.resultsContainer, 'display', 'none');
 
@@ -991,6 +1212,36 @@ function tidySidebarSearchColumn() {
   }
 }
 
+function toggleSidebarSearchFilter(toggleType) {
+  const {session} = info;
+
+  if (!toggleType) return null;
+
+  let shownAnyResults = null;
+
+  forEachFilter((type, filterLink) => {
+    if (type === toggleType) {
+      const filterActive = filterLink.classList.toggle('active');
+      const filterType = (filterActive ? type : null);
+
+      if (cssProp(filterLink, 'display') !== 'none') {
+        filterLink.classList.add(filterActive ? 'shown' : 'hidden');
+      }
+
+      if (session.activeQueryResults) {
+        shownAnyResults =
+          fillResultElements(session.activeQueryResults, {filterType});
+      }
+
+      session.activeFilterType = filterType;
+    } else {
+      filterLink.classList.remove('active');
+    }
+  });
+
+  return shownAnyResults;
+}
+
 function restoreSidebarSearchColumn() {
   const {state} = info;
 
@@ -1004,6 +1255,28 @@ function restoreSidebarSearchColumn() {
 
   state.collapsedDetailsForTidiness = [];
   state.tidiedSidebar = null;
+
+  info.searchInput.placeholder = info.standbyInputPlaceholder;
+}
+
+function considerRecallingRecentSidebarSearch() {
+  const {session, state} = info;
+
+  if (document.documentElement.dataset.urlKey === 'localized.home') {
+    return forgetRecentSidebarSearch();
+  }
+
+  info.searchInput.placeholder = session.activeQuery;
+  state.recallingRecentSearch = true;
+}
+
+function forgetRecentSidebarSearch() {
+  const {session} = info;
+
+  session.activeQuery = null;
+  session.activeQueryResults = null;
+
+  clearSidebarFilter();
 }
 
 async function handleDroppedIntoSearchInput(domEvent) {
@@ -1032,7 +1305,7 @@ async function handleDroppedIntoSearchInput(domEvent) {
   let droppedURL;
   try {
     droppedURL = new URL(droppedText);
-  } catch (error) {
+  } catch {
     droppedURL = null;
   }
 
diff --git a/src/static/js/client/sticky-heading.js b/src/static/js/client/sticky-heading.js
index ae63eab5..b65574d0 100644
--- a/src/static/js/client/sticky-heading.js
+++ b/src/static/js/client/sticky-heading.js
@@ -1,13 +1,19 @@
 /* eslint-env browser */
 
 import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js';
-import {dispatchInternalEvent, templateContent} from '../client-util.js';
+import {cssProp, dispatchInternalEvent, templateContent}
+  from '../client-util.js';
 
 export const info = {
   id: 'stickyHeadingInfo',
 
+  stickyRoots: null,
+
   stickyContainers: null,
+  staticContainers: null,
 
+  stickyHeadingRows: null,
+  stickyHeadings: null,
   stickySubheadingRows: null,
   stickySubheadings: null,
 
@@ -17,21 +23,33 @@ export const info = {
 
   contentContainers: null,
   contentHeadings: null,
+  contentCoverColumns: null,
   contentCovers: null,
   contentCoversReveal: null,
 
+  referenceCollapsedHeading: null,
+
   state: {
     displayedHeading: null,
   },
 
   event: {
     whenDisplayedHeadingChanges: [],
+    whenStuckStatusChanges: [],
   },
 };
 
 export function getPageReferences() {
+  info.stickyRoots =
+    Array.from(document.querySelectorAll('.content-sticky-heading-root:not([inert])'));
+
   info.stickyContainers =
-    Array.from(document.getElementsByClassName('content-sticky-heading-container'));
+    info.stickyRoots
+      .map(el => el.querySelector('.content-sticky-heading-container'));
+
+  info.staticContainers =
+    info.stickyRoots
+      .map(el => el.nextElementSibling);
 
   info.stickyCoverContainers =
     info.stickyContainers
@@ -45,6 +63,14 @@ export function getPageReferences() {
     info.stickyCovers
       .map(el => el?.querySelector('.image-text-area'));
 
+  info.stickyHeadingRows =
+    info.stickyContainers
+      .map(el => el.querySelector('.content-sticky-heading-row'));
+
+  info.stickyHeadings =
+    info.stickyHeadingRows
+      .map(el => el.querySelector('h1'));
+
   info.stickySubheadingRows =
     info.stickyContainers
       .map(el => el.querySelector('.content-sticky-subheading-row'));
@@ -55,11 +81,15 @@ export function getPageReferences() {
 
   info.contentContainers =
     info.stickyContainers
-      .map(el => el.parentElement);
+      .map(el => el.closest('.content-sticky-heading-root').parentElement);
 
-  info.contentCovers =
+  info.contentCoverColumns =
     info.contentContainers
-      .map(el => el.querySelector('#cover-art-container'));
+      .map(el => el.querySelector('#artwork-column'));
+
+  info.contentCovers =
+    info.contentCoverColumns
+      .map(el => el ? el.querySelector('.cover-artwork') : null);
 
   info.contentCoversReveal =
     info.contentCovers
@@ -68,6 +98,10 @@ export function getPageReferences() {
   info.contentHeadings =
     info.contentContainers
       .map(el => Array.from(el.querySelectorAll('.content-heading')));
+
+  info.referenceCollapsedHeading =
+    info.stickyHeadings
+      .map(el => el.querySelector('.reference-collapsed-heading'));
 }
 
 export function mutatePageContent() {
@@ -137,15 +171,61 @@ function topOfViewInside(el, scroll = window.scrollY) {
     scroll < el.offsetTop + el.offsetHeight);
 }
 
+function updateStuckStatus(index) {
+  const {event} = info;
+
+  const contentContainer = info.contentContainers[index];
+  const stickyContainer = info.stickyContainers[index];
+
+  const wasStuck = stickyContainer.classList.contains('stuck');
+  const stuck = topOfViewInside(contentContainer);
+
+  if (stuck === wasStuck) return;
+
+  if (stuck) {
+    stickyContainer.classList.add('stuck');
+  } else {
+    stickyContainer.classList.remove('stuck');
+  }
+
+  dispatchInternalEvent(event, 'whenStuckStatusChanges', index, stuck);
+}
+
+function updateCollapseStatus(index) {
+  const stickyContainer = info.stickyContainers[index];
+  const staticContainer = info.staticContainers[index];
+  const stickyHeading = info.stickyHeadings[index];
+  const referenceCollapsedHeading = info.referenceCollapsedHeading[index];
+
+  const {height: uncollapsedHeight} = stickyHeading.getBoundingClientRect();
+  const {height: collapsedHeight} = referenceCollapsedHeading.getBoundingClientRect();
+
+  if (
+    staticContainer.getBoundingClientRect().bottom < 4 ||
+    staticContainer.getBoundingClientRect().top < -80
+  ) {
+    if (!stickyContainer.classList.contains('collapse')) {
+      stickyContainer.classList.add('collapse');
+      cssProp(stickyContainer, '--uncollapsed-heading-height', uncollapsedHeight + 'px');
+      cssProp(stickyContainer, '--collapsed-heading-height', collapsedHeight + 'px');
+    }
+  } else {
+    stickyContainer.classList.remove('collapse');
+  }
+}
+
 function updateStickyCoverVisibility(index) {
   const stickyCoverContainer = info.stickyCoverContainers[index];
-  const contentCover = info.contentCovers[index];
+  const stickyContainer = info.stickyContainers[index];
+  const contentCoverColumn = info.contentCoverColumns[index];
 
-  if (contentCover && stickyCoverContainer) {
-    if (contentCover.getBoundingClientRect().bottom < 4) {
+  if (contentCoverColumn && stickyCoverContainer) {
+    if (contentCoverColumn.getBoundingClientRect().bottom < 4) {
       stickyCoverContainer.classList.add('visible');
+      stickyContainer.classList.add('cover-visible');
     } else {
       stickyCoverContainer.classList.remove('visible');
+      stickyContainer.classList.remove('cover-visible');
     }
   }
 }
@@ -157,26 +237,27 @@ function getContentHeadingClosestToStickySubheading(index) {
     return null;
   }
 
-  const stickySubheading = info.stickySubheadings[index];
-
-  if (stickySubheading.childNodes.length === 0) {
-    // Supply a non-breaking space to ensure correct basic line height.
-    stickySubheading.appendChild(document.createTextNode('\xA0'));
-  }
-
-  const stickyContainer = info.stickyContainers[index];
-  const stickyRect = stickyContainer.getBoundingClientRect();
+  const stickyHeadingRow = info.stickyHeadingRows[index];
+  const stickyRect = stickyHeadingRow.getBoundingClientRect();
 
-  // TODO: Should this compute with the subheading row instead of h2?
-  const subheadingRect = stickySubheading.getBoundingClientRect();
+  // Subheadings only appear when the sticky heading is collapsed,
+  // so the used bottom edge should always be *as though* it's only
+  // displaying one line of text. Subtract the current discrepancy.
+  const stickyHeading = info.stickyHeadings[index];
+  const referenceCollapsedHeading = info.referenceCollapsedHeading[index];
+  const correctBottomEdge =
+    stickyHeading.getBoundingClientRect().height -
+    referenceCollapsedHeading.getBoundingClientRect().height;
 
-  const stickyBottom = stickyRect.bottom + subheadingRect.height;
+  const stickyBottom =
+    (stickyRect.bottom
+   - correctBottomEdge);
 
   // Iterate from bottom to top of the content area.
   const contentHeadings = info.contentHeadings[index];
   for (const heading of contentHeadings.slice().reverse()) {
     const headingRect = heading.getBoundingClientRect();
-    if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) {
+    if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 40) {
       return heading;
     }
   }
@@ -187,7 +268,12 @@ function getContentHeadingClosestToStickySubheading(index) {
 function updateStickySubheadingContent(index) {
   const {event, state} = info;
 
-  const closestHeading = getContentHeadingClosestToStickySubheading(index);
+  const stickyContainer = info.stickyContainers[index];
+
+  const closestHeading =
+    (stickyContainer.classList.contains('collapse')
+      ? getContentHeadingClosestToStickySubheading(index)
+      : null);
 
   if (state.displayedHeading === closestHeading) return;
 
@@ -233,6 +319,8 @@ function updateStickySubheadingContent(index) {
 }
 
 export function updateStickyHeadings(index) {
+  updateStuckStatus(index);
+  updateCollapseStatus(index);
   updateStickyCoverVisibility(index);
   updateStickySubheadingContent(index);
 }
diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js
index cdab2cb8..b00ed98e 100644
--- a/src/static/js/rectangles.js
+++ b/src/static/js/rectangles.js
@@ -510,4 +510,46 @@ export class WikiRect extends DOMRect {
       height: this.height,
     });
   }
+
+  // Other utilities
+
+  #display = null;
+
+  display() {
+    if (!this.#display) {
+      this.#display = document.createElement('div');
+      document.body.appendChild(this.#display);
+    }
+
+    Object.assign(this.#display.style, {
+      position: 'fixed',
+      background: '#000c',
+      border: '3px solid var(--primary-color)',
+      borderRadius: '4px',
+      top: this.top + 'px',
+      left: this.left + 'px',
+      width: this.width + 'px',
+      height: this.height + 'px',
+      pointerEvents: 'none',
+    });
+
+    let i = 0;
+    const int = setInterval(() => {
+      i++;
+      if (i >= 3) clearInterval(int);
+      if (!this.#display) return;
+
+      this.#display.style.display = 'none';
+      setTimeout(() => {
+        this.#display.style.display = '';
+      }, 200);
+    }, 600);
+  }
+
+  hide() {
+    if (this.#display) {
+      this.#display.remove();
+      this.#display = null;
+    }
+  }
 }
diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js
index 1b4684ad..e32b4ad5 100644
--- a/src/static/js/search-worker.js
+++ b/src/static/js/search-worker.js
@@ -130,7 +130,7 @@ async function loadDatabase() {
 
   try {
     idb = await promisifyIDBRequest(request);
-  } catch (error) {
+  } catch {
     console.warn(`Couldn't load search IndexedDB - won't use an internal cache.`);
     console.warn(request.error);
     idb = null;
@@ -371,58 +371,75 @@ function postActionResult(id, status, value) {
 }
 
 function performSearchAction({query, options}) {
-  const {generic, ...otherIndexes} = indexes;
+  const {queriedKind} = processTerms(query);
+  const genericResults = queryGenericIndex(query, options);
+  const verbatimResults = queryVerbatimIndex(query, options);
 
-  const genericResults =
-    queryGenericIndex(generic, query, options);
+  const verbatimIDs =
+    new Set(verbatimResults?.map(result => result.id));
 
-  const otherResults =
-    withEntries(otherIndexes, entries => entries
-      .map(([indexName, index]) => [
-        indexName,
-        index.search(query, options),
-      ]));
+  const commonResults =
+    (verbatimResults && genericResults
+      ? genericResults
+          .filter(({id}) => verbatimIDs.has(id))
+      : verbatimResults ?? genericResults);
 
   return {
-    generic: genericResults,
-    ...otherResults,
+    results: commonResults,
+    queriedKind,
   };
 }
 
-function queryGenericIndex(index, query, options) {
-  const interestingFieldCombinations = [
-    ['primaryName', 'parentName', 'groups'],
-    ['primaryName', 'parentName'],
-    ['primaryName', 'groups', 'contributors'],
-    ['primaryName', 'groups', 'artTags'],
-    ['primaryName', 'groups'],
-    ['primaryName', 'contributors'],
-    ['primaryName', 'artTags'],
-    ['parentName', 'groups', 'artTags'],
-    ['parentName', 'artTags'],
-    ['groups', 'contributors'],
-    ['groups', 'artTags'],
-
-    // This prevents just matching *everything* tagged "john" if you
-    // only search "john", but it actually supports matching more than
-    // *two* tags at once: "john rose lowas" works! This is thanks to
-    // flexsearch matching multiple field values in a single query.
-    ['artTags', 'artTags'],
-
-    ['contributors', 'parentName'],
-    ['contributors', 'groups'],
-    ['primaryName', 'contributors'],
-    ['primaryName'],
-  ];
+const interestingFieldCombinations = [
+  ['primaryName', 'parentName', 'groups'],
+  ['primaryName', 'parentName'],
+  ['primaryName', 'groups', 'contributors'],
+  ['primaryName', 'groups', 'artTags'],
+  ['primaryName', 'groups'],
+  ['primaryName', 'contributors'],
+  ['primaryName', 'artTags'],
+  ['parentName', 'groups', 'artTags'],
+  ['parentName', 'artTags'],
+  ['groups', 'contributors'],
+  ['groups', 'artTags'],
+
+  // This prevents just matching *everything* tagged "john" if you
+  // only search "john", but it actually supports matching more than
+  // *two* tags at once: "john rose lowas" works! This is thanks to
+  // flexsearch matching multiple field values in a single query.
+  ['artTags', 'artTags'],
+
+  ['contributors', 'parentName'],
+  ['contributors', 'groups'],
+  ['primaryName', 'contributors'],
+  ['primaryName'],
+];
+
+function queryGenericIndex(query, options) {
+  return queryIndex({
+    indexKey: 'generic',
+    termsKey: 'genericTerms',
+  }, query, options);
+}
+
+function queryVerbatimIndex(query, options) {
+  return queryIndex({
+    indexKey: 'verbatim',
+    termsKey: 'verbatimTerms',
+  }, query, options);
+}
 
+function queryIndex({termsKey, indexKey}, query, options) {
   const interestingFields =
     unique(interestingFieldCombinations.flat());
 
-  const {genericTerms, queriedKind} =
+  const {[termsKey]: terms, queriedKind} =
     processTerms(query);
 
+  if (empty(terms)) return null;
+
   const particles =
-    particulate(genericTerms);
+    particulate(terms);
 
   const groupedParticles =
     groupArray(particles, ({length}) => length);
@@ -437,7 +454,7 @@ function queryGenericIndex(index, query, options) {
           query: values,
         }));
 
-  const boilerplate = queryBoilerplate(index);
+  const boilerplate = queryBoilerplate(indexes[indexKey]);
 
   const particleResults =
     Object.fromEntries(
@@ -459,62 +476,73 @@ function queryGenericIndex(index, query, options) {
             ])),
       ]));
 
-  const results = new Set();
+  let matchedResults = new Set();
 
   for (const interestingFieldCombination of interestingFieldCombinations) {
     for (const query of queriesBy(interestingFieldCombination)) {
-      const idToMatchingFieldsMap = new Map();
-      for (const {field, query: fieldQuery} of query) {
-        for (const id of particleResults[field][fieldQuery]) {
-          if (idToMatchingFieldsMap.has(id)) {
-            idToMatchingFieldsMap.get(id).push(field);
-          } else {
-            idToMatchingFieldsMap.set(id, [field]);
-          }
-        }
-      }
+      const [firstQueryFieldLine, ...restQueryFieldLines] = query;
 
       const commonAcrossFields =
-        Array.from(idToMatchingFieldsMap.entries())
-          .filter(([id, matchingFields]) =>
-            matchingFields.length === interestingFieldCombination.length)
-          .map(([id]) => id);
+        new Set(
+          particleResults
+            [firstQueryFieldLine.field]
+            [firstQueryFieldLine.query]);
+
+      for (const currQueryFieldLine of restQueryFieldLines) {
+        const tossResults = new Set(commonAcrossFields);
+
+        const keepResults =
+          particleResults
+            [currQueryFieldLine.field]
+            [currQueryFieldLine.query];
+
+        for (const result of keepResults) {
+          tossResults.delete(result);
+        }
+
+        for (const result of tossResults) {
+          commonAcrossFields.delete(result);
+        }
+      }
 
       for (const result of commonAcrossFields) {
-        results.add(result);
+        matchedResults.add(result);
       }
     }
   }
 
-  const constituted =
-    boilerplate.constitute(results);
+  matchedResults = Array.from(matchedResults);
 
-  const constitutedAndFiltered =
-    constituted
-      .filter(({id}) =>
-        (queriedKind
-          ? id.split(':')[0] === queriedKind
-          : true));
+  const filteredResults =
+    (queriedKind
+      ? matchedResults.filter(id => id.split(':')[0] === queriedKind)
+      : matchedResults);
 
-  return constitutedAndFiltered;
+  const constitutedResults =
+    boilerplate.constitute(filteredResults);
+
+  return constitutedResults;
 }
 
 function processTerms(query) {
   const kindTermSpec = [
-    {kind: 'album', terms: ['album']},
-    {kind: 'artist', terms: ['artist']},
-    {kind: 'flash', terms: ['flash']},
-    {kind: 'group', terms: ['group']},
-    {kind: 'tag', terms: ['art tag', 'tag']},
-    {kind: 'track', terms: ['track']},
+    {kind: 'album', terms: ['album', 'albums']},
+    {kind: 'artist', terms: ['artist', 'artists']},
+    {kind: 'flash', terms: ['flash', 'flashes']},
+    {kind: 'group', terms: ['group', 'groups']},
+    {kind: 'tag', terms: ['art tag', 'art tags', 'tag', 'tags']},
+    {kind: 'track', terms: ['track', 'tracks']},
   ];
 
   const genericTerms = [];
+  const verbatimTerms = [];
   let queriedKind = null;
 
   const termRegexp =
     new RegExp(
-      String.raw`(?<kind>${kindTermSpec.flatMap(spec => spec.terms).join('|')})` +
+      String.raw`(?<kind>(?<=^|\s)(?:${kindTermSpec.flatMap(spec => spec.terms).join('|')})(?=$|\s))` +
+      String.raw`|(?<=^|\s)(?<quote>["'])(?<regularVerbatim>.+?)\k<quote>(?=$|\s)` +
+      String.raw`|(?<=^|\s)[“”‘’](?<curlyVerbatim>.+?)[“”‘’](?=$|\s)` +
       String.raw`|[^\s\-]+`,
       'gi');
 
@@ -530,10 +558,16 @@ function processTerms(query) {
       continue;
     }
 
+    const verbatim = groups.regularVerbatim || groups.curlyVerbatim;
+    if (verbatim) {
+      verbatimTerms.push(verbatim);
+      continue;
+    }
+
     genericTerms.push(match[0]);
   }
 
-  return {genericTerms, queriedKind};
+  return {genericTerms, verbatimTerms, queriedKind};
 }
 
 function particulate(terms) {
diff --git a/src/static/misc/image.svg b/src/static/misc/image.svg
new file mode 100644
index 00000000..a251b373
--- /dev/null
+++ b/src/static/misc/image.svg
@@ -0,0 +1,11 @@
+<!-- Copyright © (c) 2019-2023 The Bootstrap authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-image-fill" viewBox="0 0 16 16">
+  <path d="M.002 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2V3zm1 9v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12zm5-6.5a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0z"/>
+</svg>