From 43b8f8f31b2201d5eec96641646e7c3c511e45f0 Mon Sep 17 00:00:00 2001
From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com>
Date: Thu, 20 Mar 2025 13:08:39 +0000
Subject: [PATCH] Split CSS into multiple files
This shouldn't result in anything breaking,
will require thorough testing
---
internal/glance/config.go | 2 +-
internal/glance/embed.go | 90 +
internal/glance/glance.go | 18 +-
internal/glance/static/css/forum-posts.css | 19 +
internal/glance/static/css/main.css | 63 +
internal/glance/static/css/mobile.css | 223 ++
internal/glance/static/css/popover.css | 65 +
internal/glance/static/css/site.css | 295 +++
internal/glance/static/css/utils.css | 561 +++++
.../glance/static/css/widget-bookmarks.css | 31 +
.../glance/static/css/widget-calendar.css | 71 +
internal/glance/static/css/widget-clock.css | 7 +
.../glance/static/css/widget-dns-stats.css | 120 +
.../static/css/widget-docker-containers.css | 26 +
internal/glance/static/css/widget-group.css | 49 +
internal/glance/static/css/widget-markets.css | 13 +
internal/glance/static/css/widget-monitor.css | 36 +
internal/glance/static/css/widget-reddit.css | 22 +
.../glance/static/css/widget-releases.css | 6 +
internal/glance/static/css/widget-rss.css | 56 +
internal/glance/static/css/widget-search.css | 79 +
.../glance/static/css/widget-server-stats.css | 81 +
internal/glance/static/css/widget-twitch.css | 47 +
internal/glance/static/css/widget-videos.css | 13 +
internal/glance/static/css/widget-weather.css | 139 ++
internal/glance/static/css/widgets.css | 88 +
internal/glance/static/main.css | 2115 -----------------
internal/glance/templates/document.html | 8 +-
28 files changed, 2222 insertions(+), 2121 deletions(-)
create mode 100644 internal/glance/static/css/forum-posts.css
create mode 100644 internal/glance/static/css/main.css
create mode 100644 internal/glance/static/css/mobile.css
create mode 100644 internal/glance/static/css/popover.css
create mode 100644 internal/glance/static/css/site.css
create mode 100644 internal/glance/static/css/utils.css
create mode 100644 internal/glance/static/css/widget-bookmarks.css
create mode 100644 internal/glance/static/css/widget-calendar.css
create mode 100644 internal/glance/static/css/widget-clock.css
create mode 100644 internal/glance/static/css/widget-dns-stats.css
create mode 100644 internal/glance/static/css/widget-docker-containers.css
create mode 100644 internal/glance/static/css/widget-group.css
create mode 100644 internal/glance/static/css/widget-markets.css
create mode 100644 internal/glance/static/css/widget-monitor.css
create mode 100644 internal/glance/static/css/widget-reddit.css
create mode 100644 internal/glance/static/css/widget-releases.css
create mode 100644 internal/glance/static/css/widget-rss.css
create mode 100644 internal/glance/static/css/widget-search.css
create mode 100644 internal/glance/static/css/widget-server-stats.css
create mode 100644 internal/glance/static/css/widget-twitch.css
create mode 100644 internal/glance/static/css/widget-videos.css
create mode 100644 internal/glance/static/css/widget-weather.css
create mode 100644 internal/glance/static/css/widgets.css
delete mode 100644 internal/glance/static/main.css
diff --git a/internal/glance/config.go b/internal/glance/config.go
index 779713d..f28548b 100644
--- a/internal/glance/config.go
+++ b/internal/glance/config.go
@@ -113,7 +113,7 @@ var configVariablePattern = regexp.MustCompile(`(^|.)\$\{(?:([a-zA-Z]+):)?([a-zA
// ${API_KEY} - gets replaced with the value of the API_KEY environment variable
// \${API_KEY} - escaped, gets used as is without the \ in the config
// ${secret:api_key} - value gets loaded from /run/secrets/api_key
-// ${loadFileFromEnv:PATH_TO_SECRET} - value gets loaded from the file path specified in the environment variable PATH_TO_SECRET
+// ${readFileFromEnv:PATH_TO_SECRET} - value gets loaded from the file path specified in the environment variable PATH_TO_SECRET
//
// TODO: don't match against commented out sections, not sure exactly how since
// variables can be placed anywhere and used to modify the YAML structure itself
diff --git a/internal/glance/embed.go b/internal/glance/embed.go
index 7bb07c9..e9ed009 100644
--- a/internal/glance/embed.go
+++ b/internal/glance/embed.go
@@ -1,13 +1,19 @@
package glance
import (
+ "bytes"
"crypto/md5"
"embed"
"encoding/hex"
+ "errors"
+ "fmt"
"io"
"io/fs"
"log"
+ "path/filepath"
+ "regexp"
"strconv"
+ "strings"
"time"
)
@@ -20,6 +26,19 @@ var _templateFS embed.FS
var staticFS, _ = fs.Sub(_staticFS, "static")
var templateFS, _ = fs.Sub(_templateFS, "templates")
+func readAllFromStaticFS(path string) ([]byte, error) {
+ // For some reason fs.FS only works with forward slashes, so in case we're
+ // running on Windows or pass paths with backslashes we need to replace them.
+ path = strings.ReplaceAll(path, "\\", "/")
+
+ file, err := staticFS.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ return io.ReadAll(file)
+}
+
var staticFSHash = func() string {
hash, err := computeFSHash(staticFS)
if err != nil {
@@ -60,3 +79,74 @@ func computeFSHash(files fs.FS) (string, error) {
return hex.EncodeToString(hash.Sum(nil))[:10], nil
}
+
+var cssImportPattern = regexp.MustCompile(`(?m)^@import "(.*?)";$`)
+var cssSingleLineCommentPattern = regexp.MustCompile(`(?m)^\s*\/\*.*?\*\/$`)
+var whitespaceAtBeginningOfLinePattern = regexp.MustCompile(`(?m)^\s+`)
+
+// Yes, we bundle at runtime, give comptime pls
+var bundledCSSContents = func() []byte {
+ const mainFilePath = "css/main.css"
+
+ var recursiveParseImports func(path string, depth int) ([]byte, error)
+ recursiveParseImports = func(path string, depth int) ([]byte, error) {
+ if depth > 20 {
+ return nil, errors.New("maximum import depth reached, is one of your imports circular?")
+ }
+
+ mainFileContents, err := readAllFromStaticFS(path)
+ if err != nil {
+ return nil, err
+ }
+
+ // Normalize line endings, otherwise the \r's make the regex not match
+ mainFileContents = bytes.ReplaceAll(mainFileContents, []byte("\r\n"), []byte("\n"))
+
+ mainFileDir := filepath.Dir(path)
+ var importLastErr error
+
+ parsed := cssImportPattern.ReplaceAllFunc(mainFileContents, func(match []byte) []byte {
+ if importLastErr != nil {
+ return nil
+ }
+
+ matches := cssImportPattern.FindSubmatch(match)
+ if len(matches) != 2 {
+ importLastErr = fmt.Errorf(
+ "import didn't return expected number of capture groups: %s, expected 2, got %d",
+ match, len(matches),
+ )
+ return nil
+ }
+
+ importFilePath := filepath.Join(mainFileDir, string(matches[1]))
+ importContents, err := recursiveParseImports(importFilePath, depth+1)
+ if err != nil {
+ importLastErr = err
+ return nil
+ }
+
+ return importContents
+ })
+
+ if importLastErr != nil {
+ return nil, importLastErr
+ }
+
+ return parsed, nil
+ }
+
+ contents, err := recursiveParseImports(mainFilePath, 0)
+ if err != nil {
+ panic(fmt.Sprintf("building CSS bundle: %v", err))
+ }
+
+ // We could strip a bunch more unnecessary characters, but the biggest
+ // win comes from removing the whitepsace at the beginning of lines
+ // since that's at least 4 bytes per property, which yielded a ~20% reduction.
+ contents = cssSingleLineCommentPattern.ReplaceAll(contents, nil)
+ contents = whitespaceAtBeginningOfLinePattern.ReplaceAll(contents, nil)
+ contents = bytes.ReplaceAll(contents, []byte("\n"), []byte(""))
+
+ return contents
+}()
diff --git a/internal/glance/glance.go b/internal/glance/glance.go
index 8fb3e40..5419176 100644
--- a/internal/glance/glance.go
+++ b/internal/glance/glance.go
@@ -20,6 +20,8 @@ var (
pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl")
)
+const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour
+
type application struct {
Version string
Config config
@@ -230,9 +232,23 @@ func (a *application) server() (func() error, func() error) {
mux.Handle(
fmt.Sprintf("GET /static/%s/{path...}", staticFSHash),
- http.StripPrefix("/static/"+staticFSHash, fileServerWithCache(http.FS(staticFS), 24*time.Hour)),
+ http.StripPrefix(
+ "/static/"+staticFSHash,
+ fileServerWithCache(http.FS(staticFS), STATIC_ASSETS_CACHE_DURATION),
+ ),
)
+ cssBundleCacheControlValue := fmt.Sprintf(
+ "public, max-age=%d",
+ int(STATIC_ASSETS_CACHE_DURATION.Seconds()),
+ )
+
+ mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Cache-Control", cssBundleCacheControlValue)
+ w.Header().Add("Content-Type", "text/css; charset=utf-8")
+ w.Write(bundledCSSContents)
+ })
+
var absAssetsPath string
if a.Config.Server.AssetsPath != "" {
absAssetsPath, _ = filepath.Abs(a.Config.Server.AssetsPath)
diff --git a/internal/glance/static/css/forum-posts.css b/internal/glance/static/css/forum-posts.css
new file mode 100644
index 0000000..e58ac6e
--- /dev/null
+++ b/internal/glance/static/css/forum-posts.css
@@ -0,0 +1,19 @@
+.forum-post-list-thumbnail {
+ flex-shrink: 0;
+ width: 6rem;
+ height: 4.1rem;
+ border-radius: var(--border-radius);
+ object-fit: cover;
+ border: 1px solid var(--color-separator);
+ margin-top: 0.1rem;
+}
+
+.forum-post-tags-container {
+ transform: translateY(-0.15rem);
+}
+
+@container widget (max-width: 550px) {
+ .forum-post-autohide {
+ display: none;
+ }
+}
diff --git a/internal/glance/static/css/main.css b/internal/glance/static/css/main.css
new file mode 100644
index 0000000..4bb3096
--- /dev/null
+++ b/internal/glance/static/css/main.css
@@ -0,0 +1,63 @@
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('../fonts/JetBrainsMono-Regular.woff2') format('woff2');
+}
+
+:root {
+ font-size: 10px;
+
+ --scheme: ;
+ --bgh: 240;
+ --bgs: 8%;
+ --bgl: 9%;
+ --bghs: var(--bgh), var(--bgs);
+ --cm: 1;
+ --tsm: 1;
+
+ --widget-gap: 23px;
+ --widget-content-vertical-padding: 15px;
+ --widget-content-horizontal-padding: 17px;
+ --widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding);
+ --content-bounds-padding: 15px;
+ --border-radius: 5px;
+ --mobile-navigation-height: 50px;
+
+ --color-primary: hsl(43, 50%, 70%);
+ --color-positive: var(--color-primary);
+ --color-negative: hsl(0, 70%, 70%);
+ --color-background: hsl(var(--bghs), var(--bgl));
+ --color-widget-background-hsl-values: var(--bghs), calc(var(--bgl) + 1%);
+ --color-widget-background: hsl(var(--color-widget-background-hsl-values));
+ --color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm))));
+ --color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
+ --color-widget-background-highlight: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
+ --color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 3%));
+ --color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 12%)));
+ --color-progress-border: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 10% * var(--cm))));
+ --color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 30% * var(--cm))));
+ --color-graph-gridlines: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 6% * var(--cm))));
+
+ --ths: var(--bgh), calc(var(--bgs) * var(--tsm));
+ --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%));
+ --color-text-paragraph: hsl(var(--ths), calc(var(--scheme) var(--cm) * 73%));
+ --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
+ --color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%));
+ --color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%));
+
+ --font-size-h1: 1.7rem;
+ --font-size-h2: 1.6rem;
+ --font-size-h3: 1.5rem;
+ --font-size-h4: 1.4rem;
+ --font-size-base: 1.3rem;
+ --font-size-h5: 1.2rem;
+ --font-size-h6: 1.1rem;
+}
+
+@import "site.css";
+@import "widgets.css";
+@import "popover.css";
+@import "mobile.css";
+@import "utils.css";
diff --git a/internal/glance/static/css/mobile.css b/internal/glance/static/css/mobile.css
new file mode 100644
index 0000000..da5024f
--- /dev/null
+++ b/internal/glance/static/css/mobile.css
@@ -0,0 +1,223 @@
+@media (max-width: 1190px) {
+ .header-container {
+ display: none;
+ }
+
+ .page-column-small .size-title-dynamic {
+ font-size: var(--font-size-h3);
+ }
+
+ .page-column-small {
+ width: 100%;
+ flex-shrink: 1;
+ }
+
+ .page-column {
+ display: none;
+ animation: columnEntrance .0s cubic-bezier(0.25, 1, 0.5, 1) backwards;
+ }
+
+ .page-columns-transitioned .page-column {
+ animation-duration: .3s;
+ }
+
+ @keyframes columnEntrance {
+ from {
+ opacity: 0;
+ transform: scaleX(0.95);
+ }
+ }
+
+ .mobile-navigation-offset {
+ height: var(--mobile-navigation-height);
+ flex-shrink: 0;
+ }
+
+ .mobile-navigation {
+ display: block;
+ position: fixed;
+ bottom: 0;
+ transform: translateY(calc(100% - var(--mobile-navigation-height)));
+ left: var(--content-bounds-padding);
+ right: var(--content-bounds-padding);
+ z-index: 10;
+ background-color: var(--color-widget-background);
+ border: 1px solid var(--color-widget-content-border);
+ border-bottom: 0;
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+ transition: transform .3s;
+ }
+
+ .mobile-navigation:has(.mobile-navigation-page-links-input:checked) .hamburger-icon {
+ --spacing: 7px;
+ color: var(--color-primary);
+ height: 2px;
+ }
+
+ .mobile-navigation:has(.mobile-navigation-page-links-input:checked) {
+ transform: translateY(0);
+ }
+
+ .mobile-navigation-page-links {
+ border-top: 1px solid var(--color-widget-content-border);
+ padding: 15px var(--content-bounds-padding);
+ display: flex;
+ align-items: center;
+ overflow-x: auto;
+ scrollbar-width: thin;
+ gap: 2.5rem;
+ }
+
+ .mobile-navigation-icons {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ }
+
+ body:has(.mobile-navigation-input[value="0"]:checked) .page-columns > :nth-child(1),
+ body:has(.mobile-navigation-input[value="1"]:checked) .page-columns > :nth-child(2),
+ body:has(.mobile-navigation-input[value="2"]:checked) .page-columns > :nth-child(3) {
+ display: block;
+ }
+
+ .mobile-navigation-label {
+ display: flex;
+ flex: 1;
+ max-width: 50px;
+ height: var(--mobile-navigation-height);
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ font-size: 15px;
+ line-height: var(--mobile-navigation-height);
+ }
+
+ .mobile-navigation-pill {
+ display: block;
+ background: var(--color-text-base);
+ height: 10px;
+ width: 10px;
+ border-radius: 10px;
+ transition: width .3s, background-color .3s;
+ }
+
+ .mobile-navigation-label:hover > .mobile-navigation-pill {
+ background-color: var(--color-text-highlight);
+ }
+
+ .mobile-navigation-label:hover {
+ color: var(--color-text-highlight);
+ }
+
+ .mobile-navigation-input:checked + .mobile-navigation-pill {
+ background: var(--color-primary);
+ width: 30px;
+ }
+
+ .mobile-navigation-input, .mobile-navigation-page-links-input {
+ display: none;
+ }
+
+ .hamburger-icon {
+ --spacing: 4px;
+ width: 1em;
+ height: 1px;
+ background-color: currentColor;
+ transition: color .3s, box-shadow .3s;
+ box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor;
+ }
+
+ .expand-toggle-button.container-expanded {
+ bottom: var(--mobile-navigation-height);
+ }
+
+ .cards-grid + .expand-toggle-button.container-expanded {
+ /* hides content that peeks through the rounded borders of the mobile navigation */
+ box-shadow: 0 var(--border-radius) 0 0 var(--color-background);
+ }
+
+ .weather-column-rain::before {
+ background-size: 7px 7px;
+ }
+
+ .ios .search-input {
+ /* so that iOS Safari does not zoom the page when the input is focused */
+ font-size: 16px;
+ }
+}
+
+@media (max-width: 1190px) and (display-mode: standalone) {
+ :root {
+ --safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
+ }
+
+ .ios .body-content {
+ height: 100dvh;
+ }
+
+ .expand-toggle-button.container-expanded {
+ bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom));
+ }
+
+ .mobile-navigation {
+ transform: translateY(calc(100% - var(--mobile-navigation-height) - var(--safe-area-inset-bottom)));
+ padding-bottom: var(--safe-area-inset-bottom);
+ }
+
+ .mobile-navigation-icons {
+ padding-bottom: var(--safe-area-inset-bottom);
+ transition: padding-bottom .3s;
+ }
+
+ .mobile-navigation-offset {
+ height: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom));
+ }
+
+ .mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) {
+ padding-bottom: 0;
+ }
+}
+
+@media (display-mode: standalone) {
+ body {
+ padding-top: env(safe-area-inset-top, 0);
+ }
+}
+
+@media (max-width: 550px) {
+ :root {
+ font-size: 9px;
+ --widget-gap: 15px;
+ --widget-content-vertical-padding: 10px;
+ --widget-content-horizontal-padding: 10px;
+ --content-bounds-padding: 10px;
+ }
+
+ .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
+
+ .row-reverse-on-mobile {
+ flex-direction: row-reverse;
+ }
+
+ .hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) {
+ display: none
+ }
+
+ .mobile-reachability-header {
+ display: block;
+ font-size: 3rem;
+ padding: 10vh 1rem;
+ text-align: center;
+ color: var(--color-text-highlight);
+ animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
+ }
+
+ .rss-detailed-thumbnail > * {
+ height: 6rem;
+ }
+
+ .rss-detailed-description {
+ line-clamp: 3;
+ -webkit-line-clamp: 3;
+ }
+}
diff --git a/internal/glance/static/css/popover.css b/internal/glance/static/css/popover.css
new file mode 100644
index 0000000..232f29c
--- /dev/null
+++ b/internal/glance/static/css/popover.css
@@ -0,0 +1,65 @@
+.popover-container, [data-popover-html] {
+ display: none;
+}
+
+.popover-container {
+ --triangle-size: 10px;
+ --triangle-offset: 50%;
+ --triangle-margin: calc(var(--triangle-size) + 3px);
+ --entrance-y-offset: 8px;
+ --entrance-direction: calc(var(--entrance-y-offset) * -1);
+
+ z-index: 20;
+ position: absolute;
+ padding-top: var(--triangle-margin);
+ padding-inline: var(--content-bounds-padding);
+}
+
+.popover-container.position-above {
+ --entrance-direction: var(--entrance-y-offset);
+ padding-top: 0;
+ padding-bottom: var(--triangle-margin);
+}
+
+.popover-frame {
+ --shadow-properties: 0 15px 20px -10px;
+ --shadow-color: hsla(var(--bghs), calc(var(--bgl) * 0.2), 0.5);
+ position: relative;
+ padding: 10px;
+ background: var(--color-popover-background);
+ border: 1px solid var(--color-popover-border);
+ border-radius: 5px;
+ animation: popoverFrameEntrance 0.3s backwards cubic-bezier(0.16, 1, 0.3, 1);
+ box-shadow: var(--shadow-properties) var(--shadow-color);
+}
+
+.popover-frame::before {
+ content: '';
+ position: absolute;
+ width: var(--triangle-size);
+ height: var(--triangle-size);
+ transform: rotate(45deg);
+ background-color: var(--color-popover-background);
+ border-top-left-radius: 2px;
+ border-left: 1px solid var(--color-popover-border);
+ border-top: 1px solid var(--color-popover-border);
+ left: calc(var(--triangle-offset) - (var(--triangle-size) / 2));
+ top: calc(var(--triangle-size) / 2 * -1 - 1px);
+}
+
+.popover-container.position-above .popover-frame::before {
+ transform: rotate(-135deg);
+ top: auto;
+ bottom: calc(var(--triangle-size) / 2 * -1 - 1px);
+}
+
+.popover-container.position-above .popover-frame {
+ --shadow-properties: 0 10px 20px -10px;
+}
+
+@keyframes popoverFrameEntrance {
+ from {
+ opacity: 0;
+ transform: translateY(var(--entrance-direction));
+ }
+}
diff --git a/internal/glance/static/css/site.css b/internal/glance/static/css/site.css
new file mode 100644
index 0000000..4604fc6
--- /dev/null
+++ b/internal/glance/static/css/site.css
@@ -0,0 +1,295 @@
+.light-scheme {
+ --scheme: 100% -;
+}
+
+.page {
+ height: 100%;
+ padding-block: var(--widget-gap);
+}
+
+.page-content, .page.content-ready .page-loading-container {
+ display: none;
+}
+
+.page.content-ready > .page-content {
+ display: block;
+}
+
+.page-column-small .size-title-dynamic {
+ font-size: var(--font-size-h4);
+}
+
+.page-column-full .size-title-dynamic {
+ font-size: var(--font-size-h3);
+}
+
+pre {
+ font: inherit;
+}
+
+::selection {
+ background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
+ color: var(--color-text-highlight);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--color-text-subdue);
+ border-radius: var(--border-radius);
+}
+
+::-webkit-scrollbar {
+ background: var(--color-background);
+ height: 5px;
+ width: 10px;
+}
+
+*:focus-visible {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 0.1rem;
+ border-radius: var(--border-radius);
+}
+
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+* {
+ padding: 0;
+ margin: 0;
+}
+
+hr {
+ border: 0;
+ height: 1px;
+ background-color: var(--color-separator);
+}
+
+img, svg {
+ display: block;
+ max-width: 100%;
+}
+
+img[loading=lazy].loaded:not(.finished-transition) {
+ transition: opacity .4s;
+}
+
+img[loading=lazy].cached:not(.finished-transition) {
+ transition: none;
+}
+
+img[loading=lazy]:not(.loaded, .cached) {
+ opacity: 0;
+}
+
+html {
+ scrollbar-color: var(--color-text-subdue) transparent;
+ scroll-behavior: smooth;
+}
+
+html, body, .body-content {
+ height: 100%;
+}
+
+h1, h2, h3, h4, h5 {
+ font: inherit;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+ overflow-wrap: break-word;
+}
+
+ul {
+ list-style: none;
+}
+
+body {
+ font-size: 1.3rem;
+ font-family: 'JetBrains Mono', monospace;
+ font-variant-ligatures: none;
+ line-height: 1.6;
+ color: var(--color-text-base);
+ background-color: var(--color-background);
+ overflow-y: scroll;
+}
+
+.page-column-small {
+ width: 300px;
+ flex-shrink: 0;
+}
+
+.page-column-full {
+ width: 100%;
+ min-width: 0;
+}
+
+.page-columns {
+ display: flex;
+ gap: var(--widget-gap);
+ animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
+}
+
+@keyframes pageColumnsEntrance {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+}
+
+.page-loading-container {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: loadingContainerEntrance 200ms backwards;
+ animation-delay: 150ms;
+ font-size: 2rem;
+}
+
+.page-loading-container > .loading-icon {
+ translate: 0 -250%;
+}
+
+@keyframes loadingContainerEntrance {
+ from {
+ opacity: 0;
+ }
+}
+
+.loading-icon {
+ min-width: 1.5em;
+ width: 1.5em;
+ height: 1.5em;
+ border: 0.25em solid hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 12%)));
+ border-top-color: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%)));
+ border-radius: 50%;
+ animation: loadingIconSpin 800ms infinite linear;
+}
+
+@keyframes loadingIconSpin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.notice-icon {
+ width: 0.7rem;
+ height: 0.7rem;
+ border-radius: 50%;
+}
+
+.notice-icon-major {
+ background: var(--color-negative);
+}
+
+.notice-icon-minor {
+ border: 1px solid var(--color-negative);
+}
+
+kbd {
+ font: inherit;
+ padding: 0.1rem 0.8rem;
+ border-radius: var(--border-radius);
+ border: 2px solid var(--color-widget-background-highlight);
+ box-shadow: 0 2px 0 var(--color-widget-background-highlight);
+ user-select: none;
+ transition: transform .1s, box-shadow .1s;
+ font-size: var(--font-size-h5);
+ cursor: pointer;
+}
+
+kbd:active {
+ transform: translateY(2px);
+ box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
+}
+
+.content-bounds {
+ max-width: 1600px;
+ width: 100%;
+ margin-inline: auto;
+ padding: 0 var(--content-bounds-padding);
+}
+
+.page-width-wide .content-bounds {
+ max-width: 1920px;
+}
+
+.page-width-slim .content-bounds {
+ max-width: 1100px;
+}
+
+.page-center-vertically .page {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+}
+
+.header-container {
+ margin-top: calc(var(--widget-gap) / 2);
+ --header-height: 45px;
+ --header-items-gap: 2.5rem;
+}
+
+.header {
+ display: flex;
+ height: var(--header-height);
+ gap: var(--header-items-gap);
+}
+
+.logo {
+ height: 100%;
+ line-height: var(--header-height);
+ font-size: 2rem;
+ color: var(--color-text-highlight);
+ border-right: 1px solid var(--color-widget-content-border);
+ padding-right: var(--widget-content-horizontal-padding);
+}
+
+.logo:has(img) {
+ display: flex;
+ align-items: center;
+}
+
+.logo img {
+ max-height: 2.7rem;
+}
+
+.nav {
+ height: 100%;
+ gap: var(--header-items-gap);
+}
+
+.nav .nav-item {
+ line-height: var(--header-height);
+}
+
+.footer {
+ padding-bottom: calc(var(--widget-gap) * 1.5);
+ padding-top: calc(var(--widget-gap) / 2);
+ animation: loadingContainerEntrance 200ms backwards;
+ animation-delay: 150ms;
+}
+
+.mobile-navigation, .mobile-reachability-header {
+ display: none;
+}
+
+.nav-item {
+ display: block;
+ height: 100%;
+ border-bottom: 2px solid transparent;
+ transition: color .3s, border-color .3s;
+ font-size: var(--font-size-h3);
+ flex-shrink: 0;
+}
+
+.nav-item:not(.nav-item-current):hover {
+ border-bottom-color: var(--color-text-subdue);
+ color: var(--color-text-highlight);
+}
+
+.nav-item.nav-item-current {
+ border-bottom-color: var(--color-primary);
+ color: var(--color-text-highlight);
+}
diff --git a/internal/glance/static/css/utils.css b/internal/glance/static/css/utils.css
new file mode 100644
index 0000000..42aef5a
--- /dev/null
+++ b/internal/glance/static/css/utils.css
@@ -0,0 +1,561 @@
+.masonry {
+ display: flex;
+ gap: var(--widget-gap);
+}
+
+.masonry-column {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.widget-small-content-bounds {
+ max-width: 350px;
+ margin: 0 auto;
+}
+
+.visually-hidden {
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+
+.list-horizontal-text {
+ display: flex;
+ list-style: none;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.list-horizontal-text > *:not(:last-child)::after {
+ content: '•' / "";
+ color: var(--color-text-subdue);
+ margin: 0 0.4rem;
+ position: relative;
+ top: 0.1rem;
+}
+
+.summary {
+ width: 100%;
+ cursor: pointer;
+ word-spacing: -0.18em;
+ user-select: none;
+ list-style: none;
+ position: relative;
+ display: flex;
+ z-index: 1;
+}
+
+.summary::-webkit-details-marker {
+ display: none;
+}
+
+.details[open] .summary {
+ margin-bottom: .8rem;
+}
+
+.summary::before {
+ content: "";
+ position: absolute;
+ inset: -.3rem -.8rem;
+ border-radius: var(--border-radius);
+ background-color: var(--color-widget-background-highlight);
+ opacity: 0;
+ transition: opacity 0.2s;
+ z-index: -1;
+}
+
+.details[open] .summary::before, .summary:hover::before {
+ opacity: 1;
+}
+
+.details:not([open]) .list-with-transition {
+ display: none;
+}
+
+.summary::after {
+ content: "◀" / "";
+ font-size: 1.2em;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ line-height: 1.3em;
+ right: 0;
+ transition: rotate .5s cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+details[open] .summary::after {
+ rotate: -90deg;
+}
+
+/* TODO: refactor, otherwise I hope I never have to change dynamic columns again */
+.dynamic-columns {
+ --list-half-gap: 0.5rem;
+ gap: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding);
+ display: grid;
+ grid-template-columns: repeat(var(--columns-per-row), 1fr);
+}
+
+.dynamic-columns > * {
+ padding-left: var(--widget-content-horizontal-padding);
+ border-left: 1px solid var(--color-separator);
+ min-width: 0;
+}
+
+.dynamic-columns > *:first-child {
+ padding-top: 0;
+ border-top: none;
+ border-left: none;
+}
+
+.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
+.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
+.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
+.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
+.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
+
+@container widget (max-width: 599px) {
+ .dynamic-columns { gap: 0; }
+ .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
+ .dynamic-columns > * {
+ border-left: none;
+ padding-left: 0;
+ }
+ .dynamic-columns > *:not(:first-child) {
+ margin-top: calc(var(--list-half-gap) * 2);
+ }
+ .dynamic-columns.list-with-separator > *:not(:first-child) {
+ margin-top: var(--list-half-gap);
+ border-top: 1px solid var(--color-separator);
+ padding-top: var(--list-half-gap);
+ }
+}
+@container widget (min-width: 600px) and (max-width: 849px) {
+ .dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
+ .dynamic-columns > :nth-child(2n-1) {
+ border-left: none;
+ padding-left: 0;
+ }
+}
+@container widget (min-width: 850px) and (max-width: 1249px) {
+ .dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
+ .dynamic-columns > :nth-child(3n+1) {
+ border-left: none;
+ padding-left: 0;
+ }
+}
+@container widget (min-width: 1250px) and (max-width: 1499px) {
+ .dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
+ .dynamic-columns > :nth-child(4n+1) {
+ border-left: none;
+ padding-left: 0;
+ }
+}
+@container widget (min-width: 1500px) {
+ .dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
+ .dynamic-columns > :nth-child(5n+1) {
+ border-left: none;
+ padding-left: 0;
+ }
+}
+
+.cards-vertical {
+ flex-direction: column;
+}
+
+.cards-horizontal {
+ --cards-per-row: 6.5;
+}
+
+.cards-horizontal, .cards-vertical {
+ --cards-gap: calc(var(--widget-content-vertical-padding) * 0.7);
+ display: flex;
+ gap: var(--cards-gap);
+}
+
+.card {
+ display: flex;
+ flex-direction: column;
+}
+
+.cards-horizontal .card {
+ flex-shrink: 0;
+ width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
+}
+
+.cards-grid .card {
+ min-width: 0;
+}
+
+.cards-horizontal {
+ overflow-x: auto;
+ scrollbar-width: thin;
+ padding-bottom: 1rem;
+}
+
+.cards-grid {
+ --cards-per-row: 6;
+ display: grid;
+ grid-template-columns: repeat(var(--cards-per-row), 1fr);
+ gap: calc(var(--widget-content-vertical-padding) * 0.7);
+}
+
+@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } }
+@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } }
+@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } }
+@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } }
+@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } }
+@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } }
+
+@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } }
+@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } }
+@container widget (max-width: 850px) { .cards-grid { --cards-per-row: 3; } }
+@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } }
+@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } }
+
+.text-truncate,
+.single-line-titles .title
+{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.single-line-titles .title {
+ display: block;
+}
+
+.text-truncate-2-lines, .text-truncate-3-lines {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+}
+
+.text-truncate-3-lines { line-clamp: 3; -webkit-line-clamp: 3; }
+.text-truncate-2-lines { line-clamp: 2; -webkit-line-clamp: 2; }
+
+.visited-indicator:not(.text-truncate)::after,
+.visited-indicator.text-truncate::before {
+ content: '↗' / "";
+ margin-left: 0.5em;
+ display: inline-block;
+ position: relative;
+ top: 0.15em;
+ color: var(--color-text-base);
+}
+
+.visited-indicator.text-truncate {
+ direction: rtl;
+ text-align: left;
+}
+
+.visited-indicator:not(:visited)::before, .visited-indicator:not(:visited)::after {
+ color: var(--color-primary);
+}
+
+.page-columns-transitioned .list-with-transition > * { animation: collapsibleItemReveal .25s backwards; }
+.list-with-transition > *:nth-child(2) { animation-delay: 30ms; }
+.list-with-transition > *:nth-child(3) { animation-delay: 60ms; }
+.list-with-transition > *:nth-child(4) { animation-delay: 90ms; }
+.list-with-transition > *:nth-child(5) { animation-delay: 120ms; }
+.list-with-transition > *:nth-child(6) { animation-delay: 150ms; }
+.list-with-transition > *:nth-child(7) { animation-delay: 180ms; }
+.list-with-transition > *:nth-child(8) { animation-delay: 210ms; }
+
+.list > *:not(:first-child) {
+ margin-top: calc(var(--list-half-gap) * 2);
+}
+
+.list.list-with-separator > *:not(:first-child) {
+ margin-top: var(--list-half-gap);
+ border-top: 1px solid var(--color-separator);
+ padding-top: var(--list-half-gap);
+}
+
+.collapsible-container:not(.container-expanded) > .collapsible-item {
+ display: none;
+}
+
+.collapsible-item {
+ animation: collapsibleItemReveal .25s backwards;
+}
+
+@keyframes collapsibleItemReveal {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+}
+
+.expand-toggle-button {
+ font: inherit;
+ border: 0;
+ cursor: pointer;
+ display: block;
+ width: 100%;
+ text-align: left;
+ color: var(--color-text-base);
+ text-transform: uppercase;
+ font-size: var(--font-size-h4);
+ padding: var(--widget-content-vertical-padding) 0;
+ background: var(--color-widget-background);
+}
+
+.expand-toggle-button.container-expanded {
+ position: sticky;
+ /* -1px to hide 1px gap on chrome */
+ bottom: -1px;
+}
+
+.expand-toggle-button-icon {
+ display: inline-block;
+ margin-left: 1rem;
+ position: relative;
+ top: -.2rem;
+}
+
+.expand-toggle-button-icon::before {
+ content: '' / "";
+ font-size: 0.8rem;
+ transform: rotate(90deg);
+ line-height: 1;
+ display: inline-block;
+ transition: transform 0.3s;
+}
+
+.expand-toggle-button.container-expanded .expand-toggle-button-icon::before {
+ transform: rotate(-90deg);
+}
+
+.cards-grid.collapsible-container + .expand-toggle-button {
+ text-align: center;
+ margin-top: 0.5rem;
+ background-color: var(--color-background);
+}
+
+.widget-content:has(.expand-toggle-button:last-child) {
+ padding-bottom: 0;
+}
+
+.carousel-container {
+ position: relative;
+}
+
+.carousel-container::before, .carousel-container::after {
+ content: '';
+ position: absolute;
+ width: 2rem;
+ top: 0;
+ bottom: 1rem;
+ z-index: 10;
+ opacity: 0;
+ pointer-events: none;
+ transition-duration: 0.2s;
+}
+
+.carousel-container::before {
+ background: linear-gradient(to right, var(--color-background), transparent);
+}
+
+.carousel-container::after {
+ right: 0;
+ background: linear-gradient(to left, var(--color-background), transparent);
+}
+
+.carousel-container.show-left-cutoff::before, .carousel-container.show-right-cutoff::after {
+ opacity: 1;
+}
+
+.attachments {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+:root:not(.light-scheme) .flat-icon {
+ filter: invert(1);
+}
+
+.attachments > * {
+ border-radius: var(--border-radius);
+ padding: 0.1rem 0.5rem;
+ font-size: var(--font-size-h6);
+ background-color: var(--color-separator);
+}
+
+.progress-bar {
+ border: 1px solid var(--color-progress-border);
+ border-radius: var(--border-radius);
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: 2px;
+ height: 1.5rem;
+ /* naughty, but oh so beautiful */
+ margin-inline: -3px;
+}
+
+.progress-bar-combined {
+ height: 3rem;
+}
+
+.popover-active > .progress-bar {
+ transition: border-color .3s;
+ border-color: var(--color-text-subdue);
+}
+
+.progress-value {
+ --half-border-radius: calc(var(--border-radius) / 2);
+ border-radius: 0 var(--half-border-radius) var(--half-border-radius) 0;
+ background: var(--color-progress-value);
+ width: calc(var(--percent) * 1%);
+ min-width: 1px;
+ flex: 1;
+}
+
+.progress-value:first-child {
+ border-top-left-radius: var(--half-border-radius);
+}
+
+.progress-value:last-child {
+ border-bottom-left-radius: var(--half-border-radius);
+}
+
+.progress-value-notice {
+ background: linear-gradient(to right, var(--color-progress-value) 65%, var(--color-negative));
+}
+
+.value-separator {
+ min-width: 2rem;
+ margin-inline: 0.8rem;
+ flex: 1;
+ height: calc(1em * 1.1);
+ border-bottom: 1px dotted var(--color-text-subdue);
+}
+
+.thumbnail {
+ filter: grayscale(0.2) contrast(0.9);
+ opacity: 0.8;
+ transition: filter 0.2s, opacity .2s;
+}
+
+.thumbnail-container {
+ flex-shrink: 0;
+ border: 1px solid var(--color-separator);
+ border-radius: var(--border-radius);
+}
+
+.thumbnail-container > * {
+ border-radius: var(--border-radius);
+ object-fit: cover;
+}
+
+.thumbnail-parent:hover .thumbnail {
+ opacity: 1;
+ filter: none;
+}
+
+.size-h1 { font-size: var(--font-size-h1); }
+.size-h2 { font-size: var(--font-size-h2); }
+.size-h3 { font-size: var(--font-size-h3); }
+.size-h4 { font-size: var(--font-size-h4); }
+.size-base { font-size: var(--font-size-base); }
+.size-h5 { font-size: var(--font-size-h5); }
+.size-h6 { font-size: var(--font-size-h6); }
+
+.color-highlight { color: var(--color-text-highlight); }
+.color-paragraph { color: var(--color-text-paragraph); }
+.color-base { color: var(--color-text-base); }
+.color-subdue { color: var(--color-text-subdue); }
+.color-negative { color: var(--color-negative); }
+.color-positive { color: var(--color-positive); }
+.color-primary { color: var(--color-primary); }
+
+.color-primary-if-not-visited:not(:visited) {
+ color: var(--color-primary);
+}
+
+.cursor-help { cursor: help; }
+.break-all { word-break: break-all; }
+.text-left { text-align: left; }
+.text-right { text-align: right; }
+.text-center { text-align: center; }
+.text-elevate { margin-top: -0.2em; }
+.text-compact { word-spacing: -0.18em; }
+.text-very-compact { word-spacing: -0.35em; }
+.rtl { direction: rtl; }
+.shrink { flex-shrink: 1; }
+.shrink-0 { flex-shrink: 0; }
+.min-width-0 { min-width: 0; }
+.max-width-100 { max-width: 100%; }
+.block { display: block; }
+.inline-block { display: inline-block; }
+.overflow-hidden { overflow: hidden; }
+.relative { position: relative; }
+.flex { display: flex; }
+.flex-1 { flex: 1; }
+.flex-wrap { flex-wrap: wrap; }
+.flex-nowrap { flex-wrap: nowrap; }
+.justify-between { justify-content: space-between; }
+.justify-stretch { justify-content: stretch; }
+.justify-evenly { justify-content: space-evenly; }
+.justify-center { justify-content: center; }
+.justify-end { justify-content: end; }
+.uppercase { text-transform: uppercase; }
+.grow { flex-grow: 1; }
+.flex-column { flex-direction: column; }
+.items-center { align-items: center; }
+.items-start { align-items: start; }
+.items-end { align-items: end; }
+.gap-5 { gap: 0.5rem; }
+.gap-7 { gap: 0.7rem; }
+.gap-10 { gap: 1rem; }
+.gap-12 { gap: 1.2rem; }
+.gap-15 { gap: 1.5rem; }
+.gap-20 { gap: 2rem; }
+.gap-25 { gap: 2.5rem; }
+.gap-35 { gap: 3.5rem; }
+.gap-45 { gap: 4.5rem; }
+.gap-55 { gap: 5.5rem; }
+.margin-left-auto { margin-left: auto; }
+.margin-top-3 { margin-top: 0.3rem; }
+.margin-top-5 { margin-top: 0.5rem; }
+.margin-top-7 { margin-top: 0.7rem; }
+.margin-top-10 { margin-top: 1rem; }
+.margin-top-15 { margin-top: 1.5rem; }
+.margin-top-20 { margin-top: 2rem; }
+.margin-top-25 { margin-top: 2.5rem; }
+.margin-top-35 { margin-top: 3.5rem; }
+.margin-top-40 { margin-top: 4rem; }
+.margin-top-auto { margin-top: auto; }
+.margin-block-3 { margin-block: 0.3rem; }
+.margin-block-5 { margin-block: 0.5rem; }
+.margin-block-7 { margin-block: 0.7rem; }
+.margin-block-8 { margin-block: 0.8rem; }
+.margin-block-10 { margin-block: 1rem; }
+.margin-block-15 { margin-block: 1.5rem; }
+.margin-bottom-3 { margin-bottom: 0.3rem; }
+.margin-bottom-5 { margin-bottom: 0.5rem; }
+.margin-bottom-7 { margin-bottom: 0.7rem; }
+.margin-bottom-10 { margin-bottom: 1rem; }
+.margin-bottom-15 { margin-bottom: 1.5rem; }
+.margin-bottom-auto { margin-bottom: auto; }
+.margin-bottom-widget { margin-bottom: var(--widget-content-vertical-padding); }
+.padding-widget { padding: var(--widget-content-padding); }
+.padding-block-widget { padding-block: var(--widget-content-vertical-padding); }
+.padding-inline-widget { padding-inline: var(--widget-content-horizontal-padding); }
+.padding-block-5 { padding-block: 0.5rem; }
+.scale-half { transform: scale(0.5); }
+.list { --list-half-gap: 0rem; }
+.list-gap-2 { --list-half-gap: 0.1rem; }
+.list-gap-4 { --list-half-gap: 0.2rem; }
+.list-gap-8 { --list-half-gap: 0.4rem; }
+.list-gap-10 { --list-half-gap: 0.5rem; }
+.list-gap-14 { --list-half-gap: 0.7rem; }
+.list-gap-20 { --list-half-gap: 1rem; }
+.list-gap-24 { --list-half-gap: 1.2rem; }
+.list-gap-34 { --list-half-gap: 1.7rem; }
diff --git a/internal/glance/static/css/widget-bookmarks.css b/internal/glance/static/css/widget-bookmarks.css
new file mode 100644
index 0000000..7f2dabd
--- /dev/null
+++ b/internal/glance/static/css/widget-bookmarks.css
@@ -0,0 +1,31 @@
+.bookmarks-group {
+ --bookmarks-group-color: var(--color-primary);
+}
+
+.bookmarks-group-title {
+ color: var(--bookmarks-group-color);
+}
+
+.bookmarks-link:not(.bookmarks-link-no-arrow)::after {
+ content: '↗' / "";
+ margin-left: 0.5em;
+ display: inline-block;
+ position: relative;
+ top: 0.15em;
+ color: var(--bookmarks-group-color);
+}
+
+.bookmarks-icon-container {
+ margin-block: 0.1rem;
+ background-color: var(--color-widget-background-highlight);
+ border-radius: var(--border-radius);
+ padding: 0.5rem;
+ opacity: 0.7;
+ flex-shrink: 0;
+}
+
+.bookmarks-icon {
+ width: 20px;
+ height: 20px;
+ opacity: 0.8;
+}
diff --git a/internal/glance/static/css/widget-calendar.css b/internal/glance/static/css/widget-calendar.css
new file mode 100644
index 0000000..f9b0d9d
--- /dev/null
+++ b/internal/glance/static/css/widget-calendar.css
@@ -0,0 +1,71 @@
+.old-calendar-day {
+ width: calc(100% / 7);
+ text-align: center;
+ padding: 0.6rem 0;
+}
+
+.old-calendar-day-today {
+ border-radius: var(--border-radius);
+ background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%)));
+ color: var(--color-text-highlight);
+}
+
+.calendar-dates {
+ text-align: center;
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 2px;
+}
+
+.calendar-date {
+ padding: 0.4rem 0;
+ color: var(--color-text-base);
+ position: relative;
+ border-radius: var(--border-radius);
+ background: none;
+ border: none;
+ font: inherit;
+}
+
+.calendar-current-date {
+ border-radius: var(--border-radius);
+ background-color: var(--color-popover-border);
+ color: var(--color-text-highlight);
+}
+
+.calendar-spillover-date {
+ color: var(--color-text-subdue);
+}
+
+.calendar-header-button {
+ position: relative;
+ cursor: pointer;
+ width: 2rem;
+ height: 2rem;
+ z-index: 1;
+ background: none;
+ border: none;
+}
+
+.calendar-header-button::before {
+ content: '';
+ position: absolute;
+ inset: -0.2rem;
+ border-radius: var(--border-radius);
+ background-color: var(--color-text-subdue);
+ opacity: 0;
+ transition: opacity 0.2s;
+ z-index: -1;
+}
+
+.calendar-header-button:hover::before {
+ opacity: 0.4;
+}
+
+.calendar-undo-button {
+ display: inline-block;
+ vertical-align: text-top;
+ width: 2rem;
+ height: 2rem;
+ margin-left: 0.7rem;
+}
diff --git a/internal/glance/static/css/widget-clock.css b/internal/glance/static/css/widget-clock.css
new file mode 100644
index 0000000..47a1a2f
--- /dev/null
+++ b/internal/glance/static/css/widget-clock.css
@@ -0,0 +1,7 @@
+.clock-time {
+ min-width: 8ch;
+}
+
+.clock-time span {
+ color: var(--color-text-highlight);
+}
diff --git a/internal/glance/static/css/widget-dns-stats.css b/internal/glance/static/css/widget-dns-stats.css
new file mode 100644
index 0000000..e94d349
--- /dev/null
+++ b/internal/glance/static/css/widget-dns-stats.css
@@ -0,0 +1,120 @@
+.dns-stats-totals {
+ transition: opacity .3s;
+ transition-delay: 50ms;
+}
+
+.dns-stats:has(.dns-stats-graph .popover-active) .dns-stats-totals {
+ opacity: 0.1;
+ transition-delay: 0s;
+}
+
+.dns-stats-graph {
+ --graph-height: 70px;
+ height: var(--graph-height);
+ position: relative;
+ margin-bottom: 2.5rem;
+}
+
+.dns-stats-graph-gridlines-container {
+ position: absolute;
+ inset: 0;
+}
+
+.dns-stats-graph-gridlines {
+ height: 100%;
+ width: 100%;
+}
+
+.dns-stats-graph-columns {
+ display: flex;
+ height: 100%;
+}
+
+.dns-stats-graph-column {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ flex-direction: column;
+ width: calc(100% / 8);
+ position: relative;
+}
+
+.dns-stats-graph-column::before {
+ content: '';
+ position: absolute;
+ inset: 1px 0;
+ opacity: 0;
+ background: var(--color-text-base);
+ transition: opacity .2s;
+}
+
+.dns-stats-graph-column:hover::before {
+ opacity: 0.05;
+}
+
+.dns-stats-graph-bar {
+ width: 14px;
+ height: calc((var(--bar-height) / 100) * var(--graph-height));
+ border: 1px solid var(--color-progress-border);
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+ display: flex;
+ background: var(--color-widget-background);
+ padding: 2px 2px 0 2px;
+ flex-direction: column;
+ gap: 2px;
+ transition: border-color .2s;
+ min-height: 10px;
+}
+
+.dns-stats-graph-column.popover-active .dns-stats-graph-bar {
+ border-color: var(--color-text-subdue);
+ border-bottom-color: var(--color-progress-border);
+}
+
+.dns-stats-graph-bar > * {
+ border-radius: 2px;
+ background: var(--color-progress-value);
+ min-height: 1px;
+}
+
+.dns-stats-graph-bar > .queries {
+ flex-grow: 1;
+}
+
+.dns-stats-graph-bar > *:last-child {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.dns-stats-graph-bar > .blocked {
+ background-color: var(--color-negative);
+ flex-basis: calc(var(--percent) - 1px);
+}
+
+.dns-stats-graph-column:nth-child(even) .dns-stats-graph-time {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.dns-stats-graph-time, .dns-stats-graph-columns:hover .dns-stats-graph-time {
+ position: absolute;
+ font-size: var(--font-size-h6);
+ inset-inline: 0;
+ text-align: center;
+ height: 2.5rem;
+ line-height: 2.5rem;
+ top: 100%;
+ user-select: none;
+ opacity: 0;
+ transform: translateY(-0.5rem);
+ transition: opacity .2s, transform .2s;
+}
+
+.dns-stats-graph-column:hover .dns-stats-graph-time {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.dns-stats-graph-columns:hover .dns-stats-graph-column:not(:hover) .dns-stats-graph-time {
+ opacity: 0;
+}
diff --git a/internal/glance/static/css/widget-docker-containers.css b/internal/glance/static/css/widget-docker-containers.css
new file mode 100644
index 0000000..ae08788
--- /dev/null
+++ b/internal/glance/static/css/widget-docker-containers.css
@@ -0,0 +1,26 @@
+.docker-container-icon {
+ display: block;
+ filter: grayscale(0.4);
+ object-fit: contain;
+ aspect-ratio: 1 / 1;
+ width: 2.7rem;
+ opacity: 0.8;
+ transition: filter 0.3s, opacity 0.3s;
+}
+
+.docker-container-icon.flat-icon {
+ opacity: 0.7;
+}
+
+.docker-container:hover .docker-container-icon {
+ opacity: 1;
+}
+
+.docker-container:hover .docker-container-icon:not(.flat-icon) {
+ filter: grayscale(0);
+}
+
+.docker-container-status-icon {
+ width: 2rem;
+ height: 2rem;
+}
diff --git a/internal/glance/static/css/widget-group.css b/internal/glance/static/css/widget-group.css
new file mode 100644
index 0000000..378853b
--- /dev/null
+++ b/internal/glance/static/css/widget-group.css
@@ -0,0 +1,49 @@
+.widget-group-header {
+ overflow-x: auto;
+ scrollbar-width: thin;
+}
+
+.widget-group-title {
+ background: none;
+ font: inherit;
+ border: none;
+ text-transform: uppercase;
+ border-bottom: 1px dotted transparent;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: color .3s, border-color .3s;
+ color: var(--color-text-subdue);
+ line-height: calc(1.6em - 1px);
+}
+
+.widget-group-title:hover:not(.widget-group-title-current) {
+ color: var(--color-text-base);
+}
+
+.widget-group-title-current {
+ border-bottom-color: var(--color-text-base-muted);
+ color: var(--color-text-base);
+}
+
+.widget-group-content {
+ animation: widgetGroupContentEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
+}
+
+.widget-group-content[data-direction="right"] {
+ --direction: 5px;
+}
+
+.widget-group-content[data-direction="left"] {
+ --direction: -5px;
+}
+
+@keyframes widgetGroupContentEntrance {
+ from {
+ opacity: 0;
+ transform: translateX(var(--direction));
+ }
+}
+
+.widget-group-content:not(.widget-group-content-current) {
+ display: none;
+}
diff --git a/internal/glance/static/css/widget-markets.css b/internal/glance/static/css/widget-markets.css
new file mode 100644
index 0000000..f670a25
--- /dev/null
+++ b/internal/glance/static/css/widget-markets.css
@@ -0,0 +1,13 @@
+.market-chart {
+ margin-left: auto;
+ width: 6.5rem;
+ flex-shrink: 0;
+}
+
+.market-chart svg {
+ width: 100%;
+}
+
+.market-values {
+ min-width: 8rem;
+}
diff --git a/internal/glance/static/css/widget-monitor.css b/internal/glance/static/css/widget-monitor.css
new file mode 100644
index 0000000..8bc629b
--- /dev/null
+++ b/internal/glance/static/css/widget-monitor.css
@@ -0,0 +1,36 @@
+.monitor-site-icon {
+ display: block;
+ opacity: 0.8;
+ filter: grayscale(0.4);
+ object-fit: contain;
+ aspect-ratio: 1 / 1;
+ width: 3.2rem;
+ position: relative;
+ top: -0.1rem;
+ transition: filter 0.3s, opacity 0.3s;
+}
+
+.monitor-site-icon.flat-icon {
+ opacity: 0.7;
+}
+
+.monitor-site:hover .monitor-site-icon {
+ opacity: 1;
+}
+
+.monitor-site:hover .monitor-site-icon:not(.flat-icon) {
+ filter: grayscale(0);
+}
+
+.monitor-site-status-icon {
+ flex-shrink: 0;
+ margin-left: auto;
+ width: 2rem;
+ height: 2rem;
+}
+
+.monitor-site-status-icon-compact {
+ width: 1.8rem;
+ height: 1.8rem;
+ flex-shrink: 0;
+}
diff --git a/internal/glance/static/css/widget-reddit.css b/internal/glance/static/css/widget-reddit.css
new file mode 100644
index 0000000..5b62e0e
--- /dev/null
+++ b/internal/glance/static/css/widget-reddit.css
@@ -0,0 +1,22 @@
+.reddit-card-thumbnail {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: 0% 20%;
+ opacity: 0.15;
+ filter: blur(1px);
+}
+
+.reddit-card-thumbnail-container {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ border-radius: var(--border-radius);
+}
+
+.reddit-card-thumbnail-container::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent);
+}
diff --git a/internal/glance/static/css/widget-releases.css b/internal/glance/static/css/widget-releases.css
new file mode 100644
index 0000000..7def4cf
--- /dev/null
+++ b/internal/glance/static/css/widget-releases.css
@@ -0,0 +1,6 @@
+.release-source-icon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ opacity: 0.4;
+}
diff --git a/internal/glance/static/css/widget-rss.css b/internal/glance/static/css/widget-rss.css
new file mode 100644
index 0000000..34b7114
--- /dev/null
+++ b/internal/glance/static/css/widget-rss.css
@@ -0,0 +1,56 @@
+.rss-card-image {
+ height: var(--rss-thumbnail-height, 10rem);
+ object-fit: cover;
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+.rss-card-2 {
+ position: relative;
+ height: var(--rss-card-height, 27rem);
+ overflow: hidden;
+}
+
+.rss-card-2::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background-image: linear-gradient(
+ 0deg,
+ var(--color-widget-background),
+ hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem
+ );
+ z-index: 2;
+}
+
+.rss-card-2-image {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ /* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */
+ border-radius: calc(var(--border-radius) + 1px);
+ opacity: 0.9;
+ z-index: 1;
+}
+
+.rss-card-2-content {
+ position: absolute;
+ inset-inline: 0;
+ bottom: var(--widget-content-vertical-padding);
+ z-index: 3;
+}
+
+.rss-detailed-description {
+ max-width: 55rem;
+ color: var(--color-text-base-muted);
+}
+
+.rss-detailed-thumbnail {
+ margin-top: 0.3rem;
+}
+
+.rss-detailed-thumbnail > * {
+ aspect-ratio: 3 / 2;
+ height: 8.7rem;
+}
diff --git a/internal/glance/static/css/widget-search.css b/internal/glance/static/css/widget-search.css
new file mode 100644
index 0000000..ebf5cbb
--- /dev/null
+++ b/internal/glance/static/css/widget-search.css
@@ -0,0 +1,79 @@
+.search-icon {
+ width: 2.3rem;
+}
+
+.search-icon-container {
+ position: relative;
+ flex-shrink: 0;
+}
+
+/* gives a wider hit area for the 3 people that will notice the animation : ) */
+.search-icon-container::before {
+ content: '';
+ position: absolute;
+ inset: -1rem;
+}
+
+.search-icon-container:hover > .search-icon {
+ animation: searchIconHover 2.9s forwards;
+}
+
+@keyframes searchIconHover {
+ 0%, 39% { translate: 0 0; }
+ 20% { scale: 1.3; }
+ 40% { scale: 1; }
+ 50% { translate: -30% 30%; }
+ 70% { translate: 30% -30%; }
+ 90% { translate: -30% -30%; }
+ 100% { translate: 0 0; }
+}
+
+.search {
+ transition: border-color .2s;
+ position: relative;
+}
+
+.search:hover {
+ border-color: var(--color-text-subdue);
+}
+
+.search:focus-within {
+ border-color: var(--color-primary);
+}
+
+.search-input {
+ border: 0;
+ background: none;
+ width: 100%;
+ height: 6rem;
+ font: inherit;
+ outline: none;
+ color: var(--color-text-highlight);
+}
+
+.search-input::placeholder {
+ color: var(--color-text-base-muted);
+ opacity: 1;
+}
+
+.search-bangs { display: none; }
+
+.search-bang {
+ border-radius: calc(var(--border-radius) * 2);
+ background: var(--color-widget-background-highlight);
+ padding: 0.3rem 1rem;
+ flex-shrink: 0;
+ font-size: var(--font-size-h5);
+ animation: searchBangsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
+}
+
+@keyframes searchBangsEntrance {
+ 0% {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+}
+
+.search-bang:empty {
+ display: none;
+}
diff --git a/internal/glance/static/css/widget-server-stats.css b/internal/glance/static/css/widget-server-stats.css
new file mode 100644
index 0000000..11f9ab5
--- /dev/null
+++ b/internal/glance/static/css/widget-server-stats.css
@@ -0,0 +1,81 @@
+.widget-type-server-info {
+ position: relative;
+}
+
+.server + .server {
+ margin-top: 3rem;
+}
+
+.server {
+ gap: 1rem;
+ display: flex;
+ flex-direction: column;
+}
+
+.server-info {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ gap: 1.5rem;
+ flex-shrink: 1;
+ min-width: 0;
+}
+
+.server-details {
+ min-width: 0;
+}
+
+.server-icon {
+ height: 3rem;
+ width: 3rem;
+}
+
+.server-spicy-cpu-icon {
+ height: 1em;
+ align-self: center;
+ margin-left: 0.4em;
+ margin-bottom: 0.2rem;
+}
+
+.server-stats {
+ display: flex;
+ gap: 1.5rem;
+ margin-top: 0.5rem;
+}
+
+.server-stat-unavailable {
+ opacity: 0.5;
+}
+
+@container widget (min-width: 650px) {
+ .server {
+ gap: 2rem;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .server + .server {
+ margin-top: 1rem;
+ }
+
+ .server-info {
+ flex-direction: row-reverse;
+ justify-content: unset;
+ margin-right: auto;
+ z-index: 1;
+ }
+
+ .server-stats {
+ flex-direction: row;
+ justify-content: right;
+ min-width: 450px;
+ margin-top: 0;
+ gap: 2rem;
+ padding-bottom: 0.8rem;
+ z-index: 1;
+ }
+
+ .server-stats > * {
+ max-width: 200px;
+ }
+}
diff --git a/internal/glance/static/css/widget-twitch.css b/internal/glance/static/css/widget-twitch.css
new file mode 100644
index 0000000..d109756
--- /dev/null
+++ b/internal/glance/static/css/widget-twitch.css
@@ -0,0 +1,47 @@
+.twitch-category-thumbnail {
+ width: 5rem;
+ aspect-ratio: 3 / 4;
+ border-radius: var(--border-radius);
+}
+
+.twitch-channel-avatar {
+ aspect-ratio: 1;
+ border-radius: 50%;
+}
+
+.twitch-channel-avatar-container {
+ width: 4.4rem;
+ height: 4.4rem;
+ border: 2px solid var(--color-text-subdue);
+ padding: 2px;
+ border-radius: 50%;
+ position: relative;
+ flex-shrink: 0;
+}
+
+.twitch-channel-live .twitch-channel-avatar-container {
+ border: 2px solid var(--color-positive);
+ margin-bottom: 1rem;
+}
+
+.twitch-channel-live .twitch-channel-avatar-container::after {
+ content: 'LIVE';
+ position: absolute;
+ background: var(--color-positive);
+ color: var(--color-widget-background);
+ font-size: var(--font-size-h6);
+ left: 50%;
+ bottom: -35%;
+ border-radius: var(--border-radius);
+ padding-inline: 0.3rem;
+ transform: translate(-50%);
+ border: 2px solid var(--color-widget-background);
+}
+
+.twitch-stream-preview {
+ max-width: 100%;
+ width: 400px;
+ aspect-ratio: 16 / 9;
+ border-radius: var(--border-radius);
+ object-fit: cover;
+}
diff --git a/internal/glance/static/css/widget-videos.css b/internal/glance/static/css/widget-videos.css
new file mode 100644
index 0000000..8d07801
--- /dev/null
+++ b/internal/glance/static/css/widget-videos.css
@@ -0,0 +1,13 @@
+.video-thumbnail {
+ width: 100%;
+ aspect-ratio: 16 / 8.9;
+ object-fit: cover;
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+.video-horizontal-list-thumbnail {
+ height: 4rem;
+ aspect-ratio: 16 / 8.9;
+ object-fit: cover;
+ border-radius: var(--border-radius);
+}
diff --git a/internal/glance/static/css/widget-weather.css b/internal/glance/static/css/widget-weather.css
new file mode 100644
index 0000000..84d50ba
--- /dev/null
+++ b/internal/glance/static/css/widget-weather.css
@@ -0,0 +1,139 @@
+.weather-column {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: end;
+ flex-direction: column;
+ width: calc(100% / 12);
+ padding-top: 3px;
+}
+
+.weather-column-value, .weather-columns:hover .weather-column-value {
+ font-size: 13px;
+ color: var(--color-text-highlight);
+ letter-spacing: -0.1rem;
+ margin-right: 0.1rem;
+ position: relative;
+ margin-bottom: 0.3rem;
+ opacity: 0;
+ transform: translateY(0.5rem);
+ transition: opacity .2s, transform .2s;
+ user-select: none;
+}
+
+.weather-column-current .weather-column-value, .weather-column:hover .weather-column-value {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.weather-column-value::after {
+ position: absolute;
+ content: '°';
+ left: 100%;
+ color: var(--color-text-subdue);
+}
+
+.weather-column-value.weather-column-value-negative::before {
+ position: absolute;
+ content: '-';
+ right: 100%;
+}
+
+.weather-bar, .weather-columns:hover .weather-bar {
+ height: calc(20px + var(--weather-bar-height) * 40px);
+ width: 6px;
+ background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 18%)));
+ border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 24%)));
+ border-bottom: 0;
+ border-radius: 6px 6px 0 0;
+ mask-image: linear-gradient(0deg, transparent 0, #000 10px);
+ -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 10px);
+ transition: background-color .2s, border-color .2s, width .2s;
+}
+
+.weather-column-current .weather-bar, .weather-column:hover .weather-bar {
+ width: 10px;
+ background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%)));
+ border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 50%)));
+}
+
+.weather-column-rain {
+ position: absolute;
+ inset: 0;
+ bottom: 20%;
+ overflow: hidden;
+ mask-image: linear-gradient(0deg, transparent 40%, #000);
+ -webkit-mask-image: linear-gradient(0deg, transparent 40%, #000);
+}
+
+.weather-column-rain::before {
+ content: '';
+ position: absolute;
+ /* TODO: figure out a way to make it look continuous between columns, right now */
+ /* depending on the width of the page the rain inside two columns next to each other */
+ /* can overlap and look bad */
+ background: radial-gradient(circle at 4px 4px, hsl(200, 90%, 70%, 0.4) 1px, transparent 0);
+ background-size: 8px 8px;
+ transform: rotate(45deg) translate(-50%, 25%);
+ height: 130%;
+ aspect-ratio: 1;
+ left: 55%;
+}
+
+.weather-column:nth-child(3) .weather-column-time,
+.weather-column:nth-child(7) .weather-column-time,
+.weather-column:nth-child(11) .weather-column-time {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.weather-column-time, .weather-columns:hover .weather-column-time {
+ margin-top: 0.3rem;
+ font-size: var(--font-size-h6);
+ opacity: 0;
+ transform: translateY(-0.5rem);
+ transition: opacity .2s, transform .2s;
+ user-select: none;
+}
+
+.weather-column:hover .weather-column-time {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.weather-column-daylight {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(0deg, transparent 30px, hsl(50, 50%, 30%, 0.2));
+}
+
+.weather-column-daylight-sunrise {
+ border-radius: 20px 0 0 0;
+}
+
+.weather-column-daylight-sunset {
+ border-radius: 0 20px 0 0;
+}
+
+.location-icon {
+ width: 0.8em;
+ height: 0.8em;
+ border-radius: 0 50% 50% 50%;
+ background-color: currentColor;
+ transform: rotate(225deg) translate(.1em, .1em);
+ position: relative;
+ flex-shrink: 0;
+}
+
+.location-icon::after {
+ content: '';
+ position: absolute;
+ z-index: 2;
+ width: .4em;
+ height: .4em;
+ border-radius: 50%;
+ background-color: var(--color-widget-background);
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
diff --git a/internal/glance/static/css/widgets.css b/internal/glance/static/css/widgets.css
new file mode 100644
index 0000000..9c9e8ee
--- /dev/null
+++ b/internal/glance/static/css/widgets.css
@@ -0,0 +1,88 @@
+@import "widget-bookmarks.css";
+@import "widget-calendar.css";
+@import "widget-clock.css";
+@import "widget-dns-stats.css";
+@import "widget-docker-containers.css";
+@import "widget-group.css";
+@import "widget-markets.css";
+@import "widget-monitor.css";
+@import "widget-reddit.css";
+@import "widget-releases.css";
+@import "widget-rss.css";
+@import "widget-search.css";
+@import "widget-server-stats.css";
+@import "widget-twitch.css";
+@import "widget-videos.css";
+@import "widget-weather.css";
+
+@import "forum-posts.css";
+
+.widget-error-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: relative;
+ margin-bottom: 1.8rem;
+ z-index: 1;
+}
+
+.widget-error-header::before {
+ content: '';
+ position: absolute;
+ inset: calc(0rem - (var(--widget-content-vertical-padding) / 2)) calc(0rem - (var(--widget-content-horizontal-padding) / 2));
+ background: var(--color-negative);
+ opacity: 0.05;
+ border-radius: var(--border-radius);
+ z-index: -1;
+}
+
+.widget-error-icon {
+ width: 2.4rem;
+ height: 2.4rem;
+ flex-shrink: 0;
+ stroke: var(--color-negative);
+ opacity: 0.6;
+}
+
+.widget-content {
+ container-type: inline-size;
+ container-name: widget;
+}
+
+.widget-content:not(.widget-content-frameless) {
+ padding: var(--widget-content-padding);
+}
+
+.widget-content:not(.widget-content-frameless), .widget-content-frame {
+ background: var(--color-widget-background);
+ border-radius: var(--border-radius);
+ border: 1px solid var(--color-widget-content-border);
+ box-shadow: 0px 3px 0px 0px hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl)) - 0.5%));
+}
+
+.widget-header {
+ padding: 0 calc(var(--widget-content-horizontal-padding) + 1px);
+ font-size: var(--font-size-h4);
+ margin-bottom: 0.9rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.widget-beta-icon {
+ width: 1.6rem;
+ height: 1.6rem;
+ flex-shrink: 0;
+ transition: transform .45s, opacity .45s, stroke .45s;
+ opacity: 0.7;
+}
+
+.widget-beta-icon:hover, .widget-header .popover-active > .widget-beta-icon {
+ fill: var(--color-text-highlight);
+ transform: translateY(-10%) scale(1.3);
+ opacity: 1;
+}
+
+.widget + .widget {
+ margin-top: var(--widget-gap);
+}
diff --git a/internal/glance/static/main.css b/internal/glance/static/main.css
deleted file mode 100644
index 7b7b592..0000000
--- a/internal/glance/static/main.css
+++ /dev/null
@@ -1,2115 +0,0 @@
-@font-face {
- font-family: 'JetBrains Mono';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url('fonts/JetBrainsMono-Regular.woff2') format('woff2');
-}
-
-:root {
- font-size: 10px;
-
- --scheme: ;
- --bgh: 240;
- --bgs: 8%;
- --bgl: 9%;
- --bghs: var(--bgh), var(--bgs);
- --cm: 1;
- --tsm: 1;
-
- --widget-gap: 23px;
- --widget-content-vertical-padding: 15px;
- --widget-content-horizontal-padding: 17px;
- --widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding);
- --content-bounds-padding: 15px;
- --border-radius: 5px;
- --mobile-navigation-height: 50px;
-
- --color-primary: hsl(43, 50%, 70%);
- --color-positive: var(--color-primary);
- --color-negative: hsl(0, 70%, 70%);
- --color-background: hsl(var(--bghs), var(--bgl));
- --color-widget-background-hsl-values: var(--bghs), calc(var(--bgl) + 1%);
- --color-widget-background: hsl(var(--color-widget-background-hsl-values));
- --color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm))));
- --color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
- --color-widget-background-highlight: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
- --color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 3%));
- --color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 12%)));
- --color-progress-border: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 10% * var(--cm))));
- --color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 30% * var(--cm))));
- --color-graph-gridlines: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 6% * var(--cm))));
-
- --ths: var(--bgh), calc(var(--bgs) * var(--tsm));
- --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%));
- --color-text-paragraph: hsl(var(--ths), calc(var(--scheme) var(--cm) * 73%));
- --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
- --color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%));
- --color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%));
-
- --font-size-h1: 1.7rem;
- --font-size-h2: 1.6rem;
- --font-size-h3: 1.5rem;
- --font-size-h4: 1.4rem;
- --font-size-base: 1.3rem;
- --font-size-h5: 1.2rem;
- --font-size-h6: 1.1rem;
-}
-
-.light-scheme {
- --scheme: 100% -;
-}
-
-.page {
- height: 100%;
- padding-block: var(--widget-gap);
-}
-
-.page-content, .page.content-ready .page-loading-container {
- display: none;
-}
-
-.page.content-ready > .page-content {
- display: block;
-}
-
-.page-column-small .size-title-dynamic {
- font-size: var(--font-size-h4);
-}
-
-.page-column-full .size-title-dynamic {
- font-size: var(--font-size-h3);
-}
-
-.color-primary-if-not-visited:not(:visited) {
- color: var(--color-primary);
-}
-
-.text-truncate,
-.single-line-titles .title
-{
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.single-line-titles .title {
- display: block;
-}
-
-.text-truncate-2-lines, .text-truncate-3-lines {
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-box-orient: vertical;
-}
-
-.text-truncate-3-lines { line-clamp: 3; -webkit-line-clamp: 3; }
-.text-truncate-2-lines { line-clamp: 2; -webkit-line-clamp: 2; }
-
-.visited-indicator:not(.text-truncate)::after,
-.visited-indicator.text-truncate::before,
-.bookmarks-link:not(.bookmarks-link-no-arrow)::after {
- content: '↗' / "";
- margin-left: 0.5em;
- display: inline-block;
- position: relative;
- top: 0.15em;
- color: var(--color-text-base);
-}
-
-.visited-indicator.text-truncate {
- direction: rtl;
- text-align: left;
-}
-
-.visited-indicator:not(:visited)::before, .visited-indicator:not(:visited)::after {
- color: var(--color-primary);
-}
-
-.page-columns-transitioned .list-with-transition > * { animation: collapsibleItemReveal .25s backwards; }
-.list-with-transition > *:nth-child(2) { animation-delay: 30ms; }
-.list-with-transition > *:nth-child(3) { animation-delay: 60ms; }
-.list-with-transition > *:nth-child(4) { animation-delay: 90ms; }
-.list-with-transition > *:nth-child(5) { animation-delay: 120ms; }
-.list-with-transition > *:nth-child(6) { animation-delay: 150ms; }
-.list-with-transition > *:nth-child(7) { animation-delay: 180ms; }
-.list-with-transition > *:nth-child(8) { animation-delay: 210ms; }
-
-.list > *:not(:first-child) {
- margin-top: calc(var(--list-half-gap) * 2);
-}
-
-.list.list-with-separator > *:not(:first-child) {
- margin-top: var(--list-half-gap);
- border-top: 1px solid var(--color-separator);
- padding-top: var(--list-half-gap);
-}
-
-.collapsible-container:not(.container-expanded) > .collapsible-item {
- display: none;
-}
-
-.collapsible-item {
- animation: collapsibleItemReveal .25s backwards;
-}
-
-@keyframes collapsibleItemReveal {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-}
-
-.expand-toggle-button {
- font: inherit;
- border: 0;
- cursor: pointer;
- display: block;
- width: 100%;
- text-align: left;
- color: var(--color-text-base);
- text-transform: uppercase;
- font-size: var(--font-size-h4);
- padding: var(--widget-content-vertical-padding) 0;
- background: var(--color-widget-background);
-}
-
-.expand-toggle-button.container-expanded {
- position: sticky;
- /* -1px to hide 1px gap on chrome */
- bottom: -1px;
-}
-
-.expand-toggle-button-icon {
- display: inline-block;
- margin-left: 1rem;
- position: relative;
- top: -.2rem;
-}
-
-.expand-toggle-button-icon::before {
- content: '' / "";
- font-size: 0.8rem;
- transform: rotate(90deg);
- line-height: 1;
- display: inline-block;
- transition: transform 0.3s;
-}
-
-.expand-toggle-button.container-expanded .expand-toggle-button-icon::before {
- transform: rotate(-90deg);
-}
-
-.widget-group-header {
- overflow-x: auto;
- scrollbar-width: thin;
-}
-
-.widget-group-title {
- background: none;
- font: inherit;
- border: none;
- text-transform: uppercase;
- border-bottom: 1px dotted transparent;
- cursor: pointer;
- flex-shrink: 0;
- transition: color .3s, border-color .3s;
- color: var(--color-text-subdue);
- line-height: calc(1.6em - 1px);
-}
-
-.widget-group-title:hover:not(.widget-group-title-current) {
- color: var(--color-text-base);
-}
-
-.widget-group-title-current {
- border-bottom-color: var(--color-text-base-muted);
- color: var(--color-text-base);
-}
-
-.widget-group-content {
- animation: widgetGroupContentEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
-}
-
-.widget-group-content[data-direction="right"] {
- --direction: 5px;
-}
-
-.widget-group-content[data-direction="left"] {
- --direction: -5px;
-}
-
-@keyframes widgetGroupContentEntrance {
- from {
- opacity: 0;
- transform: translateX(var(--direction));
- }
-}
-
-.widget-group-content:not(.widget-group-content-current) {
- display: none;
-}
-
-.widget-content:has(.expand-toggle-button:last-child) {
- padding-bottom: 0;
-}
-
-.cards-grid.collapsible-container + .expand-toggle-button {
- text-align: center;
- margin-top: 0.5rem;
- background-color: var(--color-background);
-}
-
-.attachments {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
-}
-
-.attachments > * {
- border-radius: var(--border-radius);
- padding: 0.1rem 0.5rem;
- font-size: var(--font-size-h6);
- background-color: var(--color-separator);
-}
-
-pre {
- font: inherit;
-}
-
-::selection {
- background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
- color: var(--color-text-highlight);
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--color-text-subdue);
- border-radius: var(--border-radius);
-}
-
-::-webkit-scrollbar {
- background: var(--color-background);
- height: 5px;
- width: 10px;
-}
-
-*:focus-visible {
- outline: 2px solid var(--color-primary);
- outline-offset: 0.1rem;
- border-radius: var(--border-radius);
-}
-
-*, *::before, *::after {
- box-sizing: border-box;
-}
-
-* {
- padding: 0;
- margin: 0;
-}
-
-hr {
- border: 0;
- height: 1px;
- background-color: var(--color-separator);
-}
-
-img, svg {
- display: block;
- max-width: 100%;
-}
-
-img[loading=lazy].loaded:not(.finished-transition) {
- transition: opacity .4s;
-}
-
-img[loading=lazy].cached:not(.finished-transition) {
- transition: none;
-}
-
-img[loading=lazy]:not(.loaded, .cached) {
- opacity: 0;
-}
-
-html {
- scrollbar-color: var(--color-text-subdue) transparent;
- scroll-behavior: smooth;
-}
-
-html, body, .body-content {
- height: 100%;
-}
-
-h1, h2, h3, h4, h5 {
- font: inherit;
-}
-
-.visually-hidden {
- clip-path: inset(50%);
- height: 1px;
- overflow: hidden;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-
-a {
- text-decoration: none;
- color: inherit;
- overflow-wrap: break-word;
-}
-
-ul {
- list-style: none;
-}
-
-body {
- font-size: 1.3rem;
- font-family: 'JetBrains Mono', monospace;
- font-variant-ligatures: none;
- line-height: 1.6;
- color: var(--color-text-base);
- background-color: var(--color-background);
- overflow-y: scroll;
-}
-
-.page-column-small {
- width: 300px;
- flex-shrink: 0;
-}
-
-.page-column-full {
- width: 100%;
- min-width: 0;
-}
-
-.page-columns {
- display: flex;
- gap: var(--widget-gap);
- animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
-}
-
-@keyframes pageColumnsEntrance {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-}
-
-.page-loading-container {
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- animation: loadingContainerEntrance 200ms backwards;
- animation-delay: 150ms;
- font-size: 2rem;
-}
-
-.page-loading-container > .loading-icon {
- translate: 0 -250%;
-}
-
-@keyframes loadingContainerEntrance {
- from {
- opacity: 0;
- }
-}
-
-.loading-icon {
- min-width: 1.5em;
- width: 1.5em;
- height: 1.5em;
- border: 0.25em solid hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 12%)));
- border-top-color: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%)));
- border-radius: 50%;
- animation: loadingIconSpin 800ms infinite linear;
-}
-
-@keyframes loadingIconSpin {
- to {
- transform: rotate(360deg);
- }
-}
-
-.notice-icon {
- width: 0.7rem;
- height: 0.7rem;
- border-radius: 50%;
-}
-
-.notice-icon-major {
- background: var(--color-negative);
-}
-
-.notice-icon-minor {
- border: 1px solid var(--color-negative);
-}
-
-kbd {
- font: inherit;
- padding: 0.1rem 0.8rem;
- border-radius: var(--border-radius);
- border: 2px solid var(--color-widget-background-highlight);
- box-shadow: 0 2px 0 var(--color-widget-background-highlight);
- user-select: none;
- transition: transform .1s, box-shadow .1s;
- font-size: var(--font-size-h5);
- cursor: pointer;
-}
-
-kbd:active {
- transform: translateY(2px);
- box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
-}
-
-.masonry {
- display: flex;
- gap: var(--widget-gap);
-}
-
-.masonry-column {
- flex: 1;
- display: flex;
- flex-direction: column;
-}
-
-.popover-container, [data-popover-html] {
- display: none;
-}
-
-.popover-container {
- --triangle-size: 10px;
- --triangle-offset: 50%;
- --triangle-margin: calc(var(--triangle-size) + 3px);
- --entrance-y-offset: 8px;
- --entrance-direction: calc(var(--entrance-y-offset) * -1);
-
- z-index: 20;
- position: absolute;
- padding-top: var(--triangle-margin);
- padding-inline: var(--content-bounds-padding);
-}
-
-.popover-container.position-above {
- --entrance-direction: var(--entrance-y-offset);
- padding-top: 0;
- padding-bottom: var(--triangle-margin);
-}
-
-.popover-frame {
- --shadow-properties: 0 15px 20px -10px;
- --shadow-color: hsla(var(--bghs), calc(var(--bgl) * 0.2), 0.5);
- position: relative;
- padding: 10px;
- background: var(--color-popover-background);
- border: 1px solid var(--color-popover-border);
- border-radius: 5px;
- animation: popoverFrameEntrance 0.3s backwards cubic-bezier(0.16, 1, 0.3, 1);
- box-shadow: var(--shadow-properties) var(--shadow-color);
-}
-
-.popover-frame::before {
- content: '';
- position: absolute;
- width: var(--triangle-size);
- height: var(--triangle-size);
- transform: rotate(45deg);
- background-color: var(--color-popover-background);
- border-top-left-radius: 2px;
- border-left: 1px solid var(--color-popover-border);
- border-top: 1px solid var(--color-popover-border);
- left: calc(var(--triangle-offset) - (var(--triangle-size) / 2));
- top: calc(var(--triangle-size) / 2 * -1 - 1px);
-}
-
-.popover-container.position-above .popover-frame::before {
- transform: rotate(-135deg);
- top: auto;
- bottom: calc(var(--triangle-size) / 2 * -1 - 1px);
-}
-
-.popover-container.position-above .popover-frame {
- --shadow-properties: 0 10px 20px -10px;
-}
-
-@keyframes popoverFrameEntrance {
- from {
- opacity: 0;
- transform: translateY(var(--entrance-direction));
- }
-}
-
-.summary {
- width: 100%;
- cursor: pointer;
- word-spacing: -0.18em;
- user-select: none;
- list-style: none;
- position: relative;
- display: flex;
- z-index: 1;
-}
-
-.summary::-webkit-details-marker {
- display: none;
-}
-
-.details[open] .summary {
- margin-bottom: .8rem;
-}
-
-.summary::before {
- content: "";
- position: absolute;
- inset: -.3rem -.8rem;
- border-radius: var(--border-radius);
- background-color: var(--color-widget-background-highlight);
- opacity: 0;
- transition: opacity 0.2s;
- z-index: -1;
-}
-
-.details[open] .summary::before, .summary:hover::before {
- opacity: 1;
-}
-
-.details:not([open]) .list-with-transition {
- display: none;
-}
-
-.summary::after {
- content: "◀" / "";
- font-size: 1.2em;
- position: absolute;
- top: 0;
- bottom: 0;
- line-height: 1.3em;
- right: 0;
- transition: rotate .5s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-details[open] .summary::after {
- rotate: -90deg;
-}
-
-.content-bounds {
- max-width: 1600px;
- width: 100%;
- margin-inline: auto;
- padding: 0 var(--content-bounds-padding);
-}
-
-.page-width-wide .content-bounds {
- max-width: 1920px;
-}
-
-.page-width-slim .content-bounds {
- max-width: 1100px;
-}
-
-.page-center-vertically .page {
- display: flex;
- justify-content: center;
- flex-direction: column;
-}
-
-/* TODO: refactor, otherwise I hope I never have to change dynamic columns again */
-.dynamic-columns {
- --list-half-gap: 0.5rem;
- gap: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding);
- display: grid;
- grid-template-columns: repeat(var(--columns-per-row), 1fr);
-}
-
-.dynamic-columns > * {
- padding-left: var(--widget-content-horizontal-padding);
- border-left: 1px solid var(--color-separator);
- min-width: 0;
-}
-
-.dynamic-columns > *:first-child {
- padding-top: 0;
- border-top: none;
- border-left: none;
-}
-
-.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
-.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
-.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
-.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
-.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
-
-@container widget (max-width: 599px) {
- .dynamic-columns { gap: 0; }
- .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
- .dynamic-columns > * {
- border-left: none;
- padding-left: 0;
- }
- .dynamic-columns > *:not(:first-child) {
- margin-top: calc(var(--list-half-gap) * 2);
- }
- .dynamic-columns.list-with-separator > *:not(:first-child) {
- margin-top: var(--list-half-gap);
- border-top: 1px solid var(--color-separator);
- padding-top: var(--list-half-gap);
- }
-}
-@container widget (min-width: 600px) and (max-width: 849px) {
- .dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
- .dynamic-columns > :nth-child(2n-1) {
- border-left: none;
- padding-left: 0;
- }
-}
-@container widget (min-width: 850px) and (max-width: 1249px) {
- .dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
- .dynamic-columns > :nth-child(3n+1) {
- border-left: none;
- padding-left: 0;
- }
-}
-@container widget (min-width: 1250px) and (max-width: 1499px) {
- .dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
- .dynamic-columns > :nth-child(4n+1) {
- border-left: none;
- padding-left: 0;
- }
-}
-@container widget (min-width: 1500px) {
- .dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
- .dynamic-columns > :nth-child(5n+1) {
- border-left: none;
- padding-left: 0;
- }
-}
-
-.cards-vertical {
- flex-direction: column;
-}
-
-.cards-horizontal {
- --cards-per-row: 6.5;
-}
-
-.cards-horizontal, .cards-vertical {
- --cards-gap: calc(var(--widget-content-vertical-padding) * 0.7);
- display: flex;
- gap: var(--cards-gap);
-}
-
-.card {
- display: flex;
- flex-direction: column;
-}
-
-.cards-horizontal .card {
- flex-shrink: 0;
- width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
-}
-
-.cards-grid .card {
- min-width: 0;
-}
-
-.cards-horizontal {
- overflow-x: auto;
- scrollbar-width: thin;
- padding-bottom: 1rem;
-}
-
-.cards-grid {
- --cards-per-row: 6;
- display: grid;
- grid-template-columns: repeat(var(--cards-per-row), 1fr);
- gap: calc(var(--widget-content-vertical-padding) * 0.7);
-}
-
-@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } }
-@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } }
-@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } }
-@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } }
-@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } }
-@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } }
-
-@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } }
-@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } }
-@container widget (max-width: 850px) { .cards-grid { --cards-per-row: 3; } }
-@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } }
-@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } }
-
-.widget-small-content-bounds {
- max-width: 350px;
- margin: 0 auto;
-}
-
-.widget-error-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
- margin-bottom: 1.8rem;
- z-index: 1;
-}
-
-.widget-error-header::before {
- content: '';
- position: absolute;
- inset: calc(0rem - (var(--widget-content-vertical-padding) / 2)) calc(0rem - (var(--widget-content-horizontal-padding) / 2));
- background: var(--color-negative);
- opacity: 0.05;
- border-radius: var(--border-radius);
- z-index: -1;
-}
-
-.widget-error-icon {
- width: 2.4rem;
- height: 2.4rem;
- flex-shrink: 0;
- stroke: var(--color-negative);
- opacity: 0.6;
-}
-
-.widget-content {
- container-type: inline-size;
- container-name: widget;
-}
-
-.widget-content:not(.widget-content-frameless) {
- padding: var(--widget-content-padding);
-}
-
-.widget-content:not(.widget-content-frameless), .widget-content-frame {
- background: var(--color-widget-background);
- border-radius: var(--border-radius);
- border: 1px solid var(--color-widget-content-border);
- box-shadow: 0px 3px 0px 0px hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl)) - 0.5%));
-}
-
-.padding-widget {
- padding: var(--widget-content-padding);
-}
-
-.margin-bottom-widget {
- margin-bottom: var(--widget-content-vertical-padding);
-}
-
-.padding-block-widget {
- padding-block: var(--widget-content-vertical-padding);
-}
-
-.padding-inline-widget {
- padding-inline: var(--widget-content-horizontal-padding);
-}
-
-.widget-header {
- padding: 0 calc(var(--widget-content-horizontal-padding) + 1px);
- font-size: var(--font-size-h4);
- margin-bottom: 0.9rem;
- display: flex;
- align-items: center;
- gap: 1rem;
-}
-
-.widget-beta-icon {
- width: 1.6rem;
- height: 1.6rem;
- flex-shrink: 0;
- transition: transform .45s, opacity .45s, stroke .45s;
- opacity: 0.7;
-}
-
-.widget-beta-icon:hover, .widget-header .popover-active > .widget-beta-icon {
- fill: var(--color-text-highlight);
- transform: translateY(-10%) scale(1.3);
- opacity: 1;
-}
-
-.widget + .widget {
- margin-top: var(--widget-gap);
-}
-
-.list-horizontal-text {
- display: flex;
- list-style: none;
- flex-wrap: wrap;
- align-items: center;
-}
-
-.list-horizontal-text > *:not(:last-child)::after {
- content: '•' / "";
- color: var(--color-text-subdue);
- margin: 0 0.4rem;
- position: relative;
- top: 0.1rem;
-}
-
-.header-container {
- margin-top: calc(var(--widget-gap) / 2);
- --header-height: 45px;
- --header-items-gap: 2.5rem;
-}
-
-.header {
- display: flex;
- height: var(--header-height);
- gap: var(--header-items-gap);
-}
-
-.logo {
- height: 100%;
- line-height: var(--header-height);
- font-size: 2rem;
- color: var(--color-text-highlight);
- border-right: 1px solid var(--color-widget-content-border);
- padding-right: var(--widget-content-horizontal-padding);
-}
-
-.logo:has(img) {
- display: flex;
- align-items: center;
-}
-
-.logo img {
- max-height: 2.7rem;
-}
-
-.nav {
- height: 100%;
- gap: var(--header-items-gap);
-}
-
-.nav .nav-item {
- line-height: var(--header-height);
-}
-
-.footer {
- padding-bottom: calc(var(--widget-gap) * 1.5);
- padding-top: calc(var(--widget-gap) / 2);
- animation: loadingContainerEntrance 200ms backwards;
- animation-delay: 150ms;
-}
-
-.mobile-navigation, .mobile-reachability-header {
- display: none;
-}
-
-.nav-item {
- display: block;
- height: 100%;
- border-bottom: 2px solid transparent;
- transition: color .3s, border-color .3s;
- font-size: var(--font-size-h3);
- flex-shrink: 0;
-}
-
-.nav-item:not(.nav-item-current):hover {
- border-bottom-color: var(--color-text-subdue);
- color: var(--color-text-highlight);
-}
-
-.nav-item.nav-item-current {
- border-bottom-color: var(--color-primary);
- color: var(--color-text-highlight);
-}
-
-.release-source-icon {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- opacity: 0.4;
-}
-
-.market-chart {
- margin-left: auto;
- width: 6.5rem;
- flex-shrink: 0;
-}
-
-.market-chart svg {
- width: 100%;
-}
-
-.market-values {
- min-width: 8rem;
-}
-
-.carousel-container {
- position: relative;
-}
-
-.carousel-container::before, .carousel-container::after {
- content: '';
- position: absolute;
- width: 2rem;
- top: 0;
- bottom: 1rem;
- z-index: 10;
- opacity: 0;
- pointer-events: none;
- transition-duration: 0.2s;
-}
-
-.carousel-container::before {
- left: 0;
- background: linear-gradient(to right, var(--color-background), transparent);
-}
-
-.carousel-container::after {
- right: 0;
- background: linear-gradient(to left, var(--color-background), transparent);
-}
-
-.carousel-container.show-left-cutoff::before, .carousel-container.show-right-cutoff::after {
- opacity: 1;
-}
-
-.video-thumbnail {
- width: 100%;
- aspect-ratio: 16 / 8.9;
- object-fit: cover;
- border-radius: var(--border-radius) var(--border-radius) 0 0;
-}
-
-.video-horizontal-list-thumbnail {
- height: 4rem;
- aspect-ratio: 16 / 8.9;
- object-fit: cover;
- border-radius: var(--border-radius);
-}
-
-.search-icon {
- width: 2.3rem;
-}
-
-.search-icon-container {
- position: relative;
- flex-shrink: 0;
-}
-
-/* gives a wider hit area for the 3 people that will notice the animation : ) */
-.search-icon-container::before {
- content: '';
- position: absolute;
- inset: -1rem;
-}
-
-.search-icon-container:hover > .search-icon {
- animation: searchIconHover 2.9s forwards;
-}
-
-@keyframes searchIconHover {
- 0%, 39% { translate: 0 0; }
- 20% { scale: 1.3; }
- 40% { scale: 1; }
- 50% { translate: -30% 30%; }
- 70% { translate: 30% -30%; }
- 90% { translate: -30% -30%; }
- 100% { translate: 0 0; }
-}
-
-.search {
- transition: border-color .2s;
- position: relative;
-}
-
-.search:hover {
- border-color: var(--color-text-subdue);
-}
-
-.search:focus-within {
- border-color: var(--color-primary);
-}
-
-.search-input {
- border: 0;
- background: none;
- width: 100%;
- height: 6rem;
- font: inherit;
- outline: none;
- color: var(--color-text-highlight);
-}
-
-.search-input::placeholder {
- color: var(--color-text-base-muted);
- opacity: 1;
-}
-
-.search-bangs { display: none; }
-
-.search-bang {
- border-radius: calc(var(--border-radius) * 2);
- background: var(--color-widget-background-highlight);
- padding: 0.3rem 1rem;
- flex-shrink: 0;
- font-size: var(--font-size-h5);
- animation: searchBangsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
-}
-
-@keyframes searchBangsEntrance {
- 0% {
- opacity: 0;
- transform: translateX(-10px);
- }
-}
-
-.search-bang:empty {
- display: none;
-}
-
-.forum-post-list-thumbnail {
- flex-shrink: 0;
- width: 6rem;
- height: 4.1rem;
- border-radius: var(--border-radius);
- object-fit: cover;
- border: 1px solid var(--color-separator);
- margin-top: 0.1rem;
-}
-
-.forum-post-tags-container {
- transform: translateY(-0.15rem);
-}
-
-@container widget (max-width: 550px) {
- .forum-post-autohide {
- display: none;
- }
-}
-
-.bookmarks-group {
- --bookmarks-group-color: var(--color-primary);
-}
-
-.bookmarks-group-title {
- color: var(--bookmarks-group-color);
-}
-
-.bookmarks-group .bookmarks-link::after {
- color: var(--bookmarks-group-color);
-}
-
-.bookmarks-icon-container {
- margin-block: 0.1rem;
- background-color: var(--color-widget-background-highlight);
- border-radius: var(--border-radius);
- padding: 0.5rem;
- opacity: 0.7;
- flex-shrink: 0;
-}
-
-.bookmarks-icon {
- width: 20px;
- height: 20px;
- opacity: 0.8;
-}
-
-:root:not(.light-scheme) .flat-icon {
- filter: invert(1);
-}
-
-.old-calendar-day {
- width: calc(100% / 7);
- text-align: center;
- padding: 0.6rem 0;
-}
-
-.old-calendar-day-today {
- border-radius: var(--border-radius);
- background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%)));
- color: var(--color-text-highlight);
-}
-
-.calendar-dates {
- text-align: center;
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- gap: 2px;
-}
-
-.calendar-date {
- padding: 0.4rem 0;
- color: var(--color-text-base);
- position: relative;
- border-radius: var(--border-radius);
- background: none;
- border: none;
- font: inherit;
-}
-
-.calendar-current-date {
- border-radius: var(--border-radius);
- background-color: var(--color-popover-border);
- color: var(--color-text-highlight);
-}
-
-.calendar-spillover-date {
- color: var(--color-text-subdue);
-}
-
-.calendar-header-button {
- position: relative;
- cursor: pointer;
- width: 2rem;
- height: 2rem;
- z-index: 1;
- background: none;
- border: none;
-}
-
-.calendar-header-button::before {
- content: '';
- position: absolute;
- inset: -0.2rem;
- border-radius: var(--border-radius);
- background-color: var(--color-text-subdue);
- opacity: 0;
- transition: opacity 0.2s;
- z-index: -1;
-}
-
-.calendar-header-button:hover::before {
- opacity: 0.4;
-}
-
-.calendar-undo-button {
- display: inline-block;
- vertical-align: text-top;
- width: 2rem;
- height: 2rem;
- margin-left: 0.7rem;
-}
-
-.dns-stats-totals {
- transition: opacity .3s;
- transition-delay: 50ms;
-}
-
-.dns-stats:has(.dns-stats-graph .popover-active) .dns-stats-totals {
- opacity: 0.1;
- transition-delay: 0s;
-}
-
-.dns-stats-graph {
- --graph-height: 70px;
- height: var(--graph-height);
- position: relative;
- margin-bottom: 2.5rem;
-}
-
-.dns-stats-graph-gridlines-container {
- position: absolute;
- inset: 0;
-}
-
-.dns-stats-graph-gridlines {
- height: 100%;
- width: 100%;
-}
-
-.dns-stats-graph-columns {
- display: flex;
- height: 100%;
-}
-
-.dns-stats-graph-column {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- flex-direction: column;
- width: calc(100% / 8);
- position: relative;
-}
-
-.dns-stats-graph-column::before {
- content: '';
- position: absolute;
- inset: 1px 0;
- opacity: 0;
- background: var(--color-text-base);
- transition: opacity .2s;
-}
-
-.dns-stats-graph-column:hover::before {
- opacity: 0.05;
-}
-
-.dns-stats-graph-bar {
- width: 14px;
- height: calc((var(--bar-height) / 100) * var(--graph-height));
- border: 1px solid var(--color-progress-border);
- border-radius: var(--border-radius) var(--border-radius) 0 0;
- display: flex;
- background: var(--color-widget-background);
- padding: 2px 2px 0 2px;
- flex-direction: column;
- gap: 2px;
- transition: border-color .2s;
- min-height: 10px;
-}
-
-.dns-stats-graph-column.popover-active .dns-stats-graph-bar {
- border-color: var(--color-text-subdue);
- border-bottom-color: var(--color-progress-border);
-}
-
-.dns-stats-graph-bar > * {
- border-radius: 2px;
- background: var(--color-progress-value);
- min-height: 1px;
-}
-
-.dns-stats-graph-bar > .queries {
- flex-grow: 1;
-}
-
-.dns-stats-graph-bar > *:last-child {
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
-}
-
-.dns-stats-graph-bar > .blocked {
- background-color: var(--color-negative);
- flex-basis: calc(var(--percent) - 1px);
-}
-
-.dns-stats-graph-column:nth-child(even) .dns-stats-graph-time {
- opacity: 1;
- transform: translateY(0);
-}
-
-.dns-stats-graph-time, .dns-stats-graph-columns:hover .dns-stats-graph-time {
- position: absolute;
- font-size: var(--font-size-h6);
- inset-inline: 0;
- text-align: center;
- height: 2.5rem;
- line-height: 2.5rem;
- top: 100%;
- user-select: none;
- opacity: 0;
- transform: translateY(-0.5rem);
- transition: opacity .2s, transform .2s;
-}
-
-.dns-stats-graph-column:hover .dns-stats-graph-time {
- opacity: 1;
- transform: translateY(0);
-}
-
-.dns-stats-graph-columns:hover .dns-stats-graph-column:not(:hover) .dns-stats-graph-time {
- opacity: 0;
-}
-
-.weather-column {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: end;
- flex-direction: column;
- width: calc(100% / 12);
- padding-top: 3px;
-}
-
-.weather-column-value, .weather-columns:hover .weather-column-value {
- font-size: 13px;
- color: var(--color-text-highlight);
- letter-spacing: -0.1rem;
- margin-right: 0.1rem;
- position: relative;
- margin-bottom: 0.3rem;
- opacity: 0;
- transform: translateY(0.5rem);
- transition: opacity .2s, transform .2s;
- user-select: none;
-}
-
-.weather-column-current .weather-column-value, .weather-column:hover .weather-column-value {
- opacity: 1;
- transform: translateY(0);
-}
-
-.weather-column-value::after {
- position: absolute;
- content: '°';
- left: 100%;
- color: var(--color-text-subdue);
-}
-
-.weather-column-value.weather-column-value-negative::before {
- position: absolute;
- content: '-';
- right: 100%;
-}
-
-.weather-bar, .weather-columns:hover .weather-bar {
- height: calc(20px + var(--weather-bar-height) * 40px);
- width: 6px;
- background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 18%)));
- border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 24%)));
- border-bottom: 0;
- border-radius: 6px 6px 0 0;
- mask-image: linear-gradient(0deg, transparent 0, #000 10px);
- -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 10px);
- transition: background-color .2s, border-color .2s, width .2s;
-}
-
-.weather-column-current .weather-bar, .weather-column:hover .weather-bar {
- width: 10px;
- background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%)));
- border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 50%)));
-}
-
-.weather-column-rain {
- position: absolute;
- inset: 0;
- bottom: 20%;
- overflow: hidden;
- mask-image: linear-gradient(0deg, transparent 40%, #000);
- -webkit-mask-image: linear-gradient(0deg, transparent 40%, #000);
-}
-
-.weather-column-rain::before {
- content: '';
- position: absolute;
- /* TODO: figure out a way to make it look continuous between columns, right now
- depending on the width of the page the rain inside two columns next to each other
- can overlap and look bad */
- background: radial-gradient(circle at 4px 4px, hsl(200, 90%, 70%, 0.4) 1px, transparent 0);
- background-size: 8px 8px;
- transform: rotate(45deg) translate(-50%, 25%);
- height: 130%;
- aspect-ratio: 1;
- left: 55%;
-}
-
-.weather-column:nth-child(3) .weather-column-time,
-.weather-column:nth-child(7) .weather-column-time,
-.weather-column:nth-child(11) .weather-column-time {
- opacity: 1;
- transform: translateY(0);
-}
-
-.weather-column-time, .weather-columns:hover .weather-column-time {
- margin-top: 0.3rem;
- font-size: var(--font-size-h6);
- opacity: 0;
- transform: translateY(-0.5rem);
- transition: opacity .2s, transform .2s;
- user-select: none;
-}
-
-.weather-column:hover .weather-column-time {
- opacity: 1;
- transform: translateY(0);
-}
-
-.weather-column-daylight {
- position: absolute;
- inset: 0;
- background: linear-gradient(0deg, transparent 30px, hsl(50, 50%, 30%, 0.2));
-}
-
-.weather-column-daylight-sunrise {
- border-radius: 20px 0 0 0;
-}
-
-.weather-column-daylight-sunset {
- border-radius: 0 20px 0 0;
-}
-
-.location-icon {
- width: 0.8em;
- height: 0.8em;
- border-radius: 0 50% 50% 50%;
- background-color: currentColor;
- transform: rotate(225deg) translate(.1em, .1em);
- position: relative;
- flex-shrink: 0;
-}
-
-.location-icon::after {
- content: '';
- position: absolute;
- z-index: 2;
- width: .4em;
- height: .4em;
- border-radius: 50%;
- background-color: var(--color-widget-background);
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
-
-.clock-time {
- min-width: 8ch;
-}
-
-.clock-time span {
- color: var(--color-text-highlight);
-}
-
-.monitor-site-icon {
- display: block;
- opacity: 0.8;
- filter: grayscale(0.4);
- object-fit: contain;
- aspect-ratio: 1 / 1;
- width: 3.2rem;
- position: relative;
- top: -0.1rem;
- transition: filter 0.3s, opacity 0.3s;
-}
-
-.monitor-site-icon.flat-icon {
- opacity: 0.7;
-}
-
-.monitor-site:hover .monitor-site-icon {
- opacity: 1;
-}
-
-.monitor-site:hover .monitor-site-icon:not(.flat-icon) {
- filter: grayscale(0);
-}
-
-.monitor-site-status-icon {
- flex-shrink: 0;
- margin-left: auto;
- width: 2rem;
- height: 2rem;
-}
-
-.monitor-site-status-icon-compact {
- width: 1.8rem;
- height: 1.8rem;
- flex-shrink: 0;
-}
-
-.docker-container-icon {
- display: block;
- filter: grayscale(0.4);
- object-fit: contain;
- aspect-ratio: 1 / 1;
- width: 2.7rem;
- opacity: 0.8;
- transition: filter 0.3s, opacity 0.3s;
-}
-
-.docker-container-icon.flat-icon {
- opacity: 0.7;
-}
-
-.docker-container:hover .docker-container-icon {
- opacity: 1;
-}
-
-.docker-container:hover .docker-container-icon:not(.flat-icon) {
- filter: grayscale(0);
-}
-
-.docker-container-status-icon {
- width: 2rem;
- height: 2rem;
-}
-
-.widget-type-server-info {
- position: relative;
-}
-
-.server + .server {
- margin-top: 3rem;
-}
-
-.server {
- gap: 1rem;
- display: flex;
- flex-direction: column;
-}
-
-.server-info {
- align-items: center;
- display: flex;
- justify-content: space-between;
- gap: 1.5rem;
- flex-shrink: 1;
- min-width: 0;
-}
-
-.server-details {
- min-width: 0;
-}
-
-.server-icon {
- height: 3rem;
- width: 3rem;
-}
-
-.server-spicy-cpu-icon {
- height: 1em;
- align-self: center;
- margin-left: 0.4em;
- margin-bottom: 0.2rem;
-}
-
-.server-stats {
- display: flex;
- gap: 1.5rem;
- margin-top: 0.5rem;
-}
-
-.server-stat-unavailable {
- opacity: 0.5;
-}
-
-.progress-bar {
- border: 1px solid var(--color-progress-border);
- border-radius: var(--border-radius);
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 2px;
- height: 1.5rem;
- margin-inline: -3px; /* naughty, but oh so beautiful */
-}
-
-.progress-bar-combined {
- height: 3rem;
-}
-
-.popover-active > .progress-bar {
- transition: border-color .3s;
- border-color: var(--color-text-subdue);
-}
-
-.progress-value {
- --half-border-radius: calc(var(--border-radius) / 2);
- border-radius: 0 var(--half-border-radius) var(--half-border-radius) 0;
- background: var(--color-progress-value);
- width: calc(var(--percent) * 1%);
- min-width: 1px;
- flex: 1;
-}
-
-.progress-value:first-child {
- border-top-left-radius: var(--half-border-radius);
-}
-
-.progress-value:last-child {
- border-bottom-left-radius: var(--half-border-radius);
-}
-
-.progress-value-notice {
- background: linear-gradient(to right, var(--color-progress-value) 65%, var(--color-negative));
-}
-
-.value-separator {
- min-width: 2rem;
- margin-inline: 0.8rem;
- flex: 1;
- height: calc(1em * 1.1);
- border-bottom: 1px dotted var(--color-text-subdue);
-}
-
-@container widget (min-width: 650px) {
- .server {
- gap: 2rem;
- flex-direction: row;
- align-items: center;
- }
-
- .server + .server {
- margin-top: 1rem;
- }
-
- .server-info {
- flex-direction: row-reverse;
- justify-content: unset;
- margin-right: auto;
- z-index: 1;
- }
-
- .server-stats {
- flex-direction: row;
- justify-content: right;
- min-width: 450px;
- margin-top: 0;
- gap: 2rem;
- padding-bottom: 0.8rem;
- z-index: 1;
- }
-
- .server-stats > * {
- max-width: 200px;
- }
-}
-
-.thumbnail {
- filter: grayscale(0.2) contrast(0.9);
- opacity: 0.8;
- transition: filter 0.2s, opacity .2s;
-}
-
-.thumbnail-container {
- flex-shrink: 0;
- border: 1px solid var(--color-separator);
- border-radius: var(--border-radius);
-}
-
-.thumbnail-container > * {
- border-radius: var(--border-radius);
- object-fit: cover;
-}
-
-.thumbnail-parent:hover .thumbnail {
- opacity: 1;
- filter: none;
-}
-
-.rss-card-image {
- height: var(--rss-thumbnail-height, 10rem);
- object-fit: cover;
- border-radius: var(--border-radius) var(--border-radius) 0 0;
-}
-
-.rss-card-2 {
- position: relative;
- height: var(--rss-card-height, 27rem);
- overflow: hidden;
-}
-
-.rss-card-2::before {
- content: '';
- position: absolute;
- inset: 0;
- pointer-events: none;
- background-image: linear-gradient(
- 0deg,
- var(--color-widget-background),
- hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem
- );
- z-index: 2;
-}
-
-.rss-card-2-image {
- position: absolute;
- width: 100%;
- height: 100%;
- object-fit: cover;
- /* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */
- border-radius: calc(var(--border-radius) + 1px);
- opacity: 0.9;
- z-index: 1;
-}
-
-.rss-card-2-content {
- position: absolute;
- inset-inline: 0;
- bottom: var(--widget-content-vertical-padding);
- z-index: 3;
-}
-
-.rss-detailed-description {
- max-width: 55rem;
- color: var(--color-text-base-muted);
-}
-
-.rss-detailed-thumbnail {
- margin-top: 0.3rem;
-}
-
-.rss-detailed-thumbnail > * {
- aspect-ratio: 3 / 2;
- height: 8.7rem;
-}
-
-.twitch-category-thumbnail {
- width: 5rem;
- aspect-ratio: 3 / 4;
- border-radius: var(--border-radius);
-}
-
-.twitch-channel-avatar {
- aspect-ratio: 1;
- border-radius: 50%;
-}
-
-.twitch-channel-avatar-container {
- width: 4.4rem;
- height: 4.4rem;
- border: 2px solid var(--color-text-subdue);
- padding: 2px;
- border-radius: 50%;
- position: relative;
- flex-shrink: 0;
-}
-
-.twitch-channel-live .twitch-channel-avatar-container {
- border: 2px solid var(--color-positive);
- margin-bottom: 1rem;
-}
-
-.twitch-channel-live .twitch-channel-avatar-container::after {
- content: 'LIVE';
- position: absolute;
- background: var(--color-positive);
- color: var(--color-widget-background);
- font-size: var(--font-size-h6);
- left: 50%;
- bottom: -35%;
- border-radius: var(--border-radius);
- padding-inline: 0.3rem;
- transform: translate(-50%);
- border: 2px solid var(--color-widget-background);
-}
-
-.twitch-stream-preview {
- max-width: 100%;
- width: 400px;
- aspect-ratio: 16 / 9;
- border-radius: var(--border-radius);
- object-fit: cover;
-}
-
-.reddit-card-thumbnail {
- width: 100%;
- height: 100%;
- object-fit: cover;
- object-position: 0% 20%;
- opacity: 0.15;
- filter: blur(1px);
-}
-
-.reddit-card-thumbnail-container {
- position: absolute;
- inset: 0;
- overflow: hidden;
- border-radius: var(--border-radius);
-}
-
-.reddit-card-thumbnail-container::after {
- content: '';
- position: absolute;
- inset: 0;
- background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent);
-}
-
-@media (max-width: 1190px) {
- .header-container {
- display: none;
- }
-
- .page-column-small .size-title-dynamic {
- font-size: var(--font-size-h3);
- }
-
- .page-column-small {
- width: 100%;
- flex-shrink: 1;
- }
-
- .page-column {
- display: none;
- animation: columnEntrance .0s cubic-bezier(0.25, 1, 0.5, 1) backwards;
- }
-
- .page-columns-transitioned .page-column {
- animation-duration: .3s;
- }
-
- @keyframes columnEntrance {
- from {
- opacity: 0;
- transform: scaleX(0.95);
- }
- }
-
- .mobile-navigation-offset {
- height: var(--mobile-navigation-height);
- flex-shrink: 0;
- }
-
- .mobile-navigation {
- display: block;
- position: fixed;
- bottom: 0;
- transform: translateY(calc(100% - var(--mobile-navigation-height)));
- left: var(--content-bounds-padding);
- right: var(--content-bounds-padding);
- z-index: 10;
- background-color: var(--color-widget-background);
- border: 1px solid var(--color-widget-content-border);
- border-bottom: 0;
- border-radius: var(--border-radius) var(--border-radius) 0 0;
- transition: transform .3s;
- }
-
- .mobile-navigation:has(.mobile-navigation-page-links-input:checked) .hamburger-icon {
- --spacing: 7px;
- color: var(--color-primary);
- height: 2px;
- }
-
- .mobile-navigation:has(.mobile-navigation-page-links-input:checked) {
- transform: translateY(0);
- }
-
- .mobile-navigation-page-links {
- border-top: 1px solid var(--color-widget-content-border);
- padding: 15px var(--content-bounds-padding);
- display: flex;
- align-items: center;
- overflow-x: auto;
- scrollbar-width: thin;
- gap: 2.5rem;
- }
-
- .mobile-navigation-icons {
- display: flex;
- justify-content: space-around;
- align-items: center;
- }
-
- body:has(.mobile-navigation-input[value="0"]:checked) .page-columns > :nth-child(1),
- body:has(.mobile-navigation-input[value="1"]:checked) .page-columns > :nth-child(2),
- body:has(.mobile-navigation-input[value="2"]:checked) .page-columns > :nth-child(3) {
- display: block;
- }
-
- .mobile-navigation-label {
- display: flex;
- flex: 1;
- max-width: 50px;
- height: var(--mobile-navigation-height);
- justify-content: center;
- align-items: center;
- cursor: pointer;
- font-size: 15px;
- line-height: var(--mobile-navigation-height);
- }
-
- .mobile-navigation-pill {
- display: block;
- background: var(--color-text-base);
- height: 10px;
- width: 10px;
- border-radius: 10px;
- transition: width .3s, background-color .3s;
- }
-
- .mobile-navigation-label:hover > .mobile-navigation-pill {
- background-color: var(--color-text-highlight);
- }
-
- .mobile-navigation-label:hover {
- color: var(--color-text-highlight);
- }
-
- .mobile-navigation-input:checked + .mobile-navigation-pill {
- background: var(--color-primary);
- width: 30px;
- }
-
- .mobile-navigation-input, .mobile-navigation-page-links-input {
- display: none;
- }
-
- .hamburger-icon {
- --spacing: 4px;
- width: 1em;
- height: 1px;
- background-color: currentColor;
- transition: color .3s, box-shadow .3s;
- box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor;
- }
-
- .expand-toggle-button.container-expanded {
- bottom: var(--mobile-navigation-height);
- }
-
- .cards-grid + .expand-toggle-button.container-expanded {
- /* hides content that peeks through the rounded borders of the mobile navigation */
- box-shadow: 0 var(--border-radius) 0 0 var(--color-background);
- }
-
- .weather-column-rain::before {
- background-size: 7px 7px;
- }
-
- .ios .search-input {
- /* so that iOS Safari does not zoom the page when the input is focused */
- font-size: 16px;
- }
-}
-
-@media (max-width: 1190px) and (display-mode: standalone) {
- :root {
- --safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
- }
-
- .ios .body-content {
- height: 100dvh;
- }
-
- .expand-toggle-button.container-expanded {
- bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom));
- }
-
- .mobile-navigation {
- transform: translateY(calc(100% - var(--mobile-navigation-height) - var(--safe-area-inset-bottom)));
- padding-bottom: var(--safe-area-inset-bottom);
- }
-
- .mobile-navigation-icons {
- padding-bottom: var(--safe-area-inset-bottom);
- transition: padding-bottom .3s;
- }
-
- .mobile-navigation-offset {
- height: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom));
- }
-
- .mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) {
- padding-bottom: 0;
- }
-}
-
-@media (display-mode: standalone) {
- body {
- padding-top: env(safe-area-inset-top, 0);
- }
-}
-
-@media (max-width: 550px) {
- :root {
- font-size: 9px;
- --widget-gap: 15px;
- --widget-content-vertical-padding: 10px;
- --widget-content-horizontal-padding: 10px;
- --content-bounds-padding: 10px;
- }
-
- .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
-
- .row-reverse-on-mobile {
- flex-direction: row-reverse;
- }
-
- .hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) {
- display: none
- }
-
- .mobile-reachability-header {
- display: block;
- font-size: 3rem;
- padding: 10vh 1rem;
- text-align: center;
- color: var(--color-text-highlight);
- animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
- }
-
- .rss-detailed-thumbnail > * {
- height: 6rem;
- }
-
- .rss-detailed-description {
- line-clamp: 3;
- -webkit-line-clamp: 3;
- }
-}
-
-.size-h1 { font-size: var(--font-size-h1); }
-.size-h2 { font-size: var(--font-size-h2); }
-.size-h3 { font-size: var(--font-size-h3); }
-.size-h4 { font-size: var(--font-size-h4); }
-.size-base { font-size: var(--font-size-base); }
-.size-h5 { font-size: var(--font-size-h5); }
-.size-h6 { font-size: var(--font-size-h6); }
-
-.color-highlight { color: var(--color-text-highlight); }
-.color-paragraph { color: var(--color-text-paragraph); }
-.color-base { color: var(--color-text-base); }
-.color-subdue { color: var(--color-text-subdue); }
-.color-negative { color: var(--color-negative); }
-.color-positive { color: var(--color-positive); }
-.color-primary { color: var(--color-primary); }
-
-.cursor-help { cursor: help; }
-.break-all { word-break: break-all; }
-.text-left { text-align: left; }
-.text-right { text-align: right; }
-.text-center { text-align: center; }
-.text-elevate { margin-top: -0.2em; }
-.text-compact { word-spacing: -0.18em; }
-.text-very-compact { word-spacing: -0.35em; }
-.rtl { direction: rtl; }
-.shrink { flex-shrink: 1; }
-.shrink-0 { flex-shrink: 0; }
-.min-width-0 { min-width: 0; }
-.max-width-100 { max-width: 100%; }
-.block { display: block; }
-.inline-block { display: inline-block; }
-.overflow-hidden { overflow: hidden; }
-.relative { position: relative; }
-.flex { display: flex; }
-.flex-1 { flex: 1; }
-.flex-wrap { flex-wrap: wrap; }
-.flex-nowrap { flex-wrap: nowrap; }
-.justify-between { justify-content: space-between; }
-.justify-stretch { justify-content: stretch; }
-.justify-evenly { justify-content: space-evenly; }
-.justify-center { justify-content: center; }
-.justify-end { justify-content: end; }
-.uppercase { text-transform: uppercase; }
-.grow { flex-grow: 1; }
-.flex-column { flex-direction: column; }
-.items-center { align-items: center; }
-.items-start { align-items: start; }
-.items-end { align-items: end; }
-.gap-5 { gap: 0.5rem; }
-.gap-7 { gap: 0.7rem; }
-.gap-10 { gap: 1rem; }
-.gap-12 { gap: 1.2rem; }
-.gap-15 { gap: 1.5rem; }
-.gap-20 { gap: 2rem; }
-.gap-25 { gap: 2.5rem; }
-.gap-35 { gap: 3.5rem; }
-.gap-45 { gap: 4.5rem; }
-.gap-55 { gap: 5.5rem; }
-.margin-left-auto { margin-left: auto; }
-.margin-top-3 { margin-top: 0.3rem; }
-.margin-top-5 { margin-top: 0.5rem; }
-.margin-top-7 { margin-top: 0.7rem; }
-.margin-top-10 { margin-top: 1rem; }
-.margin-top-15 { margin-top: 1.5rem; }
-.margin-top-20 { margin-top: 2rem; }
-.margin-top-25 { margin-top: 2.5rem; }
-.margin-top-35 { margin-top: 3.5rem; }
-.margin-top-40 { margin-top: 4rem; }
-.margin-top-auto { margin-top: auto; }
-.margin-block-3 { margin-block: 0.3rem; }
-.margin-block-5 { margin-block: 0.5rem; }
-.margin-block-7 { margin-block: 0.7rem; }
-.margin-block-8 { margin-block: 0.8rem; }
-.margin-block-10 { margin-block: 1rem; }
-.margin-block-15 { margin-block: 1.5rem; }
-.margin-bottom-3 { margin-bottom: 0.3rem; }
-.margin-bottom-5 { margin-bottom: 0.5rem; }
-.margin-bottom-7 { margin-bottom: 0.7rem; }
-.margin-bottom-10 { margin-bottom: 1rem; }
-.margin-bottom-15 { margin-bottom: 1.5rem; }
-.margin-bottom-auto { margin-bottom: auto; }
-.padding-block-5 { padding-block: 0.5rem; }
-.scale-half { transform: scale(0.5); }
-.list { --list-half-gap: 0rem; }
-.list-gap-2 { --list-half-gap: 0.1rem; }
-.list-gap-4 { --list-half-gap: 0.2rem; }
-.list-gap-8 { --list-half-gap: 0.4rem; }
-.list-gap-10 { --list-half-gap: 0.5rem; }
-.list-gap-14 { --list-half-gap: 0.7rem; }
-.list-gap-20 { --list-half-gap: 1rem; }
-.list-gap-24 { --list-half-gap: 1.2rem; }
-.list-gap-34 { --list-half-gap: 1.7rem; }
diff --git a/internal/glance/templates/document.html b/internal/glance/templates/document.html
index a26f854..a6896e0 100644
--- a/internal/glance/templates/document.html
+++ b/internal/glance/templates/document.html
@@ -12,11 +12,11 @@
-
-
+
+
-
-
+
+
{{ block "document-head-after" . }}{{ end }}