privacy pass: degoogle, CSP, referrer
- Replace Google Fonts with system font stacks across all themes - Add Referrer-Policy, X-Content-Type-Options, X-Frame-Options, CSP headers - Add rel="noreferrer noopener" on all outbound links - Add no-referrer and dns-prefetch-control meta tags to all themes - Clean tracking params on outbound links from trusted/remote sources - Remove Google domains from CSP whitelists
This commit is contained in:
parent
9738d28b60
commit
a9f426132e
6 changed files with 285 additions and 275 deletions
|
|
@ -123,6 +123,14 @@ class GatewayHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
self.send_response(resp["status"])
|
self.send_response(resp["status"])
|
||||||
self.send_header("Content-Type", resp.get("content_type", "text/html; charset=utf-8"))
|
self.send_header("Content-Type", resp.get("content_type", "text/html; charset=utf-8"))
|
||||||
|
self.send_header("Referrer-Policy", "no-referrer")
|
||||||
|
self.send_header("X-Content-Type-Options", "nosniff")
|
||||||
|
self.send_header("X-Frame-Options", "DENY")
|
||||||
|
self.send_header("Content-Security-Policy",
|
||||||
|
"default-src 'self'; "
|
||||||
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
|
"script-src 'self' 'unsafe-inline'; "
|
||||||
|
"img-src 'self' data:")
|
||||||
for k, v in resp.get("headers", {}).items():
|
for k, v in resp.get("headers", {}).items():
|
||||||
self.send_header(k, v)
|
self.send_header(k, v)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
|
||||||
19
handlers.py
19
handlers.py
|
|
@ -245,7 +245,7 @@ def handle_search(query):
|
||||||
snip_html = f'<br>{esc(r["summary"])}' if r["summary"] else ""
|
snip_html = f'<br>{esc(r["summary"])}' if r["summary"] else ""
|
||||||
result_html += (
|
result_html += (
|
||||||
f'<div class="result">'
|
f'<div class="result">'
|
||||||
f'<a href="{esc(r["url"])}">{esc(r["title"])}</a><br>'
|
f'<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["title"])}</a><br>'
|
||||||
f'<small>{esc(r["url"])}</small>'
|
f'<small>{esc(r["url"])}</small>'
|
||||||
f'{snip_html}'
|
f'{snip_html}'
|
||||||
f'{note_html}{tags_html}'
|
f'{note_html}{tags_html}'
|
||||||
|
|
@ -276,7 +276,7 @@ def handle_search(query):
|
||||||
items = ""
|
items = ""
|
||||||
for l in trusted:
|
for l in trusted:
|
||||||
items += (
|
items += (
|
||||||
f'<li><a href="{esc(l["url"])}">{esc(l["label"])}</a> '
|
f'<li><a href="{esc(clean_url(l["url"]))}" rel="noreferrer noopener">{esc(l["label"])}</a> '
|
||||||
f'<small>— from {esc(l["source_title"])}</small></li>'
|
f'<small>— from {esc(l["source_title"])}</small></li>'
|
||||||
)
|
)
|
||||||
trusted_html = (
|
trusted_html = (
|
||||||
|
|
@ -311,8 +311,8 @@ def handle_search(query):
|
||||||
for r in items:
|
for r in items:
|
||||||
note_html = f' — <em>{esc(r["note"])}</em>' if r["note"] else ""
|
note_html = f' — <em>{esc(r["note"])}</em>' if r["note"] else ""
|
||||||
source_items += (
|
source_items += (
|
||||||
f'<li><a href="{esc(r["url"])}">{esc(r["title"])}</a>'
|
f'<li><a href="{esc(clean_url(r["url"]))}" rel="noreferrer noopener">{esc(r["title"])}</a>'
|
||||||
f'{note_html} <small>({esc(r["url"])})</small></li>'
|
f'{note_html} <small>({esc(clean_url(r["url"]))})</small></li>'
|
||||||
)
|
)
|
||||||
remote_html += (
|
remote_html += (
|
||||||
f'<details class="remote" open>'
|
f'<details class="remote" open>'
|
||||||
|
|
@ -473,7 +473,7 @@ def handle_add_manual_submit(body):
|
||||||
# Log error but don't fail the whole operation
|
# Log error but don't fail the whole operation
|
||||||
print(f"Error generating embeddings: {e}")
|
print(f"Error generating embeddings: {e}")
|
||||||
|
|
||||||
return handle_add_form(f'Added manually: <a href="{esc(url)}">{esc(manual_title)}</a>')
|
return handle_add_form(f'Added manually: <a href="{esc(url)}" rel="noreferrer noopener">{esc(manual_title)}</a>')
|
||||||
finally:
|
finally:
|
||||||
return_db(db)
|
return_db(db)
|
||||||
|
|
||||||
|
|
@ -500,7 +500,7 @@ def handle_pages(query=None):
|
||||||
tags_html = f' {tag_links}'
|
tags_html = f' {tag_links}'
|
||||||
items += (
|
items += (
|
||||||
f'<li>{esc(r["title"])}{note_html}{tags_html} '
|
f'<li>{esc(r["title"])}{note_html}{tags_html} '
|
||||||
f'<small>(<a href="{esc(r["url"])}">{esc(r["url"])}</a>)</small> '
|
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small> '
|
||||||
f'<a href="/edit/{r["id"]}">edit</a> '
|
f'<a href="/edit/{r["id"]}">edit</a> '
|
||||||
f'<a href="/delete/{r["id"]}">remove</a></li>'
|
f'<a href="/delete/{r["id"]}">remove</a></li>'
|
||||||
)
|
)
|
||||||
|
|
@ -700,7 +700,7 @@ def handle_style_form(msg=""):
|
||||||
f"<small>Default: reticulum.derickphan.com:4242</small><br>"
|
f"<small>Default: reticulum.derickphan.com:4242</small><br>"
|
||||||
f'<input name="transport_host" value="{esc(transport_host)}" placeholder="hostname" size="30">'
|
f'<input name="transport_host" value="{esc(transport_host)}" placeholder="hostname" size="30">'
|
||||||
f' <input name="transport_port" value="{esc(transport_port)}" placeholder="port" size="6"><br>'
|
f' <input name="transport_port" value="{esc(transport_port)}" placeholder="port" size="6"><br>'
|
||||||
f'<p><a href="https://rmap.world/" target="_blank">discover more nodes</a></p><br>'
|
f'<p><a href="https://rmap.world/" target="_blank" rel="noreferrer noopener">discover more nodes</a></p><br>'
|
||||||
f"<h2>search</h2>"
|
f"<h2>search</h2>"
|
||||||
f"<h3>ai</h3>"
|
f"<h3>ai</h3>"
|
||||||
f'<label><input type="checkbox" name="semantic_search" value="1"{semantic_checked} '
|
f'<label><input type="checkbox" name="semantic_search" value="1"{semantic_checked} '
|
||||||
|
|
@ -851,7 +851,7 @@ def handle_tag_browse(tag_name, query=None):
|
||||||
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags)
|
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags)
|
||||||
items += (
|
items += (
|
||||||
f'<li>{esc(r["title"])}{note_html} {tag_links} '
|
f'<li>{esc(r["title"])}{note_html} {tag_links} '
|
||||||
f'<small>(<a href="{esc(r["url"])}">{esc(r["url"])}</a>)</small></li>'
|
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small></li>'
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
return_db(db)
|
return_db(db)
|
||||||
|
|
@ -1397,8 +1397,7 @@ def dispatch_request(data):
|
||||||
resp["headers"]["Content-Security-Policy"] = (
|
resp["headers"]["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"script-src 'self' 'unsafe-inline'; "
|
"script-src 'self' 'unsafe-inline'; "
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"font-src 'self' https://fonts.gstatic.com; "
|
|
||||||
"img-src * data:; "
|
"img-src * data:; "
|
||||||
"frame-ancestors 'none'; "
|
"frame-ancestors 'none'; "
|
||||||
"form-action 'self'; "
|
"form-action 'self'; "
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ def esc(s):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TEMPLATE = "<html>\n<head>\n</head>\n<body>\n{{content}}\n</body>\n</html>"
|
DEFAULT_TEMPLATE = "<html>\n<head>\n<meta name=\"referrer\" content=\"no-referrer\">\n<meta http-equiv=\"x-dns-prefetch-control\" content=\"off\">\n</head>\n<body>\n{{content}}\n</body>\n</html>"
|
||||||
|
|
||||||
|
|
||||||
def _default_template():
|
def _default_template():
|
||||||
name = esc(get_setting("site_name", "tinyweb"))
|
name = esc(get_setting("site_name", "tinyweb"))
|
||||||
return (
|
return (
|
||||||
"<html>\n<head>\n</head>\n<body>\n"
|
'<html>\n<head>\n<meta name="referrer" content="no-referrer">\n<meta http-equiv="x-dns-prefetch-control" content="off">\n</head>\n<body>\n'
|
||||||
f'<p><b><a href="/">{name}</a></b>'
|
f'<p><b><a href="/">{name}</a></b>'
|
||||||
' | <a href="/">search</a> | <a href="/pages">browse</a>'
|
' | <a href="/">search</a> | <a href="/pages">browse</a>'
|
||||||
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
|
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,600;0,700;1,400&family=Fira+Code:wght@400;500&display=swap');
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
html, body { min-height: 100vh; }
|
html, body { min-height: 100vh; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Nunito', -apple-system, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
color: #e0d8c8;
|
color: #e0d8c8;
|
||||||
|
|
@ -81,7 +82,7 @@
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
@ -289,7 +290,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .site {
|
nav .site {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #f0d878;
|
color: #f0d878;
|
||||||
|
|
@ -383,7 +384,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.6rem 0.85rem;
|
padding: 0.6rem 0.85rem;
|
||||||
color: #d0c8b8;
|
color: #d0c8b8;
|
||||||
font-family: 'Nunito', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
transition: border-color 0.2s, box-shadow 0.3s;
|
transition: border-color 0.2s, box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
|
|
@ -400,7 +401,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.6rem 1.1rem;
|
padding: 0.6rem 1.1rem;
|
||||||
color: #a098b0;
|
color: #a098b0;
|
||||||
font-family: 'Nunito', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +444,7 @@
|
||||||
.tags { margin-top: 0.3rem; }
|
.tags { margin-top: 0.3rem; }
|
||||||
|
|
||||||
.tag, .tags a {
|
.tag, .tags a {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #8878a0;
|
color: #8878a0;
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
|
@ -486,7 +487,7 @@
|
||||||
|
|
||||||
/* code */
|
/* code */
|
||||||
pre {
|
pre {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background: rgba(15, 10, 40, 0.5);
|
background: rgba(15, 10, 40, 0.5);
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
|
@ -498,7 +499,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
background: rgba(15, 10, 40, 0.4);
|
background: rgba(15, 10, 40, 0.4);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -513,7 +514,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0.7rem 0.9rem;
|
||||||
color: #d0c8b8;
|
color: #d0c8b8;
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
|
@ -547,7 +548,7 @@
|
||||||
hr { border: none; border-top: 1px solid rgba(255,255,255,0.06); margin: 1rem 0; }
|
hr { border: none; border-top: 1px solid rgba(255,255,255,0.06); margin: 1rem 0; }
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #5a5070;
|
color: #5a5070;
|
||||||
}
|
}
|
||||||
|
|
@ -562,7 +563,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .clock {
|
footer .clock {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: #3a3050;
|
color: #3a3050;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap');
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
color: #c8c8c8;
|
color: #c8c8c8;
|
||||||
|
|
@ -71,16 +72,16 @@
|
||||||
z-index: 998;
|
z-index: 998;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* kodama spirits */
|
/* kodama spirits */
|
||||||
#kodama {
|
#kodama {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0; left: 0;
|
top: 0; left: 0;
|
||||||
width: 100%; height: 100%;
|
width: 100%; height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
max-width: 660px;
|
max-width: 660px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
@ -99,7 +100,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .site {
|
nav .site {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #e8e8e8;
|
color: #e8e8e8;
|
||||||
|
|
@ -193,7 +194,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.6rem 0.85rem;
|
padding: 0.6rem 0.85rem;
|
||||||
color: #d0d0d0;
|
color: #d0d0d0;
|
||||||
font-family: 'IBM Plex Sans', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
transition: border-color 0.2s, box-shadow 0.3s;
|
transition: border-color 0.2s, box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +211,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.6rem 1.1rem;
|
padding: 0.6rem 1.1rem;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-family: 'IBM Plex Sans', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +254,7 @@
|
||||||
.tags { margin-top: 0.3rem; }
|
.tags { margin-top: 0.3rem; }
|
||||||
|
|
||||||
.tag, .tags a {
|
.tag, .tags a {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
border: 1px solid #252525;
|
border: 1px solid #252525;
|
||||||
|
|
@ -296,7 +297,7 @@
|
||||||
|
|
||||||
/* code */
|
/* code */
|
||||||
pre {
|
pre {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background: #151515;
|
background: #151515;
|
||||||
border: 1px solid #232323;
|
border: 1px solid #232323;
|
||||||
|
|
@ -308,7 +309,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -323,7 +324,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0.7rem 0.9rem;
|
||||||
color: #c8c8c8;
|
color: #c8c8c8;
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
|
@ -357,7 +358,7 @@
|
||||||
hr { border: none; border-top: 1px solid #1e1e1e; margin: 1rem 0; }
|
hr { border: none; border-top: 1px solid #1e1e1e; margin: 1rem 0; }
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #484848;
|
color: #484848;
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +373,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .clock {
|
footer .clock {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: #282828;
|
color: #282828;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
@ -393,7 +394,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="particles"></canvas>
|
<canvas id="particles"></canvas>
|
||||||
<canvas id="trail"></canvas>
|
<canvas id="trail"></canvas>
|
||||||
<canvas id="kodama"></canvas>
|
<canvas id="kodama"></canvas>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<nav>
|
<nav>
|
||||||
|
|
@ -521,224 +522,224 @@
|
||||||
requestAnimationFrame(drawTrail);
|
requestAnimationFrame(drawTrail);
|
||||||
}
|
}
|
||||||
drawTrail();
|
drawTrail();
|
||||||
|
|
||||||
// kodama (tree spirits)
|
// kodama (tree spirits)
|
||||||
var kc = document.getElementById('kodama');
|
var kc = document.getElementById('kodama');
|
||||||
var kctx = kc.getContext('2d');
|
var kctx = kc.getContext('2d');
|
||||||
var spirits = [];
|
var spirits = [];
|
||||||
var numSpirits = 8;
|
var numSpirits = 8;
|
||||||
|
|
||||||
function resizeKodama() {
|
function resizeKodama() {
|
||||||
kc.width = window.innerWidth;
|
kc.width = window.innerWidth;
|
||||||
kc.height = window.innerHeight;
|
kc.height = window.innerHeight;
|
||||||
}
|
}
|
||||||
resizeKodama();
|
resizeKodama();
|
||||||
window.addEventListener('resize', resizeKodama);
|
window.addEventListener('resize', resizeKodama);
|
||||||
|
|
||||||
for (var i = 0; i < numSpirits; i++) {
|
for (var i = 0; i < numSpirits; i++) {
|
||||||
// each spirit gets unique proportions
|
// each spirit gets unique proportions
|
||||||
var headR = 0.38 + Math.random() * 0.12; // head radius ratio (bigger = bigger head)
|
var headR = 0.38 + Math.random() * 0.12; // head radius ratio (bigger = bigger head)
|
||||||
var bodyH = 0.25 + Math.random() * 0.2; // body height ratio
|
var bodyH = 0.25 + Math.random() * 0.2; // body height ratio
|
||||||
var bodyW = 0.3 + Math.random() * 0.15; // body width ratio
|
var bodyW = 0.3 + Math.random() * 0.15; // body width ratio
|
||||||
var eyeSpread = 0.18 + Math.random() * 0.1; // how far apart eyes are
|
var eyeSpread = 0.18 + Math.random() * 0.1; // how far apart eyes are
|
||||||
var eyeSize = 0.055 + Math.random() * 0.03; // eye dot size
|
var eyeSize = 0.055 + Math.random() * 0.03; // eye dot size
|
||||||
var eyeY = -0.08 + Math.random() * 0.08; // eye vertical position
|
var eyeY = -0.08 + Math.random() * 0.08; // eye vertical position
|
||||||
var hasMouth = Math.random() > 0.3; // 70% have visible mouth
|
var hasMouth = Math.random() > 0.3; // 70% have visible mouth
|
||||||
var mouthSize = 0.03 + Math.random() * 0.03;
|
var mouthSize = 0.03 + Math.random() * 0.03;
|
||||||
var mouthY = 0.15 + Math.random() * 0.1;
|
var mouthY = 0.15 + Math.random() * 0.1;
|
||||||
var hasArms = Math.random() > 0.4; // 60% have little arm bumps
|
var hasArms = Math.random() > 0.4; // 60% have little arm bumps
|
||||||
var glowSize = 1.4 + Math.random() * 0.8; // glow radius multiplier
|
var glowSize = 1.4 + Math.random() * 0.8; // glow radius multiplier
|
||||||
var glowAlpha = 0.08 + Math.random() * 0.12; // glow brightness
|
var glowAlpha = 0.08 + Math.random() * 0.12; // glow brightness
|
||||||
var tint = Math.floor(Math.random() * 15); // slight warm/cool variation
|
var tint = Math.floor(Math.random() * 15); // slight warm/cool variation
|
||||||
|
|
||||||
spirits.push({
|
spirits.push({
|
||||||
x: Math.random() * 0.8 + 0.1,
|
x: Math.random() * 0.8 + 0.1,
|
||||||
baseY: 0.75 + Math.random() * 0.18,
|
baseY: 0.75 + Math.random() * 0.18,
|
||||||
size: 10 + Math.random() * 12,
|
size: 10 + Math.random() * 12,
|
||||||
phase: Math.random() * Math.PI * 2,
|
phase: Math.random() * Math.PI * 2,
|
||||||
tiltSpeed: 1.2 + Math.random() * 2,
|
tiltSpeed: 1.2 + Math.random() * 2,
|
||||||
bobSpeed: 0.6 + Math.random() * 0.8,
|
bobSpeed: 0.6 + Math.random() * 0.8,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
targetOpacity: 0.4 + Math.random() * 0.45,
|
targetOpacity: 0.4 + Math.random() * 0.45,
|
||||||
fadeSpeed: 0.002 + Math.random() * 0.004,
|
fadeSpeed: 0.002 + Math.random() * 0.004,
|
||||||
appearing: true,
|
appearing: true,
|
||||||
timer: Math.random() * 600,
|
timer: Math.random() * 600,
|
||||||
lifespan: 500 + Math.random() * 600,
|
lifespan: 500 + Math.random() * 600,
|
||||||
rattleTime: 0,
|
rattleTime: 0,
|
||||||
rattling: false,
|
rattling: false,
|
||||||
// unique shape params
|
// unique shape params
|
||||||
headR: headR,
|
headR: headR,
|
||||||
bodyH: bodyH,
|
bodyH: bodyH,
|
||||||
bodyW: bodyW,
|
bodyW: bodyW,
|
||||||
eyeSpread: eyeSpread,
|
eyeSpread: eyeSpread,
|
||||||
eyeSize: eyeSize,
|
eyeSize: eyeSize,
|
||||||
eyeY: eyeY,
|
eyeY: eyeY,
|
||||||
hasMouth: hasMouth,
|
hasMouth: hasMouth,
|
||||||
mouthSize: mouthSize,
|
mouthSize: mouthSize,
|
||||||
mouthY: mouthY,
|
mouthY: mouthY,
|
||||||
hasArms: hasArms,
|
hasArms: hasArms,
|
||||||
glowSize: glowSize,
|
glowSize: glowSize,
|
||||||
glowAlpha: glowAlpha,
|
glowAlpha: glowAlpha,
|
||||||
tint: tint,
|
tint: tint,
|
||||||
// 8 offsets that warp the head into a unique rock-like blob
|
// 8 offsets that warp the head into a unique rock-like blob
|
||||||
hw: [
|
hw: [
|
||||||
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
|
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
|
||||||
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25,
|
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25,
|
||||||
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
|
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
|
||||||
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25
|
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25
|
||||||
],
|
],
|
||||||
headTall: 0.8 + Math.random() * 0.5 // overall tall vs wide
|
headTall: 0.8 + Math.random() * 0.5 // overall tall vs wide
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawKodamaSpirit(x, y, size, tilt, opacity, sp) {
|
function drawKodamaSpirit(x, y, size, tilt, opacity, sp) {
|
||||||
kctx.save();
|
kctx.save();
|
||||||
kctx.translate(x, y);
|
kctx.translate(x, y);
|
||||||
kctx.globalAlpha = opacity;
|
kctx.globalAlpha = opacity;
|
||||||
|
|
||||||
var r = size * sp.headR; // head radius
|
var r = size * sp.headR; // head radius
|
||||||
|
|
||||||
// outer glow aura
|
// outer glow aura
|
||||||
var grd = kctx.createRadialGradient(0, -size * 0.1, r * 0.3, 0, -size * 0.1, r * sp.glowSize);
|
var grd = kctx.createRadialGradient(0, -size * 0.1, r * 0.3, 0, -size * 0.1, r * sp.glowSize);
|
||||||
grd.addColorStop(0, 'rgba(255, 255, 250, ' + sp.glowAlpha + ')');
|
grd.addColorStop(0, 'rgba(255, 255, 250, ' + sp.glowAlpha + ')');
|
||||||
grd.addColorStop(0.5, 'rgba(255, 255, 250, ' + (sp.glowAlpha * 0.3) + ')');
|
grd.addColorStop(0.5, 'rgba(255, 255, 250, ' + (sp.glowAlpha * 0.3) + ')');
|
||||||
grd.addColorStop(1, 'rgba(255, 255, 250, 0)');
|
grd.addColorStop(1, 'rgba(255, 255, 250, 0)');
|
||||||
kctx.fillStyle = grd;
|
kctx.fillStyle = grd;
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.arc(0, -size * 0.1, r * sp.glowSize, 0, Math.PI * 2);
|
kctx.arc(0, -size * 0.1, r * sp.glowSize, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
|
|
||||||
// body - stubby rounded shape
|
// body - stubby rounded shape
|
||||||
var bw = size * sp.bodyW;
|
var bw = size * sp.bodyW;
|
||||||
var bh = size * sp.bodyH;
|
var bh = size * sp.bodyH;
|
||||||
var by = size * 0.15;
|
var by = size * 0.15;
|
||||||
kctx.fillStyle = 'rgb(' + (238 + sp.tint) + ',' + (237 + sp.tint) + ',' + (230 + sp.tint) + ')';
|
kctx.fillStyle = 'rgb(' + (238 + sp.tint) + ',' + (237 + sp.tint) + ',' + (230 + sp.tint) + ')';
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.ellipse(0, by + bh * 0.4, bw * 0.5, bh * 0.55, 0, 0, Math.PI * 2);
|
kctx.ellipse(0, by + bh * 0.4, bw * 0.5, bh * 0.55, 0, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
|
|
||||||
// arms - tiny bumps on sides
|
// arms - tiny bumps on sides
|
||||||
if (sp.hasArms) {
|
if (sp.hasArms) {
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.ellipse(-bw * 0.5 - size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, -0.3, 0, Math.PI * 2);
|
kctx.ellipse(-bw * 0.5 - size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, -0.3, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.ellipse(bw * 0.5 + size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, 0.3, 0, Math.PI * 2);
|
kctx.ellipse(bw * 0.5 + size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, 0.3, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// head (tilts)
|
// head (tilts)
|
||||||
kctx.save();
|
kctx.save();
|
||||||
kctx.rotate(tilt);
|
kctx.rotate(tilt);
|
||||||
|
|
||||||
// head - unique rock-like blob shape per spirit
|
// head - unique rock-like blob shape per spirit
|
||||||
var hx = 0, hy = -size * 0.15;
|
var hx = 0, hy = -size * 0.15;
|
||||||
var rx = r, ry = r * sp.headTall;
|
var rx = r, ry = r * sp.headTall;
|
||||||
var w = sp.hw;
|
var w = sp.hw;
|
||||||
kctx.fillStyle = 'rgb(' + (243 + sp.tint) + ',' + (242 + sp.tint) + ',' + (237 + sp.tint) + ')';
|
kctx.fillStyle = 'rgb(' + (243 + sp.tint) + ',' + (242 + sp.tint) + ',' + (237 + sp.tint) + ')';
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
// top
|
// top
|
||||||
kctx.moveTo(hx + r * w[0], hy - ry);
|
kctx.moveTo(hx + r * w[0], hy - ry);
|
||||||
// top-right
|
// top-right
|
||||||
kctx.bezierCurveTo(
|
kctx.bezierCurveTo(
|
||||||
hx + rx * (0.55 + w[0]), hy - ry * (0.9 + w[1]),
|
hx + rx * (0.55 + w[0]), hy - ry * (0.9 + w[1]),
|
||||||
hx + rx * (1.0 + w[1]), hy - ry * (0.4 + w[0]),
|
hx + rx * (1.0 + w[1]), hy - ry * (0.4 + w[0]),
|
||||||
hx + rx * (1.0 + w[2]), hy + ry * w[2]);
|
hx + rx * (1.0 + w[2]), hy + ry * w[2]);
|
||||||
// bottom-right
|
// bottom-right
|
||||||
kctx.bezierCurveTo(
|
kctx.bezierCurveTo(
|
||||||
hx + rx * (1.0 + w[3]), hy + ry * (0.5 + w[2]),
|
hx + rx * (1.0 + w[3]), hy + ry * (0.5 + w[2]),
|
||||||
hx + rx * (0.5 + w[3]), hy + ry * (1.0 + w[3]),
|
hx + rx * (0.5 + w[3]), hy + ry * (1.0 + w[3]),
|
||||||
hx + r * w[4], hy + ry * (0.95 + w[4] * 0.3));
|
hx + r * w[4], hy + ry * (0.95 + w[4] * 0.3));
|
||||||
// bottom-left
|
// bottom-left
|
||||||
kctx.bezierCurveTo(
|
kctx.bezierCurveTo(
|
||||||
hx - rx * (0.5 + w[5]), hy + ry * (1.0 + w[5]),
|
hx - rx * (0.5 + w[5]), hy + ry * (1.0 + w[5]),
|
||||||
hx - rx * (1.0 + w[5]), hy + ry * (0.5 + w[4]),
|
hx - rx * (1.0 + w[5]), hy + ry * (0.5 + w[4]),
|
||||||
hx - rx * (1.0 + w[6]), hy + ry * w[6]);
|
hx - rx * (1.0 + w[6]), hy + ry * w[6]);
|
||||||
// top-left
|
// top-left
|
||||||
kctx.bezierCurveTo(
|
kctx.bezierCurveTo(
|
||||||
hx - rx * (1.0 + w[7]), hy - ry * (0.4 + w[6]),
|
hx - rx * (1.0 + w[7]), hy - ry * (0.4 + w[6]),
|
||||||
hx - rx * (0.55 + w[7]),hy - ry * (0.9 + w[7]),
|
hx - rx * (0.55 + w[7]),hy - ry * (0.9 + w[7]),
|
||||||
hx + r * w[0], hy - ry);
|
hx + r * w[0], hy - ry);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
|
|
||||||
// subtle inner highlight
|
// subtle inner highlight
|
||||||
kctx.fillStyle = 'rgba(255, 255, 252, 0.25)';
|
kctx.fillStyle = 'rgba(255, 255, 252, 0.25)';
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.arc(-rx * 0.12, hy - ry * 0.1, r * 0.45, 0, Math.PI * 2);
|
kctx.arc(-rx * 0.12, hy - ry * 0.1, r * 0.45, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
|
|
||||||
// eyes - small round dark dots
|
// eyes - small round dark dots
|
||||||
kctx.fillStyle = 'rgba(15, 15, 15, 0.9)';
|
kctx.fillStyle = 'rgba(15, 15, 15, 0.9)';
|
||||||
var ey = -size * 0.15 + size * sp.eyeY;
|
var ey = -size * 0.15 + size * sp.eyeY;
|
||||||
var es = size * sp.eyeSize;
|
var es = size * sp.eyeSize;
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.arc(-size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
|
kctx.arc(-size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.arc(size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
|
kctx.arc(size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
|
|
||||||
// mouth - tiny dot, not all have one
|
// mouth - tiny dot, not all have one
|
||||||
if (sp.hasMouth) {
|
if (sp.hasMouth) {
|
||||||
kctx.fillStyle = 'rgba(15, 15, 15, 0.7)';
|
kctx.fillStyle = 'rgba(15, 15, 15, 0.7)';
|
||||||
kctx.beginPath();
|
kctx.beginPath();
|
||||||
kctx.arc(0, ey + size * sp.mouthY, size * sp.mouthSize, 0, Math.PI * 2);
|
kctx.arc(0, ey + size * sp.mouthY, size * sp.mouthSize, 0, Math.PI * 2);
|
||||||
kctx.fill();
|
kctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
kctx.restore(); // head tilt
|
kctx.restore(); // head tilt
|
||||||
kctx.restore(); // position
|
kctx.restore(); // position
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawKodama() {
|
function drawKodama() {
|
||||||
kctx.clearRect(0, 0, kc.width, kc.height);
|
kctx.clearRect(0, 0, kc.width, kc.height);
|
||||||
var t = Date.now() * 0.001;
|
var t = Date.now() * 0.001;
|
||||||
|
|
||||||
for (var i = 0; i < spirits.length; i++) {
|
for (var i = 0; i < spirits.length; i++) {
|
||||||
var s = spirits[i];
|
var s = spirits[i];
|
||||||
s.timer++;
|
s.timer++;
|
||||||
|
|
||||||
if (s.appearing) {
|
if (s.appearing) {
|
||||||
s.opacity += s.fadeSpeed;
|
s.opacity += s.fadeSpeed;
|
||||||
if (s.opacity >= s.targetOpacity) s.opacity = s.targetOpacity;
|
if (s.opacity >= s.targetOpacity) s.opacity = s.targetOpacity;
|
||||||
if (s.timer > s.lifespan) s.appearing = false;
|
if (s.timer > s.lifespan) s.appearing = false;
|
||||||
} else {
|
} else {
|
||||||
s.opacity -= s.fadeSpeed;
|
s.opacity -= s.fadeSpeed;
|
||||||
if (s.opacity <= 0) {
|
if (s.opacity <= 0) {
|
||||||
s.opacity = 0;
|
s.opacity = 0;
|
||||||
s.x = Math.random() * 0.8 + 0.1;
|
s.x = Math.random() * 0.8 + 0.1;
|
||||||
s.baseY = 0.75 + Math.random() * 0.18;
|
s.baseY = 0.75 + Math.random() * 0.18;
|
||||||
s.targetOpacity = 0.4 + Math.random() * 0.45;
|
s.targetOpacity = 0.4 + Math.random() * 0.45;
|
||||||
s.timer = 0;
|
s.timer = 0;
|
||||||
s.lifespan = 500 + Math.random() * 600;
|
s.lifespan = 500 + Math.random() * 600;
|
||||||
s.appearing = true;
|
s.appearing = true;
|
||||||
s.rattleTime = 0;
|
s.rattleTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.opacity <= 0) continue;
|
if (s.opacity <= 0) continue;
|
||||||
|
|
||||||
if (!s.rattling && Math.random() < 0.004) {
|
if (!s.rattling && Math.random() < 0.004) {
|
||||||
s.rattling = true;
|
s.rattling = true;
|
||||||
s.rattleTime = 0;
|
s.rattleTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tilt = Math.sin(t * s.tiltSpeed + s.phase) * 0.1;
|
var tilt = Math.sin(t * s.tiltSpeed + s.phase) * 0.1;
|
||||||
if (s.rattling) {
|
if (s.rattling) {
|
||||||
s.rattleTime++;
|
s.rattleTime++;
|
||||||
tilt = Math.sin(s.rattleTime * 0.9) * 0.35 * Math.max(0, 1 - s.rattleTime / 25);
|
tilt = Math.sin(s.rattleTime * 0.9) * 0.35 * Math.max(0, 1 - s.rattleTime / 25);
|
||||||
if (s.rattleTime > 25) s.rattling = false;
|
if (s.rattleTime > 25) s.rattling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bobY = Math.sin(t * s.bobSpeed + s.phase) * 2.5;
|
var bobY = Math.sin(t * s.bobSpeed + s.phase) * 2.5;
|
||||||
var px = s.x * kc.width;
|
var px = s.x * kc.width;
|
||||||
var py = s.baseY * kc.height + bobY;
|
var py = s.baseY * kc.height + bobY;
|
||||||
|
|
||||||
drawKodamaSpirit(px, py, s.size, tilt, s.opacity, s);
|
drawKodamaSpirit(px, py, s.size, tilt, s.opacity, s);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(drawKodama);
|
requestAnimationFrame(drawKodama);
|
||||||
}
|
}
|
||||||
drawKodama();
|
drawKodama();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap');
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box;
|
* { margin: 0; padding: 0; box-sizing: border-box;
|
||||||
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='3' fill='none' stroke='%23557766' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='1' fill='%2377aa88'/%3E%3C/svg%3E") 10 10, default;
|
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='3' fill='none' stroke='%23557766' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='1' fill='%2377aa88'/%3E%3C/svg%3E") 10 10, default;
|
||||||
|
|
@ -13,7 +14,7 @@
|
||||||
html, body { min-height: 100vh; }
|
html, body { min-height: 100vh; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
color: #9ab4b8;
|
color: #9ab4b8;
|
||||||
|
|
@ -65,7 +66,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .site {
|
nav .site {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #b0ccc4;
|
color: #b0ccc4;
|
||||||
|
|
@ -158,7 +159,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.6rem 0.85rem;
|
padding: 0.6rem 0.85rem;
|
||||||
color: #90b4ac;
|
color: #90b4ac;
|
||||||
font-family: 'IBM Plex Sans', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
transition: border-color 0.2s, box-shadow 0.3s;
|
transition: border-color 0.2s, box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +176,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.6rem 1.1rem;
|
padding: 0.6rem 1.1rem;
|
||||||
color: #5a7880;
|
color: #5a7880;
|
||||||
font-family: 'IBM Plex Sans', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +241,7 @@
|
||||||
.tags { margin-top: 0.3rem; }
|
.tags { margin-top: 0.3rem; }
|
||||||
|
|
||||||
.tag, .tags a {
|
.tag, .tags a {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #3a5e55;
|
color: #3a5e55;
|
||||||
border: 1px solid rgba(40, 70, 60, 0.35);
|
border: 1px solid rgba(40, 70, 60, 0.35);
|
||||||
|
|
@ -273,7 +274,7 @@
|
||||||
li a:hover { border-bottom: 1px solid rgba(80, 130, 110, 0.4); }
|
li a:hover { border-bottom: 1px solid rgba(80, 130, 110, 0.4); }
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background: rgba(6, 14, 16, 0.6);
|
background: rgba(6, 14, 16, 0.6);
|
||||||
border: 1px solid rgba(30, 55, 50, 0.3);
|
border: 1px solid rgba(30, 55, 50, 0.3);
|
||||||
|
|
@ -285,7 +286,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
background: rgba(8, 18, 22, 0.6);
|
background: rgba(8, 18, 22, 0.6);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -299,7 +300,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0.7rem 0.9rem;
|
||||||
color: #9ab4b8;
|
color: #9ab4b8;
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
|
@ -330,7 +331,7 @@
|
||||||
hr { border: none; border-top: 1px solid rgba(30, 55, 50, 0.3); margin: 1rem 0; }
|
hr { border: none; border-top: 1px solid rgba(30, 55, 50, 0.3); margin: 1rem 0; }
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #28454e;
|
color: #28454e;
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +345,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .clock {
|
footer .clock {
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: #162a30;
|
color: #162a30;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue