diff options
Diffstat (limited to 'src/static')
41 files changed, 7241 insertions, 3843 deletions
diff --git a/src/static/css/features.css b/src/static/css/features.css new file mode 100644 index 00000000..33a7b9c9 --- /dev/null +++ b/src/static/css/features.css @@ -0,0 +1,2365 @@ +/* Additional files lists */ + +@layer layout { + .additional-files-list { + padding-left: 0; + } + + .additional-files-list > li { + list-style-type: none; + } + + .additional-files-list summary { + /* Sorry, Safari! + * https://bugs.webkit.org/show_bug.cgi?id=157323 + */ + list-style-position: outside; + margin-left: 40px; + } + + .additional-files-list details ul { + margin-left: 40px; + margin-top: 2px; + margin-bottom: 10px; + } + + .additional-files-list .entry-description { + list-style-type: none; + max-width: 540px; + + /* This should be margin-bottom, but cascading rules + * cause some awkwardness - `#content li` takes precedence. + */ + padding-bottom: 3px; + } +} + +/* Additional names (heading and box) */ + +@layer layout { + #additional-names-box { + --custom-scroll-offset: calc(0.5em - 2px); + + margin: 1em 0 1em -10px; + max-width: min(60vw, 600px); + + padding: 15px 20px 10px 20px; + } + + #additional-names-box > :first-child { margin-top: 0; } + #additional-names-box > :last-child { margin-bottom: 0; } + + #additional-names-box p { + padding-left: 10px; + padding-right: 10px; + margin-bottom: 0; + } + + #additional-names-box ul { + padding-left: 10px; + margin-top: 0.5em; + } + + #additional-names-box li .additional-name { + margin-right: 0.25em; + } + + #additional-names-box li .additional-name .content-image { + margin-bottom: 0.25em; + margin-top: 0.5em; + } + + #content.top-index #additional-names-box { + margin-left: auto; + margin-right: auto; + + /* So like, these were in two separate, right-nearby + * rules with the same selector, lol + */ + margin-bottom: 2em; + margin-bottom: 0.75em; + } +} + +@layer print { + #additional-names-box p { + font-style: oblique; + } + + #additional-names-box li .accent { + opacity: 0.8; + } + + #additional-names-box li .additional-name > img { + vertical-align: text-bottom; + } + + #content.top-index #additional-names-box { + text-align: center; + } +} + +@layer interaction { + h1 a[href="#additional-names-box"] { + color: inherit; + text-decoration: underline; + text-decoration-style: dotted; + } + + h1 a[href="#additional-names-box"]:hover { + text-decoration-style: solid; + } + + #additional-names-box:not(.always-visible) { + display: none; + } +} + +/* Carousels */ + +@layer layout { + .carousel-container { + --carousel-tile-min-width: 120px; + --carousel-row-count: 3; + --carousel-column-count: 6; + + position: relative; + overflow: hidden; + margin: 20px 0 5px 0; + padding: 8px 0; + } + + .carousel-container + .quick-info, + .carousel-container + .quick-description { + margin-top: 25px; + } + + html[data-url-key="localized.home"] .carousel-container { + /* This is currently unused... */ + --carousel-tile-size: 140px; + } + + .carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; } + .carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; } + .carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; } + .carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; } + .carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; } + .carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; } + + .carousel-grid { + /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */ + --carousel-gap-count: calc(var(--carousel-column-count) - 1); + --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px); + --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count)); + + position: relative; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr)); + grid-template-rows: repeat(var(--carousel-row-count), auto); + grid-auto-flow: dense; + grid-auto-rows: 0; + overflow: hidden; + margin: auto; + + z-index: 5; + } + + .carousel-grid:nth-child(2), + .carousel-grid:nth-child(3) { + position: absolute; + top: 8px; + left: 0; + right: 0; + } + + .carousel-item { + display: inline-block; + margin: 0; + flex: 1 1 150px; + padding: 3px; + } + + .carousel-item .image-outer-area { + padding: 0; + } + + .carousel-item .image { + width: 100%; + height: 100%; + margin-top: auto; + margin-bottom: auto; + } +} + +@layer material { + .carousel-container { + border-radius: 4px; + } + + .carousel-container::before, + .carousel-container::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + .carousel-container::before { + background-color: var(--dim-color); + filter: brightness(0.6); + } + + .carousel-container::after { + z-index: 40; + pointer-events: none; + + border: 1px solid var(--primary-color); + border-radius: 4px; + + box-shadow: + inset 20px 2px 40px var(--shadow-color), + inset -20px -2px 40px var(--shadow-color); + } + + .carousel-item { + border-radius: 7px; + } + + .carousel-item .image-container { + border: none; + border-radius: 6px; + } + + .carousel-item .image-inner-area::after { + box-shadow: none; + } +} + +@layer interaction { + .carousel-container:hover .carousel-grid { + animation-play-state: running; + } + + .carousel-grid { + transform: translateX(0); + animation: carousel-marquee1 40s linear infinite; + animation-play-state: paused; + } + + .carousel-grid:nth-child(2) { + animation-name: carousel-marquee2; + } + + .carousel-grid:nth-child(3) { + animation-name: carousel-marquee3; + } + + @keyframes carousel-marquee1 { + 0% { + transform: translateX(-100%) translateX(70px); + } + + 100% { + transform: translateX(-200%) translateX(70px); + } + } + + @keyframes carousel-marquee2 { + 0% { + transform: translateX(0%) translateX(70px); + } + + 100% { + transform: translateX(-100%) translateX(70px); + } + } + + @keyframes carousel-marquee3 { + 0% { + transform: translateX(100%) translateX(70px); + } + + 100% { + transform: translateX(0%) translateX(70px); + } + } + + .carousel-item { + filter: brightness(0.8); + } + + .carousel-item:hover { + filter: brightness(1); + background: var(--dim-color); + } +} + +/* Content entries (at large) */ + +@layer layout { + #content blockquote { + margin-left: 40px; + margin-right: min(8vw, 75px); + width: auto; + } + + #content blockquote blockquote { + margin-left: 10px; + padding-left: 10px; + margin-right: 20px; + padding-top: 6px; + padding-bottom: 6px; + } + + #content blockquote blockquote > :first-child { + margin-top: 0; + } + + #content blockquote blockquote > :last-child { + margin-bottom: 0; + } +} + +@layer material { + #content blockquote blockquote { + border-left: dotted 1px; + } +} + +@layer print { + #content blockquote h2 { + font-size: 1em; + font-weight: 800; + } + + #content blockquote h3 { + font-size: 1em; + font-weight: normal; + font-style: oblique; + } +} + +/* Commentary-style content entries */ + +@layer layout { + .inherited-commentary-section { + clear: right; + margin-top: 1em; + margin-bottom: 1.5em; + margin-right: min(4vw, 60px); + } + + .commentary-art { + float: right; + width: 30%; + max-width: 250px; + margin: 15px 0 10px 20px; + } +} + +@layer material { + .inherited-commentary-section { + border: 2px solid var(--deep-color); + border-radius: 4px; + background: #ffffff07; + } + + .cover-artwork.commentary-art { + box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important; + } +} + +@layer print { + .content-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); + } + + .content-entry-accent { + margin-left: 0.25ch; + } + + .content-entry-heading.dated .content-entry-accent { + margin-right: 0.75ch; + } + + .content-entry-annotation { + font-style: oblique; + } + + .content-entry-heading .commentary-date { + display: inline-block; + text-indent: 0; + } + + .content-entry-heading .hoverable { + box-shadow: 1px 2px 6px 5px #04040460; + } + + .content-entry-body summary { + list-style-position: outside; + } + + .content-entry-body summary > span { + color: var(--primary-color); + } + + .wiki-commentary s:not(.spoiler) { + text-decoration-color: #fff9; + text-decoration-thickness: 1.4px; + color: #fffb; + } +} + +/* Content headings */ + +@layer layout { + .content-heading { + border-bottom: 3px double transparent; + margin-bottom: -3px; + } + + dl dt.content-heading { + /* Basic margin-bottom for dt is 2px, + * so just subtract 3px from that. + */ + margin-bottom: -1px; + } + + h3.content-heading { + clear: both; + } +} + +@layer construction { + summary.content-heading { + list-style-type: none; + } + + summary.content-heading .cue { + display: inline-flex; + } + + summary.content-heading .cue::after { + content: ""; + padding-left: 0.5ch; + display: list-item; + list-style-position: inside; + } +} + +@layer interaction { + .content-heading.highlight-hash-link { + animation: highlight-hash-link 4s; + animation-delay: 125ms; + } + + /* This animation's name is referenced in JavaScript */ + @keyframes highlight-hash-link { + 0% { + border-bottom-color: transparent; + } + + 10% { + border-bottom-color: white; + } + + 25% { + border-bottom-color: white; + } + + 100% { + border-bottom-color: transparent; + } + } + + summary.content-heading .cue { + color: var(--primary-color); + } + + summary.content-heading .cue::after { + list-style-type: disclosure-closed; + } + + details[open] > summary.content-heading .cue::after { + list-style-type: disclosure-open; + } + + summary.content-heading > span:hover { + text-decoration: none !important; + } + + summary.content-heading > span:hover .cue { + text-decoration: underline; + text-decoration-style: wavy; + } + + summary.content-heading .when-open { + display: none; + } + + details[open] > summary.content-heading .when-open { + display: unset; + } + + details[open] > summary.content-heading .when-collapsed { + display: none; + } +} + +/* Cover artworks, music videos, and album art info */ + +@layer layout { + .cover-artwork .image { + display: block; + width: 100%; + height: 100%; + } + + .music-video .image { + display: block; + aspect-ratio: 16 / 9; + width: 100%; + height: 100%; + } + + #artwork-column .album-art-info { + margin: 10px min(15px, 1vw) 15px; + } + + #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; + } + + #artwork-column *:where(.cover-artwork, .music-video):where(:not(:first-child)) { + margin-top: 20px; + } + + #artwork-column *:is(.cover-artwork, .music-video):last-child:not(:first-child) { + margin-bottom: 25px; + } + + .cover-artwork-joiner { + z-index: -2; + } + + .cover-artwork-joiner + .cover-artwork { + margin-top: 0 !important; + } + + #artwork-column .music-video { + width: 88.888888%; + margin-left: auto; + } + + #artwork-column .cover-artwork + .music-video { + margin-top: 25px; + } + + #artwork-column .cover-artwork + .cover-artwork + .music-video { + margin-top: 30px; + } +} + +@layer material { + .cover-artwork, + .music-video, + .album-art-info { + border: 2px solid var(--primary-color); + background: var(--bg-black-color); + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); + } + + .cover-artwork { + border-radius: 0 0 4px 4px; + } + + .cover-artwork:has(.image-details), + .cover-artwork.has-image-details { + border-radius: 0 0 6px 6px; + } + + .music-video { + border-radius: 4px; + padding: 0 4px; + } + + .album-art-info { + border-color: var(--deep-color); + border-radius: 5px; + + background: var(--bg-black-color); + padding: 6px; + } + + .album-art-info p { + margin: 0; + } + + .cover-artwork .image-container, + .music-video .image-container { + border: none; + border-radius: 0 !important; + } + + .music-video .image-container { + background: transparent; + border-style: dashed none; + border-width: 2px; + border-color: var(--dim-color); + } + + .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 the .cover-artwork needs to cut off its child .image-container + * (which has a background that otherwise causes sharp corners). + */ + overflow: hidden; + } + + #artwork-column .cover-artwork, + #artwork-column .music-video { + --normal-shadow: 0 0 12px 12px #00000080; + + box-shadow: + 0 2px 14px -6px var(--primary-color), + var(--normal-shadow); + } + + #artwork-column .cover-artwork:not(:first-child) { + --normal-shadow: 0 0 9px 9px #00000068; + } + + .cover-artwork .image-details { + border-top-color: var(--deep-color); + } + + .cover-artwork .image-details + .image-details { + border-top-color: var(--primary-color); + } + + .image-details { + display: block; + margin-top: 0; + margin-bottom: 0; + } + + .image-details:not(.image-details + .image-details) { + margin-left: 0; + margin-right: 0; + padding-left: 9px; + padding-right: 9px; + + padding-top: 6px; + padding-bottom: 4px; + + border-top: 1px dashed var(--deep-color); + } + + .image-details + .image-details { + display: block; + + margin-left: 6px; + margin-right: 6px; + padding-left: 3px; + padding-right: 3px; + + padding-top: 4px; + padding-bottom: 4px; + + border-top: 1px dotted var(--primary-color); + } + + .image-details:last-child { + margin-bottom: 2px; + } +} + +@layer print { + .cover-artwork, + .music-video, + .album-art-info { + font-size: 0.8em; + } + + p.image-details.illustrator-details { + text-align: center; + font-style: oblique; + } + + p.image-details.origin-details { + margin-bottom: 2px; + } + + p.image-details.origin-details .origin-details-line { + display: block; + margin-top: 0.25em; + } + + p.image-details.origin-details .filename-line { + display: block; + margin-top: 0.25em; + } + + .music-video .music-video-label { + margin: 6px 12px 3px; + text-align: center; + } + + .music-video .music-video-label.title-style { + margin-bottom: 4px; + font-style: oblique; + } + + .music-video .image-container ~ p { + margin: 3px 5px; + } + + .music-video .image-container + p { + margin-top: 5px; + } + + .music-video .image-container ~ p:last-child { + margin-bottom: 6px; + } + + .music-video .artists-line { + display: block; + padding-left: 1.2ch; + text-indent: -1.2ch; + } + + .music-video .artists-line > * { + text-indent: 0; + } + + .music-video .artists-line + br { + display: none; + } +} + +@layer construction { + ul.image-details.art-tag-details { + padding-bottom: 0; + } + + ul.image-details.art-tag-details li { + display: inline-block; + } + + ul.image-details.art-tag-details li:not(:last-child)::after { + content: " \00b7 "; + } + + .cover-artwork-joiner::after { + content: ""; + display: block; + width: 0; + height: 15px; + margin-left: auto; + margin-right: auto; + border-right: 3px solid var(--primary-color); + } +} + +@layer interaction { + .music-video .image-link::after { + content: "▶︎"; + background: #0008; + color: white; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + } + + .music-video .image-link:hover::after { + font-size: 1.4em; + background: #0006; + } + + .music-video .image-inner-area::after { + /* We're overriding normal image-inner-area stuff. */ + opacity: 0.8; + box-shadow: 0 0 4px inset black; + } + + .music-video .image-link:hover .image { + transform: scale(1.02); + } +} + +/* Dot switchers */ + +@layer print { + .dot-switcher > span { + color: #ffffffcc; + } + + .dot-switcher > span.current { + font-weight: normal; + color: #ffffffff; + } + + .dot-switcher > span.current a { + font-weight: 800; + } + + .group-view-switcher { + margin-left: 1ch; + } +} + +@layer construction { + .dot-switcher > span:not(:first-child) { + display: inline-block; + white-space: nowrap; + } + + .dot-switcher > span:not(:first-child)::before { + content: "\0020\00b7\0020"; + white-space: pre; + font-weight: 800; + color: white; + } + + /* 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; + } +} + +@layer interaction { + .dot-switcher.intrapage > span:not(.current) a { + text-decoration: underline; + text-decoration-style: dotted; + } + + .dot-switcher.intrapage > span.current a { + /* Keeping cursor: pointer (the default) is intentional here. */ + text-decoration: none !important; + } +} + +/* External icons */ + +@layer print { + .external-icon { + display: inline-block; + padding: 0 3px; + width: 24px; + height: 1em; + position: relative; + } + + .external-icon svg { + width: 24px; + height: 24px; + top: -0.25em; + position: absolute; + fill: var(--primary-color); + } +} + +/* Gallery switchers (of various sorts) */ + +@layer layout { + .gallery-set-switcher { + text-align: center; + } + + .gallery-view-switcher, + .gallery-style-selector { + margin-left: auto; + margin-right: auto; + text-align: center; + line-height: 1.4; + } + + .gallery-style-selector .styles { + display: inline-flex; + justify-content: center; + } + + .gallery-style-selector .styles label:not(:last-child) { + margin-right: 1.25ch; + } + + .gallery-style-selector .count { + position: relative; + bottom: -0.25em; + } +} + +@layer print { + .gallery-view-switcher, + .gallery-style-selector { + line-height: 1.4; + } + + .gallery-style-selector .count { + font-size: 0.85em; + opacity: 0.9; + } +} + +/* Grid listings and grid items */ + +@layer layout { + .grid-listing { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + padding: 5px 15px; + box-sizing: border-box; + margin-top: 1em; + } + + .grid-listing .reveal-all-container { + flex-basis: 100%; + } + + .grid-listing .reveal-all-container.has-nearby-tab { + margin-bottom: 0.6em; + } + + .grid-listing .reveal-all { + max-width: 400px; + margin: 0.20em auto 0; + text-align: center; + } + + .grid-listing .reveal-all a { + display: inline-block; + margin-bottom: 0.15em; + } + + .grid-listing .reveal-all b { + white-space: nowrap; + } + + .grid-item { + --tab-lift: 0px; + --tabnt-offset: 0px; + + margin: 10px; + margin-top: + calc( + 10px + - var(--tab-lift) + + var(--tabnt-offset)); + + padding: 5px; + } + + .grid-item.has-tab { + border-radius: 8px 8px 3px 3px; + } + + .grid-item:not(.has-tab) { + --tabnt-offset: calc(1.2em - 4px); + } + + .grid-item .image-container { + width: 100%; + } + + .grid-item .image { + width: 100%; + height: 100% !important; + margin-top: auto; + margin-bottom: auto; + object-fit: contain; + } + + .grid-listing > .grid-item { + flex: 1 25%; + max-width: 200px; + } + + .grid-actions { + margin: 15px; + align-self: center; + + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + .grid-actions > .grid-item { + flex-basis: unset !important; + margin: 5px; + width: 120px; + --primary-color: inherit !important; + --dim-color: inherit !important; + } + + .grid-listing > .grid-caption, + .grid-listing > .grid-expando { + flex-basis: 100%; + } + + .grid-expando { + margin-top: 1em; + margin-bottom: 2em; + + display: flex; + flex-direction: row; + justify-content: space-around; + } + + .grid-expando-content { + margin: 0; + } +} + +@layer material { + .grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) { + padding-bottom: 140px; + background: #cccccc07; + border-radius: 10px; + border: 1px dashed #fff3; + } + + .grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) .reveal-all-container { + display: none; + } + + .grid-item { + background-color: #111111; + border: 1px dotted var(--primary-color); + border-radius: 2px; + } + + .grid-item .image-inner-area { + border-radius: 0; + box-shadow: none; + } + + .grid-item .image-inner-area::after { + box-shadow: none; + } + + .grid-item .image { + --shadow-filter: + drop-shadow(0 3px 2px #0004) + drop-shadow(0 1px 5px #0001) + drop-shadow(0 3px 4px #0001); + } +} + +@layer print { + .grid-item { + text-align: center; + line-height: 1.2; + font-size: 0.9em; + } + + .grid-item > span { + display: block; + overflow-wrap: break-word; + hyphens: auto; + } + + /* tab */ + .grid-item > span:first-child { + margin-bottom: calc(3px + var(--tab-lift)); + + font-style: oblique; + } + + /* title */ + .grid-item > .image-container + span { + margin-top: 6px; + } + + .grid-name-marker { + color: white; + } + + /* info */ + .grid-item > .image-container + span ~ span { + margin-top: 2px; + + font-size: 0.9em; + opacity: 0.8; + } + + .grid-caption { + text-align: center; + line-height: 1.5; + } + + .grid-expando-content { + text-align: center; + line-height: 1.5; + } +} + +@layer interaction { + .grid-listing .reveal-all a { + text-decoration: underline; + text-decoration-style: dotted; + } + + .grid-listing .reveal-all .warnings { + opacity: 0.4; + } + + .grid-listing .reveal-all:hover .warnings { + opacity: 1; + } + + .grid-item.has-tab:hover { + --tab-lift: 3px; + } + + .grid-item[class*="hidden-by-"] { + display: none; + } + + .grid-item:hover { + text-decoration: none; + } + + .grid-actions .grid-item:hover { + text-decoration: underline; + } + + /* title */ + .grid-item:hover > .image-container + span { + text-decoration: underline; + } + + .grid-expando-toggle { + text-decoration: underline; + text-decoration-style: dotted; + } + + .grid-item.shown-by-expandable-cut { + animation: expand-cover-grid 0.8s forwards; + } + + @keyframes expand-cover-grid { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } +} + +/* Group-contributions tables */ + +@layer layout { + .group-contributions-table { + display: inline-block; + } + + .group-contributions-table .group-contributions-row { + display: flex; + justify-content: space-between; + } + + .group-contributions-table .group-contributions-metrics { + margin-left: 1.5ch; + white-space: nowrap; + } +} + +@layer interaction { + .group-contributions-sorted-by-count:not(.visible), + .group-contributions-sorted-by-duration:not(.visible) { + display: none; + } + + .group-contributions-sort-button { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* Image and media containers */ + +@layer layout { + .image-container { + display: block; + box-sizing: border-box; + position: relative; + height: 100%; + overflow: hidden; + } + + .content-image-container, + .content-video-container, + .content-audio-container { + margin-top: 1em; + margin-bottom: 1em; + } + + .content-video-container, + .content-audio-container { + width: fit-content; + max-width: 100%; + } + + .content-video-container video, + .content-audio-container audio { + display: block; + max-width: 100%; + } + + .image-text-area { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 5px 15px; + } + + .image-outer-area { + width: 100%; + height: 100%; + padding: 5px; + box-sizing: border-box; + } + + .image-link { + display: block; + overflow: hidden; + } + + .square .image-link { + width: 100%; + height: 100%; + } + + .image-inner-area { + position: relative; + width: 100%; + height: 100%; + } + + .image-link .image-inner-area { + /* Jankily fix a rendering issue with border-radius on Safari. + * The `-webkit-` prefix is only to keep this from applying on + * other browsers (well, Firefox), where it doesn't *break* + * anything, but also isn't necessary. + */ + -webkit-transform: translateZ(0); + } + + img { + object-fit: cover; + } + + .square .image { + width: 100%; + height: 100%; + } + + .image-link .image { + display: block; + max-width: 100%; + height: auto; + } + + .reveal-text-container { + position: absolute; + top: 15px; + left: 10px; + right: 10px; + bottom: 10px; + display: flex; + flex-direction: column; + justify-content: center; + } + + .reveal-text { + padding-bottom: 0.5em; + } + + .reveal .image.reveal-thumbnail { + position: absolute; + top: 0; + left: 0; + } + + .reveal.has-reveal-thumbnail:not(.revealed) .image:not(.reveal-thumbnail) { + /* Keep the main image as part of the box model. + * It's what actually defines the dimensions of the + * image-container, so those dimensions never shift + * once the image is actually revealed. + */ + visibility: hidden; + } +} + +@layer material { + .image-container { + background-color: var(--dim-color); + border: 2px solid var(--primary-color); + border-radius: 0; + box-shadow: 0 2px 4px -2px var(--bg-black-color) inset; + } + + .content-video-container, + .content-audio-container { + background-color: var(--dark-color); + border: 2px solid var(--primary-color); + border-radius: 2.5px 2.5px 3px 3px; + padding: 5px; + } + + .image-text-area { + background: rgba(0, 0, 0, 0.65); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset; + + line-height: 1.35em; + color: var(--primary-color); + font-style: oblique; + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75); + } + + .image-link { + border-bottom: 1px solid #ffffff03; + border-radius: 2.5px 2.5px 3px 3px; + box-shadow: + 0 1px 8px -3px var(--bg-black-color); + } + + .image-inner-area::after { + content: ""; + display: block; + position: absolute; + pointer-events: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-bottom-left-radius: 0.5px; + opacity: 0.035; + box-shadow: + 6px -6px 2px -4px white inset; + } + + .image { + --reveal-filter: ; + --shadow-filter: ; + + backdrop-filter: blur(0); + filter: + var(--reveal-filter) + var(--shadow-filter); + } + + .reveal .image.reveal-thumbnail { + image-rendering: pixelated; + } +} + +@layer print { + .image-container { + text-align: left; + color: white; + } + + .content-image-container.align-center { + text-align: center; + margin-top: 1.5em; + margin-bottom: 1.5em; + } + + .content-video-container.align-center, + .content-audio-container.align-center { + margin-left: auto; + margin-right: auto; + } + + .content-image-container.align-full, + .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; + } + + .reveal-text { + color: white; + text-align: center; + font-weight: bold; + font-size: 0.8rem; + } + + .grid-item .reveal-text { + font-size: 0.9em; + } + + .reveal-symbol { + display: inline-block; + width: 1em; + height: 1em; + margin-bottom: 0.1em; + + font-size: 1.6em; + opacity: 0.8; + } + + .sidebar .image-container { + max-width: 350px; + } +} + +@layer interaction { + .image-link:focus { + outline: 3px double white; + } + + .image-link:focus:not(:focus-visible) { + outline: none; + } + + .image-container.has-link:not(.no-image-preview) { + background: var(--deep-color); + box-shadow: none; + border-radius: 0 0 4px 4px; + } + + .reveal-interaction { + opacity: 0.8; + text-decoration: underline; + text-decoration-style: dotted; + } + + .reveal:not(.revealed) .image { + opacity: 0.7; + --reveal-filter: blur(20px) brightness(0.7); + } + + .reveal:not(.revealed) .image-outer-area > * { + position: relative; + border-radius: 6px; + } + + .reveal:not(.revealed) .image-outer-area > *::after { + content: ""; + position: absolute; + box-sizing: border-box; + top: 0; + left: 0; + bottom: 0; + right: 0; + border: 1px dotted var(--primary-color); + border-radius: 6px; + pointer-events: none; + + /* By an awkward DOM intersection, this element might be + * .image-inner-area::after, which is already styled with + * a slight visual effect. Guarantee that the properties + * set to that end are overwritten, and fully co-opt it + * to serve as the interaction cue instead. + */ + box-shadow: none; + opacity: 1; + } + + .reveal:not(.revealed) .image-inner-area { + background: var(--deep-color); + } + + .reveal:not(.revealed) .image-outer-area > *:hover::after { + border-style: solid; + box-shadow: 0 0 0 1.5px #00000099 inset; + } + + .reveal:not(.revealed) .image-outer-area > *:hover .image { + --reveal-filter: blur(20px) brightness(0.6); + opacity: 0.6; + } + + .reveal:not(.revealed) .image-outer-area > *:hover .reveal-interaction { + text-decoration-style: solid; + } + + .reveal.revealed.has-reveal-thumbnail .image.reveal-thumbnail { + display: none !important; + } + + .reveal.revealed .image { + opacity: 1; + } + + .reveal.revealed .reveal-text-container { + display: none; + } +} + +/* Image overlay */ + +@layer layout { + #image-overlay-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 4000; + + padding: 20px 40px; + box-sizing: border-box; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + #image-overlay-content-container { + display: flex; + flex-direction: column; + + overflow: hidden; + } + + #image-overlay-image-area { + display: block; + overflow: hidden; + width: 80vmin; + margin-left: auto; + margin-right: auto; + } + + #image-overlay-image-layout { + display: block; + position: relative; + max-height: 100%; + margin: 4px 3px; + } + + #image-overlay-image, + #image-overlay-image-thumb { + display: block; + width: 100%; + height: auto; + } + + #image-overlay-image { + position: absolute; + max-height: 100%; + object-fit: contain; + } + + #image-overlay-container.no-thumb #image-overlay-image { + position: static; + } + + #image-overlay-action-container { + padding: 7px 4px 7px 4px; + text-align: center; + } +} + +@layer material { + #image-overlay-container { + background: rgba(0, 0, 0, 0.8); + color: white; + } + + #image-overlay-container::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background: var(--deep-color); + opacity: 0.20; + } + + #image-overlay-content-container { + border-radius: 0 0 8px 8px; + border: 2px solid var(--primary-color); + background: var(--deep-ghost-color); + + box-shadow: + 0 0 90px 30px #00000060, + 0 0 20px 10px #00000040, + 0 0 10px 3px #00000080; + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); + } + + #image-overlay-image-layout { + background: rgba(0, 0, 0, 0.65); + } + + #image-overlay-image-thumb { + filter: blur(16px); + transform: scale(1.5); + } + + #image-overlay-action-container { + border-radius: 0 0 5px 5px; + background: var(--bg-black-color); + box-shadow: + 0 3px 8px -5px var(--primary-color) inset; + } +} + +@layer print { + #image-overlay-action-container { + color: white; + font-style: oblique; + text-align: center; + } + + #image-overlay-file-size-warning { + opacity: 0.8; + font-size: 0.9em; + } +} + +@layer construction { + #image-overlay-image-area::after { + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + height: 4px; + width: var(--download-progress); + background: var(--primary-color); + box-shadow: 0 -3px 12px 4px var(--primary-color); + transition: 0.25s; + } + + #image-overlay-container.loaded #image-overlay-image-area::after { + width: 100%; + background: white; + opacity: 0; + } + + #image-overlay-container.errored #image-overlay-image-area::after { + width: 100%; + background: red; + } + + #image-overlay-container:not(.visible) #image-overlay-image-area::after { + width: 0 !important; + } + + #image-overlay-container #image-overlay-action-content-without-size:not(.visible), + #image-overlay-container #image-overlay-action-content-with-size:not(.visible), + #image-overlay-container #image-overlay-file-size-warning:not(.visible), + #image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), + #image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { + display: none; + } +} + +@layer interaction { + #image-overlay-container { + opacity: 0; + pointer-events: none; + transition: opacity 0.4s; + } + + #image-overlay-container.visible { + opacity: 1; + pointer-events: auto; + } + + #image-overlay-container.loaded #image-overlay-image-thumb { + opacity: 0; + pointer-events: none; + transition: opacity 0.25s; + } +} + +/* Info card */ + +@layer interactivity { + #info-card-container { + pointer-events: none; /* Padding area shouldn't 8e interactive. */ + } + + .info-card { + pointer-events: none; + } + + #info-card-container.show .info-card { + animation: 0.01s linear 0.2s forwards info-card-become-interactive; + } + + @keyframes info-card-become-interactive { + to { + pointer-events: auto; + } + } +} + +@layer layout { + #info-card-container { + position: absolute; + left: 0; + right: 10px; + + display: none; + } + + #info-card-container.show, + #info-card-container.hide { + display: flex; + } + + #info-card-container > * { + flex-basis: 400px; + } + + .info-card { + padding: 5px; + } + + .info-card-art-container { + float: right; + width: 40%; + margin: 5px; + + /* Dynamically shown. */ + display: none; + } + + .info-card-art-container .image-container { + padding: 2px; + } + + .info-card-art { + display: block; + width: 100%; + height: 100%; + } + + .info-card::after { + content: ""; + display: block; + clear: both; + } +} + +@layer material { + .info-card-decor { + padding-left: 3ch; + border-top: 1px solid white; + } + + .info-card { + background-color: black; + color: white; + + border: 1px dotted var(--primary-color); + border-radius: 3px; + box-shadow: 0 5px 5px black; + } + + .info-card-name { + font-size: 1em; + border-bottom: 1px dotted; + margin: 0; + } + + .info-card p { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + + .info-card p:last-child { + margin-bottom: 0; + } +} + +@layer print { + .info-card { + font-size: 0.9em; + } + + .info-card-art-container { + font-size: 0.8em; + } +} + +@layer interaction { + #info-card-container.show { + animation: 0.2s linear forwards info-card-show; + transition: top 0.1s, left 0.1s; + } + + #info-card-container.hide { + animation: 0.2s linear forwards info-card-hide; + } + + @keyframes info-card-show { + 0% { + opacity: 0; + margin-top: -5px; + } + + 100% { + opacity: 1; + margin-top: 0; + } + } + + @keyframes info-card-hide { + 0% { + opacity: 1; + margin-top: 0; + } + + 100% { + opacity: 0; + margin-top: 5px; + display: none !important; + } + } +} + +/* Lyrics */ + +@layer layout { + .lyrics-switcher { + padding-left: 20px; + } +} + +@layer print { + .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; + } + + .lyrics-entry sup { + vertical-align: text-top; + opacity: 0.8; + cursor: default; + } +} + +@layer interaction { + .lyrics-entry.long-lyrics { + clip-path: inset(-15px -20px); + } + + .lyrics-entry.long-lyrics::after { + content: ""; + pointer-events: none; + display: block; + + /* Slight stretching past the bottom of the screen seems + * to make resizing the window (and "revealing" that area) + * a bit smoother. + */ + position: fixed; + bottom: -20px; + left: 0; + right: 0; + + height: calc(20px + min(90px, 13.5vh)); + background: linear-gradient(to bottom, transparent, black 70%, black); + opacity: 0.6; + } +} + +/* Quick description */ + +@layer layout { + .quick-description:not(.has-external-links-only) { + --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06); + margin-left: auto; + margin-right: auto; + padding-left: calc(0.40 * var(--clamped-padding-ratio) * 100%); + padding-right: calc(0.40 * var(--clamped-padding-ratio) * 100%); + padding-top: 0.25em; + padding-bottom: 0.75em; + max-width: 500px; + } + + .quick-description.has-external-links-only { + padding-left: 12%; + padding-right: 12%; + } + + .quick-description.has-content-only { + padding-bottom: 0.5em; + } +} + +@layer material { + .quick-description:not(.has-external-links-only) { + border-left: 1px solid var(--dim-color); + border-right: 1px solid var(--dim-color); + } +} + +@layer print { + .quick-description:not(.has-external-links-only) { + line-height: 1.25em; + } + + .quick-description p { + text-align: center; + } + + .quick-description > blockquote { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .quick-description .description-content.long hr ~ p { + text-align: left; + } + + .quick-description .description-content :first-child { + margin-top: 0; + } + + .quick-description .quick-description-actions, + .quick-description.has-content-only .description-content :last-child { + margin-bottom: 0; + } +} + +@layer interaction { + .quick-description:not(.collapsed) .description-content.short, + .quick-description:not(.collapsed) .quick-description-actions.when-collapsed, + .quick-description:not(.expanded) .description-content.long, + .quick-description:not(.expanded) .quick-description-actions.when-expanded { + display: none; + } + + .quick-description .quick-description-actions .expand-link, + .quick-description .quick-description-actions .collapse-link { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* Quick info */ + +@layer layout { + .quick-info { + padding-left: calc(var(--responsive-padding-ratio) * 100%); + padding-right: calc(var(--responsive-padding-ratio) * 100%); + } +} + +@layer print { + .quick-info { + text-align: center; + line-height: 1.25em; + } +} + +@layer construction { + ul.quick-info { + list-style: none; + padding-left: 0; + padding-right: 0; + } + + ul.quick-info li { + display: inline-block; + } + + ul.quick-info li:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; + } +} + +/* Sticky heading */ + +@layer layout { + :where([id]) { + --custom-scroll-offset: 0px; + } + + :where(#content [id]) { + /* Adjust scroll margin. */ + scroll-margin-top: calc( + 74px /* Sticky heading */ + + 33px /* Sticky subheading */ + - 1em /* One line of text (align bottom) */ + - 12px /* Padding for hanging letters & focus ring */ + + var(--custom-scroll-offset) /* Customizable offset */ + ) !important; + } + + .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; + } + + .content-sticky-heading-anchor:not(:where(.content-sticky-heading-root[inert]) *) { + position: relative; + } + + .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 { + padding-left: 0; + padding-right: 0; + } + + main.long-content .content-sticky-heading-container .content-sticky-heading-row, + main.long-content .content-sticky-heading-container .content-sticky-subheading-row { + padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); + padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); + } + + .content-sticky-heading-row { + box-sizing: border-box; + padding: + calc(1.25 * var(--content-padding) + 5px) + 20px + calc(0.75 * var(--content-padding)) + 20px; + + width: 100%; + margin: 0; + } + + .content-sticky-heading-container.has-cover .content-sticky-heading-row, + .content-sticky-heading-container.has-cover .content-sticky-subheading-row { + display: grid; + grid-template-areas: + "title cover"; + 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-row h1 { + position: relative; + margin: 0; + padding-right: 20px; + overflow-x: hidden; + } + + .content-sticky-heading-row h1 .reference-collapsed-heading { + position: absolute; + white-space: nowrap; + visibility: hidden; + } + + .content-sticky-heading-cover-container { + position: relative; + height: 0; + margin: -15px 0px -5px -5px; + } + + .content-sticky-heading-cover { + position: absolute; + top: 0; + width: 80px; + right: 10px; + } + + .content-sticky-heading-container .image-outer-area { + padding: 3px; + } + + .content-sticky-heading-cover .image { + display: block; + width: 100%; + height: 100%; + } + + .content-sticky-subheading-row { + position: absolute; + width: 100%; + box-sizing: border-box; + padding: 10px 20px 5px 20px; + margin-top: 0; + z-index: -1; + } + + .content-sticky-subheading-row h2 { + margin: 0; + } + + .content-sticky-subheading { + padding-right: 20px; + } +} + +@layer material { + .content-sticky-heading-row { + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 8px -4px #000000b0; + + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + } + + .content-sticky-heading-cover { + box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25); + } + + .content-sticky-heading-cover .cover-artwork { + border-width: 1px; + border-radius: 1.25px; + box-shadow: none; + } + + .content-sticky-heading-container .image-inner-area { + border-radius: 1.75px; + + /* Editor's note: I don't know what this overflow: hidden is for. */ + overflow: hidden; + } + + .content-sticky-subheading-row { + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + box-shadow: + 0 2px 2px -1px #00000060, + 0 4px 12px -4px #00000090; + + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + } +} + +@layer print { + .content-sticky-subheading-row h2 { + font-size: 0.9em !important; + font-weight: normal; + font-style: oblique; + color: #eee; + } +} + +@layer interaction { + .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-needs-reveal { + display: none; + } + + .content-sticky-heading-cover { + transition: transform 0.35s, opacity 0.25s; + } + + .content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover { + opacity: 0; + transform: translateY(15px); + transition: transform 0.35s, opacity 0.30s; + } + + .content-sticky-subheading-row { + transition: margin-top 0.35s, opacity 0.25s; + } + + .content-sticky-subheading-row:not(.visible) { + margin-top: -20px; + opacity: 0; + } +} + +/* Sticky sidebar */ + +@layer layout { + .sidebar-column:not(.sticky-column) { + align-self: stretch; + } + + .sidebar-column.sticky-column { + align-self: flex-start; + + position: sticky; + top: 10px; + max-height: calc(100vh - 20px); + + display: flex; + flex-direction: column; + } + + .sidebar-multiple.sticky-column .sidebar:last-child { + flex-shrink: 1; + overflow-y: scroll; + } + + .wiki-search-sidebar-box .wiki-search-results-container { + overflow-y: scroll; + } + + .sidebar-column.sidebar.sticky-column > h1 { + position: sticky; + top: 0; + z-index: 2; + + margin: 0 calc(-1 * var(--content-padding)); + margin-bottom: 10px; + + padding: 10px 5px; + } +} + +@layer material { + .sidebar-multiple.sticky-column .sidebar:last-child { + scrollbar-width: thin; + scrollbar-color: var(--dim-color) var(--dark-color); + } + + .wiki-search-sidebar-box .wiki-search-results-container { + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--dim-color) var(--dark-color); + } + + .sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar, + .wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar { + background: var(--dark-color); + width: 12px; + } + + .sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar-thumb, + .wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar-thumb { + transition: background 0.2s; + background: rgba(255, 255, 255, 0.2); + border: 3px solid transparent; + border-radius: 10px; + background-clip: content-box; + } + + .sidebar-column.sidebar.sticky-column > h1 { + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + background: var(--bg-black-color); + + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + + box-shadow: + 0 2px 3px -1px #0006, + 0 4px 8px -2px #0009; + } +} diff --git a/src/static/css/miscellany.css b/src/static/css/miscellany.css new file mode 100644 index 00000000..a9c24732 --- /dev/null +++ b/src/static/css/miscellany.css @@ -0,0 +1,600 @@ +/* Squares */ + +@layer layout { + .square { + position: relative; + width: 100%; + } + + .square::after { + content: ""; + display: block; + padding-bottom: 100%; + } + + .square-content { + position: absolute; + width: 100%; + height: 100%; + } +} + +/* Utility spans */ + +@layer print { + .nowrap { + white-space: nowrap; + } + + .blockwrap, .chunkwrap { + display: inline-block; + } + + p .current { + font-weight: 800; + } + + .js-hide, + .js-show-once-data, + .js-hide-once-data { + display: none; + } +} + +/* Totally miscellaneous typography */ + +@layer layout { + dl dt { + padding-left: 40px; + } + + dl dt { + /* Heads up, this affects the measurement + * for dl dt which are .content-heading! + */ + margin-bottom: 2px; + } + + dl dt[id]:not(.content-heading) { + --custom-scroll-offset: calc(2.5em - 2px); + } + + dl dd { + margin-bottom: 1em; + } + + dl ul, + dl ol { + margin-top: 0; + margin-bottom: 0; + } + + li .origin-details { + display: block; + margin-left: 2ch; + } + + li > ul { + margin-top: 5px; + } +} + +@layer print { + center { + margin-top: 1em; + margin-bottom: 1em; + } + + .flash-act-title { + display: inline-block; + text-decoration: inherit; + } + + a .flash-act-title { + color: var(--primary-color); + } + + .other-group-accent, + .rerelease-line { + opacity: 0.7; + font-style: oblique; + } + + .other-group-accent { + white-space: nowrap; + } + + .other-group-accent a { + color: var(--page-primary-color); + } + + dt .by, li .by { + font-style: oblique; + } + + dt .by a, li .by a { + display: inline-block; + } + + p code { + font-size: 0.95em; + font-family: "courier new", monospace; + font-weight: 800; + line-height: 1.1; + } + + /* "has-details" means a "has a <details> element" here. */ + ul > li.has-details { + list-style-type: none; + margin-left: -17px; + } + + li .origin-details { + font-size: 0.9em; + font-style: oblique; + } +} + +/* Ordinary links */ + +@layer print { + a { + color: var(--primary-color); + text-decoration: none; + } + + a.current { + font-weight: 800; + } + + a.series { + font-style: oblique; + } + + a:not([href]) { + cursor: default; + } + + a:not([href]):hover { + text-decoration: none; + } +} + +@Layer interaction { + /* This is an !important rule because it's really applicable + * pretty much anywhere, but the selector specificity is lower + * than most selectors which, say, add a dotted underline to + * begin with - those would take cascading priority. + * + * Similarly, this rule has to live in the interaction layer, + * because that's where other underline hover cues are set. + * If we set set it any higher (e.g. in print) the !important + * takes universal priority over !important's set in deeper + * layers - like interaction. + */ + a:hover { + text-decoration: underline; + text-decoration-style: solid !important; + } +} + +/* Links with symbols */ + +@layer print { + a .normal-content { + color: white; + } + + .external-link:not(.from-content) { + white-space: nowrap; + } + + .external-link.indicate-external::after { + content: '\00a0➚'; + font-style: normal; + } + + .external-link.indicate-external:hover::after { + color: white; + } + + .image-media-link::after { + /* Thanks to Jay Freestone for being awesome: + * https://www.jayfreestone.com/writing/wrapping-and-inline-pseudo-elements/ + */ + + pointer-events: none; + content: '\200b'; + padding-left: 22px; + + 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); + } + + .image-media-link:hover::after { + background-color: white; + } +} + +/* Label elements */ + +@layer interaction { + label:hover span { + text-decoration: underline; + text-decoration-style: solid; + } + + label > input[type=checkbox]:not(:checked) + span { + opacity: 0.8; + } +} + +/* Progress elements */ + +@layer material { + progress { + accent-color: var(--primary-color); + } +} + +/* Code blocks */ + +@layer print { + pre.content-code { + position: relative; + white-space: nowrap; + + max-width: calc(100vw - 180px); + + /* Welcome to heck. */ + font-family: inherit; + + border: 1px dashed var(--primary-color); + } + + pre.content-code span { + display: block; + overflow-x: scroll; + padding: 5px 20px 5px 5px; + background: black; + color: white; + } + + pre.content-code::before { + content: ""; + display: block; + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + box-shadow: -15px 0 24px -8px black inset; + pointer-events: none; + } + + pre.content-code code { + font-family: "courier new", monospace; + font-weight: 800; + font-size: 0.8em; + } +} + +/* HR elements, of various flavors */ + +@layer print { + #content hr { + border: 1px inset #808080; + } + + #content hr.split { + color: #808080; + } + + #content hr.split::before { + content: "(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.main-separator::after { + display: inline-block; + content: ""; + width: calc(100% - min(calc(8vw - 35px), 45px)); + position: absolute; + top: 50%; + margin-left: 10px; + } + + #content hr.split::after { + border: 1px inset currentColor; + margin-top: -2px; + } + + #content hr.main-separator::after { + border-bottom: 1px solid currentColor; + } + + hr.cute, + #content hr.cute, + .sidebar hr.cute { + border-color: var(--primary-color); + border-style: none none dotted none; + } +} + +/* Images and media embedded right into content, effects */ + +@layer layout { + a.align-center, img.align-center, + audio.align-center, video.align-center { + display: block; + margin-left: auto; + margin-right: auto; + } + + a.align-full, a.align-full img, + img.align-full, video.align-full { + width: 100%; + } + + .content-image { + display: inline-block !important; + } +} + +@layer material { + img.pixelate, .pixelate img, + video.pixelate, .pixelate video { + image-rendering: crisp-edges; + } +} + +@layer print { + p > img, li > img { + max-width: 100%; + object-fit: contain; + height: auto; + vertical-align: text-bottom; + } +} + +/* Paths and filenames */ + +@layer print { + 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; + } + + blockquote :is(span.path, code.filename) { + font-size: 0.9em; + } + + .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; + } +} + +/* Spoiler elements */ + +@layer print { + s.spoiler { + display: inline-block; + color: transparent; + text-decoration: underline; + text-decoration-color: white; + text-decoration-style: dashed; + text-decoration-skip: none; + text-decoration-skip-ink: none; + } + + s.spoiler::selection { + color: black; + background: white; + } + + s.spoiler::-moz-selection { + color: black; + background: white; + } +} + +/* Summary elements */ + +@layer print { + summary > span b { + font-weight: normal; + color: var(--primary-color); + } + + summary > span:hover { + cursor: pointer; + text-decoration: underline; + text-decoration-color: var(--primary-color); + } + + summary > span:hover a { + text-decoration: none !important; + } + + summary > span:hover:has(a:hover), + summary > span:hover:has(a.nested-hover), + summary.has-nested-hover > span { + text-decoration: none !important; + } + + summary > span:hover:has(a:hover) a, + summary > span:hover:has(a.nested-hover) a, + summary.has-nested-hover > span a { + text-decoration: underline !important; + } + + summary.underline-white > span:hover { + text-decoration-color: white; + } + + /* This link isn't supposed to be underlined *at all* + * when the summary (and not link) is hovered, but + * for some reason Safari is still applying its colored + * and dotted(!) underline. Get around the apparent + * effect by just making it white. + */ + summary.underline-white > span:hover a:not(:hover) { + text-decoration-color: white; + } +} + +/* "Drops" */ + +@layer layout { + .drop { + padding: 15px 20px; + width: max-content; + max-width: min(60vw, 600px); + } + + .commentary-drop { + margin-top: 25px; + margin-bottom: 15px; + margin-left: 20px; + padding: 10px 20px; + max-width: min(60vw, 300px); + } +} + +@layer material { + .drop { + 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); + } +} + +@layer interaction { + .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); + } + } +} + +/* Track sections, and similar */ + +@layer layout { + .album-group-list dt, + .group-series-list dt { + padding-left: 0; + } + + .album-group-list dd, + .group-series-list dd { + 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, + .group-series-list blockquote { + max-width: 540px; + margin-bottom: 9px; + margin-top: 3px; + } + + .album-group-list blockquote p:first-child, + .group-series-list blockquote p:first-child { + margin-top: 0; + } + + .album-group-list blockquote p:last-child, + .group-series-list blockquote p:last-child { + margin-bottom: 0; + } +} + +@layer print { + .album-group-list dt, + .group-series-list dt { + font-style: oblique; + } +} diff --git a/src/static/css/page.css b/src/static/css/page.css new file mode 100644 index 00000000..fdd15358 --- /dev/null +++ b/src/static/css/page.css @@ -0,0 +1,915 @@ +/* Document root */ + +@property --box-opacity { + /* This property rule doesn't actually do anything in practice. + * It just lets us see the effective value of --box-opacity + * in browser CSS inspectors. + */ + syntax: "<number>"; + inherits: true; + initial-value: 0.6; +} + +:root { + color-scheme: dark; + + --initial-wallpaper-opacity: 0.5; + --wallpaper-brightness: var(--initial-wallpaper-opacity); + --box-opacity: calc(0.6 + 0.3 * (clamp(0.75, var(--wallpaper-brightness), 0.95) - 0.75) / (0.95 - 0.75)); +} + +/* Body */ + +@layer layout { + body { + position: relative; + margin: 0; + padding: 10px; + overflow-y: scroll; + } + + body::before { + content: ""; + } +} + +/* Wallpaper */ + +@layer layout { + body::before, .wallpaper-part { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: -1; + + /* NB: these are 100 LVW, "largest view width", etc. + * Stabilizes background on viewports with modal dimensions, + * e.g. expanding/shrinking tab bar or collapsible find bar. + * 100% dimensions are kept above for browser compatibility. + */ + width: 100lvw; + height: 100lvh; + } +} + +@layer material { + body { + background: black; + } + + body::before { + /* 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 { + background-position: center; + background-size: cover; + opacity: var(--initial-wallpaper-opacity); + } +} + +/* Page container */ + +@layer layout { + #page-container { + max-width: 1100px; + + margin: 0 auto 40px; + padding: calc(15px - var(--page-border-width)) 0; + + --page-border-width: 0; + border: var(--page-border-width) solid transparent; + box-sizing: border-box; + } + + #page-container > * { + margin-left: calc(15px - var(--page-border-width)); + margin-right: calc(15px - var(--page-border-width)); + } + + .layout-columns { + display: flex; + align-items: stretch; + } +} + +@layer material { + #page-container { + background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); + + --adjust-page-opacity: calc(alpha * (1.0 + 0.55 * (var(--box-opacity, 0.6) - 0.6))); + + background-color: + color-mix(in oklab, + rgb(from var(--bg-color, rgba(35, 35, 35, 0.8)) + r g b / var(--adjust-page-opacity)), + rgb(from var(--bg-color, rgba(35, 35, 35, 0.8)) + 0 0 0 / var(--adjust-page-opacity)) + calc((var(--box-opacity, 0.6) - 0.6) * 40%)); + + color: #ffffff; + + --page-border-width: 0.5px; + border-bottom-width: 1px; + border-radius: 4px 4px 5px 5px; + + --page-border-color: rgba( + 144, 144, 144, + calc(0.4 + 0.5 * (var(--box-opacity) - 0.6) / 0.4)); + border-color: var(--page-border-color); + + box-shadow: + 0 0 40px #0008, + 0 20px 15px -4px #0002, + 0 6px 15px -3px #2221, + 0 4px 6px 2px #1113, + 0 1px 4px 1px #1114; + } +} + +/* Skippers */ + +@layer layout { + #skippers > * { + display: inline-block; + } +} + +@layer construction { + #skippers > .skipper-list:not(:last-child)::after { + display: inline-block; + content: "\00a0"; + margin-left: 2px; + margin-right: -2px; + border-left: 1px dotted; + } + + #skippers .skipper-list > .skipper:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; + } +} + +@layer interaction { + #skippers { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + } + + #skippers:focus-within { + position: static; + width: unset; + height: unset; + } + + #page-container:not(.showing-sidebar-left) #skippers .skipper[data-for=sidebar-left], + #page-container:not(.showing-sidebar-right) #skippers .skipper[data-for=sidebar-right] { + display: none; + } +} + +/* Banner */ + +@property --banner-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + +@layer interactivity { + #banner::before, #banner::after { + pointer-events: none; + } +} + +@layer layout { + #banner { + position: relative; + } + + /* Used for top border */ + #banner::before { + content: ""; + position: absolute; + top: -1px; + left: var(--page-border-width); + right: var(--page-border-width); + bottom: 0; + } + + /* Used for inset shadow */ + #banner::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + #banner { + margin: 10px calc(-1 * var(--page-border-width)); + } + + #banner img { + display: block; + width: 100%; + height: auto; + } + + #banner.short, #banner.short img { + /* 1100px -> 72.5px, 900px -> 60px + * 12.5px height per 200px width + * Viewport maxes at 1124px, so: + * Start from 72.5px + * Get width below maximum: + * (1124px - min(1124px, 100vw)) + * Multiply by rate of change: + * (12.5 / 200) + */ + max-height: calc(72.5px - (1124px - min(1124px, 100vw)) * (12.5 / 200)); + } + + #banner.short img { + object-position: 0 var(--short-banner-alignment, 50%); + } +} + +@layer material { + #banner { + background-color: var(--abyss-color); + border-bottom: 1px solid var(--primary-color); + } + + #banner::before { + border-top: 1px dashed var(--page-border-color); + } + + #banner::after { + box-shadow: + inset 0 -2px 3px rgba(0, 0, 0, 0.35), + inset 0 1px 4px 2px rgba(0, 0, 0, 0.15); + } + + #banner.dim img { + opacity: 0.32; + } +} + +/* Banner - shine effect */ + +@supports (-webkit-box-reflect: below) { + @layer material { + #banner { + -webkit-box-reflect: below -12px linear-gradient(transparent, color-mix(in srgb, transparent, var(--banner-shine, 0%) white)); + } + } + + @layer interaction { + #banner { + transition: --banner-shine 0.8s; + } + + #banner:not(.dim, .collapsed) { + --banner-shine: 4%; + } + + #banner:not(.dim, .collapsed):hover { + --banner-shine: 35%; + transition-delay: 0.3s; + } + } +} + +/* Boxes (in general) */ + +@layer layout { + #skippers, + #header, + #secondary-nav, + #footer { + padding: 5px; + } + + #skippers, + #header, + #secondary-nav { + margin-bottom: 10px; + } + + #footer { + margin-top: 10px; + } +} + +@layer material { + #skippers, + #header, + #secondary-nav, + #content, + #footer, + .sidebar { + background-color: rgba(0, 0, 0, var(--box-opacity, 0.6)); + border: 1px dotted var(--primary-color); + border-radius: 3px; + } +} + +@layer print { + #skippers, + #header, + #secondary-nav, + #footer, + .sidebar { + font-size: 0.85em; + } +} + +/* Header and navigation links */ + +@layer layout { + #header { display: grid; } + + .nav-main-links { grid-area: main-links; } + .nav-content { grid-area: content; } + .nav-bottom-row { grid-area: bottom-row; } + + #header.nav-has-main-links.nav-has-content { + grid-template-columns: 2.5fr 3fr; + grid-template-rows: min-content 1fr; + grid-template-areas: + "main-links content" + "bottom-row content"; + } + + #header.nav-has-main-links:not(.nav-has-content) { + grid-template-columns: 1fr; + grid-template-areas: + "main-links" + "bottom-row"; + } + + .nav-main-links { + margin-right: 20px; + } + + .nav-bottom-row { + align-self: start; + } +} + +@layer print { + .nav-link { + display: inline-block; + } + + .nav-main-links .nav-link.current > span.nav-link-content > a { + font-weight: 800; + } + + .nav-main-links .nav-link-accent { + display: inline-block; + } + + .series-nav-links { + display: inline-block; + } +} + +@layer construction { + .nav-links-index .nav-link:not(:first-child)::before, + .nav-links-groups .nav-link:not(:first-child)::before { + content: "\0020\00b7\0020"; + font-weight: 800; + } + + .nav-links-hierarchical .nav-link + .nav-link::before, + .nav-links-hierarchical .nav-link + .blockwrap .nav-link::before { + content: "\0020/\0020"; + } + + .group-with-series .group-nav-links + .series-nav-links::before { + content: "\00a0»\00a0"; + font-weight: normal; + } + + .group-with-series .series-nav-links:not(:last-child)::after { + content: ",\00a0"; + font-weight: normal; + } + + .group-with-series .series-nav-links + .series-nav-links::before { + content: ""; + } +} + +@layer interaction { + .inert-previous-next-link { + opacity: 0.7; + } +} + +/* Secondary nav */ + +@layer layout { + #secondary-nav { + text-align: center; + } + + #secondary-nav.album-secondary-nav { + display: flex; + justify-content: space-around; + padding-left: 7.5% !important; + padding-right: 7.5% !important; + flex-wrap: wrap; + } + + #secondary-nav.album-secondary-nav.with-previous-next .group-with-series { + width: 100%; + } + + #secondary-nav.album-secondary-nav.with-previous-next > * { + margin-left: 5px; + margin-right: 5px; + } +} + +@layer print { + #secondary-nav.album-secondary-nav { + line-height: 1.4; + } + + #secondary-nav.album-secondary-nav .group-nav-links .dot-switcher, + #secondary-nav.album-secondary-nav .series-nav-links .dot-switcher { + white-space: nowrap; + } +} + +/* Sidebar column */ + +@layer layout { + .sidebar-column { + flex: 1 1 35%; + min-width: 150px; + max-width: 250px; + align-self: flex-start; + } + + .sidebar-column.wide { + max-width: 350px; + flex-basis: 300px; + flex-shrink: 0; + flex-grow: 1; + } + + .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; + } + + .sidebar-multiple .sidebar:not(:first-child) { + margin-top: 15px; + } +} + +@layer interaction { + .sidebar-column.initially-hidden { + display: none; + } + + .sidebar-column.search-showing-results { + position: sticky; + top: 5px; + align-self: flex-start !important; /* pls */ + } +} + +/* Sidebar boxes */ + +@layer layout { + .sidebar { + --content-padding: 5px; + padding: var(--content-padding); + } + + #sidebar-left { + margin-right: 10px; + } + + #sidebar-right { + margin-left: 10px; + } + + .sidebar-box-joiner { + height: 10px; + } + + .sidebar-box-joiner + .sidebar { + margin-top: 0 !important; + } +} + +@layer material { + .sidebar-box-joiner { + width: 0; + margin-left: auto; + margin-right: auto; + border-right: 1px dashed var(--primary-color); + } +} + +@layer print { + .sidebar > h1, + .sidebar > h2, + .sidebar > h3, + .sidebar > p { + text-align: center; + padding-left: 4px; + padding-right: 4px; + } + + .sidebar h1 { + font-size: 1.25em; + } + + .sidebar h2 { + font-size: 1.1em; + margin: 0; + } + + .sidebar h2:first-child { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + .sidebar h3 { + font-size: 1.1em; + font-style: oblique; + font-variant: small-caps; + margin-top: 0.3em; + margin-bottom: 0em; + } + + .sidebar > p { + margin: 0.5em 0; + padding: 0 5px; + } + + .sidebar hr { + color: #555; + margin: 10px 5px; + } + + .sidebar > ol, + .sidebar > ul { + padding-left: 30px; + padding-right: 15px; + } + + .sidebar > dl { + padding-right: 15px; + padding-left: 0; + } + + .sidebar > dl dt { + padding-left: 10px; + margin-top: 0.5em; + } + + .sidebar > dl dt.current { + font-weight: 800; + } + + .sidebar > dl dd { + margin-left: 0; + } + + .sidebar > dl dd ul { + padding-left: 30px; + margin-left: 0; + } + + .sidebar > dl .side { + 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; + } + + .sidebar li { + overflow-wrap: break-word; + padding-right: 3px; + } + + .sidebar li.structured { + margin-bottom: 3px; + } + + .sidebar > details.current summary { + font-weight: 800; + } + + .sidebar > details summary { + margin-top: 0.5em; + padding-left: 5px; + } + + .sidebar > details.current summary span b { + font-weight: 800; + } + + .sidebar > details ul, + .sidebar > details ol { + margin-top: 0; + margin-bottom: 0; + } + + .sidebar > details:last-child { + margin-bottom: 10px; + } + + .sidebar > details[open] { + margin-bottom: 1em; + } + + .sidebar article { + text-align: left; + margin: 5px 5px 15px 5px; + } + + .sidebar article:last-child { + margin-bottom: 5px; + } + + .sidebar article h2, + .news-index h2 { + border-bottom: 1px dotted; + } + + .sidebar article h2 time, + .news-index time { + float: right; + font-weight: normal; + } + + .group-chronology-link, + .series-chronology-link { + font-style: oblique; + } + + .group-chronology-link a, + .series-chronology-link a { + font-style: normal; + } +} + +/* Track release sidebar box */ + +@layer layout { + .track-release-sidebar-box { + --content-padding: 3px; + } + + .track-release-sidebar-box h1 { + margin: 0; + } + + .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 !important; + border-top-right-radius: 0 !important; + } + + .track-release-sidebar-box:has(+ .track-list-sidebar-box), + .track-list-sidebar-box:has(+ .track-release-sidebar-box) { + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } +} + +@layer print { + .track-release-sidebar-box h1 { + font-weight: normal; + font-size: 0.9em; + font-style: oblique; + } +} + +/* Track list sidebar box */ + +@layer print { + .track-list-sidebar-box summary { + padding-left: 20px !important; + text-indent: -15px !important; + } + + .track-list-sidebar-box .track-section-range { + white-space: nowrap; + } +} + +/* Content area */ + +@layer layout { + #content { + flex-grow: 1; + flex-shrink: 3; + + position: relative; + box-sizing: border-box; + + --content-padding: 20px; + padding: var(--content-padding); + } +} + +@layer print { + #content { + overflow-wrap: anywhere; + } + + #content h1 { + font-size: 1.5em; + } + + #content li { + margin-bottom: 4px; + } + + #content li i { + white-space: nowrap; + } + + #content li.divider { + list-style-type: none; + max-width: 220px; + margin-top: 0.6em; + margin-bottom: 0.6em; + } + + #content li.divider hr { + color: #888; + border: none; + border-bottom: 1px solid; + } + + #content details { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + + #content.top-index h1, + #content.flash-index h1 { + text-align: center; + font-size: 2em; + } + + #content.flash-index h2 { + text-align: center; + font-size: 2.5em; + font-variant: small-caps; + font-style: oblique; + margin-bottom: 0; + text-align: center; + width: 100%; + } + + #content.flash-index h2 .flash-act-title, + #content.flash-index h1 .flash-act-title { + display: block; + } + + #content.flash-index h2 .flash-act-title { + text-transform: uppercase; + } + + #content.top-index h2 { + text-align: center; + font-size: 2em; + font-weight: normal; + margin-bottom: 0.25em; + } + + #content.top-index.has-subtitle h1 { + margin-bottom: 0.35em; + } + + #content.top-index h2.page-subtitle { + font-size: 1.8em; + margin-top: 0.35em; + margin-bottom: 0.5em; + } +} + +/* Footer */ + +@layer layout { + .footer-content { + margin: 5px 12%; + } + + .footer-content > :first-child { + margin-top: 0; + } + + .footer-content > :last-child { + margin-bottom: 0; + } + + .footer-localization-links { + margin: 5px 12%; + } +} + +@layer print { + footer { + text-align: center; + font-style: oblique; + } +} + +@layer construction { + .footer-localization-links > span:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; + } +} + +/* Content (main box) */ + +@layer layout { + main { + --responsive-padding-ratio: 0.10; + } + + main.long-content { + --long-content-padding-ratio: var(--responsive-padding-ratio); + } + + main.long-content .main-content-container, + main.long-content > h1 { + padding-left: calc(var(--long-content-padding-ratio) * 100%); + padding-right: calc(var(--long-content-padding-ratio) * 100%); + } + + #content.top-index section { + margin-bottom: 1.5em; + } +} diff --git a/src/static/css/responsive.css b/src/static/css/responsive.css new file mode 100644 index 00000000..86cd7eb6 --- /dev/null +++ b/src/static/css/responsive.css @@ -0,0 +1,223 @@ +/* Layout - Wide (most computers) */ + +@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; + } +} + +/* Layout - Medium (tablets, some landscape mobiles) + * + * Note: Rules defined here are exclusive to "medium" width, i.e. they don't + * additionally apply to "thin". Use the later section which applies to both + * if so desired. + */ + +@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. + */ + #page-container.showing-sidebar-left main, + #page-container.showing-sidebar-right main { + --responsive-padding-ratio: 0.06; + } +} + +/* Layout - Wide or Medium */ + +@media (min-width: 600px) { + .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 + * + * See also: https://stackoverflow.com/questions/50224855/ + */ + transform: translate3d(0, 0, 0); + z-index: 1; + } + + /* Cover art floats to the right. It's positioned in HTML beneath the + * heading, so pull it up a little to "float" on top. + */ + #artwork-column { + float: right; + width: 40%; + min-width: 220px; + max-width: 280px; + margin: -60px 0 10px 20px; + + position: relative; + z-index: 2; + } + + /* ...Except on top-indexes, where cover art is displayed prominently + * between the heading and subheading. + */ + #content.top-index #artwork-column { + float: none; + margin: 2em auto 2.5em auto; + max-width: 375px; + } + + html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:not(:nth-child(n+7)) { + flex-basis: 23%; + margin: 15px; + } + + html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:nth-child(n+7) { + flex-basis: 18%; + margin: 10px; + } +} + +/* Layout - Medium or Thin */ + +@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; + } + + .layout-columns > *:not(:last-child) { + margin-bottom: 10px; + } + + .sidebar-column { + position: static !important; + max-width: unset !important; + flex-basis: unset !important; + margin-right: 0 !important; + margin-left: 0 !important; + width: 100%; + } + + .sidebar .news-entry:not(.first-news-entry) { + 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) { + main { + --responsive-padding-ratio: 0.02; + } + + #artwork-column { + margin: 25px 0 5px 0; + width: 100%; + max-width: unset; + } + + #artwork-column .cover-artwork { + --normal-shadow: 0 0 transparent; + } + + #artwork-column .cover-artwork:not(:first-child), + #artwork-column .cover-artwork-joiner { + margin-left: 30px; + margin-right: 30px; + } + + .music-video { + width: 70%; + margin-left: auto; + margin-right: auto; + } + + .music-video .image { + aspect-ratio: 32 / 9; + } + + #additional-names-box { + width: unset; + max-width: unset; + } + + .nav-has-content .nav-main-links .nav-link-accent { + display: block; + } + + /* Show sticky heading above cover art */ + + .content-sticky-heading-root { + z-index: 2; + } + + .content-sticky-heading-row h1 { + padding-right: 10px; + } + + /* Let sticky heading text span past lower-index cover art */ + + .content-sticky-heading-container.has-cover .content-sticky-heading-row, + .content-sticky-heading-container.has-cover .content-sticky-subheading-row { + grid-template-columns: 1fr 90px; + } + + /* Disable grid features, just line header children up vertically */ + + #header { + display: block; + } + + #header > div:not(:first-child) { + margin-top: 0.5em; + } +} diff --git a/src/static/css/search.css b/src/static/css/search.css new file mode 100644 index 00000000..f421803b --- /dev/null +++ b/src/static/css/search.css @@ -0,0 +1,490 @@ +/* Sidebar box */ + +@layer layout { + .wiki-search-sidebar-box { + padding: 1px 0 0 0; + + z-index: 100; + max-height: calc(100vh - 20px); + + display: flex; + flex-direction: column; + } +} + +@layer material { + .wiki-search-sidebar-box { + background-color: #000000c0; + + -webkit-backdrop-filter: + brightness(1.2) blur(4px); + backdrop-filter: + brightness(1.2) blur(4px); + } + + .wiki-search-sidebar-box.showing-results { + box-shadow: + 0 4px 16px -8px var(--primary-color), + 0 10px 6px var(--bg-black-color), + 0 6px 4px #00000040; + } +} + +/* Interactions with other sidebar boxes */ + +@layer interaction { + /* This is to say, any sidebar that's *not* + * the first sidebar after the search box. + */ + .wiki-search-sidebar-box.showing-results + .sidebar ~ .sidebar { + margin-top: 5px; + } + + .wiki-search-sidebar-box.showing-results ~ .sidebar:not(:hover) { + opacity: 0.8; + filter: brightness(0.7); + } +} + +/* Label and input elements */ + +@layer layout { + .wiki-search-label { + width: calc(100% - 4px); + padding: 2px 4px; + margin: 2px 2px 3px 2px; + box-sizing: border-box; + + display: flex; + flex-direction: row; + } +} + +@layer material { + .wiki-search-label { + background: transparent; + border: 1px solid var(--dim-color); + border-radius: 3px; + } + + .wiki-search-label::before { + display: inline-block; + padding-left: 3px; + padding-right: 3px; + margin-right: 3px; + width: 1.8em; + text-align: center; + content: '\1f50d\fe0e'; + } + + .wiki-search-input { + background: transparent; + border: transparent; + color: inherit; + flex-grow: 1; + } + + .wiki-search-input::-webkit-search-cancel-button { + filter: grayscale(1) invert(1); + } + + .wiki-search-input::placeholder { + color: var(--primary-color); + font-style: oblique; + } +} + +@layer interaction { + .wiki-search-label.disabled { + opacity: 0.6; + } + + .wiki-search-label.disabled, + .wiki-search-input[disabled] { + cursor: not-allowed; + } + + .wiki-search-label:not(.disabled):hover, + .wiki-search-label:focus-within { + background: var(--light-ghost-color); + } + + .wiki-search-label:focus-within { + border-color: var(--primary-color); + } + + .wiki-search-label:focus-within::before { + opacity: 0.7; + } + + .wiki-search-input:focus { + border: none; + outline: none; + } + + /* This is always the "Search for anything" text, + * if any placeholder is visible while focused. + */ + .wiki-search-input:focus::placeholder { + color: var(--dim-color); + } +} + +/* Progress area ("Loading data..." etc) */ + +@layer layout { + .wiki-search-progress-container { + padding: 2px 6px 4px 6px; + display: flex; + flex-direction: row; + } + + .wiki-search-progress-label { + margin-right: 1ch; + } + + .wiki-search-progress-bar { + flex-grow: 1; + } +} + +@layer print { + .wiki-search-progress-label { + font-size: 0.9em; + font-style: oblique; + } +} + +@layer interaction { + .wiki-search-progress-label { + cursor: default; + } +} + +/* "Failed to search" and "no results" info */ + +@layer layout { + .wiki-search-failed-container, + .wiki-search-no-results { + padding: 2px 3px 4px 6px; + } + + .wiki-search-failed-container p { + margin: 0; + } +} + +@layer print { + .wiki-search-no-results { + font-size: 0.9em; + } +} + +@layer interaction { + .wiki-search-no-results { + cursor: default; + } +} + +/* Context area ("Return to...") */ + +@layer layout { + .wiki-search-context-container { + padding: 2px 12px 4px; + } +} + +@layer material { + .wiki-search-context-container { + border-bottom: 1px solid var(--dim-color); + } +} + +@layer print { + .wiki-search-context-container { + font-size: 0.9em; + } +} + +/* Filters */ + +@layer layout { + .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; + } +} + +@layer interaction { + .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; } + } +} + +/* Divider above search result list */ + +@layer material { + .wiki-search-sidebar-box hr { + margin-top: 3px !important; + margin-bottom: 3px !important; + } +} + +@layer layout { + .wiki-search-sidebar-box hr { + border-color: var(--primary-color); + border-style: none none dotted none; + } +} + +/* Result list */ + +@layer layout { + .wiki-search-results-container { + margin-bottom: 0; + padding: 2px; + } +} + +/* Basic result styling, including interactions */ + +@layer layout { + .wiki-search-result { + position: relative; + display: flex; + padding: 4px 3px 4px 6px; + } + + .wiki-search-result::before { + content: ''; + position: absolute; + top: -2px; + bottom: -2px; + left: 0; + right: 0; + } +} + +@layer material { + .wiki-search-result::before { + border: 1.5px solid var(--primary-color); + border-radius: 4px; + } +} + +@layer interaction { + .wiki-search-result:hover { + text-decoration: none !important; + } + + .wiki-search-result::before { + display: none; + } + + .wiki-search-result:hover::before, + .wiki-search-result:focus::before { + display: block; + background: var(--light-ghost-color); + } + + .wiki-search-result.current-result { + background: var(--light-ghost-color); + border-top: 1px solid var(--dim-color); + border-bottom: 1px solid var(--dim-color); + } + + .wiki-search-result.current-result:hover { + background: none; + border-color: transparent; + } +} + +/* Stuff inside results */ + +@layer layout { + .wiki-search-result-text-area { + align-self: center; + flex-grow: 1; + min-width: 0; + padding-bottom: 2px; + } + + .wiki-search-result-name { + margin-right: 0.25em; + } + + .wiki-search-result-image-container { + align-self: flex-start; + flex-shrink: 0; + margin-right: 6px; + border-radius: 2px; + overflow: hidden; + } + + .wiki-search-result-image, + .wiki-search-result-image-placeholder { + display: block; + width: 1.8em; + height: 1.8em; + aspect-ratio: 1 / 1; + border-radius: 1.5px; + } +} + +@layer material { + .wiki-search-result-image-container { + background-color: var(--deep-color); + border: 2px solid var(--deep-color); + } + + .wiki-search-result-image-placeholder { + background-color: #0004; + box-shadow: 0 1px 3px -1px #0008 inset; + } + + .wiki-search-result-image.has-warning { + filter: blur(2px) brightness(0.8); + } +} + +@layer print { + .wiki-search-result-text-area { + overflow-wrap: break-word; + } + + .wiki-search-result:hover .wiki-search-result-name { + text-decoration: underline; + } + + .wiki-search-current-result-text, + .wiki-search-result-kind, + .wiki-search-result-disambiguator { + opacity: 0.9; + display: inline-block; + } + + .wiki-search-current-result-text, + .wiki-search-result-kind { + font-style: oblique; + } +} + +@layer interaction { + .wiki-search-result.current-result:hover .wiki-search-current-result-text { + filter: saturate(0.8) brightness(1.4); + } + + .wiki-search-results:not(:has(.wiki-search-result-image)) .wiki-search-result-image-container { + display: none; + } +} + +/* "I'm done searching" line */ + +@layer layout { + .wiki-search-end-search-line { + text-align: center; + margin-top: 6px; + margin-bottom: 2px; + } +} + +@layer material { + .wiki-search-end-search-line a { + display: inline-block; + font-style: oblique; + opacity: 0.9; + padding: 3px 6px 4px 6px; + border: 1.5px solid transparent; + border-radius: 4px; + } +} + +@layer interaction { + .wiki-search-end-search-line a:hover { + opacity: 1; + background: var(--light-ghost-color); + border-color: var(--deep-color); + } +} diff --git a/src/static/css/site.css b/src/static/css/site.css index e4057620..b3fff34c 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -1,3622 +1,14 @@ -/* A frontend file! Wow. - * This file is just loaded statically 8y <link>s in the HTML files, so there's - * no need to re-run upd8.js when tweaking values here. Handy! - */ - -/* Squares */ - -/* This styling is kind of awkwardly placed at the very top. Sorry! - * We need to rework what order sets of styles get applied in to be - * much more explicit (so that overriding isn't a headache), and - * hopefully that can be done through @imports, but it'll take some - * reworking and cleaning up. - */ - -.square { - position: relative; - width: 100%; -} - -.square::after { - content: ""; - display: block; - padding-bottom: 100%; -} - -.square-content { - position: absolute; - width: 100%; - height: 100%; -} - -/* Layout - Common */ - -body { - position: relative; - margin: 0; - padding: 10px; - overflow-y: scroll; -} - -body::before { - content: ""; -} - -body::before, .wallpaper-part { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: -1; - - /* NB: these are 100 LVW, "largest view width", etc. - * Stabilizes background on viewports with modal dimensions, - * e.g. expanding/shrinking tab bar or collapsible find bar. - * 100% dimensions are kept above for browser compatibility. - */ - width: 100lvw; - height: 100lvh; -} - -#page-container { - max-width: 1100px; - margin: 0 auto 40px; - padding: 15px 0; -} - -#page-container > * { - margin-left: 15px; - margin-right: 15px; -} - -#skippers:focus-within { - position: static; - width: unset; - height: unset; -} - -#banner { - margin: 10px 0; - width: 100%; - position: relative; -} - -#banner::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -#banner img { - display: block; - width: 100%; - height: auto; -} - -#skippers { - position: absolute; - left: -10000px; - top: auto; - width: 1px; - height: 1px; -} - -.layout-columns { - display: flex; - align-items: stretch; -} - -#header, -#secondary-nav, -#skippers, -#footer { - padding: 5px; -} - -#header, -#secondary-nav, -#skippers { - margin-bottom: 10px; -} - -#footer { - margin-top: 10px; -} - -#header { - display: grid; -} - -#header.nav-has-main-links.nav-has-content { - grid-template-columns: 2.5fr 3fr; - grid-template-rows: min-content 1fr; - grid-template-areas: - "main-links content" - "bottom-row content"; -} - -#header.nav-has-main-links:not(.nav-has-content) { - grid-template-columns: 1fr; - grid-template-areas: - "main-links" - "bottom-row"; -} - -.nav-main-links { - grid-area: main-links; - margin-right: 20px; -} - -.nav-content { - grid-area: content; -} - -.nav-bottom-row { - grid-area: bottom-row; - align-self: start; -} - -.sidebar-column { - flex: 1 1 20%; - min-width: 150px; - max-width: 250px; - flex-basis: 250px; - align-self: flex-start; -} - -.sidebar-column.wide { - max-width: 350px; - flex-basis: 300px; - flex-shrink: 0; - flex-grow: 1; -} - -.sidebar-column.initially-hidden { - 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; -} - -.sidebar-multiple .sidebar:not(:first-child) { - margin-top: 15px; -} - -.sidebar { - --content-padding: 5px; - padding: var(--content-padding); -} - -#sidebar-left { - margin-right: 10px; -} - -#sidebar-right { - margin-left: 10px; -} - -#content { - position: relative; - --content-padding: 20px; - box-sizing: border-box; - padding: var(--content-padding); - flex-grow: 1; - flex-shrink: 3; -} - -.footer-content { - margin: 5px 12%; -} - -.footer-content > :first-child { - margin-top: 0; -} - -.footer-content > :last-child { - margin-bottom: 0; -} - -.footer-localization-links { - margin: 5px 12%; -} - -/* Design & Appearance - Layout elements */ - -:root { - color-scheme: dark; -} - -body { - background: black; -} - -body::before { - /* 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 { - background-position: center; - background-size: cover; - opacity: 0.5; -} - -#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); -} - -#skippers > * { - display: inline-block; -} - -#skippers > .skipper-list:not(:last-child)::after { - display: inline-block; - content: "\00a0"; - margin-left: 2px; - margin-right: -2px; - border-left: 1px dotted; -} - -#skippers .skipper-list > .skipper:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -#page-container:not(.showing-sidebar-left) #skippers .skipper[data-for=sidebar-left], -#page-container:not(.showing-sidebar-right) #skippers .skipper[data-for=sidebar-right] { - display: none; -} - -#banner { - background: black; - background-color: var(--dim-color); - border-bottom: 1px solid var(--primary-color); -} - -#banner::after { - box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35); - pointer-events: none; -} - -#banner.dim img { - opacity: 0.8; -} - -#header, -#secondary-nav, -#skippers, -#footer, -.sidebar { - font-size: 0.85em; -} - -.sidebar, -#content, -#header, -#secondary-nav, -#skippers, -#footer { - background-color: rgba(0, 0, 0, 0.6); - border: 1px dotted var(--primary-color); - border-radius: 3px; - transition: background-color 0.2s; -} - -/* -.sidebar:focus-within, -#content:focus-within, -#header:focus-within, -#secondary-nav:focus-within, -#skippers:focus-within, -#footer:focus-within { - background-color: rgba(0, 0, 0, 0.85); - border-style: solid; -} -*/ - -.sidebar > h1, -.sidebar > h2, -.sidebar > h3, -.sidebar > p { - text-align: center; - padding-left: 4px; - padding-right: 4px; -} - -.sidebar h1 { - font-size: 1.25em; -} - -.sidebar h2 { - font-size: 1.1em; - margin: 0; -} - -.sidebar h2:first-child { - margin-top: 0.5em; - margin-bottom: 0.5em; -} - -.sidebar h3 { - font-size: 1.1em; - font-style: oblique; - font-variant: small-caps; - margin-top: 0.3em; - margin-bottom: 0em; -} - -.sidebar > p { - margin: 0.5em 0; - padding: 0 5px; -} - -.sidebar hr { - color: #555; - margin: 10px 5px; -} - -.sidebar > ol, -.sidebar > ul { - padding-left: 30px; - padding-right: 15px; -} - -.sidebar > dl { - padding-right: 15px; - padding-left: 0; -} - -.sidebar > dl dt { - padding-left: 10px; - margin-top: 0.5em; -} - -.sidebar > dl dt.current { - font-weight: 800; -} - -.sidebar > dl dd { - margin-left: 0; -} - -.sidebar > dl dd ul { - padding-left: 30px; - margin-left: 0; -} - -.sidebar > dl .side { - 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; -} - -.sidebar li { - overflow-wrap: break-word; -} - -.sidebar > details.current summary { - font-weight: 800; -} - -.sidebar > details summary { - margin-top: 0.5em; - padding-left: 5px; -} - -.sidebar > details.current summary span b { - font-weight: 800; -} - -summary > span b { - font-weight: normal; - color: var(--primary-color); -} - -summary > span:hover { - cursor: pointer; - text-decoration: underline; - text-decoration-color: var(--primary-color); -} - -summary > span:hover a { - text-decoration: none !important; -} - -summary > span:hover:has(a:hover), -summary > span:hover:has(a.nested-hover), -summary.has-nested-hover > span { - text-decoration: none !important; -} - -summary > span:hover:has(a:hover) a, -summary > span:hover:has(a.nested-hover) a, -summary.has-nested-hover > span a { - text-decoration: underline !important; -} - -summary.underline-white > span:hover { - text-decoration-color: white; -} - -/* This link isn't supposed to be underlined *at all* - * when the summary (and not link) is hovered, but - * for some reason Safari is still applying its colored - * and dotted(!) underline. Get around the apparent - * effect by just making it white. - */ -summary.underline-white > span:hover a:not(:hover) { - text-decoration-color: white; -} - -.sidebar > details ul, -.sidebar > details ol { - margin-top: 0; - margin-bottom: 0; -} - -.sidebar > details:last-child { - margin-bottom: 10px; -} - -.sidebar > details[open] { - margin-bottom: 1em; -} - -.sidebar article { - text-align: left; - margin: 5px 5px 15px 5px; -} - -.sidebar article:last-child { - margin-bottom: 5px; -} - -.sidebar article h2, -.news-index h2 { - border-bottom: 1px dotted; -} - -.sidebar article h2 time, -.news-index time { - float: right; - font-weight: normal; -} - -.sidebar-column.search-showing-results { - position: sticky; - top: 5px; - align-self: flex-start !important; /* pls */ -} - -.sidebar-box-joiner { - width: 0; - margin-left: auto; - margin-right: auto; - border-right: 1px dashed var(--primary-color); - height: 10px; -} - -.sidebar-box-joiner + .sidebar { - 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; - - z-index: 100; - max-height: calc(100vh - 20px); - - display: flex; - flex-direction: column; - - background-color: #000000c0; - - -webkit-backdrop-filter: - brightness(1.2) blur(4px); - - backdrop-filter: - brightness(1.2) blur(4px); -} - -.wiki-search-sidebar-box.showing-results { - box-shadow: - 0 4px 16px -8px var(--primary-color), - 0 10px 6px var(--bg-black-color), - 0 6px 4px #00000040; -} - -/* This is to say, any sidebar that's *not* - * the first sidebar after the search box. - */ -.wiki-search-sidebar-box.showing-results + .sidebar ~ .sidebar { - margin-top: 5px; -} - -.wiki-search-sidebar-box.showing-results ~ .sidebar:not(:hover) { - opacity: 0.8; - filter: brightness(0.7); -} - -.wiki-search-label { - width: calc(100% - 4px); - padding: 2px 4px; - margin: 2px 2px 3px 2px; - box-sizing: border-box; - - display: flex; - flex-direction: row; - - background: transparent; - border: 1px solid var(--dim-color); - border-radius: 3px; -} - -.wiki-search-label::before { - display: inline-block; - padding-left: 3px; - padding-right: 3px; - margin-right: 3px; - width: 1.8em; - text-align: center; - content: '\1f50d\fe0e'; -} - -.wiki-search-input { - background: transparent; - border: transparent; - color: inherit; - flex-grow: 1; -} - -.wiki-search-input::-webkit-search-cancel-button { - filter: grayscale(1) invert(1); -} - -.wiki-search-label.disabled { - opacity: 0.6; -} - -.wiki-search-label.disabled, -.wiki-search-input[disabled] { - cursor: not-allowed; -} - -.wiki-search-label:not(.disabled):hover, -.wiki-search-label:focus-within { - background: var(--light-ghost-color); -} - -.wiki-search-label:focus-within { - border-color: var(--primary-color); -} - -.wiki-search-label:focus-within::before { - opacity: 0.7; -} - -.wiki-search-input:focus { - border: none; - outline: none; -} - -.wiki-search-input::placeholder { - color: var(--primary-color); - font-style: oblique; -} - -.wiki-search-input:focus::placeholder { - color: var(--dim-color); -} - -.wiki-search-sidebar-box hr { - border-color: var(--primary-color); - border-style: none none dotted none; - margin-top: 3px; - margin-bottom: 3px; -} - -.wiki-search-progress-container { - padding: 2px 6px 4px 6px; - display: flex; - flex-direction: row; -} - -.wiki-search-progress-label { - font-size: 0.9em; - font-style: oblique; - cursor: default; - margin-right: 1ch; -} - -.wiki-search-progress-bar { - flex-grow: 1; -} - -.wiki-search-failed-container { - padding: 2px 3px 4px 6px; -} - -.wiki-search-failed-container p { - margin: 0; -} - -.wiki-search-results-container { - margin-bottom: 0; - padding: 2px; -} - -.wiki-search-no-results { - font-size: 0.9em; - padding: 2px 3px 4px 6px; - cursor: default; -} - -.wiki-search-result { - position: relative; - display: flex; - padding: 4px 3px 4px 6px; -} - -.wiki-search-result:hover { - text-decoration: none !important; -} - -.wiki-search-result::before { - content: ''; - position: absolute; - top: -2px; - bottom: -2px; - left: 0; - right: 0; - - border: 1.5px solid var(--primary-color); - border-radius: 4px; - display: none; -} - -.wiki-search-result.current-result { - background: var(--light-ghost-color); - border-top: 1px solid var(--dim-color); - border-bottom: 1px solid var(--dim-color); -} - -.wiki-search-result:hover::before, -.wiki-search-result:focus::before { - display: block; - background: var(--light-ghost-color); -} - -.wiki-search-result.current-result:hover { - background: none; - border-color: transparent; -} - -.wiki-search-result.current-result:hover .wiki-search-current-result-text { - filter: saturate(0.8) brightness(1.4); -} - -.wiki-search-result-text-area { - align-self: center; - flex-grow: 1; - min-width: 0; - overflow-wrap: break-word; - padding-bottom: 2px; -} - -.wiki-search-result-name { - margin-right: 0.25em; -} - -.wiki-search-result:hover .wiki-search-result-name { - text-decoration: underline; -} - -.wiki-search-current-result-text, -.wiki-search-result-kind { - font-style: oblique; - opacity: 0.9; - display: inline-block; -} - -.wiki-search-result-image-container { - align-self: flex-start; - flex-shrink: 0; - margin-right: 6px; - border-radius: 2px; - overflow: hidden; - - background-color: var(--deep-color); - border: 2px solid var(--deep-color); -} - -.wiki-search-results:not(:has(.wiki-search-result-image)) .wiki-search-result-image-container { - display: none; -} - -.wiki-search-result-image, -.wiki-search-result-image-placeholder { - display: block; - width: 1.8em; - height: 1.8em; - aspect-ratio: 1 / 1; - border-radius: 1.5px; -} - -.wiki-search-result-image-placeholder { - background-color: #0004; - box-shadow: 0 1px 3px -1px #0008 inset; -} - -.wiki-search-result-image.has-warning { - filter: blur(2px) brightness(0.8); -} - -.wiki-search-end-search-line { - text-align: center; - margin-top: 6px; - margin-bottom: 2px; -} - -.wiki-search-end-search-line a { - display: inline-block; - font-style: oblique; - opacity: 0.9; - padding: 3px 6px 4px 6px; - border-radius: 4px; - border: 1.5px solid transparent; -} - -.wiki-search-end-search-line a:hover { - opacity: 1; - background: var(--light-ghost-color); - border-color: var(--deep-color); -} - -#content { - overflow-wrap: anywhere; -} - -footer { - text-align: center; - font-style: oblique; -} - -.footer-localization-links > span:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -/* Design & Appearance - Content elements */ - -a { - color: var(--primary-color); - text-decoration: none; -} - -a:hover { - text-decoration: underline; - text-decoration-style: solid !important; -} - -a.current { - font-weight: 800; -} - -a.series { - font-style: oblique; -} - -a:not([href]) { - cursor: default; -} - -a:not([href]):hover { - text-decoration: none; -} - -a .normal-content { - color: white; -} - -.external-link:not(.from-content) { - white-space: nowrap; -} - -.external-link.indicate-external::after { - content: '\00a0➚'; - font-style: normal; -} - -.external-link.indicate-external:hover::after { - color: white; -} - -.image-media-link::after { - content: ''; - display: inline-block; - width: 22px; - height: 1em; - - background-color: var(--primary-color); - - mask-image: url(/static-4p1/misc/image.svg); - mask-repeat: no-repeat; - mask-position: calc(100% - 2px); - vertical-align: text-bottom; -} - -.image-media-link:hover::after { - background-color: white; -} - -.nav-link { - display: inline-block; -} - -.nav-main-links .nav-link.current > span.nav-link-content > a { - font-weight: 800; -} - -.nav-main-links .nav-link-accent { - display: inline-block; -} - -.nav-links-index .nav-link:not(:first-child)::before, -.nav-links-groups .nav-link:not(:first-child)::before { - content: "\0020\00b7\0020"; - font-weight: 800; -} - -.nav-links-hierarchical .nav-link + .nav-link::before, -.nav-links-hierarchical .nav-link + .blockwrap .nav-link::before { - content: "\0020/\0020"; -} - -.series-nav-links { - display: inline-block; -} - -.series-nav-links:not(:first-child)::before { - content: "\00a0»\00a0"; - font-weight: normal; -} - -.series-nav-links:not(:last-child)::after { - content: ",\00a0"; -} - -.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; -} - -.dot-switcher > span.current { - font-weight: 800; -} - -.dot-switcher.intrapage > span:not(.current) a { - text-decoration: underline; - text-decoration-style: dotted; -} - -.dot-switcher.intrapage > span.current a { - /* Keeping cursor: pointer (the default) is intentional here. */ - text-decoration: none !important; -} - -#secondary-nav { - text-align: center; - - /* Default to visible. It'll automatically be hidden - * in layouts where the sidebar is visible. - */ - display: block; -} - -#secondary-nav.album-secondary-nav { - display: flex; - justify-content: space-around; - padding-left: 7.5% !important; - padding-right: 7.5% !important; - flex-wrap: wrap; - line-height: 1.4; -} - -#secondary-nav.album-secondary-nav.with-previous-next .group-with-series { - width: 100%; -} - -#secondary-nav.album-secondary-nav.with-previous-next > * { - margin-left: 5px; - margin-right: 5px; -} - -#secondary-nav.album-secondary-nav .group-nav-links .dot-switcher, -#secondary-nav.album-secondary-nav .series-nav-links .dot-switcher { - white-space: nowrap; -} - -.inert-previous-next-link { - opacity: 0.7; -} - -.nowrap { - white-space: nowrap; -} - -.blockwrap, .chunkwrap { - display: inline-block; -} - -.text-with-tooltip { - position: relative; -} - -.text-with-tooltip .text-with-tooltip-interaction-cue { - text-decoration: underline; - text-decoration-style: dotted; -} - -.text-with-tooltip > .hoverable:hover .text-with-tooltip-interaction-cue, -.text-with-tooltip > .hoverable.has-visible-tooltip .text-with-tooltip-interaction-cue { - text-decoration-style: wavy !important; -} - -.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.rerelease .text-with-tooltip-interaction-cue, -.text-with-tooltip.first-release .text-with-tooltip-interaction-cue { - cursor: default; -} - -.text-with-tooltip.missing-duration > .hoverable { - opacity: 0.5; -} - -.text-with-tooltip.missing-duration > .hoverable:hover, -.text-with-tooltip.missing-duration > .hoverable.has-visible-tooltip { - opacity: 1; -} - -.text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue { - 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; - top: calc(1em + 1px); - display: none; -} - -.cover-artwork .tooltip, -#sidebar .tooltip { - font-size: 0.9rem; -} - -li:not(:first-child:last-child) .tooltip, -.offset-tooltips > :not(:first-child:last-child) .tooltip { - left: 14px; -} - -.tooltip-content { - display: block; - - background: var(--bg-black-color); - border: 1px dotted var(--primary-color); - border-radius: 6px; - - -webkit-backdrop-filter: - brightness(1.5) saturate(1.4) blur(4px); - - backdrop-filter: - brightness(1.5) saturate(1.4) blur(4px); - - box-shadow: - 0 3px 4px 4px #000000aa, - 0 -2px 4px -2px var(--primary-color) inset; - - text-indent: 0; -} - -.contribution-tooltip { - padding: 3px 6px 6px 6px; - left: -34px; -} - -.datetimestamp-tooltip, -.missing-duration-tooltip, -.commentary-date-tooltip, -.rerelease-tooltip, -.first-release-tooltip { - padding: 3px 4px 2px 2px; - left: -10px; -} - -.thing-name-tooltip, -.wiki-edits-tooltip { - padding: 3px 4px 2px 2px; - left: -6px !important; -} - -.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; } - -.contribution-tooltip .tooltip-content { - padding: 6px 2px 2px 2px; - - -webkit-user-select: none; - user-select: none; - - cursor: default; - - display: grid; - - grid-template-columns: - [icon-start] 26px [icon-end handle-start] auto [handle-end platform-start] auto [platform-end]; -} - -.contribution-tooltip .external-link { - display: grid; - grid-column-start: icon-start; - grid-column-end: handle-end; - grid-template-columns: subgrid; - - height: 1.4em; -} - -.contribution-tooltip .chronology-link { - display: grid; - grid-column-start: icon-start; - grid-column-end: handle-end; - grid-template-columns: subgrid; - - height: 1.2em; -} - -.contribution-tooltip .external-icon, -.contribution-tooltip .chronology-symbol { - grid-column-start: icon-start; - grid-column-end: icon-end; -} - -.contribution-tooltip .external-icon svg { - width: 18px; - height: 18px; - top: -0.1em; -} - -.contribution-tooltip .chronology-symbol { - text-align: center; -} - -.contribution-tooltip .external-handle, -.contribution-tooltip .chronology-text { - grid-column-start: handle-start; - grid-column-end: handle-end; - - width: max-content; - max-width: 200px; - - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.contribution-tooltip .external-handle { - padding-right: 8px; -} - -.contribution-tooltip .chronology-text { - padding-right: 6px; -} - -.contribution-tooltip .chronology-text, -.contribution-tooltip .chronology-info { - font-size: 0.85em; -} - -.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; -} - -.contribution-tooltip .external-platform, -.contribution-tooltip .chronology-info { - display: none; - - grid-column-start: platform-start; - grid-column-end: platform-end; - - --external-platform-opacity: 0.8; - opacity: 0.8; - padding-right: 4px; - - white-space: nowrap; -} - -.contribution-tooltip.show-info .external-platform, -.contribution-tooltip.show-info .chronology-info { - display: inline; - animation: external-platform 0.2s forwards linear; -} - -@keyframes external-platform { - from { - opacity: 0; - } - - to { - opacity: var(--external-platform-opacity); - } -} - -.contribution-tooltip .external-link:hover, -.contribution-tooltip .chronology-link:hover { - filter: brightness(1.4); - text-decoration: none; -} - -.contribution-tooltip .external-link:hover .external-handle, -.contribution-tooltip .chronology-link:hover .chronology-text { - text-decoration: underline; -} - -.contribution-tooltip .external-link:hover + .external-platform, -.contribution-tooltip .chronology-link:hover + .chronology-info { - --external-platform-opacity: 1; - text-decoration: underline; - text-decoration-color: #ffffffaa; -} - -.datetimestamp-tooltip .tooltip-content, -.missing-duration-tooltip .tooltip-content, -.commentary-date-tooltip .tooltip-content { - padding: 5px 6px; - white-space: nowrap; - font-size: 0.9em; -} - -.thing-name-tooltip .tooltip-content, -.wiki-edits-tooltip .tooltip-content { - padding: 3px 4.5px; -} - -.rerelease-tooltip .tooltip-content, -.first-release-tooltip .tooltip-content { - padding: 3px 4.5px; - width: 260px; - font-size: 0.9em; -} - -.external-icon { - display: inline-block; - padding: 0 3px; - width: 24px; - height: 1em; - position: relative; -} - -.external-icon svg { - width: 24px; - height: 24px; - top: -0.25em; - position: absolute; - fill: var(--primary-color); -} - -.other-group-accent, -.rerelease-line { - opacity: 0.7; - font-style: oblique; -} - -.other-group-accent { - white-space: nowrap; -} - -.other-group-accent a { - color: var(--page-primary-color); -} - -s.spoiler { - display: inline-block; - color: transparent; - text-decoration: underline; - text-decoration-color: white; - text-decoration-style: dashed; - text-decoration-skip: none; - text-decoration-skip-ink: none; -} - -s.spoiler::selection { - color: black; - background: white; -} - -s.spoiler::-moz-selection { - color: black; - background: white; -} - -progress { - accent-color: var(--primary-color); -} - -.content-columns { - columns: 2; -} - -.content-columns .column { - break-inside: avoid; -} - -.content-columns .column h2 { - margin-top: 0; - font-size: 1em; -} - -p .current { - font-weight: 800; -} - -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); - - border-radius: 0 0 4px 4px; - background: var(--bg-black-color); - - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); -} - -.cover-artwork:has(.image-details), -.cover-artwork.has-image-details { - border-radius: 0 0 6px 6px; -} - -.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 the .cover-artwork needs to cut off its child .image-container - * (which has a background that otherwise causes sharp corners). - */ - overflow: hidden; -} - -#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) { - margin-top: 20px; - margin-left: 30px; - margin-right: 5px; -} - -#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 !important; -} - -.cover-artwork .image-details { - border-top-color: var(--deep-color); -} - -.cover-artwork .image-details + .image-details { - border-top-color: var(--primary-color); -} - -.cover-artwork .image { - display: block; - width: 100%; - height: 100%; -} - -.image-details { - display: block; - - margin-top: 0; - margin-bottom: 0; - - /* Styles below only apply for first image-details. */ - - margin-left: 0; - margin-right: 0; - padding-left: 9px; - padding-right: 9px; - - padding-top: 6px; - padding-bottom: 4px; - - border-top: 1px dashed var(--dim-color); -} - -.image-details + .image-details { - display: block; - - margin-left: 6px; - margin-right: 6px; - padding-left: 3px; - padding-right: 3px; - - padding-top: 4px; - padding-bottom: 4px; - - border-top: 1px dotted var(--primary-color); -} - -.image-details:last-child { - margin-bottom: 2px; -} - -ul.image-details.art-tag-details { - padding-bottom: 0; -} - -ul.image-details.art-tag-details li { - display: inline-block; -} - -ul.image-details.art-tag-details li:not(:last-child)::after { - content: " \00b7 "; -} - -p.image-details.illustrator-details { - text-align: center; - font-style: oblique; -} - -p.image-details.origin-details { - margin-bottom: 2px; -} - -.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; -} - -p.content-heading:has(+ .commentary-entry-heading.dated) { - clear: right; -} - -.commentary-entry-heading { - display: flex; - flex-direction: row; - - margin-left: 15px; - padding-left: 5px; - max-width: 625px; - padding-bottom: 0.2em; - - border-bottom: 1px solid var(--dim-color); -} - -.commentary-entry-heading-text { - flex-grow: 1; - padding-left: 1.25ch; - text-indent: -1.25ch; -} - -.commentary-entry-accent { - font-style: oblique; -} - -.commentary-entry-heading .commentary-date { - flex-shrink: 0; - - margin-left: 0.75ch; - align-self: flex-end; - - padding-left: 0.5ch; - padding-right: 0.25ch; -} - -.commentary-entry-heading .hoverable { - box-shadow: 1px 2px 6px 5px #04040460; -} - -.commentary-entry-body summary { - list-style-position: outside; -} - -.commentary-entry-body summary > span { - color: var(--primary-color); -} - -.commentary-art { - float: right; - width: 30%; - max-width: 250px; - margin: 15px 0 10px 20px; - - /* This !important is unfortunate, but it's necessary - * even if the rule itself is placed lower, because this - * is a relatively low-priority selector compared to - * others that alter image shadows. - */ - box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important; -} - -.js-hide, -.js-show-once-data, -.js-hide-once-data { - display: none; -} - -.content-image-container, -.content-video-container { - margin-top: 1em; - margin-bottom: 1em; -} - -.content-image-container.align-center, -.content-video-container.align-center, -.content-audio-container.align-center { - text-align: center; - margin-top: 1.5em; - margin-bottom: 1.5em; -} - -a.align-center, img.align-center, audio.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -center { - margin-top: 1em; - margin-bottom: 1em; -} - -.content-image { - display: inline-block !important; -} - -.image-link { - display: block; - overflow: hidden; -} - -.image-link:focus { - outline: 3px double white; -} - -.image-link:focus:not(:focus-visible) { - outline: none; -} - -.image-link .image { - display: block; - max-width: 100%; - height: auto; -} - -.square .image-link { - width: 100%; - height: 100%; -} - -.square .image { - width: 100%; - height: 100%; -} - -h1 { - font-size: 1.5em; -} - -#content li { - margin-bottom: 4px; -} - -#content li i { - white-space: nowrap; -} - -#content.top-index h1, -#content.flash-index h1 { - text-align: center; - font-size: 2em; -} - -#content.flash-index h2 { - text-align: center; - font-size: 2.5em; - font-variant: small-caps; - font-style: oblique; - margin-bottom: 0; - text-align: center; - width: 100%; -} - -#content.top-index h2 { - text-align: center; - font-size: 2em; - font-weight: normal; - margin-bottom: 0.25em; -} - -#content.top-index.has-subtitle h1 { - margin-bottom: 0.35em; -} - -#content.top-index h2.page-subtitle { - font-size: 1.8em; - margin-top: 0.35em; - margin-bottom: 0.5em; -} - -.quick-info { - text-align: center; - padding-left: calc(var(--responsive-padding-ratio) * 100%); - padding-right: calc(var(--responsive-padding-ratio) * 100%); - line-height: 1.25em; -} - -ul.quick-info { - list-style: none; - padding-left: 0; - padding-right: 0; -} - -ul.quick-info li { - display: inline-block; -} - -ul.quick-info li:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -.carousel-container + .quick-info, -.carousel-container + .quick-description { - margin-top: 25px; -} - -.quick-description:not(.has-external-links-only) { - --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06); - margin-left: auto; - margin-right: auto; - padding-left: calc(0.40 * var(--clamped-padding-ratio) * 100%); - padding-right: calc(0.40 * var(--clamped-padding-ratio) * 100%); - max-width: 500px; - - padding-top: 0.25em; - padding-bottom: 0.75em; - border-left: 1px solid var(--dim-color); - border-right: 1px solid var(--dim-color); - line-height: 1.25em; -} - -.quick-description.has-external-links-only { - padding-left: 12%; - padding-right: 12%; -} - -.quick-description.has-content-only { - padding-bottom: 0.5em; -} - -.quick-description p { - text-align: center; -} - -.quick-description > blockquote { - margin-left: 0 !important; -} - -.quick-description .description-content.long hr ~ p { - text-align: left; -} - -.quick-description > .description-content :first-child { - margin-top: 0; -} - -.quick-description > .quick-description-actions, -.quick-description.has-content-only .description-content :last-child { - margin-bottom: 0; -} - -.quick-description:not(.collapsed) .description-content.short, -.quick-description:not(.collapsed) .quick-description-actions.when-collapsed, -.quick-description:not(.expanded) .description-content.long, -.quick-description:not(.expanded) .quick-description-actions.when-expanded { - display: none; -} - -.quick-description .quick-description-actions .expand-link, -.quick-description .quick-description-actions .collapse-link { - text-decoration: underline; - text-decoration-style: dotted; -} - -#intro-menu { - margin: 24px 0; - padding: 10px; - background-color: #222222; - text-align: center; - border: 1px dotted var(--primary-color); - border-radius: 2px; -} - -#intro-menu p { - margin: 12px 0; -} - -#intro-menu a { - margin: 0 6px; -} - -li .by { - font-style: oblique; - max-width: 600px; -} - -li .by a { - display: inline-block; -} - -p code { - font-size: 0.95em; - font-family: "courier new", monospace; - font-weight: 800; - line-height: 1.1; -} - -#content blockquote { - margin-left: 40px; - max-width: 600px; - margin-right: 0; -} - -#content blockquote blockquote { - margin-left: 10px; - padding-left: 10px; - margin-right: 20px; - border-left: dotted 1px; - padding-top: 6px; - padding-bottom: 6px; -} - -#content blockquote blockquote > :first-child { - margin-top: 0; -} - -#content blockquote blockquote > :last-child { - margin-bottom: 0; -} - -#content blockquote h2 { - font-size: 1em; - font-weight: 800; -} - -#content blockquote h3 { - font-size: 1em; - font-weight: normal; - font-style: oblique; -} - -main { - --responsive-padding-ratio: 0.10; -} - -main.long-content { - --long-content-padding-ratio: var(--responsive-padding-ratio); -} - -main.long-content .main-content-container, -main.long-content > h1 { - padding-left: calc(var(--long-content-padding-ratio) * 100%); - padding-right: calc(var(--long-content-padding-ratio) * 100%); -} - -dl dt { - padding-left: 40px; - max-width: 600px; -} - -dl dt { - /* Heads up, this affects the measurement - * for dl dt which are .content-heading! - */ - margin-bottom: 2px; -} - -dl dt[id]:not(.content-heading) { - --custom-scroll-offset: calc(2.5em - 2px); -} - -dl dd { - margin-bottom: 1em; -} - -dl ul, -dl ol { - margin-top: 0; - margin-bottom: 0; -} - -ul > li.has-details { - list-style-type: none; - margin-left: -17px; -} - -.album-group-list dt, -.group-series-list dt { - font-style: oblique; - padding-left: 0; -} - -.album-group-list dd, -.group-series-list dd { - margin-left: 0; -} - -.album-group-list blockquote { - max-width: 540px; - margin-bottom: 9px; - margin-top: 3px; -} - -.album-group-list blockquote p:first-child { - margin-top: 0; -} - -.album-group-list blockquote p:last-child { - margin-bottom: 0; -} - -.group-chronology-link, -.series-chronology-link { - font-style: oblique; -} - -.group-chronology-link a, -.series-chronology-link a { - font-style: normal; -} - -.group-view-switcher { - margin-left: 1ch; -} - -#content hr { - border: 1px inset #808080; - width: 100%; -} - -#content hr.split::before { - content: "(split)"; - color: #808080; -} - -#content hr.split { - position: relative; - overflow: hidden; - border: none; -} - -#content hr.split::after { - display: inline-block; - content: ""; - border: 1px inset #808080; - width: 100%; - position: absolute; - top: 50%; - margin-top: -2px; - margin-left: 10px; -} - -li > ul { - margin-top: 5px; -} - -.additional-files-list { - padding-left: 0; -} - -.additional-files-list > li { - list-style-type: none; -} - -.additional-files-list summary { - /* Sorry, Safari! - * https://bugs.webkit.org/show_bug.cgi?id=157323 - */ - list-style-position: outside; - margin-left: 40px; -} - -.additional-files-list details ul { - margin-left: 40px; - margin-top: 2px; - margin-bottom: 10px; -} - -.additional-files-list .entry-description { - list-style-type: none; - max-width: 540px; - - /* This should be margin-bottom, but cascading rules - * cause some awkwardness - `#content li` takes precedence. - */ - padding-bottom: 3px; -} - -.group-contributions-table { - display: inline-block; -} - -.group-contributions-table .group-contributions-row { - display: flex; - justify-content: space-between; -} - -.group-contributions-table .group-contributions-metrics { - margin-left: 1.5ch; - white-space: nowrap; -} - -.group-contributions-sorted-by-count:not(.visible), -.group-contributions-sorted-by-duration:not(.visible) { - display: none; -} - -.group-contributions-sort-button { - text-decoration: underline; - text-decoration-style: dotted; -} - -html[data-url-key="localized.albumCommentary"] li.no-commentary { - opacity: 0.7; -} - -html[data-url-key="localized.albumCommentary"] .content-heading-main-title { - margin-right: 0.25em; -} - -html[data-url-key="localized.albumCommentary"] .content-heading-accent { - font-weight: normal; - font-style: oblique; - font-size: 0.9rem; - display: inline-block; -} - -html[data-url-key="localized.albumCommentary"] p.track-info { - margin-left: 20px; -} - -html[data-url-key="localized.groupInfo"] .by a { - color: var(--page-primary-color); -} - -html[data-url-key="localized.listing"][data-url-value0="random"] #data-loading-line, -html[data-url-key="localized.listing"][data-url-value0="random"] #data-loaded-line, -html[data-url-key="localized.listing"][data-url-value0="random"] #data-error-line { - display: none; -} - -html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not([href]) { - opacity: 0.7; -} - -html[data-url-key="localized.newsEntry"] .read-another-links { - font-style: oblique; - font-size: 0.9em; -} - -/* Additional names (heading and box) */ - -h1 a[href="#additional-names-box"] { - color: inherit; - text-decoration: underline; - text-decoration-style: dotted; -} - -h1 a[href="#additional-names-box"]:hover { - text-decoration-style: solid; -} - -#additional-names-box { - --custom-scroll-offset: calc(0.5em - 2px); - - margin: 1em 0 1em -10px; - max-width: min(60vw, 600px); - - padding: 15px 20px 10px 20px; - - display: none; -} - -#additional-names-box > :first-child { margin-top: 0; } -#additional-names-box > :last-child { margin-bottom: 0; } - -#additional-names-box p { - padding-left: 10px; - padding-right: 10px; - margin-bottom: 0; - font-style: oblique; -} - -#additional-names-box ul { - padding-left: 10px; - margin-top: 0.5em; -} - -#additional-names-box li .additional-name { - margin-right: 0.25em; -} - -#additional-names-box li .additional-name .content-image { - margin-bottom: 0.25em; - margin-top: 0.5em; -} - -#additional-names-box li .accent { - opacity: 0.8; -} - -#additional-names-box li .additional-name > img { - 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); - - box-shadow: 0 -2px 6px -1px var(--dim-color) inset; -} - -.commentary-drop { - margin-top: 25px; - margin-bottom: 15px; - margin-left: 20px; - padding: 10px 20px; - max-width: min(60vw, 300px); -} - -/* Images */ - -.image-container { - display: block; - box-sizing: border-box; - position: relative; - height: 100%; - overflow: hidden; - - background-color: var(--dim-color); - border: 2px solid var(--primary-color); - border-radius: 0; - box-shadow: 0 2px 4px -2px var(--bg-black-color) inset; - - text-align: left; - color: white; -} - -/* Videos and audios (in content) get a lite version of image-container. */ -.content-video-container, -.content-audio-container { - width: min-content; - 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; -} - -.image-text-area { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 5px 15px; - - background: rgba(0, 0, 0, 0.65); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset; - - line-height: 1.35em; - color: var(--primary-color); - font-style: oblique; - text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75); -} - -.image-outer-area { - width: 100%; - height: 100%; - padding: 5px; - box-sizing: border-box; -} - -.image-link { - border-bottom: 1px solid #ffffff03; - border-radius: 2.5px 2.5px 3px 3px; - box-shadow: - 0 1px 8px -3px var(--bg-black-color); -} - -.image-inner-area { - position: relative; - width: 100%; - height: 100%; -} - -.image-link .image-inner-area { - /* Jankily fix a rendering issue with border-radius on Safari. - * The `-webkit-` prefix is only to keep this from applying on - * other browsers (well, Firefox), where it doesn't *break* - * anything, but also isn't necessary. - */ - -webkit-transform: translateZ(0); -} - -img { - object-fit: cover; -} - -.image-inner-area::after { - content: ""; - display: block; - position: absolute; - pointer-events: none; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-bottom-left-radius: 0.5px; - opacity: 0.035; - box-shadow: - 6px -6px 2px -4px white inset; -} - -img.pixelate, .pixelate img, -video.pixelate, .pixelate video { - image-rendering: crisp-edges; -} - -.reveal-text-container { - position: absolute; - top: 15px; - left: 10px; - right: 10px; - bottom: 10px; - display: flex; - flex-direction: column; - justify-content: center; -} - -.grid-item .reveal-text { - font-size: 0.9em; -} - -.reveal-text { - color: white; - text-align: center; - font-weight: bold; - padding-bottom: 0.5em; - font-size: 0.8rem; -} - -.reveal-symbol { - display: inline-block; - width: 1em; - height: 1em; - margin-bottom: 0.1em; - - font-size: 1.6em; - opacity: 0.8; -} - -.reveal-interaction { - opacity: 0.8; - text-decoration: underline; - text-decoration-style: dotted; -} - -.reveal .image { - opacity: 0.7; - filter: blur(20px) brightness(0.7); -} - -.reveal .image.reveal-thumbnail { - position: absolute; - top: 0; - left: 0; - image-rendering: pixelated; -} - -.reveal.has-reveal-thumbnail:not(.revealed) .image:not(.reveal-thumbnail) { - /* Keep the main image as part of the box model. - * It's what actually defines the dimensions of the - * image-container, so those dimensions never shift - * once the image is actually revealed. - */ - visibility: hidden; -} - -.reveal.revealed.has-reveal-thumbnail .image.reveal-thumbnail { - display: none !important; -} - -.reveal.revealed .image { - filter: none; - opacity: 1; -} - -.reveal.revealed .reveal-text-container { - display: none; -} - -.reveal:not(.revealed) .image-outer-area > * { - --reveal-border-radius: 6px; - position: relative; - overflow: hidden; - border-radius: var(--reveal-border-radius); -} - -.reveal:not(.revealed) .image-outer-area > *::after { - content: ""; - position: absolute; - box-sizing: border-box; - top: 0; - left: 0; - bottom: 0; - right: 0; - border: 1px dotted var(--primary-color); - border-radius: var(--reveal-border-radius); - pointer-events: none; - - /* By an awkward DOM intersection, this element might be - * .image-inner-area::after, which is already styled with - * a slight visual effect. Guarantee that the properties - * set to that end are overwritten, and fully co-opt it - * to serve as the interaction cue instead. - */ - box-shadow: none; - opacity: 1; -} - -.reveal:not(.revealed) .image-inner-area { - background: var(--deep-color); -} - -.reveal:not(.revealed) .image-outer-area > *:hover::after { - border-style: solid; - box-shadow: 0 0 0 1.5px #00000099 inset; -} - -.reveal:not(.revealed) .image-outer-area > *:hover .image { - filter: blur(20px) brightness(0.6); - opacity: 0.6; -} - -.reveal:not(.revealed) .image-outer-area > *:hover .reveal-interaction { - text-decoration-style: solid; -} - -.image-container.has-link:not(.no-image-preview) { - background: var(--deep-color); - box-shadow: none; - border-radius: 0 0 4px 4px; -} - -.sidebar .image-container { - max-width: 350px; -} - -/* Grid listings */ - -.grid-listing { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; - padding: 5px 15px; -} - -.grid-item { - font-size: 0.9em; -} - -.grid-item { - display: inline-block; - text-align: center; - background-color: #111111; - border: 1px dotted var(--primary-color); - border-radius: 2px; - padding: 5px; - margin: 10px; -} - -.grid-item .image-container { - width: 100%; -} - -.grid-item .image-inner-area { - border-radius: 0; - box-shadow: none; -} - -.grid-item .image-inner-area::after { - box-shadow: none; -} - -.grid-item .image { - width: 100%; - height: 100% !important; - margin-top: auto; - margin-bottom: auto; -} - -.grid-item:hover { - text-decoration: none; -} - -.grid-actions .grid-item:hover { - text-decoration: underline; -} - -.grid-item > span { - display: block; - overflow-wrap: break-word; - hyphens: auto; -} - -.grid-item > span:not(:first-child) { - margin-top: 2px; -} - -.grid-item > span:first-of-type { - margin-top: 6px; -} - -.grid-item > span:not(:first-of-type) { - font-size: 0.9em; - opacity: 0.8; -} - -.grid-item:hover > span:first-of-type { - text-decoration: underline; -} - -.grid-listing > .grid-item { - flex: 1 25%; - max-width: 200px; -} - -.grid-actions { - display: flex; - flex-direction: row; - margin: 15px; - align-self: center; - flex-wrap: wrap; - justify-content: center; -} - -.grid-actions > .grid-item { - flex-basis: unset !important; - margin: 5px; - width: 120px; - --primary-color: inherit !important; - --dim-color: inherit !important; -} - -/* Carousel */ - -.carousel-container { - --carousel-tile-min-width: 120px; - --carousel-row-count: 3; - --carousel-column-count: 6; - - position: relative; - overflow: hidden; - margin: 20px 0 5px 0; - padding: 8px 0; -} - -.carousel-container::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: -20; - background-color: var(--dim-color); - filter: brightness(0.6); -} - -.carousel-container::after { - content: ""; - pointer-events: none; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid var(--primary-color); - border-radius: 4px; - z-index: 40; - box-shadow: - inset 20px 2px 40px var(--shadow-color), - inset -20px -2px 40px var(--shadow-color); -} - -.carousel-container:hover .carousel-grid { - animation-play-state: running; -} - -html[data-url-key="localized.home"] .carousel-container { - --carousel-tile-size: 140px; -} - -.carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; } -.carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; } -.carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; } -.carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; } -.carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; } -.carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; } - -.carousel-grid:nth-child(2), -.carousel-grid:nth-child(3) { - position: absolute; - top: 8px; - left: 0; - right: 0; -} - -.carousel-grid:nth-child(2) { - animation-name: carousel-marquee2; -} - -.carousel-grid:nth-child(3) { - animation-name: carousel-marquee3; -} - -@keyframes carousel-marquee1 { - 0% { - transform: translateX(-100%) translateX(70px); - } - - 100% { - transform: translateX(-200%) translateX(70px); - } -} - -@keyframes carousel-marquee2 { - 0% { - transform: translateX(0%) translateX(70px); - } - - 100% { - transform: translateX(-100%) translateX(70px); - } -} - -@keyframes carousel-marquee3 { - 0% { - transform: translateX(100%) translateX(70px); - } - - 100% { - transform: translateX(0%) translateX(70px); - } -} - -.carousel-grid { - /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */ - --carousel-gap-count: calc(var(--carousel-column-count) - 1); - --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px); - --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count)); - - display: grid; - grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr)); - grid-template-rows: repeat(var(--carousel-row-count), auto); - grid-auto-flow: dense; - grid-auto-rows: 0; - overflow: hidden; - margin: auto; - - transform: translateX(0); - animation: carousel-marquee1 40s linear infinite; - animation-play-state: paused; - z-index: 5; -} - -.carousel-item { - display: inline-block; - margin: 0; - flex: 1 1 150px; - padding: 3px; - border-radius: 10px; - filter: brightness(0.8); -} - -.carousel-item .image-container { - border: none; - border-radius: 5px; -} - -.carousel-item .image-outer-area { - padding: 0; -} - -.carousel-item .image-inner-area::after { - box-shadow: none; -} - -.carousel-item .image { - width: 100%; - height: 100%; - margin-top: auto; - margin-bottom: auto; -} - -.carousel-item:hover { - filter: brightness(1); - background: var(--dim-color); -} - -/* Info card */ - -#info-card-container { - position: absolute; - - left: 0; - right: 10px; - - pointer-events: none; /* Padding area shouldn't 8e interactive. */ - display: none; -} - -#info-card-container.show, -#info-card-container.hide { - display: flex; -} - -#info-card-container > * { - flex-basis: 400px; -} - -#info-card-container.show { - animation: 0.2s linear forwards info-card-show; - transition: top 0.1s, left 0.1s; -} - -#info-card-container.hide { - animation: 0.2s linear forwards info-card-hide; -} - -@keyframes info-card-show { - 0% { - opacity: 0; - margin-top: -5px; - } - - 100% { - opacity: 1; - margin-top: 0; - } -} - -@keyframes info-card-hide { - 0% { - opacity: 1; - margin-top: 0; - } - - 100% { - opacity: 0; - margin-top: 5px; - display: none !important; - } -} - -.info-card-decor { - padding-left: 3ch; - border-top: 1px solid white; -} - -.info-card { - background-color: black; - color: white; - - border: 1px dotted var(--primary-color); - border-radius: 3px; - box-shadow: 0 5px 5px black; - - padding: 5px; - font-size: 0.9em; - - pointer-events: none; -} - -.info-card::after { - content: ""; - display: block; - clear: both; -} - -#info-card-container.show .info-card { - animation: 0.01s linear 0.2s forwards info-card-become-interactive; -} - -@keyframes info-card-become-interactive { - to { - pointer-events: auto; - } -} - -.info-card-art-container { - float: right; - - width: 40%; - margin: 5px; - font-size: 0.8em; - - /* Dynamically shown. */ - display: none; -} - -.info-card-art-container .image-container { - padding: 2px; -} - -.info-card-art { - display: block; - width: 100%; - height: 100%; -} - -.info-card-name { - font-size: 1em; - border-bottom: 1px dotted; - margin: 0; -} - -.info-card p { - margin-top: 0.25em; - margin-bottom: 0.25em; -} - -.info-card p:last-child { - margin-bottom: 0; -} - -/* Custom hash links */ - -.content-heading { - border-bottom: 3px double transparent; - margin-bottom: -3px; -} - -.content-heading.highlight-hash-link { - animation: highlight-hash-link 4s; - animation-delay: 125ms; -} - -dl dt.content-heading { - /* Basic margin-bottom for dt is 2px, - * so just subtract 3px from that. - */ - margin-bottom: -1px; -} - -h3.content-heading { - clear: both; -} - -/* This animation's name is referenced in JavaScript */ -@keyframes highlight-hash-link { - 0% { - border-bottom-color: transparent; - } - - 10% { - border-bottom-color: white; - } - - 25% { - border-bottom-color: white; - } - - 100% { - border-bottom-color: transparent; - } -} - -/* Sticky heading */ - -[id] { - --custom-scroll-offset: 0px; -} - -#content [id] { - /* Adjust scroll margin. */ - scroll-margin-top: calc( - 74px /* Sticky heading */ - + 33px /* Sticky subheading */ - - 1em /* One line of text (align bottom) */ - - 12px /* Padding for hanging letters & focus ring */ - + var(--custom-scroll-offset) /* Customizable offset */ - ); -} - -.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; -} - -.content-sticky-heading-anchor:not(:where(.content-sticky-heading-root[inert]) *) { - position: relative; -} - -.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 { - padding-left: 0; - padding-right: 0; -} - -main.long-content .content-sticky-heading-container .content-sticky-heading-row, -main.long-content .content-sticky-heading-container .content-sticky-subheading-row { - padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); - padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); -} - -.content-sticky-heading-row { - box-sizing: border-box; - padding: - calc(1.25 * var(--content-padding) + 5px) - 20px - calc(0.75 * var(--content-padding)) - 20px; - - width: 100%; - margin: 0; - - background: var(--bg-black-color); - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - - -webkit-backdrop-filter: blur(6px); - backdrop-filter: blur(6px); -} - -.content-sticky-heading-container.has-cover .content-sticky-heading-row, -.content-sticky-heading-container.has-cover .content-sticky-subheading-row { - display: grid; - grid-template-areas: - "title cover"; - 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 { - position: relative; - height: 0; - margin: -15px 0px -5px -5px; -} - -.content-sticky-heading-cover-needs-reveal { - display: none; -} - -.content-sticky-heading-cover { - position: absolute; - top: 0; - width: 80px; - right: 10px; - box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25); - transition: transform 0.35s, opacity 0.25s; -} - -.content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover { - opacity: 0; - transform: translateY(15px); - transition: transform 0.35s, opacity 0.30s; -} - -.content-sticky-heading-cover .cover-artwork { - border-width: 1px; - border-radius: 1.25px; - box-shadow: none; -} - -.content-sticky-heading-container .image-outer-area { - padding: 3px; -} - -.content-sticky-heading-container .image-inner-area { - border-radius: 1.75px; - overflow: hidden; -} - -.content-sticky-heading-cover .image { - display: block; - width: 100%; - height: 100%; -} - -.content-sticky-subheading-row { - position: absolute; - width: 100%; - box-sizing: border-box; - padding: 10px 20px 5px 20px; - margin-top: 0; - z-index: -1; - - background: var(--bg-black-color); - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - box-shadow: - 0 2px 2px -1px #00000060, - 0 4px 12px -4px #00000090; - - -webkit-backdrop-filter: blur(4px); - backdrop-filter: blur(4px); - - transition: margin-top 0.35s, opacity 0.25s; -} - -.content-sticky-subheading-row h2 { - margin: 0; - - font-size: 0.9em !important; - font-weight: normal; - font-style: oblique; - color: #eee; -} - -.content-sticky-subheading-row:not(.visible) { - margin-top: -20px; - opacity: 0; -} - -.content-sticky-subheading { - padding-right: 20px; -} - -.content-sticky-heading-container h2.visible { - margin-top: 0; - opacity: 1; -} - -.content-sticky-heading-row { - box-shadow: - inset 0 10px 10px -5px var(--shadow-color), - 0 4px 8px -4px #000000b0; -} - -#content, .sidebar { - contain: paint; -} - -/* Sticky sidebar */ - -.sidebar-column:not(.sticky-column) { - align-self: stretch; -} - -.sidebar-column.sticky-column { - position: sticky; - top: 10px; - align-self: flex-start; - max-height: calc(100vh - 20px); - display: flex; - flex-direction: column; -} - -.sidebar-multiple.sticky-column .sidebar:last-child { - flex-shrink: 1; - overflow-y: scroll; - scrollbar-width: thin; - scrollbar-color: var(--dim-color) var(--dark-color); -} - -.wiki-search-sidebar-box .wiki-search-results-container { - overflow-y: scroll; - scrollbar-width: thin; - scrollbar-color: var(--dim-color) var(--dark-color); -} - -.sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar, -.wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar { - background: var(--dark-color); - width: 12px; -} - -.sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar-thumb, -.wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar-thumb { - transition: background 0.2s; - background: rgba(255, 255, 255, 0.2); - border: 3px solid transparent; - border-radius: 10px; - background-clip: content-box; -} - -.sidebar-column.sidebar.sticky-column > h1 { - position: sticky; - top: 0; - z-index: 2; - - margin: 0 calc(-1 * var(--content-padding)); - margin-bottom: 10px; - - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - padding: 10px 5px; - - background: var(--bg-black-color); - -webkit-backdrop-filter: blur(4px); - backdrop-filter: blur(4px); - - box-shadow: - 0 2px 3px -1px #0006, - 0 4px 8px -2px #0009; -} - -/* Image overlay */ - -#image-overlay-container { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 4000; - - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 20px 40px; - box-sizing: border-box; - - opacity: 0; - pointer-events: none; - transition: opacity 0.4s; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -#image-overlay-container::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - - background: var(--deep-color); - opacity: 0.20; -} - -#image-overlay-container.visible { - opacity: 1; - pointer-events: auto; -} - -#image-overlay-content-container { - border-radius: 0 0 8px 8px; - border: 2px solid var(--primary-color); - background: var(--deep-ghost-color); - overflow: hidden; - - box-shadow: - 0 0 90px 30px #00000060, - 0 0 20px 10px #00000040, - 0 0 10px 3px #00000080; - - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); -} - -#image-overlay-image-area { - display: block; - overflow: hidden; - width: 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: block; - width: 100%; - height: auto; -} - -#image-overlay-image { - position: absolute; -} - -#image-overlay-container.no-thumb #image-overlay-image { - position: static; -} - -#image-overlay-image-thumb { - filter: blur(16px); - transform: scale(1.5); -} - -#image-overlay-container.loaded #image-overlay-image-thumb { - opacity: 0; - pointer-events: none; - transition: opacity 0.25s; -} - -#image-overlay-image-area::after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - height: 4px; - width: var(--download-progress); - background: var(--primary-color); - box-shadow: 0 -3px 12px 4px var(--primary-color); - transition: 0.25s; -} - -#image-overlay-container.loaded #image-overlay-image-area::after { - width: 100%; - background: white; - opacity: 0; -} - -#image-overlay-container.errored #image-overlay-image-area::after { - width: 100%; - background: red; -} - -#image-overlay-container:not(.visible) #image-overlay-image-area::after { - width: 0 !important; -} - -#image-overlay-action-container { - padding: 7px 4px 7px 4px; - border-radius: 0 0 5px 5px; - background: var(--bg-black-color); - color: white; - font-style: oblique; - text-align: center; - box-shadow: - 0 3px 8px -5px var(--primary-color) inset; -} - -#image-overlay-container #image-overlay-action-content-without-size:not(.visible), -#image-overlay-container #image-overlay-action-content-with-size:not(.visible), -#image-overlay-container #image-overlay-file-size-warning:not(.visible), -#image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), -#image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { - display: none; -} - -#image-overlay-file-size-warning { - opacity: 0.8; - font-size: 0.9em; -} - -/* Layout - Wide (most computers) */ - -@media (min-width: 900px) { - #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; - } -} - -/* Layout - Medium (tablets, some landscape mobiles) - * - * Note: Rules defined here are exclusive to "medium" width, i.e. they don't - * additionally apply to "thin". Use the later section which applies to both - * if so desired. - */ - -@media (min-width: 600px) and (max-width: 899.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. - */ - #page-container.showing-sidebar-left main, - #page-container.showing-sidebar-right main { - --responsive-padding-ratio: 0.06; - } -} - -/* Layout - Wide or Medium */ - -@media (min-width: 600px) { - .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 - * - * See also: https://stackoverflow.com/questions/50224855/ - */ - transform: translate3d(0, 0, 0); - z-index: 1; - } - - /* Cover art floats to the right. It's positioned in HTML beneath the - * heading, so pull it up a little to "float" on top. - */ - #artwork-column { - float: right; - width: 40%; - max-width: 400px; - margin: -60px 0 10px 20px; - - position: relative; - z-index: 2; - } - - /* ...Except on top-indexes, where cover art is displayed prominently - * between the heading and subheading. - */ - #content.top-index #artwork-column { - float: none; - margin: 2em auto 2.5em auto; - max-width: 375px; - } - - html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:not(:nth-child(n+10)) { - flex-basis: 23%; - margin: 15px; - } - - html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:nth-child(n+10) { - flex-basis: 18%; - margin: 10px; - } -} - -/* Layout - Medium or Thin */ - -@media (max-width: 899.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; - } - - .layout-columns > *:not(:last-child) { - margin-bottom: 10px; - } - - .sidebar-column { - position: static !important; - max-width: unset !important; - flex-basis: unset !important; - margin-right: 0 !important; - margin-left: 0 !important; - width: 100%; - } - - .sidebar .news-entry:not(.first-news-entry) { - 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) { - .content-columns { - columns: 1; - } - - main { - --responsive-padding-ratio: 0.02; - } - - #artwork-column { - margin: 25px 0 5px 0; - width: 100%; - max-width: unset; - } - - #additional-names-box { - width: unset; - max-width: unset; - } - - .nav-has-content .nav-main-links .nav-link-accent { - display: block; - } - - /* Show sticky heading above cover art */ - - .content-sticky-heading-root { - z-index: 2; - } - - .content-sticky-heading-row h1 { - padding-right: 10px; - } - - /* Let sticky heading text span past lower-index cover art */ - - .content-sticky-heading-container.has-cover .content-sticky-heading-row, - .content-sticky-heading-container.has-cover .content-sticky-subheading-row { - grid-template-columns: 1fr 90px; - } - - /* Disable grid features, just line header children up vertically */ - - #header { - display: block; - } - - #header > div:not(:first-child) { - margin-top: 0.5em; - } -} +@layer interactivity; +@layer layout; +@layer material, print; +@layer construction; +@layer interaction; + +@import url(miscellany.css); +@import url(page.css); +@import url(search.css); +@import url(tooltips.css); +@import url(features.css); +@import url(responsive.css); + +@import url(specific-pages.css); diff --git a/src/static/css/specific-pages.css b/src/static/css/specific-pages.css new file mode 100644 index 00000000..ef3a0c54 --- /dev/null +++ b/src/static/css/specific-pages.css @@ -0,0 +1,288 @@ +/* Obviously this file should be split up. + * Just porting from old CSS for now. + */ + +/* Album commentary page */ + +@layer layout { + html[data-url-key="localized.albumCommentary"] .content-heading-main-title { + margin-right: 0.25em; + } + + html[data-url-key="localized.albumCommentary"] .content-heading-accent { + display: inline-block; + } + + html[data-url-key="localized.albumCommentary"] p.track-info { + margin-left: 20px; + } +} + +@layer print { + html[data-url-key="localized.albumCommentary"] li.no-commentary { + opacity: 0.7; + } + + html[data-url-key="localized.albumCommentary"] .content-heading-accent { + font-weight: normal; + font-style: oblique; + font-size: 0.9rem; + } +} + +/* Art tag gallery page */ + +@layer layout { + 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; + } +} + +@layer interaction { + 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; + } +} + +/* "Art Tag Network" listing */ + +@layer layout { + 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-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"] dl dt { + padding-left: 10px; + margin-left: 20px; + margin-bottom: 0; + padding-bottom: 2px; + max-width: unset; + position: relative; + } +} + +@layer material { + 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: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; + } +} + +@layer print { + html[data-url-key="localized.listing"][data-url-value0="tags/network"] .network-tag-stat { + text-align: right; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dt:has(+ dd) .network-tag-stat { + text-align: center; + } +} + +@layer interaction { + html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-stat-line a { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* Artist rolling window page */ + +@layer layout { + html[data-url-key="localized.artistRollingWindow"] #content p { + text-align: center; + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a { + display: inline-block; + padding: 5px; + } + + + html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area { + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 260px); + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area .grid-listing { + width: 100%; + } +} + +@layer material { + html[data-url-key="localized.artistRollingWindow"] #content input[type=number] { + width: 3em; + margin: 0 0.25em; + background: black; + color: white; + border: 1px dotted var(--primary-color); + padding: 4px; + border-radius: 3px; + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area { + border: 1px dashed #ffffff42; + border-top-style: solid; + border-bottom-style: solid; + } +} + +@layer material { + html[data-url-key="localized.artistRollingWindow"] .grid-item.peeking { + opacity: 0.8; + background: #ffffff24; + } +} + +@layer construction { + html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + } +} + +@layer interaction { + html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a { + text-decoration: underline; + text-decoration-style: dotted; + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a:not([href]) { + text-decoration: none; + opacity: 0.7; + } +} + +@layer construction { + html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) > *:not([style*="display: none"]) ~ *::before { + content: '\00b7'; + margin-left: 0.5ch; + margin-right: 0.5ch; + } +} + +/* Group info page */ + +@layer print { + html[data-url-key="localized.groupInfo"] .by a { + color: var(--page-primary-color); + } +} + +/* Homepage */ + +@layer print { + 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; + } +} + +/* "Random Pages" listing */ + +@layer interaction { + html[data-url-key="localized.listing"][data-url-value0="random"] #data-loading-line, + html[data-url-key="localized.listing"][data-url-value0="random"] #data-loaded-line, + html[data-url-key="localized.listing"][data-url-value0="random"] #data-error-line { + display: none; + } + + html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not([href]) { + opacity: 0.7; + } +} + +/* News entries */ + +@layer print { + html[data-url-key="localized.newsEntry"] .read-another-links { + font-style: oblique; + font-size: 0.9em; + } +} diff --git a/src/static/css/tooltips.css b/src/static/css/tooltips.css new file mode 100644 index 00000000..f4f9416e --- /dev/null +++ b/src/static/css/tooltips.css @@ -0,0 +1,419 @@ +@layer layout { + .text-with-tooltip { + position: relative; + } + + .tooltip { + position: absolute; + z-index: 3; + left: -10px; + top: calc(1em + 1px); + + display: none; + } + + .tooltip-content { + display: block; + } + + :where(.isolate-tooltip-z-indexing) { + position: relative; + z-index: 1; + } + + :where(.isolate-tooltip-z-indexing > *) { + position: relative; + z-index: -1; + } +} + +@layer material { + .tooltip { + font-size: 1rem; + } + + .cover-artwork .tooltip, + #sidebar .tooltip { + font-size: 0.9rem; + } + + .offset-tooltips .tooltip { + left: 14px; + } + + .tooltip-content { + background: var(--bg-black-color); + border: 1px dotted var(--primary-color); + border-radius: 6px; + + -webkit-backdrop-filter: + brightness(1.5) saturate(1.4) blur(4px); + + backdrop-filter: + brightness(1.5) saturate(1.4) blur(4px); + + box-shadow: + 0 3px 4px 4px #000000aa, + 0 -2px 4px -2px var(--primary-color) inset; + + text-indent: 0; + } +} + +@layer print { + .tooltip-content { + font-size: 1rem; + } + + .cover-artwork .content-tooltip .tooltip-content { + font-size: 0.85rem; + } +} + +@layer interaction { + .tooltip { + display: none; + } + + .text-with-tooltip .text-with-tooltip-interaction-cue { + text-decoration-line: underline; + text-decoration-style: dotted; + } + + .text-with-tooltip > .hoverable:hover .text-with-tooltip-interaction-cue, + .text-with-tooltip > .hoverable.has-visible-tooltip .text-with-tooltip-interaction-cue { + text-decoration-style: wavy !important; + } +} + +/* "General" tooltips */ + +@layer layout { + .datetimestamp-tooltip, + .missing-duration-tooltip, + .commentary-date-tooltip, + .rerelease-tooltip, + .first-release-tooltip, + .other-release-tooltip, + .content-tooltip { + padding: 3px 4px 2px 2px; + left: -10px; + } + + .datetimestamp-tooltip .tooltip-content, + .missing-duration-tooltip .tooltip-content, + .commentary-date-tooltip .tooltip-content { + padding: 5px 6px; + white-space: nowrap; + font-size: 0.9em; + } +} + +@layer interaction { + .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.rerelease .text-with-tooltip-interaction-cue, + .text-with-tooltip.first-release .text-with-tooltip-interaction-cue { + cursor: default; + } +} + +/* Contribution tooltip */ + +@layer interactivity { + .contribution-tooltip .tooltip-content { + -webkit-user-select: none; + user-select: none; + } +} + +@layer layout { + .contribution-tooltip { + padding: 3px 6px 6px 6px; + left: -34px; + } + + .contribution-tooltip .tooltip-content { + display: grid; + grid-template-columns: + [icon-start] 26px [icon-end handle-start] auto [handle-end platform-start] auto [platform-end]; + + padding: 6px 2px 2px 2px; + } + + .contribution-tooltip .external-link { + display: grid; + grid-template-columns: subgrid; + grid-column-start: icon-start; + grid-column-end: handle-end; + + height: 1.4em; + } + + .contribution-tooltip .chronology-heading { + grid-column-start: handle-start; + grid-column-end: platform-end; + + min-width: 30ch; + margin-bottom: 2px; + } + + .contribution-tooltip .chronology-link { + display: grid; + grid-column-start: icon-start; + grid-column-end: handle-end; + grid-template-columns: subgrid; + + height: 1.2em; + } + + .contribution-tooltip .external-icon, + .contribution-tooltip .chronology-symbol { + grid-column-start: icon-start; + grid-column-end: icon-end; + } + + .contribution-tooltip .external-icon svg { + width: 18px !important; + height: 18px !important; + top: -0.1em; + } + + .contribution-tooltip .chronology-symbol { + text-align: center; + } + + .contribution-tooltip .external-handle, + .contribution-tooltip .chronology-text { + grid-column-start: handle-start; + grid-column-end: handle-end; + + width: max-content; + max-width: 200px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .contribution-tooltip .external-handle { + padding-right: 8px; + } + + .contribution-tooltip .chronology-text { + padding-right: 6px; + } + + .contribution-tooltip .external-platform, + .contribution-tooltip .chronology-info { + grid-column-start: platform-start; + grid-column-end: platform-end; + white-space: nowrap; + padding-right: 4px; + } +} + +@layer print { + .contribution-tooltip .tooltip-divider, + .tooltip-content hr.cute, + .tooltip-content span.cute-break { + display: block; + 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; + } + + .contribution-tooltip .chronology-heading { + font-size: 0.85em; + font-style: oblique; + } + + .contribution-tooltip .chronology-text, + .contribution-tooltip .chronology-info { + font-size: 0.85em; + } +} + +@layer interaction { + .contribution-tooltip .tooltip-content { + cursor: default; + } + + .contribution-tooltip.show-info .external-platform, + .contribution-tooltip.show-info .chronology-info { + display: inline; + animation: external-platform 0.2s forwards linear; + } + + @keyframes external-platform { + from { + opacity: 0; + } + + to { + opacity: var(--external-platform-opacity); + } + } + + .contribution-tooltip .external-platform, + .contribution-tooltip .chronology-info { + display: none; + + --external-platform-opacity: 0.8; + opacity: 0.8; + } + + .contribution-tooltip .external-link:hover, + .contribution-tooltip .chronology-link:hover { + filter: brightness(1.4); + text-decoration: none; + } + + .contribution-tooltip .external-link:hover .external-handle, + .contribution-tooltip .chronology-link:hover .chronology-text { + text-decoration: underline; + } + + .contribution-tooltip .external-link:hover + .external-platform, + .contribution-tooltip .chronology-link:hover + .chronology-info { + --external-platform-opacity: 1; + text-decoration: underline; + text-decoration-color: #ffffffaa; + } +} + +/* Missing duration tooltip */ + +@layer interaction { + .text-with-tooltip.missing-duration > .hoverable { + opacity: 0.5; + } + + .text-with-tooltip.missing-duration > .hoverable:hover, + .text-with-tooltip.missing-duration > .hoverable.has-visible-tooltip { + opacity: 1; + } + + .text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue { + text-decoration: none !important; + } +} + +/* Rerelease, first release, other release tooltips */ + +@layer layout { + .rerelease-tooltip .tooltip-content, + .first-release-tooltip .tooltip-content, + .other-release-tooltip .tooltip-content { + padding: 3px 4.5px; + } + + .rerelease-tooltip .tooltip-content, + .first-release-tooltip .tooltip-content { + width: 260px; + } + + .other-release-tooltip .tooltip-content { + width: max-content; + max-width: 250px; + } + + .other-release-tooltip .tooltip-content .when { + /* technically just putting this in a <span> was enough + * to keep it from wrapping all tight-like, for some + * reason, but im not taking any chances... + */ + white-space: nowrap; + } +} + +@layer print { + .rerelease-tooltip .tooltip-content, + .first-release-tooltip .tooltip-content, + .other-release-tooltip .tooltip-content { + font-size: 0.9rem; + } + + .rerelease-tooltip .not-credited-on-first-release { + opacity: 0.9; + } +} + +/* Thing name & wiki edits tooltips */ + +@layer layout { + .thing-name-tooltip, + .wiki-edits-tooltip { + padding: 3px 4px 2px 2px; + left: -6px; + } + + .thing-name-tooltip .tooltip-content, + .wiki-edits-tooltip .tooltip-content { + width: max-content; + padding: 3px 4.5px; + } + + .thing-name-tooltip .tooltip-content, + max-width: 120px; + } + + .wiki-edits-tooltip .tooltip-content { + max-width: 200px; + } +} + +@layer print { + .text-with-tooltip.wiki-edits > .hoverable { + white-space: nowrap; + } + + .thing-name-tooltip .tooltip-content, + .wiki-edits-tooltip .tooltip-content { + font-size: 0.85rem; + } +} + +/* Content tooltips */ + +@layer layout { + .content-tooltip-guy { + display: inline-block; + } + + .content-tooltip-guy:not(.has-link) .hoverable { + cursor: default; + } + + .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: max-content; + max-width: 240px; + } + + .cover-artwork .content-tooltip { + padding: 2px 3px; + width: max-content; + max-width: 220px; + } +} + +@layer interaction { + .content-tooltip-guy .hoverable a { + text-decoration-color: transparent; + text-decoration-style: dotted; + } +} diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js index 71112313..0c113758 100644 --- a/src/static/js/client-util.js +++ b/src/static/js/client-util.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export function rebase(href, rebaseKey = 'rebaseLocalized') { let result = document.documentElement.dataset[rebaseKey] || './'; @@ -37,7 +35,7 @@ export function cssProp(el, ...args) { } } -export function templateContent(el) { +export function templateContent(el, slots = {}) { if (el === null) { return null; } @@ -46,7 +44,33 @@ export function templateContent(el) { throw new Error(`Expected a <template> element`); } - return el.content.cloneNode(true); + const content = el.content.cloneNode(true); + + for (const [key, value] of Object.entries(slots)) { + const slot = content.querySelector(`slot[name="${key}"]`); + + if (!slot) { + console.warn(`Slot ${key} missing in template:`, el); + continue; + } + + if (value === null || value === undefined) { + console.warn(`Valueless slot ${key} in template:`, el); + continue; + } + + slot.replaceWith(value); + } + + return content; +} + +export function decodeEntities(string) { + if (!string) return string; + + const textarea = document.createElement('textarea'); + textarea.innerHTML = string; + return textarea.value; } // Curry-style, so multiple points can more conveniently be tested at once. @@ -81,12 +105,12 @@ export function getVisuallyContainingElement(child) { const getLinkHref = (type, directory) => rebase(`${type}/${directory}`); */ -export const openAlbum = d => rebase(`album/${d}`); -export const openArtTag = d => rebase(`tag/${d}`); -export const openArtist = d => rebase(`artist/${d}`); -export const openFlash = d => rebase(`flash/${d}`); -export const openGroup = d => rebase(`group/${d}`); -export const openTrack = d => rebase(`track/${d}`); +export const openAlbum = d => rebase(`album/${d}/`); +export const openArtTag = d => rebase(`tag/${d}/`); +export const openArtist = d => rebase(`artist/${d}/`); +export const openFlash = d => rebase(`flash/${d}/`); +export const openGroup = d => rebase(`group/${d}/`); +export const openTrack = d => rebase(`track/${d}/`); // TODO: This should also use urlSpec. @@ -127,3 +151,10 @@ export function dispatchInternalEvent(event, eventName, ...args) { return results; } + +const languageCode = document.documentElement.getAttribute('lang'); + +export function formatDate(inputDate) { + const date = new Date(inputDate); + return date.toLocaleDateString(languageCode); +} diff --git a/src/static/js/client/additional-names-box.js b/src/static/js/client/additional-names-box.js index 195ba25d..a6d9b098 100644 --- a/src/static/js/client/additional-names-box.js +++ b/src/static/js/client/additional-names-box.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {cssProp} from '../client-util.js'; import {info as hashLinkInfo} from './hash-link.js'; diff --git a/src/static/js/client/album-commentary-sidebar.js b/src/static/js/client/album-commentary-sidebar.js index c5eaf81b..d7c4a591 100644 --- a/src/static/js/client/album-commentary-sidebar.js +++ b/src/static/js/client/album-commentary-sidebar.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {empty} from '../../shared-util/sugar.js'; import {info as hashLinkInfo} from './hash-link.js'; diff --git a/src/static/js/client/art-tag-gallery-filter.js b/src/static/js/client/art-tag-gallery-filter.js index fd40d1a2..b7fff70d 100644 --- a/src/static/js/client/art-tag-gallery-filter.js +++ b/src/static/js/client/art-tag-gallery-filter.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: 'artTagGalleryFilterInfo', @@ -142,8 +140,8 @@ export function addPageListeners() { currentFeaturedLine.style.display = 'none'; currentShowingLine.style.display = 'none'; - nextFeaturedLine.style.display = 'block'; - nextShowingLine.style.display = 'block'; + nextFeaturedLine.style.display = 'inline'; + nextShowingLine.style.display = 'inline'; filterArtTagGallery(nextShowing); }); diff --git a/src/static/js/client/art-tag-network.js b/src/static/js/client/art-tag-network.js index 44e10c11..d0576152 100644 --- a/src/static/js/client/art-tag-network.js +++ b/src/static/js/client/art-tag-network.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {cssProp} from '../client-util.js'; import {atOffset, stitchArrays} from '../../shared-util/sugar.js'; diff --git a/src/static/js/client/artist-external-link-tooltip.js b/src/static/js/client/artist-external-link-tooltip.js index 21ddfb91..2eadf916 100644 --- a/src/static/js/client/artist-external-link-tooltip.js +++ b/src/static/js/client/artist-external-link-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {accumulateSum, empty} from '../../shared-util/sugar.js'; import {info as hoverableTooltipInfo, repositionCurrentTooltip} diff --git a/src/static/js/client/artist-rolling-window.js b/src/static/js/client/artist-rolling-window.js new file mode 100644 index 00000000..b8ff7354 --- /dev/null +++ b/src/static/js/client/artist-rolling-window.js @@ -0,0 +1,571 @@ +import {cssProp, formatDate} from '../client-util.js'; + +import {sortByDate} from '../../shared-util/sort.js'; +import {chunkByConditions, chunkByProperties, empty, stitchArrays} + from '../../shared-util/sugar.js'; + +export const info = { + id: 'artistRollingWindowInfo', + + timeframeMonthsBefore: null, + timeframeMonthsAfter: null, + timeframeMonthsPeek: null, + + contributionKind: null, + contributionGroup: null, + + timeframeSelectionSomeLine: null, + timeframeSelectionNoneLine: null, + + timeframeSelectionContributionCount: null, + timeframeSelectionTimeframeCount: null, + timeframeSelectionFirstDate: null, + timeframeSelectionLastDate: null, + + timeframeSelectionControl: null, + timeframeSelectionMenu: null, + timeframeSelectionPrevious: null, + timeframeSelectionNext: null, + + timeframeEmptyLine: null, + + sourceArea: null, + sourceGrid: null, + sources: null, +}; + +export function getPageReferences() { + if (document.documentElement.dataset.urlKey !== 'localized.artistRollingWindow') { + return; + } + + info.timeframeMonthsBefore = + document.getElementById('timeframe-months-before'); + + info.timeframeMonthsAfter = + document.getElementById('timeframe-months-after'); + + info.timeframeMonthsPeek = + document.getElementById('timeframe-months-peek'); + + info.contributionKind = + document.getElementById('contribution-kind'); + + info.contributionGroup = + document.getElementById('contribution-group'); + + info.timeframeSelectionSomeLine = + document.getElementById('timeframe-selection-some'); + + info.timeframeSelectionNoneLine = + document.getElementById('timeframe-selection-none'); + + info.timeframeSelectionContributionCount = + document.getElementById('timeframe-selection-contribution-count'); + + info.timeframeSelectionTimeframeCount = + document.getElementById('timeframe-selection-timeframe-count'); + + info.timeframeSelectionFirstDate = + document.getElementById('timeframe-selection-first-date'); + + info.timeframeSelectionLastDate = + document.getElementById('timeframe-selection-last-date'); + + info.timeframeSelectionControl = + document.getElementById('timeframe-selection-control'); + + info.timeframeSelectionMenu = + document.getElementById('timeframe-selection-menu'); + + info.timeframeSelectionPrevious = + document.getElementById('timeframe-selection-previous'); + + info.timeframeSelectionNext = + document.getElementById('timeframe-selection-next'); + + info.timeframeEmptyLine = + document.getElementById('timeframe-empty'); + + info.sourceArea = + document.getElementById('timeframe-source-area'); + + info.sourceGrid = + info.sourceArea.querySelector('.grid-listing'); + + info.sources = + info.sourceGrid.getElementsByClassName('grid-item'); +} + +export function addPageListeners() { + if (!info.sourceArea) { + return; + } + + for (const input of [ + info.timeframeMonthsBefore, + info.timeframeMonthsAfter, + info.timeframeMonthsPeek, + info.contributionKind, + info.contributionGroup, + ]) { + input.addEventListener('change', () => { + updateArtistRollingWindow() + }); + } + + info.timeframeSelectionMenu.addEventListener('change', () => { + updateRollingWindowTimeframeSelection(); + }); + + const eatClicks = (element, callback) => { + element.addEventListener('click', domEvent => { + domEvent.preventDefault(); + callback(); + }); + + element.addEventListener('mousedown', domEvent => { + if (domEvent.detail > 1) { + domEvent.preventDefault(); + } + }); + }; + + eatClicks(info.timeframeSelectionNext, nextRollingTimeframeSelection); + eatClicks(info.timeframeSelectionPrevious, previousRollingTimeframeSelection); +} + +export function mutatePageContent() { + if (!info.sourceArea) { + return; + } + + updateArtistRollingWindow(); +} + +function previousRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex > 0) { + menu.selectedIndex--; + } + + updateRollingWindowTimeframeSelection(); +} + +function nextRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex < menu.length - 1) { + menu.selectedIndex++; + } + + updateRollingWindowTimeframeSelection(); +} + +function getArtistRollingWindowSourceInfo() { + const sourceElements = + Array.from(info.sources); + + const sourceTimeElements = + sourceElements + .map(el => Array.from(el.getElementsByTagName('time'))); + + const sourceTimeClasses = + sourceTimeElements + .map(times => times + .map(time => Array.from(time.classList))); + + const sourceKinds = + sourceTimeClasses + .map(times => times + .map(classes => classes + .find(cl => cl.endsWith('-contribution-date')) + .slice(0, -'-contribution-date'.length))); + + const sourceGroups = + sourceElements + .map(el => + Array.from(el.querySelectorAll('.contribution-group')) + .map(data => data.value)); + + const sourceDates = + sourceTimeElements + .map(times => times + .map(time => new Date(time.getAttribute('datetime')))); + + return stitchArrays({ + element: sourceElements, + kinds: sourceKinds, + groups: sourceGroups, + dates: sourceDates, + }); +} + +function getArtistRollingWindowTimeframeInfo() { + const contributionKind = + info.contributionKind.value; + + const contributionGroup = + info.contributionGroup.value; + + const sourceInfo = + getArtistRollingWindowSourceInfo(); + + const principalSources = + sourceInfo.filter(source => { + if (!source.kinds.includes(contributionKind)) { + return false; + } + + if (contributionGroup !== '-') { + if (!source.groups.includes(contributionGroup)) { + return false; + } + } + + return true; + }); + + const principalSourceDates = + principalSources.map(source => + stitchArrays({ + kind: source.kinds, + date: source.dates, + }).find(({kind}) => kind === contributionKind) + .date); + + const getPeekDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value) + - parseInt(info.timeframeMonthsPeek.value))); + + return date; + }; + + const getEntranceDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value))); + + return date; + }; + + const getExitDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + + parseInt(info.timeframeMonthsAfter.value))); + + return date; + }; + + const principalSourceIndices = + Array.from({length: principalSources.length}, (_, i) => i); + + const timeframeSourceChunks = + chunkByConditions(principalSourceIndices, [ + (previous, next) => + +principalSourceDates[previous] !== + +principalSourceDates[next], + ]); + + const timeframeSourceChunkDates = + timeframeSourceChunks + .map(indices => indices[0]) + .map(index => principalSourceDates[index]); + + const timeframeSourceChunkPeekDates = + timeframeSourceChunkDates + .map(getPeekDate); + + const timeframeSourceChunkEntranceDates = + timeframeSourceChunkDates + .map(getEntranceDate); + + const timeframeSourceChunkExitDates = + timeframeSourceChunkDates + .map(getExitDate); + + const peekDateInfo = + stitchArrays({ + peek: timeframeSourceChunkPeekDates, + indices: timeframeSourceChunks, + }).map(({peek, indices}) => ({ + date: peek, + peek: indices, + })); + + const entranceDateInfo = + stitchArrays({ + entrance: timeframeSourceChunkEntranceDates, + indices: timeframeSourceChunks, + }).map(({entrance, indices}) => ({ + date: entrance, + entrance: indices, + })); + + const exitDateInfo = + stitchArrays({ + exit: timeframeSourceChunkExitDates, + indices: timeframeSourceChunks, + }).map(({exit, indices}) => ({ + date: exit, + exit: indices, + })); + + const dateInfoChunks = + chunkByProperties( + sortByDate([ + ...peekDateInfo, + ...entranceDateInfo, + ...exitDateInfo, + ]), + ['date']); + + const dateInfo = + dateInfoChunks + .map(({chunk}) => + Object.assign({ + peek: null, + entrance: null, + exit: null, + }, ...chunk)); + + const timeframeInfo = + dateInfo.reduce( + (accumulator, {date, peek, entrance, exit}) => { + const previous = accumulator.at(-1); + + // These mustn't be mutated! + let peeking = (previous ? previous.peeking : []); + let tracking = (previous ? previous.tracking : []); + + if (peek) { + peeking = + peeking.concat(peek); + } + + if (entrance) { + peeking = + peeking.filter(index => !entrance.includes(index)); + + tracking = + tracking.concat(entrance); + } + + if (exit) { + tracking = + tracking.filter(index => !exit.includes(index)); + } + + return [...accumulator, { + date, + peeking, + tracking, + peek, + entrance, + exit, + }]; + }, + []); + + const indicesToSources = indices => + (indices + ? indices.map(index => principalSources[index]) + : null); + + const finalizedTimeframeInfo = + timeframeInfo.map(({ + date, + peeking, + tracking, + peek, + entrance, + exit, + }) => ({ + date, + peeking: indicesToSources(peeking), + tracking: indicesToSources(tracking), + peek: indicesToSources(peek), + entrance: indicesToSources(entrance), + exit: indicesToSources(exit), + })); + + return finalizedTimeframeInfo; +} + +function updateArtistRollingWindow() { + const timeframeInfo = + getArtistRollingWindowTimeframeInfo(); + + if (empty(timeframeInfo)) { + cssProp(info.timeframeSelectionControl, 'display', 'none'); + cssProp(info.timeframeSelectionSomeLine, 'display', 'none'); + cssProp(info.timeframeSelectionNoneLine, 'display', null); + + updateRollingWindowTimeframeSelection(timeframeInfo); + + return; + } + + cssProp(info.timeframeSelectionControl, 'display', null); + cssProp(info.timeframeSelectionSomeLine, 'display', null); + cssProp(info.timeframeSelectionNoneLine, 'display', 'none'); + + // The last timeframe is just the exit of the final tracked sources, + // so we aren't going to display a menu option for it, and will just use + // it as the end of the final option's date range. + + const usedTimeframes = timeframeInfo.slice(0, -1); + const firstTimeframe = timeframeInfo.at(0); + const lastTimeframe = timeframeInfo.at(-1); + + const sourceCount = + timeframeInfo + .flatMap(({entrance}) => entrance ?? []) + .length; + + const timeframeCount = + usedTimeframes.length; + + info.timeframeSelectionContributionCount.innerText = sourceCount; + info.timeframeSelectionTimeframeCount.innerText = timeframeCount; + + const firstDate = firstTimeframe.date; + const lastDate = lastTimeframe.date; + + info.timeframeSelectionFirstDate.innerText = formatDate(firstDate); + info.timeframeSelectionLastDate.innerText = formatDate(lastDate); + + while (info.timeframeSelectionMenu.firstChild) { + info.timeframeSelectionMenu.firstChild.remove(); + } + + for (const [index, timeframe] of usedTimeframes.entries()) { + const nextTimeframe = timeframeInfo[index + 1]; + + const option = document.createElement('option'); + + option.appendChild(document.createTextNode( + `${formatDate(timeframe.date)} – ${formatDate(nextTimeframe.date)}`)); + + info.timeframeSelectionMenu.appendChild(option); + } + + updateRollingWindowTimeframeSelection(timeframeInfo); +} + +function updateRollingWindowTimeframeSelection(timeframeInfo) { + timeframeInfo ??= getArtistRollingWindowTimeframeInfo(); + + updateRollingWindowTimeframeSelectionControls(timeframeInfo); + updateRollingWindowTimeframeSelectionSources(timeframeInfo); +} + +function updateRollingWindowTimeframeSelectionControls(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const atFirstTimeframe = + currentIndex === 0; + + // The last actual timeframe is empty and not displayed as a menu option. + const atLastTimeframe = + currentIndex === timeframeInfo.length - 2; + + if (atFirstTimeframe) { + info.timeframeSelectionPrevious.removeAttribute('href'); + } else { + info.timeframeSelectionPrevious.setAttribute('href', '#'); + } + + if (atLastTimeframe) { + info.timeframeSelectionNext.removeAttribute('href'); + } else { + info.timeframeSelectionNext.setAttribute('href', '#'); + } +} + +function updateRollingWindowTimeframeSelectionSources(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const contributionGroup = + info.contributionGroup.value; + + cssProp(info.sourceGrid, 'display', null); + + const {peeking: peekingSources, tracking: trackingSources} = + (empty(timeframeInfo) + ? {peeking: [], tracking: []} + : timeframeInfo[currentIndex]); + + const peekingElements = + peekingSources.map(source => source.element); + + const trackingElements = + trackingSources.map(source => source.element); + + const showingElements = + [...trackingElements, ...peekingElements]; + + const hidingElements = + Array.from(info.sources) + .filter(element => + !peekingElements.includes(element) && + !trackingElements.includes(element)); + + for (const element of peekingElements) { + element.classList.add('peeking'); + element.classList.remove('tracking'); + } + + for (const element of trackingElements) { + element.classList.remove('peeking'); + element.classList.add('tracking'); + } + + for (const element of hidingElements) { + element.classList.remove('peeking'); + element.classList.remove('tracking'); + cssProp(element, 'display', 'none'); + } + + for (const element of showingElements) { + cssProp(element, 'display', null); + + for (const time of element.getElementsByTagName('time')) { + for (const className of time.classList) { + if (!className.endsWith('-contribution-date')) continue; + + const kind = className.slice(0, -'-contribution-date'.length); + if (kind === info.contributionKind.value) { + cssProp(time, 'display', null); + } else { + cssProp(time, 'display', 'none'); + } + } + } + + for (const data of element.getElementsByClassName('contribution-group')) { + if (contributionGroup === '-' || data.value !== contributionGroup) { + cssProp(data, 'display', null); + } else { + cssProp(data, 'display', 'none'); + } + } + } + + if (empty(peekingElements) && empty(trackingElements)) { + cssProp(info.timeframeEmptyLine, 'display', null); + } else { + cssProp(info.timeframeEmptyLine, 'display', 'none'); + } +} diff --git a/src/static/js/client/css-compatibility-assistant.js b/src/static/js/client/css-compatibility-assistant.js index aa637cc4..37b0645a 100644 --- a/src/static/js/client/css-compatibility-assistant.js +++ b/src/static/js/client/css-compatibility-assistant.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { diff --git a/src/static/js/client/datetimestamp-tooltip.js b/src/static/js/client/datetimestamp-tooltip.js index 46d1cd5b..00530484 100644 --- a/src/static/js/client/datetimestamp-tooltip.js +++ b/src/static/js/client/datetimestamp-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // TODO: Maybe datetimestamps can just be incorporated into text-with-tooltip? import {stitchArrays} from '../../shared-util/sugar.js'; diff --git a/src/static/js/client/dragged-link.js b/src/static/js/client/dragged-link.js index 56021e7f..3a4ee314 100644 --- a/src/static/js/client/dragged-link.js +++ b/src/static/js/client/dragged-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: `draggedLinkInfo`, diff --git a/src/static/js/client/expandable-grid-section.js b/src/static/js/client/expandable-grid-section.js new file mode 100644 index 00000000..4d6e0058 --- /dev/null +++ b/src/static/js/client/expandable-grid-section.js @@ -0,0 +1,83 @@ +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'expandableGallerySectionInfo', + + items: null, + toggles: null, + expandCues: null, + collapseCues: null, +}; + +export function getPageReferences() { + const expandos = + Array.from(document.querySelectorAll('.grid-expando')); + + const grids = + expandos + .map(expando => expando.closest('.grid-listing')); + + info.items = + grids + .map(grid => grid.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.toggles = + expandos + .map(expando => expando.querySelector('.grid-expando-toggle')); + + info.expandCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-expand-cue')); + + info.collapseCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-collapse-cue')); +} + +export function addPageListeners() { + stitchArrays({ + items: info.items, + toggle: info.toggles, + expandCue: info.expandCues, + collapseCue: info.collapseCues, + }).forEach(({ + items, + toggle, + expandCue, + collapseCue, + }) => { + toggle.addEventListener('click', domEvent => { + domEvent.preventDefault(); + + const collapsed = + items.some(item => + item.classList.contains('hidden-by-expandable-cut')); + + for (const item of items) { + if ( + !item.classList.contains('hidden-by-expandable-cut') && + !item.classList.contains('shown-by-expandable-cut') + ) continue; + + if (collapsed) { + item.classList.remove('hidden-by-expandable-cut'); + item.classList.add('shown-by-expandable-cut'); + } else { + item.classList.add('hidden-by-expandable-cut'); + item.classList.remove('shown-by-expandable-cut'); + } + } + + if (collapsed) { + cssProp(expandCue, 'display', 'none'); + cssProp(collapseCue, 'display', null); + } else { + cssProp(expandCue, 'display', null); + cssProp(collapseCue, 'display', 'none'); + } + }); + }); +} diff --git a/src/static/js/client/gallery-style-selector.js b/src/static/js/client/gallery-style-selector.js new file mode 100644 index 00000000..44f98ac3 --- /dev/null +++ b/src/static/js/client/gallery-style-selector.js @@ -0,0 +1,121 @@ +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'galleryStyleSelectorInfo', + + selectors: null, + sections: null, + + selectorStyleInputs: null, + selectorStyleInputStyles: null, + + selectorReleaseItems: null, + selectorReleaseItemStyles: null, + + selectorCountAll: null, + selectorCountFiltered: null, + selectorCountFilteredCount: null, + selectorCountNone: null, +}; + +export function getPageReferences() { + info.selectors = + Array.from(document.querySelectorAll('.gallery-style-selector')); + + info.sections = + info.selectors + .map(selector => selector.closest('section')); + + info.selectorStyleInputs = + info.selectors + .map(selector => selector.querySelectorAll('.styles input')) + .map(inputs => Array.from(inputs)); + + info.selectorStyleInputStyles = + info.selectorStyleInputs + .map(inputs => inputs + .map(input => input.closest('label').dataset.style)); + + info.selectorReleaseItems = + info.sections + .map(section => section.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.selectorReleaseItemStyles = + info.selectorReleaseItems + .map(items => items + .map(item => item.dataset.style)); + + info.selectorCountAll = + info.selectors + .map(selector => selector.querySelector('.count.all')); + + info.selectorCountFiltered = + info.selectors + .map(selector => selector.querySelector('.count.filtered')); + + info.selectorCountFilteredCount = + info.selectorCountFiltered + .map(selector => selector.querySelector('span')); + + info.selectorCountNone = + info.selectors + .map(selector => selector.querySelector('.count.none')); +} + +export function addPageListeners() { + for (const index of info.selectors.keys()) { + for (const input of info.selectorStyleInputs[index]) { + input.addEventListener('input', () => updateVisibleReleases(index)); + } + } +} + +function updateVisibleReleases(index) { + const inputs = info.selectorStyleInputs[index]; + const inputStyles = info.selectorStyleInputStyles[index]; + + const selectedStyles = + stitchArrays({input: inputs, style: inputStyles}) + .filter(({input}) => input.checked) + .map(({style}) => style); + + const releases = info.selectorReleaseItems[index]; + const releaseStyles = info.selectorReleaseItemStyles[index]; + + let visible = 0; + + stitchArrays({ + release: releases, + style: releaseStyles, + }).forEach(({release, style}) => { + if (selectedStyles.includes(style)) { + release.classList.remove('hidden-by-style-mismatch'); + visible++; + } else { + release.classList.add('hidden-by-style-mismatch'); + } + }); + + const countAll = info.selectorCountAll[index]; + const countFiltered = info.selectorCountFiltered[index]; + const countFilteredCount = info.selectorCountFilteredCount[index]; + const countNone = info.selectorCountNone[index]; + + if (visible === releases.length) { + cssProp(countAll, 'display', null); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', 'none'); + } else if (visible === 0) { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', null); + } else { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', null); + cssProp(countNone, 'display', 'none'); + countFilteredCount.innerHTML = visible; + } +} diff --git a/src/static/js/client/hash-link.js b/src/static/js/client/hash-link.js index 27035e29..02ffdc23 100644 --- a/src/static/js/client/hash-link.js +++ b/src/static/js/client/hash-link.js @@ -1,6 +1,5 @@ -/* eslint-env browser */ - -import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js'; +import {filterMultipleArrays, stitchArrays, unique} + from '../../shared-util/sugar.js'; import {dispatchInternalEvent} from '../client-util.js'; @@ -11,6 +10,9 @@ export const info = { hrefs: null, targets: null, + details: null, + detailsIDs: null, + state: { highlightedTarget: null, scrollingAfterClick: false, @@ -40,6 +42,19 @@ export function getPageReferences() { info.hrefs, info.targets, (_link, _href, target) => target); + + info.details = + unique([ + ...document.querySelectorAll('details[id]'), + ... + Array.from(document.querySelectorAll('summary[id]')) + .map(summary => summary.closest('details')), + ]); + + info.detailsIDs = + info.details.map(details => + details.id || + details.querySelector('summary').id); } function processScrollingAfterHashLinkClicked() { @@ -60,6 +75,15 @@ function processScrollingAfterHashLinkClicked() { }, 200); } +export function mutatePageContent() { + if (location.hash.length > 1) { + const target = document.getElementById(location.hash.slice(1)); + if (target) { + expandDetails(target); + } + } +} + export function addPageListeners() { // Instead of defining a scroll offset (to account for the sticky heading) // in JavaScript, we interface with the CSS property 'scroll-margin-top'. @@ -94,6 +118,8 @@ export function addPageListeners() { return; } + expandDetails(target); + // Hide skipper box right away, so the layout is updated on time for the // math operations coming up next. const skipper = document.getElementById('skippers'); @@ -143,4 +169,32 @@ export function addPageListeners() { state.highlightedTarget = null; }); } + + stitchArrays({ + details: info.details, + id: info.detailsIDs, + }).forEach(({details, id}) => { + details.addEventListener('toggle', () => { + if (!details.open) { + detractHash(id); + } + }); + }); +} + +function expandDetails(target) { + if (target.nodeName === 'SUMMARY') { + const details = target.closest('details'); + if (details) { + details.open = true; + } + } else if (target.nodeName === 'DETAILS') { + target.open = true; + } +} + +function detractHash(id) { + if (location.hash === '#' + id) { + history.pushState({}, undefined, location.href.replace(/#.*$/, '')); + } } diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js index 9569de3e..22b9471c 100644 --- a/src/static/js/client/hoverable-tooltip.js +++ b/src/static/js/client/hoverable-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {empty, filterMultipleArrays} from '../../shared-util/sugar.js'; import {WikiRect} from '../rectangles.js'; @@ -118,17 +116,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 +156,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 +414,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 +424,7 @@ function handleTooltipHoverableClicked(hoverable) { state.currentlyActiveHoverable === hoverable && state.hoverableWasRecentlyTouched ) { - event.preventDefault(); + domEvent.preventDefault(); } } diff --git a/src/static/js/client/image-overlay.js b/src/static/js/client/image-overlay.js index da192178..0595bff7 100644 --- a/src/static/js/client/image-overlay.js +++ b/src/static/js/client/image-overlay.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {getColors} from '../../shared-util/colors.js'; import {cssProp} from '../client-util.js'; @@ -96,7 +94,10 @@ function handleContainerClicked(evt) { // 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 && evt.clientY <= rect.bottom + 40) { + if ( + evt.clientY >= rect.top - 40 && evt.clientY <= rect.bottom + 40 && + evt.clientX >= rect.left + 20 && evt.clientX <= rect.right - 20 + ) { return; } @@ -146,7 +147,8 @@ function getImageLinkDetails(imageLink) { a.href, embeddedSrc: - img?.src ?? + img?.src || + img?.currentSrc || a.dataset.embedSrc, originalFileSize: diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 81ea3415..16ebe89f 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import '../group-contributions-table.js'; import * as additionalNamesBoxModule from './additional-names-box.js'; @@ -7,15 +5,20 @@ 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 artistRollingWindowModule from './artist-rolling-window.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 expandableGridSectionModule from './expandable-grid-section.js'; +import * as galleryStyleSelectorModule from './gallery-style-selector.js'; import * as hashLinkModule from './hash-link.js'; import * as hoverableTooltipModule from './hoverable-tooltip.js'; import * as imageOverlayModule from './image-overlay.js'; import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js'; import * as liveMousePositionModule from './live-mouse-position.js'; +import * as memorableDetailsModule from './memorable-details.js'; import * as quickDescriptionModule from './quick-description.js'; +import * as revealAllGridControlModule from './reveal-all-grid-control.js'; import * as scriptedLinkModule from './scripted-link.js'; import * as sidebarSearchModule from './sidebar-search.js'; import * as stickyHeadingModule from './sticky-heading.js'; @@ -29,15 +32,20 @@ export const modules = [ artTagGalleryFilterModule, artTagNetworkModule, artistExternalLinkTooltipModule, + artistRollingWindowModule, cssCompatibilityAssistantModule, datetimestampTooltipModule, draggedLinkModule, + expandableGridSectionModule, + galleryStyleSelectorModule, hashLinkModule, hoverableTooltipModule, imageOverlayModule, intrapageDotSwitcherModule, liveMousePositionModule, + memorableDetailsModule, quickDescriptionModule, + revealAllGridControlModule, scriptedLinkModule, sidebarSearchModule, stickyHeadingModule, @@ -48,7 +56,10 @@ export const modules = [ const clientInfo = window.hsmusicClientInfo = Object.create(null); -const clientSteps = { +// These steps are always run in the listed order, on page load. +// So for example, all modules' getPageReferences steps are evaluated, then +// all modules' addInternalListeners steps are evaluated, and so on. +const setupSteps = { getPageReferences: [], addInternalListeners: [], mutatePageContent: [], @@ -56,6 +67,18 @@ const clientSteps = { addPageListeners: [], }; +// These steps are run only on certain triggers. Those are global events, +// so all modules (which specify that step) respond in sequence. +const situationalSteps = { + /* There's none yet... sorry... */ +}; + +const stepInfoSymbol = Symbol(); + +const boundSessionStorage = + window.hsmusicBoundSessionStorage = + Object.create(null); + for (const module of modules) { const {info} = module; @@ -140,12 +163,47 @@ for (const module of modules) { const storageKey = `hsmusic.${infoKey}.${key}`; + // There are two storage systems besides actual session storage in play. + // + // "Fallback" is for if session storage is not available, which may + // suddenly become the case, i.e. access is temporarily revoked or fails. + // The fallback value is controlled completely internally i.e. in this + // infrastructure, in this lexical scope. + // + // "Bound" is for if the value kept in session storage was saved to + // the page when the page was initially loaded, rather than a living + // window on session storage (which may be affected by pages later in + // the history stack). Whether or not bound storage is in effect is + // controlled at page load (of course), by each module's own logic. + // + // Asterisk: Bound storage can't work miracles and if the page is + // actually deloaded with its JavaScript state discarded, the bound + // values are lost, even if the browser recreates on-page form state. + let fallbackValue = defaultValue; + let boundValue = undefined; + + const updateBoundValue = (givenValue = undefined) => { + if (givenValue) { + if ( + infoKey in boundSessionStorage && + key in boundSessionStorage[infoKey] + ) { + boundSessionStorage[infoKey][key] = givenValue; + } + } else { + boundValue = boundSessionStorage[infoKey]?.[key]; + } + }; Object.defineProperty(info.session, key, { get: () => { + updateBoundValue(); + let value; - try { + if (boundValue !== undefined) { + value = boundValue ?? defaultValue; + } else try { value = sessionStorage.getItem(storageKey) ?? defaultValue; } catch (error) { if (error instanceof DOMException) { @@ -181,21 +239,23 @@ for (const module of modules) { return; } - let operation; + let sessionOperation; if (value === '') { fallbackValue = null; - operation = () => { + updateBoundValue(null); + sessionOperation = () => { sessionStorage.removeItem(storageKey); }; } else { fallbackValue = value; - operation = () => { + updateBoundValue(value); + sessionOperation = () => { sessionStorage.setItem(storageKey, value); }; } try { - operation(); + sessionOperation(); } catch (error) { if (!(error instanceof DOMException)) { throw error; @@ -208,28 +268,68 @@ for (const module of modules) { Object.preventExtensions(info.session); } - for (const key of Object.keys(clientSteps)) { - if (Object.hasOwn(module, key)) { - const fn = module[key]; + for (const stepsObject of [setupSteps, situationalSteps]) { + for (const key of Object.keys(stepsObject)) { + if (Object.hasOwn(module, key)) { + const fn = module[key]; - Object.defineProperty(fn, 'name', { - value: `${infoKey}/${fn.name}`, - }); + fn[stepInfoSymbol] = info; + + Object.defineProperty(fn, 'name', { + value: `${infoKey}/${fn.name}`, + }); + + stepsObject[key].push(fn); + } + } + } +} + +function evaluateBindSessionStorageStep(bindSessionStorage) { + const {id: infoKey, session: moduleExposedSessionObject} = + bindSessionStorage[stepInfoSymbol]; + + const generator = bindSessionStorage(); - clientSteps[key].push(fn); + let lastBoundValue; + while (true) { + const {value: key, done} = generator.next(lastBoundValue); + const storageKey = `hsmusic.${infoKey}.${key}`; + + let value = undefined; + try { + value = sessionStorage.getItem(storageKey); + } catch (error) { + if (!(error instanceof DOMException)) { + throw error; + } } + + if (value === undefined) { + // This effectively gets the default value. + value = moduleExposedSessionObject[key]; + } + + boundSessionStorage[infoKey] ??= Object.create(null); + boundSessionStorage[infoKey][key] = value; + + lastBoundValue = value; + + if (done) break; } } -for (const [key, steps] of Object.entries(clientSteps)) { - for (const step of steps) { +function evaluateStep(stepsObject, key) { + for (const step of stepsObject[key]) { try { step(); } catch (error) { - // TODO: Be smarter about not running later steps for the same module! - // Or maybe not, since an error is liable to cause explosions anyway. console.error(`During ${key}, failed to run ${step.name}`); console.error(error); } } } + +for (const key of Object.keys(setupSteps)) { + evaluateStep(setupSteps, key); +} diff --git a/src/static/js/client/intrapage-dot-switcher.js b/src/static/js/client/intrapage-dot-switcher.js index d06bc5a6..b9a27a9b 100644 --- a/src/static/js/client/intrapage-dot-switcher.js +++ b/src/static/js/client/intrapage-dot-switcher.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; import {cssProp} from '../client-util.js'; diff --git a/src/static/js/client/live-mouse-position.js b/src/static/js/client/live-mouse-position.js index 36a28429..32fc5bf4 100644 --- a/src/static/js/client/live-mouse-position.js +++ b/src/static/js/client/live-mouse-position.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: 'liveMousePositionInfo', diff --git a/src/static/js/client/memorable-details.js b/src/static/js/client/memorable-details.js new file mode 100644 index 00000000..57d9fde8 --- /dev/null +++ b/src/static/js/client/memorable-details.js @@ -0,0 +1,62 @@ +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'memorableDetailsInfo', + + details: null, + ids: null, + + session: { + openDetails: { + type: 'json', + maxLength: settings => settings.maxOpenDetailsStorage, + }, + }, + + settings: { + maxOpenDetailsStorage: 1000, + }, +}; + +export function getPageReferences() { + info.details = + Array.from(document.querySelectorAll('details.memorable')); + + info.ids = + info.details.map(details => details.getAttribute('data-memorable-id')); +} + +export function mutatePageContent() { + stitchArrays({ + details: info.details, + id: info.ids, + }).forEach(({details, id}) => { + if (info.session.openDetails?.includes(id)) { + details.open = true; + } + }); +} + +export function addPageListeners() { + for (const [index, details] of info.details.entries()) { + details.addEventListener('toggle', () => { + handleDetailsToggled(index); + }); + } +} + +function handleDetailsToggled(index) { + const details = info.details[index]; + const id = info.ids[index]; + + if (details.open) { + if (info.session.openDetails) { + info.session.openDetails = [...info.session.openDetails, id]; + } else { + info.session.openDetails = [id]; + } + } else if (info.session.openDetails?.includes(id)) { + info.session.openDetails = + info.session.openDetails.filter(item => item !== id); + } +} diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js index cff82252..9117d48c 100644 --- a/src/static/js/client/quick-description.js +++ b/src/static/js/client/quick-description.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { diff --git a/src/static/js/client/reveal-all-grid-control.js b/src/static/js/client/reveal-all-grid-control.js new file mode 100644 index 00000000..0572a190 --- /dev/null +++ b/src/static/js/client/reveal-all-grid-control.js @@ -0,0 +1,70 @@ +import {cssProp} from '../client-util.js'; + +export const info = { + id: 'revealAllGridControlInfo', + + revealAllLinks: null, + revealables: null, + + revealLabels: null, + concealLabels: null, +}; + +export function getPageReferences() { + info.revealAllLinks = + Array.from(document.querySelectorAll('.reveal-all a')); + + info.revealables = + info.revealAllLinks + .map(link => link.closest('.grid-listing')) + .map(listing => listing.querySelectorAll('.reveal')); + + info.revealLabels = + info.revealAllLinks + .map(link => link.querySelector('.reveal-label')); + + info.concealLabels = + info.revealAllLinks + .map(link => link.querySelector('.conceal-label')); +} + +export function addPageListeners() { + for (const [index, link] of info.revealAllLinks.entries()) { + link.addEventListener('click', domEvent => { + domEvent.preventDefault(); + handleRevealAllLinkClicked(index); + }); + } +} + +export function addInternalListeners() { + // Don't even think about it. "Reveal all artworks" is a stable control, + // meaning it only changes because the user interacted with it directly. +} + +function handleRevealAllLinkClicked(index) { + const revealables = info.revealables[index]; + const revealLabel = info.revealLabels[index]; + const concealLabel = info.concealLabels[index]; + + const shouldReveal = + (cssProp(revealLabel, 'display') === 'none' + ? false + : true); + + for (const revealable of revealables) { + if (shouldReveal) { + revealable.classList.add('revealed'); + } else { + revealable.classList.remove('revealed'); + } + } + + if (shouldReveal) { + cssProp(revealLabel, 'display', 'none'); + cssProp(concealLabel, 'display', null); + } else { + cssProp(revealLabel, 'display', null); + cssProp(concealLabel, 'display', 'none'); + } +} diff --git a/src/static/js/client/scripted-link.js b/src/static/js/client/scripted-link.js index 8b8d8a13..badc6ccb 100644 --- a/src/static/js/client/scripted-link.js +++ b/src/static/js/client/scripted-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {pick, stitchArrays} from '../../shared-util/sugar.js'; import { diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index fb902636..8b29cf63 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -1,10 +1,15 @@ -/* eslint-env browser */ - import {getColors} from '../../shared-util/colors.js'; -import {accumulateSum, empty} from '../../shared-util/sugar.js'; + +import { + accumulateSum, + compareArrays, + empty, + unique, +} from '../../shared-util/sugar.js'; import { cssProp, + decodeEntities, openAlbum, openArtist, openArtTag, @@ -41,6 +46,17 @@ export const info = { failedRule: null, failedContainer: null, + contextContainer: null, + contextBackLink: null, + + filterContainer: null, + albumFilterLink: null, + artistFilterLink: null, + flashFilterLink: null, + groupFilterLink: null, + tagFilterLink: null, + trackFilterLink: null, + resultsRule: null, resultsContainer: null, results: null, @@ -60,11 +76,27 @@ export const info = { currentResultString: null, endSearchString: null, + backString: null, + albumResultKindString: null, artistResultKindString: null, + flashResultKindString: null, groupResultKindString: null, + singleResultKindString: null, tagResultKindString: null, + groupResultDisambiguatorString: null, + flashResultDisambiguatorString: null, + trackResultDisambiguatorString1: null, + trackResultDisambiguatorString2: null, + + albumResultFilterString: null, + artistResultFilterString: null, + flashResultFilterString: null, + groupResultFilterString: null, + tagResultFilterString: null, + trackResultFilterString: null, + state: { sidebarColumnShownForSearch: null, @@ -92,11 +124,19 @@ export const info = { type: 'string', }, + activeQueryContextPageName: {type: 'string'}, + activeQueryContextPagePathname: {type: 'string'}, + activeQueryContextPageColor: {type: 'string'}, + activeQueryResults: { type: 'json', maxLength: settings => settings.maxActiveResultsStorage, }, + activeFilterType: { + type: 'string', + }, + repeatQueryOnReload: { type: 'boolean', default: false, @@ -118,6 +158,17 @@ export const info = { }, }; +export function* bindSessionStorage() { + if (yield 'activeQuery') { + yield 'activeQueryContextPageName'; + yield 'activeQueryContextPagePathname'; + yield 'activeQueryContextPageColor'; + yield 'activeQueryResults'; + yield 'activeFilterType'; + yield 'resultsScrollOffset'; + } +} + export function getPageReferences() { info.pageContainer = document.getElementById('page-container'); @@ -159,6 +210,9 @@ export function getPageReferences() { info.noResultsString = findString('no-results'); + info.backString = + findString('back'); + info.currentResultString = findString('current-result'); @@ -171,11 +225,47 @@ export function getPageReferences() { info.artistResultKindString = findString('artist-result-kind'); + info.flashResultKindString = + findString('flash-result-kind'); + info.groupResultKindString = findString('group-result-kind'); + info.singleResultKindString = + findString('single-result-kind'); + info.tagResultKindString = findString('tag-result-kind'); + + info.groupResultDisambiguatorString = + findString('group-result-disambiguator'); + + info.flashResultDisambiguatorString = + findString('flash-result-disambiguator'); + + info.trackResultDisambiguatorString1 = + findString('track-result-album-disambiguator'); + + info.trackResultDisambiguatorString2 = + findString('track-result-artist-disambiguator'); + + 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() { @@ -265,6 +355,57 @@ export function mutatePageContent() { info.searchBox.appendChild(info.failedRule); info.searchBox.appendChild(info.failedContainer); + // Context section + + info.contextContainer = + document.createElement('div'); + + info.contextContainer.classList.add('wiki-search-context-container'); + + info.contextBackLink = + document.createElement('a'); + + info.contextContainer.appendChild( + templateContent(info.backString, { + page: info.contextBackLink, + })); + + cssProp(info.contextContainer, 'display', 'none'); + + info.searchBox.appendChild(info.contextContainer); + + // 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 = @@ -313,6 +454,12 @@ export function mutatePageContent() { info.searchBox.appendChild(info.endSearchRule); info.searchBox.appendChild(info.endSearchLine); + + // Accommodate the web browser reconstructing the search input with a value + // that was previously entered (or restored after recall), i.e. because + // the user is traversing very far back in history and yet the browser is + // trying to rebuild the page as-was anyway, by telling it "no don't". + info.searchInput.value = ''; } export function addPageListeners() { @@ -371,7 +518,7 @@ export function addPageListeners() { const {settings, state} = info; if (!info.searchInput.value) { - clearSidebarSearch(); + clearSidebarSearch(); // ...but don't clear filter return; } @@ -433,10 +580,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; @@ -449,6 +604,18 @@ export function addPageListeners() { saveSidebarSearchResultsScrollOffset(); }, settings.stoppedScrollingDelay); }); + + document.addEventListener('keypress', domEvent => { + const {tagName} = document.activeElement ?? {}; + if (tagName === 'INPUT' || tagName === 'TEXTAREA') { + return; + } + + if (event.shiftKey && event.code === 'Slash') { + domEvent.preventDefault(); + info.searchLabel.click(); + } + }); } export function initializeState() { @@ -518,6 +685,21 @@ 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; @@ -549,6 +731,8 @@ async function activateSidebarSearch(query) { state.searchStage = 'complete'; updateSidebarSearchStatus(); + recordActiveQueryContext(); + session.activeQuery = query; session.activeQueryResults = results; session.resultsScrollOffset = 0; @@ -562,6 +746,25 @@ async function activateSidebarSearch(query) { } } +function recordActiveQueryContext() { + const {session} = info; + + if (document.documentElement.dataset.urlKey === 'localized.home') { + return; + } + + session.activeQueryContextPageName = + decodeEntities(document.querySelector('title').dataset.withoutWikiName) || + document.title; + + session.activeQueryContextPagePathname = + location.pathname; + + session.activeQueryContextPageColor = + document.querySelector('.color-style')?.dataset.color ?? + null; +} + function clearSidebarSearch() { const {session, state} = info; @@ -584,6 +787,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; @@ -670,63 +883,185 @@ 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) { + showContextControls(); + + 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) { - const el = generateSidebarSearchResult(result); + 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); + + let filteredResults = tidiedResults; + + if (filterType) { + filteredResults = filteredResults + .filter(result => result.referenceType === filterType); + } + + if (!filterType) { + filteredResults = filteredResults + .filter(result => { + if (result.referenceType !== 'track') return true; + if (result.data.classification !== 'single') return true; + return !filteredResults.find(otherResult => { + if (otherResult.referenceType !== 'album') return false; + return otherResult.name === result.parentName; + }); + }); + } + + filteredResults = filteredResults + + 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) { + let el; + try { + el = generateSidebarSearchResult(result, filteredResults); + } catch (error) { + console.error(`Error showing result:`, result); + console.error(error); + } + 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; + + 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'); } +} - restoreSidebarSearchResultsScrollOffset(); +function showContextControls() { + const {session} = info; + + const shouldShow = + session.activeQueryContextPagePathname && + location.pathname !== session.activeQueryContextPagePathname; + + if (shouldShow) { + info.contextBackLink.href = + session.activeQueryContextPagePathname; + + cssProp(info.contextBackLink, + '--primary-color', + session.activeQueryContextPageColor); + + while (info.contextBackLink.firstChild) { + info.contextBackLink.firstChild.remove(); + } + + info.contextBackLink.appendChild( + document.createTextNode( + session.activeQueryContextPageName)); + + cssProp(info.contextContainer, 'display', 'block'); + } else { + cssProp(info.contextContainer, 'display', 'none'); + } } -function generateSidebarSearchResult(result) { +function generateSidebarSearchResult(result, results) { const preparedSlots = { color: result.data.color ?? null, name: - result.data.name ?? result.data.primaryName ?? null, + getSearchResultName(result), imageSource: getSearchResultImageSource(result), @@ -738,7 +1073,9 @@ function generateSidebarSearchResult(result) { openAlbum(result.directory); preparedSlots.kindString = - info.albumResultKindString; + (result.data.classification === 'single' + ? info.singleResultKindString + : info.albumResultKindString); break; } @@ -767,6 +1104,9 @@ function generateSidebarSearchResult(result) { preparedSlots.href = openFlash(result.directory); + preparedSlots.kindString = + info.flashResultKindString; + break; } @@ -791,9 +1131,95 @@ function generateSidebarSearchResult(result) { return null; } + const compareReferenceType = otherResult => + otherResult.referenceType === result.referenceType; + + const compareName = otherResult => + getSearchResultName(otherResult) === getSearchResultName(result); + + const ambiguousWith = + results.filter(otherResult => + otherResult !== result && + compareReferenceType(otherResult) && + compareName(otherResult)); + + if (!empty(ambiguousWith)) disambiguate: { + const allAmbiguous = [result, ...ambiguousWith]; + + // First search for an ideal disambiguation, which disambiguates + // all ambiguous results in the same way. + let disambiguation = null, i; + for (i = 0; i < result.data.disambiguators.length; i++) { + const disambiguations = + allAmbiguous.map(r => r.data.disambiguators[i]); + + if (unique(disambiguations).length === allAmbiguous.length) { + disambiguation = result.data.disambiguators[i]; + break; + } + } + + // Otherwise, search for a disambiguation which disambiguates + // *this result* with at least one other result which it is + // *otherwise* ambiguous with. + if (!disambiguation) { + for (i = 1; i < result.data.disambiguators.length; i++) { + const otherwiseAmbiguousWith = + ambiguousWith.filter(otherResult => + compareArrays( + otherResult.data.disambiguators.slice(0, i), + result.data.disambiguators.slice(0, i))); + + if ( + otherwiseAmbiguousWith.find(otherResult => + otherResult.data.disambiguators[i] !== + result.data.disambiguators[i]) + ) { + disambiguation = result.data.disambiguators[i]; + break; + } + } + } + + // Otherwise, search for a disambiguation which disambiguates + // this result at all. + if (!disambiguation) { + for (i = 0; i < result.data.disambiguators.length; i++) { + if ( + ambiguousWith.find(otherResult => + otherResult.data.disambiguators[i] !== + result.data.disambiguators[i]) + ) { + disambiguation = result.data.disambiguators[i]; + break; + } + } + } + + if (!disambiguation) { + break disambiguate; + } + + const string = + info[result.referenceType + 'ResultDisambiguatorString' + (i + 1)]; + + if (!string) break disambiguate; + + preparedSlots.disambiguate = disambiguation; + preparedSlots.disambiguatorString = string; + } + return generateSidebarSearchResultTemplate(preparedSlots); } +function getSearchResultName(result) { + return ( + result.data.name ?? + result.data.primaryName ?? + null + ); +} + function getSearchResultImageSource(result) { const {artwork} = result.data; if (!artwork) return null; @@ -869,6 +1295,15 @@ function generateSidebarSearchResultTemplate(slots) { } } + if (!accentSpan && slots.disambiguate) { + accentSpan = document.createElement('span'); + accentSpan.classList.add('wiki-search-result-disambiguator'); + accentSpan.appendChild( + templateContent(slots.disambiguatorString, { + disambiguator: slots.disambiguate, + })); + } + if (!accentSpan && slots.kindString) { accentSpan = document.createElement('span'); accentSpan.classList.add('wiki-search-result-kind'); @@ -908,6 +1343,9 @@ function generateSidebarSearchResultTemplate(slots) { } function hideSidebarSearchResults() { + cssProp(info.contextContainer, 'display', 'none'); + cssProp(info.filterContainer, 'display', 'none'); + cssProp(info.resultsRule, 'display', 'none'); cssProp(info.resultsContainer, 'display', 'none'); @@ -1040,6 +1478,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; @@ -1072,7 +1540,14 @@ function forgetRecentSidebarSearch() { const {session} = info; session.activeQuery = null; + + session.activeQueryContextPageName = null; + session.activeQueryContextPagePathname = null; + session.activeQueryContextPageColor = null; + session.activeQueryResults = null; + + clearSidebarFilter(); } async function handleDroppedIntoSearchInput(domEvent) { @@ -1101,7 +1576,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 fba05b84..c69e137f 100644 --- a/src/static/js/client/sticky-heading.js +++ b/src/static/js/client/sticky-heading.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js'; import {cssProp, dispatchInternalEvent, templateContent} from '../client-util.js'; @@ -23,6 +21,7 @@ export const info = { contentContainers: null, contentHeadings: null, + contentCoverColumns: null, contentCovers: null, contentCoversReveal: null, @@ -82,9 +81,13 @@ export function getPageReferences() { info.stickyContainers .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 @@ -212,10 +215,10 @@ function updateCollapseStatus(index) { function updateStickyCoverVisibility(index) { const stickyCoverContainer = info.stickyCoverContainers[index]; const stickyContainer = info.stickyContainers[index]; - const contentCover = info.contentCovers[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 { @@ -250,7 +253,11 @@ function getContentHeadingClosestToStickySubheading(index) { // Iterate from bottom to top of the content area. const contentHeadings = info.contentHeadings[index]; - for (const heading of contentHeadings.slice().reverse()) { + for (const heading of contentHeadings.toReversed()) { + if (heading.nodeName === 'SUMMARY' && !heading.closest('details').open) { + continue; + } + const headingRect = heading.getBoundingClientRect(); if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 40) { return heading; diff --git a/src/static/js/client/summary-nested-link.js b/src/static/js/client/summary-nested-link.js index 23857fa5..1c4e7e4b 100644 --- a/src/static/js/client/summary-nested-link.js +++ b/src/static/js/client/summary-nested-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import { empty, filterMultipleArrays, diff --git a/src/static/js/client/text-with-tooltip.js b/src/static/js/client/text-with-tooltip.js index dd207e04..2b855756 100644 --- a/src/static/js/client/text-with-tooltip.js +++ b/src/static/js/client/text-with-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; import {registerTooltipElement, registerTooltipHoverableElement} diff --git a/src/static/js/client/wiki-search.js b/src/static/js/client/wiki-search.js index 2446c172..9a6e29c1 100644 --- a/src/static/js/client/wiki-search.js +++ b/src/static/js/client/wiki-search.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {promiseWithResolvers} from '../../shared-util/sugar.js'; import {dispatchInternalEvent} from '../client-util.js'; diff --git a/src/static/js/group-contributions-table.js b/src/static/js/group-contributions-table.js index 72ad2327..bef85fad 100644 --- a/src/static/js/group-contributions-table.js +++ b/src/static/js/group-contributions-table.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // TODO: Update to clientSteps style. const groupContributionsTableInfo = diff --git a/src/static/js/info-card.js b/src/static/js/info-card.js index 1d9f7c86..05d5d801 100644 --- a/src/static/js/info-card.js +++ b/src/static/js/info-card.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // Note: This is a super ancient chunk of code which isn't actually in use, // so it's just commented out here. diff --git a/src/static/js/lazy-loading.js b/src/static/js/lazy-loading.js index 1df56f08..0c8aef31 100644 --- a/src/static/js/lazy-loading.js +++ b/src/static/js/lazy-loading.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // Lazy loading! Roll your own. Woot. // This file includes a 8unch of fall8acks and stuff like that, and is written // with fairly Olden JavaScript(TM), so as to work on pretty much any 8rowser diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js index cdab2cb8..24382ef8 100644 --- a/src/static/js/rectangles.js +++ b/src/static/js/rectangles.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {info as liveMousePositionInfo} from './client/live-mouse-position.js'; export class WikiRect extends DOMRect { @@ -510,4 +508,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..b79df3d4 100644 --- a/src/static/js/search-worker.js +++ b/src/static/js/search-worker.js @@ -1,8 +1,7 @@ -/* eslint-env worker */ - import FlexSearch from '../lib/flexsearch/flexsearch.bundle.module.min.js'; -import {makeSearchIndex, searchSpec} from '../shared-util/search-spec.js'; +import {default as searchSpec, makeSearchIndex} + from '../shared-util/search-shape.js'; import { empty, @@ -33,7 +32,7 @@ postStatus('alive'); Promise.all([ loadDependencies(), loadDatabase(), -]).then(main) +]).then(() => main()) .then( () => { postStatus('ready'); @@ -130,7 +129,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; @@ -183,7 +182,7 @@ function fetchIndexes(keysNeedingFetch) { }))); } -async function main() { +async function main(fromRetry = false) { const prepareIndexDataPromise = prepareIndexData(); indexes = @@ -195,17 +194,50 @@ async function main() { const {indexData, idbIndexData} = await prepareIndexDataPromise; + const understoodKeys = Object.keys(searchSpec); + const unexpectedKeysFromCache = + Object.keys(idbIndexData) + .filter(key => !understoodKeys.includes(key)); + + // This step is largely "unnecessary" because the rest of the code pays + // attention to which keys are understood anyway, but we delete unexpected + // keys from the index anyway, to trim stored data that isn't being used. + if (idb && !empty(unexpectedKeysFromCache)) { + for (const key of unexpectedKeysFromCache) { + console.warn(`Unexpected search index in cache, deleting: ${key}`); + } + + const transaction = + idb.transaction(['indexes'], 'readwrite'); + + const store = + transaction.objectStore('indexes'); + + for (const [key] of unexpectedKeysFromCache) { + try { + await promisifyIDBRequest(store.delete(key)); + } catch (error) { + console.warn(`Error deleting ${key} from internal search cache`); + console.warn(error); + continue; + } + } + } + const keysNeedingFetch = (idbIndexData ? Object.keys(indexData) + .filter(key => understoodKeys.includes(key)) .filter(key => indexData[key].md5 !== idbIndexData[key]?.md5) - : Object.keys(indexData)); + : Object.keys(indexData) + .filter(key => understoodKeys.includes(key))); const keysFromCache = Object.keys(indexData) - .filter(key => !keysNeedingFetch.includes(key)) + .filter(key => understoodKeys.includes(key)) + .filter(key => !keysNeedingFetch.includes(key)); const cacheArrayBufferPromises = keysFromCache @@ -234,10 +266,20 @@ async function main() { } function importIndexes(keys, jsons) { + const succeeded = []; + const failed = []; + stitchArrays({key: keys, json: jsons}) .forEach(({key, json}) => { - importIndex(key, json); + try { + importIndex(key, json); + succeeded.push([key, null]); + } catch (caughtError) { + failed.push([key, caughtError]); + } }); + + return {succeeded, failed}; } if (idb) { @@ -245,6 +287,8 @@ async function main() { console.debug(`Fetching indexes anew:`, keysNeedingFetch); } + let signalRetryNeeded = false; + await Promise.all([ async () => { const cacheArrayBuffers = @@ -254,7 +298,34 @@ async function main() { cacheArrayBuffers .map(arrayBufferToJSON); - importIndexes(keysFromCache, cacheJSONs); + const importResults = + importIndexes(keysFromCache, cacheJSONs); + + if (empty(importResults.failed)) return; + if (!idb) return; + + const transaction = + idb.transaction(['indexes'], 'readwrite'); + + const store = + transaction.objectStore('indexes'); + + for (const [key, error] of importResults.failed) { + console.warn(`Failed to import search index from cache: ${key}`); + console.warn(error); + } + + for (const [key] of importResults.failed) { + try { + await promisifyIDBRequest(store.delete(key)); + } catch (error) { + console.warn(`Error deleting ${key} from internal search cache`); + console.warn(error); + continue; + } + } + + signalRetryNeeded = true; }, async () => { @@ -265,7 +336,21 @@ async function main() { fetchArrayBuffers .map(arrayBufferToJSON); - importIndexes(keysNeedingFetch, fetchJSONs); + const importResults = + importIndexes(keysNeedingFetch, fetchJSONs); + + if (empty(importResults.failed)) return; + + for (const [key, error] of importResults.failed) { + console.warn(`Failed to import search index from fetch: ${key}`); + console.warn(error); + } + + console.warn( + `Trying again would just mean fetching this same data, ` + + `so this is needs outside intervention.`); + + throw new Error(`Failed to load search data from fresh fetch`); }, async () => { @@ -300,11 +385,19 @@ async function main() { } }, ].map(fn => fn())); + + if (signalRetryNeeded) { + if (fromRetry) { + console.error(`Already retried, this is probably a logic / code flow error.`); + throw new Error(`Failed to load good search data even on a retry`); + } else { + console.warn(`Trying to load search data again, hopefully from fresh conditions`); + return main(true); + } + } } function importIndex(indexKey, indexData) { - // If this fails, it's because an outdated index was cached. - // TODO: If this fails, try again once with a cache busting url. for (const [key, value] of Object.entries(indexData)) { indexes[indexKey].import(key, JSON.stringify(value)); } @@ -371,58 +464,78 @@ 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'], + ['additionalNames'], + + ['primaryName', 'parentName', 'groups'], + ['primaryName', 'parentName'], + ['primaryName', 'groups', 'contributors'], + ['primaryName', 'groups', 'artTags'], + ['primaryName', 'groups'], + ['additionalNames', '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'], +]; + +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 +550,7 @@ function queryGenericIndex(index, query, options) { query: values, })); - const boilerplate = queryBoilerplate(index); + const boilerplate = queryBoilerplate(indexes[indexKey]); const particleResults = Object.fromEntries( @@ -459,62 +572,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 filteredResults = + (queriedKind + ? matchedResults.filter(id => id.split(':')[0] === queriedKind) + : matchedResults); - const constitutedAndFiltered = - constituted - .filter(({id}) => - (queriedKind - ? id.split(':')[0] === queriedKind - : true)); + const constitutedResults = + boilerplate.constitute(filteredResults); - return constitutedAndFiltered; + 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', 'release', 'releases']}, + {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 +654,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/js/xhr-util.js b/src/static/js/xhr-util.js index 8a43072c..bc0698da 100644 --- a/src/static/js/xhr-util.js +++ b/src/static/js/xhr-util.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - /** * This fetch function is adapted from a `loadImage` function * credited to Parziphal, Feb 13, 2017. |