Browse Source

implement dark mode

imwald
Silberengel 2 days ago
parent
commit
3f170a5240
  1. 6
      assets/bootstrap.js
  2. 67
      assets/controllers/color_scheme_controller.js
  3. 43
      assets/styles/layout.css
  4. BIN
      assets/theme/default/og-image.jpg
  5. 62
      assets/theme/default/theme-dark.css
  6. BIN
      assets/theme/local/og-image.jpg
  7. 2
      config/packages/twig.yaml
  8. 8
      config/unfold.yaml
  9. 61
      scripts/build-og-image.sh
  10. 58
      templates/base.html.twig
  11. 25
      templates/components/Footer.html.twig

6
assets/bootstrap.js vendored

@ -4,6 +4,7 @@ import CommentReplyController from './controllers/comment_reply_controller.js';
import CopyTextController from './controllers/copy_text_controller.js'; import CopyTextController from './controllers/copy_text_controller.js';
import UserHighlightTooltipController from './controllers/user_highlight_tooltip_controller.js'; import UserHighlightTooltipController from './controllers/user_highlight_tooltip_controller.js';
import NostrShareMenuController from './controllers/nostr_share_menu_controller.js'; import NostrShareMenuController from './controllers/nostr_share_menu_controller.js';
import ColorSchemeController from './controllers/color_scheme_controller.js';
const app = startStimulusApp(); const app = startStimulusApp();
if (typeof app.debug === 'boolean') { if (typeof app.debug === 'boolean') {
app.debug = false; app.debug = false;
@ -35,3 +36,8 @@ try {
} catch { } catch {
/* already registered by the bundle */ /* already registered by the bundle */
} }
try {
app.register('color-scheme', ColorSchemeController);
} catch {
/* already registered by the bundle */
}

67
assets/controllers/color_scheme_controller.js

@ -0,0 +1,67 @@
import { Controller } from '@hotwired/stimulus';
const STORAGE_KEY = 'unfold-color-scheme';
export default class extends Controller {
static targets = ['moon', 'sun'];
connect() {
this.link = document.getElementById('theme-magazine-stylesheet');
this._syncFromDom();
this._refreshIcons();
}
toggle() {
const cur = document.documentElement.getAttribute('data-color-scheme') || 'light';
const next = cur === 'dark' ? 'light' : 'dark';
this.apply(next, true);
}
/**
* @param {'light'|'dark'} scheme
* @param {boolean} persist
*/
apply(scheme, persist) {
const siteDefault = document.documentElement.getAttribute('data-color-scheme-default') || 'dark';
if (scheme !== 'dark' && scheme !== 'light') {
scheme = siteDefault;
}
const darkHref = this.link?.getAttribute('data-href-dark');
if (scheme === 'dark' && !darkHref) {
scheme = 'light';
}
document.documentElement.setAttribute('data-color-scheme', scheme);
if (persist) {
try {
localStorage.setItem(STORAGE_KEY, scheme);
} catch (_) {
/* private mode */
}
}
if (this.link) {
const light = this.link.getAttribute('data-href-light');
this.link.setAttribute('href', scheme === 'dark' && darkHref ? darkHref : light);
}
this._refreshIcons();
}
_syncFromDom() {
/* Link href was set by inline script; icons follow current scheme. */
this._refreshIcons();
}
_refreshIcons() {
const dark = document.documentElement.getAttribute('data-color-scheme') === 'dark';
if (this.hasMoonTarget) {
this.moonTarget.hidden = dark;
}
if (this.hasSunTarget) {
this.sunTarget.hidden = !dark;
}
const btn = this.element.querySelector('.color-scheme-toggle');
if (btn) {
btn.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
btn.setAttribute('title', dark ? 'Light mode' : 'Dark mode');
}
}
}

43
assets/styles/layout.css

@ -1250,6 +1250,45 @@ footer {
font-size: 0.95rem; font-size: 0.95rem;
} }
.site-footer__color-scheme {
margin: 0.75rem 0 0;
display: flex;
justify-content: center;
align-items: center;
}
.color-scheme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
padding: 0;
border: 1px solid var(--color-border);
border-radius: 9999px;
background: var(--color-bg-light);
color: var(--color-text-mid);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.color-scheme-toggle:hover {
background: var(--color-bg);
color: var(--color-primary);
border-color: var(--color-primary);
}
.color-scheme-toggle:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.color-scheme-toggle svg {
width: 1.1rem;
height: 1.1rem;
flex-shrink: 0;
}
@media (min-width: 900px) { @media (min-width: 900px) {
.site-footer { .site-footer {
flex-direction: row; flex-direction: row;
@ -1271,6 +1310,10 @@ footer {
.site-footer__legal { .site-footer__legal {
text-align: right; text-align: right;
} }
.site-footer__main .site-footer__color-scheme {
justify-content: flex-end;
}
} }
footer .footer-links { footer .footer-links {

BIN
assets/theme/default/og-image.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

62
assets/theme/default/theme-dark.css

@ -0,0 +1,62 @@
/*
* imwald / magazine dark color scheme (second stylesheet).
* Loaded only when data-color-scheme="dark" and config theme_stylesheet_dark is set.
* Override tokens here; keep components using var(--color-).
*/
html[data-color-scheme="dark"] {
/* Backgrounds */
--color-bg: #1c1a18;
--color-bg-light: #262320;
--color-bg-primary: #2f2b27;
/* Text */
--color-text: #e8e1d9;
--color-text-mid: #b7ada3;
--color-text-contrast: #1c1a18;
/* Accents (muted greens) */
--color-primary: #8faf8f;
--color-secondary: #a8c3a8;
--color-primary-strong: #6f9a6f;
--color-border: #3a3632;
--color-border-soft: #2a2724;
--color-text-light: var(--color-text-mid);
--color-footer-bg: #1c1a18;
--color-footer-text: var(--color-text);
--color-footer-link: var(--color-primary);
--color-highlight-mark-fg: #1c1a18;
--color-link: #8faf8f;
--color-link-hover: #a8c3a8;
--color-link-visited: #7e9f7e;
--color-focus-ring: #8faf8f;
--color-shadow: color-mix(in srgb, #000 28%, transparent);
--brand-color: #e8e1d9;
--accent-color: var(--color-secondary);
/* Reading pane / headline stack: clearly lifted from --color-bg (88/12 was visually identical). */
--article-reading-pane-bg: color-mix(in srgb, var(--color-bg) 28%, var(--color-bg-light) 72%);
--article-reading-prose-color: color-mix(in srgb, var(--color-text-mid) 38%, var(--color-text) 62%);
}
html[data-color-scheme="dark"] a:visited {
color: var(--color-link-visited);
}
html[data-color-scheme="dark"] .article-main p,
html[data-color-scheme="dark"] .article-main ul,
html[data-color-scheme="dark"] .article-main ol,
html[data-color-scheme="dark"] .article-main li {
font-weight: 450;
}
html[data-color-scheme="dark"] .home-aside-highlights__item-inner {
border-left: 2px solid var(--color-primary);
padding-left: 0.5rem;
background: color-mix(in srgb, var(--color-bg-light) 55%, transparent);
}
html[data-color-scheme="dark"] .home-aside-highlights__quote {
color: #d8d0c8;
}
html[data-color-scheme="dark"] .home-aside-highlights__item-inner:hover,
html[data-color-scheme="dark"] .home-aside-highlights__item-inner:has(.home-aside-highlights__hit:focus-visible) {
background: #2f2b27;
}

BIN
assets/theme/local/og-image.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 91 KiB

2
config/packages/twig.yaml

@ -8,6 +8,8 @@ twig:
website_theme: '%theme%' website_theme: '%theme%'
website_theme_color: '%theme_color%' website_theme_color: '%theme_color%'
website_bg_color: '%theme_bg_color%' website_bg_color: '%theme_bg_color%'
website_theme_stylesheet_light: '%theme_stylesheet_light%'
website_theme_stylesheet_dark: '%theme_stylesheet_dark%'
magazine_community_articles: '%community_articles%' magazine_community_articles: '%community_articles%'
when@test: when@test:

8
config/unfold.yaml

@ -1,4 +1,6 @@
# Site identity and theme (see assets/theme/local/ to override default assets). # Site identity and theme (see assets/theme/local/ to override default assets).
# Magazine color tokens: theme_stylesheet_light + optional theme_stylesheet_dark (logical asset names).
# Use theme_stylesheet_dark: '' to disable the footer scheme toggle and load only the light sheet.
parameters: parameters:
# Per-relay WebSocket I/O (seconds) in NostrClient; also default_socket_timeout during app:prewarm. # Per-relay WebSocket I/O (seconds) in NostrClient; also default_socket_timeout during app:prewarm.
@ -39,9 +41,15 @@ parameters:
# - 'wss://nos.lol' # - 'wss://nos.lol'
# - 'wss://relay.ditto.pub' # - 'wss://relay.ditto.pub'
# Magazine identity for data-theme=… (CSS hooks). Unrelated to light/dark color scheme.
theme: 'imwald' theme: 'imwald'
theme_color: '#8c2f1c' theme_color: '#8c2f1c'
theme_bg_color: '#f1ebe4' theme_bg_color: '#f1ebe4'
# Per–color-scheme stylesheets: logical asset names under assets/theme/{local,default}/ (see asset_mapper paths).
# imwald: light = editorial cream tokens; dark = second sheet (warm charcoal). Set theme_stylesheet_dark to ''
# to ship only one magazine theme and hide the footer sun/moon control.
theme_stylesheet_light: 'theme.css'
theme_stylesheet_dark: 'theme-dark.css'
npub: 'npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl' npub: 'npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl'
# Kind 30040 magazine root #d (NIP-33). Exposed as `d_tag` for backward compatibility. # Kind 30040 magazine root #d (NIP-33). Exposed as `d_tag` for backward compatibility.

61
scripts/build-og-image.sh

@ -1,9 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Regenerate assets/theme/local/og-image.jpg (1200×630, standard OG; JPEG for smaller file than PNG24). # Regenerate default Open Graph JPEG (1200×630): dark editorial panel + left painting.
# Layout: ~45% left = painting full-bleed (no card frame); ~55% right = warm panel + type. # Writes assets/theme/default/og-image.jpg (committed default) and assets/theme/local/og-image.jpg (local override).
# Soft gradient seam (no hard divider); headline + subheading + description from config/unfold.yaml. # Requires: ImageMagick (convert), fold. From repository root:
# Bottom-right: default theme mark (favicon) at reduced opacity.
# Requires: ImageMagick (convert), fold. Run from repository root:
# ./scripts/build-og-image.sh # ./scripts/build-og-image.sh
set -euo pipefail set -euo pipefail
@ -13,20 +11,17 @@ cd "$ROOT"
YAML="${ROOT}/config/unfold.yaml" YAML="${ROOT}/config/unfold.yaml"
LOGO="${ROOT}/assets/laeserin_logo.png" LOGO="${ROOT}/assets/laeserin_logo.png"
DEFAULT_MARK="${ROOT}/assets/theme/default/icons/favicon-96x96.png" DEFAULT_MARK="${ROOT}/assets/theme/default/icons/favicon-96x96.png"
OUT="${ROOT}/assets/theme/local/og-image.jpg" OUT_DEFAULT="${ROOT}/assets/theme/default/og-image.jpg"
OUT_LOCAL="${ROOT}/assets/theme/local/og-image.jpg"
FONT_TITLE="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" FONT_TITLE="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
FONT_SUB="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" FONT_SUB="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
FONT_BODY="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" FONT_BODY="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
W=1200 W=1200
H=630 H=630
# Safe inset for *type* and corner mark (previews crop edges); painting still bleeds left.
PAD=48 PAD=48
# Left column width (45%).
LW=$((W * 45 / 100)) LW=$((W * 45 / 100))
# Text starts after seam blend region.
TEXT_X=618 TEXT_X=618
# Vertical start for headline (shifted down for balance; sub/body computed from headline lines).
TITLE_Y=108 TITLE_Y=108
if [[ ! -f "$LOGO" ]]; then if [[ ! -f "$LOGO" ]]; then
@ -51,7 +46,7 @@ if [[ -z "$NAME" || -z "$DESC" ]]; then
exit 1 exit 1
fi fi
mkdir -p "$(dirname "$OUT")" mkdir -p "$(dirname "$OUT_DEFAULT")" "$(dirname "$OUT_LOCAL")"
TMP="$(mktemp -d)" TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT trap 'rm -rf "$TMP"' EXIT
@ -62,60 +57,54 @@ DESC_MULTILINE=$(printf '%s\n' "${_OG_LINES[@]}")
mapfile -t _HL_LINES < <(fold -s -w 22 <<<"$HEADLINE") mapfile -t _HL_LINES < <(fold -s -w 22 <<<"$HEADLINE")
HEAD_MULTILINE=$(printf '%s\n' "${_HL_LINES[@]}") HEAD_MULTILINE=$(printf '%s\n' "${_HL_LINES[@]}")
# Right panel: deeper warm linen (reads better on dark-mode surfaces than very pale beige). # Dark editorial right panel (warm charcoal, not pure black).
CANVAS='#d8cdc0' CANVAS='#1c1a18'
convert -size "${W}x${H}" xc:"$CANVAS" "$TMP/canvas.png" convert -size "${W}x${H}" xc:"$CANVAS" "$TMP/canvas.png"
# Left: full-bleed cover crop; richer reds / saturation, slightly darker mids for depth. # Left: painting — darker, slightly desaturated so it reads as “lamplight” beside the panel.
convert "$LOGO" -fuzz 8% -trim +repage \ convert "$LOGO" -fuzz 8% -trim +repage \
-resize "${LW}x${H}^" -gravity center -extent "${LW}x${H}" \ -resize "${LW}x${H}^" -gravity center -extent "${LW}x${H}" \
-gamma 0.96 -modulate 93,138,104 -unsharp 0x0.6+0.52+0.014 \ -gamma 1.05 -modulate 78,92,98 -unsharp 0x0.5+0.45+0.012 \
"$TMP/left.png" "$TMP/left.png"
convert "$TMP/canvas.png" "$TMP/left.png" -geometry +0+0 -compose over -composite "$TMP/base.png" convert "$TMP/canvas.png" "$TMP/left.png" -geometry +0+0 -compose over -composite "$TMP/base.png"
# Full-width wash: transparent at the far left → gentle panel tone on the right. # Seam: transparent at left → soft lift into panel (#262320).
# Horizontal blur removes a visible “stripe” at the old painting/canvas boundary (LW). convert -size "${W}x${H}" 'gradient:rgba(28,26,24,0)-rgba(38,35,32,0.55)' "$TMP/seam_raw.png"
# Text is drawn after this step so it stays crisp on top. convert "$TMP/seam_raw.png" -rotate 90 -blur 0x38 -rotate -90 "$TMP/seam_blur.png"
# Lighter wash than before so bodice reds survive; tint anchors toward the darker canvas.
convert -size "${W}x${H}" 'gradient:rgba(216,205,192,0)-rgba(175,158,142,0.26)' "$TMP/seam_raw.png"
# Blur mostly horizontally (rotate trick) so the ramp has no sharp column.
convert "$TMP/seam_raw.png" -rotate 90 -blur 0x42 -rotate -90 "$TMP/seam_blur.png"
convert "$TMP/base.png" "$TMP/seam_blur.png" -compose over -composite "$TMP/blended.png" convert "$TMP/base.png" "$TMP/seam_blur.png" -compose over -composite "$TMP/blended.png"
# Stack subheading / body under multi-line headline.
HL_COUNT=${#_HL_LINES[@]} HL_COUNT=${#_HL_LINES[@]}
LINE_H=58 LINE_H=58
SUB_Y=$((TITLE_Y + HL_COUNT * LINE_H + 20)) SUB_Y=$((TITLE_Y + HL_COUNT * LINE_H + 20))
BODY_Y=$((SUB_Y + 42)) BODY_Y=$((SUB_Y + 42))
# Type colours tuned for the darker panel. # Type: warm off-white headline, sage sub, muted body (matches theme-dark.css tokens).
convert "$TMP/blended.png" \ convert "$TMP/blended.png" \
-font "$FONT_TITLE" -pointsize 52 -fill '#14100e' -annotate +${TEXT_X}+${TITLE_Y} "$HEAD_MULTILINE" \ -font "$FONT_TITLE" -pointsize 52 -fill '#e8e1d9' -annotate +${TEXT_X}+${TITLE_Y} "$HEAD_MULTILINE" \
-font "$FONT_SUB" -pointsize 27 -fill '#6e2218' -annotate +${TEXT_X}+${SUB_Y} "$SUBHEAD" \ -font "$FONT_SUB" -pointsize 27 -fill '#8faf8f' -annotate +${TEXT_X}+${SUB_Y} "$SUBHEAD" \
-font "$FONT_BODY" -pointsize 23 -fill '#2a221c' -interline-spacing 6 \ -font "$FONT_BODY" -pointsize 23 -fill '#b7ada3' -interline-spacing 6 \
-annotate +${TEXT_X}+${BODY_Y} "$DESC_MULTILINE" \ -annotate +${TEXT_X}+${BODY_Y} "$DESC_MULTILINE" \
PNG24:"$TMP/with_type.png" PNG24:"$TMP/with_type.png"
# Default newsroom mark — tinted, low opacity so it belongs in the corner.
if [[ -f "$DEFAULT_MARK" ]]; then if [[ -f "$DEFAULT_MARK" ]]; then
# Circular mark (soft vs a square stamp on the OG card).
convert "$DEFAULT_MARK" -resize 88x88 \ convert "$DEFAULT_MARK" -resize 88x88 \
\( -size 88x88 xc:none -fill white -draw "circle 44,44 44,0" \) \ \( -size 88x88 xc:none -fill white -draw "circle 44,44 44,0" \) \
-compose DstIn -composite -alpha set \ -compose DstIn -composite -alpha set \
-modulate 105,85,95 \ -modulate 110,90,98 \
-channel A -evaluate multiply 0.48 +channel \ -channel A -evaluate multiply 0.42 +channel \
"$TMP/mark.png" "$TMP/mark.png"
convert "$TMP/with_type.png" \( "$TMP/mark.png" \) -gravity SouthEast -geometry +${PAD}+${PAD} -compose over -composite "$TMP/marked.png" convert "$TMP/with_type.png" \( "$TMP/mark.png" \) -gravity SouthEast -geometry +${PAD}+${PAD} -compose over -composite "$TMP/marked.png"
else else
cp "$TMP/with_type.png" "$TMP/marked.png" cp "$TMP/with_type.png" "$TMP/marked.png"
fi fi
# Global: a touch darker + more saturated so the whole card holds up on dark UI chrome. # Slight depth; keep warmth (not neon).
# JPEG (not PNG24): social previews are fine with lossy compression; much smaller on-disk. convert "$TMP/marked.png" -modulate 98,105,100 -unsharp 0x0.5+0.42+0.01 \
convert "$TMP/marked.png" -modulate 96,118,102 -unsharp 0x0.55+0.48+0.012 \ -quality 86 -sampling-factor 4:2:0 -strip "$OUT_DEFAULT"
-quality 86 -sampling-factor 4:2:0 -strip "$OUT"
cp "$OUT_DEFAULT" "$OUT_LOCAL"
rm -f "${ROOT}/assets/theme/local/og-image.png" rm -f "${ROOT}/assets/theme/local/og-image.png"
echo "Wrote $OUT ($(wc -c <"$OUT") bytes)" echo "Wrote $OUT_DEFAULT and $OUT_LOCAL ($(wc -c <"$OUT_DEFAULT") bytes each)"

58
templates/base.html.twig

@ -1,6 +1,28 @@
{% set _theme_dark_cfg = website_theme_stylesheet_dark|default('')|trim %}
{% set _color_scheme_site_default = _theme_dark_cfg != '' ? 'dark' : 'light' %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="{{ website_theme }}"> <html
lang="en"
data-theme="{{ website_theme }}"
data-color-scheme-default="{{ _color_scheme_site_default }}"
>
<head> <head>
{# Apply saved color scheme before first paint; site default is dark when a dark magazine sheet is configured. #}
<script>
(function () {
var def = document.documentElement.getAttribute('data-color-scheme-default') || 'dark';
var p = def;
try {
var s = localStorage.getItem('unfold-color-scheme');
if (def === 'light' && s === 'dark') {
p = 'light';
} else if (s === 'light' || s === 'dark') {
p = s;
}
} catch (e) {}
document.documentElement.setAttribute('data-color-scheme', p);
})();
</script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ website_name }}{% endblock %}</title> <title>{% block title %}{{ website_name }}{% endblock %}</title>
@ -26,7 +48,39 @@
{% block importmap %}{{ importmap('app') }}{% endblock %} {% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %} {% endblock %}
{% block stylesheets %} {% block stylesheets %}
<link rel="stylesheet" href="{{ asset('theme.css') }}"> {% set _theme_light = website_theme_stylesheet_light|default('theme.css') %}
{% set _theme_dark = _theme_dark_cfg %}
<link
rel="stylesheet"
id="theme-magazine-stylesheet"
href="{{ _theme_dark != '' ? asset(_theme_dark) : asset(_theme_light) }}"
data-href-light="{{ asset(_theme_light)|e('html_attr') }}"
{% if _theme_dark != '' %}
data-href-dark="{{ asset(_theme_dark)|e('html_attr') }}"
{% endif %}
>
{% if _theme_dark != '' %}
<script>
(function () {
var el = document.getElementById('theme-magazine-stylesheet');
if (!el) {
return;
}
var scheme = document.documentElement.getAttribute('data-color-scheme')
|| document.documentElement.getAttribute('data-color-scheme-default')
|| 'dark';
var dark = el.getAttribute('data-href-dark');
var light = el.getAttribute('data-href-light');
if (scheme === 'light') {
el.setAttribute('href', light);
} else if (dark) {
el.setAttribute('href', dark);
} else {
el.setAttribute('href', light);
}
})();
</script>
{% endif %}
{% endblock %} {% endblock %}
</head> </head>
<body data-controller="service-worker"> <body data-controller="service-worker">

25
templates/components/Footer.html.twig

@ -1,4 +1,9 @@
<div class="site-footer"> <div
class="site-footer"
{% if website_theme_stylesheet_dark|default('')|trim != '' %}
data-controller="color-scheme"
{% endif %}
>
<div class="site-footer__syndication"> <div class="site-footer__syndication">
<h2 class="site-footer__syndication-title">Sitemap and feeds</h2> <h2 class="site-footer__syndication-title">Sitemap and feeds</h2>
<p class="site-footer__syndication-hint">For search engines and feed readers. Atom is supported by most clients.</p> <p class="site-footer__syndication-hint">For search engines and feed readers. Atom is supported by most clients.</p>
@ -37,6 +42,24 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if website_theme_stylesheet_dark|default('')|trim != '' %}
<div class="site-footer__color-scheme">
<button
type="button"
class="color-scheme-toggle"
data-action="click->color-scheme#toggle"
aria-label="Switch to dark mode"
title="Dark mode"
>
<span data-color-scheme-target="moon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</span>
<span data-color-scheme-target="sun" hidden aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
</span>
</button>
</div>
{% endif %}
<p class="site-footer__legal"> <p class="site-footer__legal">
{{ "now"|date("Y") }} {{ website_name }} <span class="publisher">by <twig:Molecules:UserFromNpub :ident="publisher_npub" /></span> {{ "now"|date("Y") }} {{ website_name }} <span class="publisher">by <twig:Molecules:UserFromNpub :ident="publisher_npub" /></span>
</p> </p>

Loading…
Cancel
Save