website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
assets/css/shared.css
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { line-height: 1.5; }
img, picture, video, canvas, svg { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; }
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
table { border-collapse: collapse; }
@view-transition {
navigation: auto;
}
/* Cross-document view transitions (native browser MPA navigation, no JS).
Astro's <ClientRouter /> does this via a client-side router; here the
browser handles it directly off `@view-transition { navigation: auto }`
above — this block just replaces the default cross-fade with a slightly
nicer slide+fade, and keeps header/footer stable instead of fading with
the rest of the page. The named ::view-transition-group(...) pair below
operates on the browser's top-layer snapshot tree, not the regular DOM,
so unlike the view-transition-name declarations themselves (nested
inside header/footer below) it can't be scoped and has to stay global. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 220ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(root) {
animation-name: view-transition-fade-out, view-transition-slide-out;
}
::view-transition-new(root) {
animation-name: view-transition-fade-in, view-transition-slide-in;
}
@keyframes view-transition-fade-out {
to { opacity: 0; }
}
@keyframes view-transition-fade-in {
from { opacity: 0; }
}
@keyframes view-transition-slide-out {
to { transform: translateY(-8px); }
}
@keyframes view-transition-slide-in {
from { transform: translateY(8px); }
}
::view-transition-group(site-header),
::view-transition-group(site-footer) {
animation: none;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation: none !important;
}
}
:root {
--color-black: light-dark(hsl(0, 0%, 0%), hsl(0, 0%, 0%));
--color-white: light-dark(hsl(0, 0%, 100%), hsl(0, 0%, 100%));
--color-accent: hsl(57.25deg 100% 70%);
--color-body-bg: light-dark(hsl(0, 0%, 100%), hsl(210, 13%, 15%));
--color-box-bg: light-dark(hsl(0, 0%, 95%), hsl(210, 13%, 9%));
--color-text: light-dark(hsl(0, 0%, 13%), hsl(210, 17%, 98%));
--color-link: light-dark(hsl(211, 100%, 45%), hsl(211, 100%, 60%));
--color-post-link: light-dark(hsl(0, 0%, 0%), hsl(211, 100%, 60%));
--color-highlight-bg: light-dark(hsl(210, 10%, 88%), hsl(210, 10%, 24%));
--color-code-fg: light-dark(hsl(348, 86%, 43%), hsl(348, 86%, 80%));
--color-code-bg: light-dark(hsl(0, 0%, 89%), hsl(210, 13%, 6%));
--color-pre-bg: light-dark(hsl(0, 0%, 89%), hsl(210, 13%, 6%));
--color-tag: light-dark(hsl(152, 96%, 35%), hsl(152, 96%, 44%));
--token-comment: light-dark(hsl(220, 10%, 45%), hsl(220, 10%, 60%));
--token-keyword: light-dark(hsl(310, 60%, 45%), hsl(310, 70%, 72%));
--token-string: light-dark(hsl(95, 45%, 35%), hsl(95, 55%, 65%));
--token-function: light-dark(hsl(210, 70%, 45%), hsl(210, 80%, 72%));
--token-number: light-dark(hsl(25, 80%, 42%), hsl(25, 90%, 65%));
--token-builtin: light-dark(hsl(175, 55%, 32%), hsl(175, 60%, 60%));
--token-punctuation: light-dark(hsl(220, 8%, 40%), hsl(220, 8%, 70%));
--token-tag: light-dark(hsl(0, 60%, 45%), hsl(0, 70%, 72%));
--token-attr-name: light-dark(hsl(35, 70%, 38%), hsl(35, 80%, 65%));
color-scheme: light dark;
--btn-light: none;
--btn-dark: block;
/* Fluid type/space scale (utopia.fyi methodology): each token scales
continuously between its min size at a 320px viewport and its max
size at a 1280px viewport, replacing what used to be a hard jump at
a single max-width:720px breakpoint. Structural changes (flex
direction, grid column counts, sidebar widths) aren't continuous
properties and stay as ordinary breakpoints elsewhere. */
--fs-sm: clamp(1rem, calc(0.9667rem + 0.1667vw), 1.1rem);
--fs-body: clamp(1.1rem, calc(1.05rem + 0.25vw), 1.25rem);
--fs-md: clamp(1.15rem, calc(1.0667rem + 0.4167vw), 1.4rem);
--fs-title-lg: clamp(1.6rem, calc(1.5083rem + 0.4583vw), 1.875rem);
--fs-heading-md: clamp(1.2rem, calc(1.1rem + 0.5vw), 1.5rem);
--fs-heading-sm: clamp(1rem, calc(0.9167rem + 0.4167vw), 1.25rem);
--fs-repo-title: clamp(1.7rem, calc(1.6rem + 0.5vw), 2rem);
--fs-repo-clone: clamp(0.7rem, calc(0.7rem + 0.5vw), 0.9rem);
--fs-repo-subtitle: clamp(0.9rem, calc(0.9rem + 0.5vw), 1.1rem);
--fs-hero-xl: clamp(1.65rem, calc(1.5333rem + 0.5833vw), 2rem);
--fs-features: clamp(0.925rem, calc(0.8583rem + 0.3333vw), 1.125rem);
--space-gap-fluid: clamp(0rem, calc(-0.6667rem + 3.3333vw), 2rem);
--space-section-mb: clamp(0rem, calc(-0.3333rem + 1.6667vw), 1rem);
}
/* data-theme is tri-state: absent = follow OS (color-scheme: light dark
above already does that), "light"/"dark" = explicit user override that
must win even against an opposite OS preference. Every color below is
driven by light-dark() reading this color-scheme value — no need to
redeclare each variable per theme. */
:root[data-theme="light"] {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--btn-light: block;
--btn-dark: none;
}
}
:root[data-theme="dark"] {
--btn-light: block;
--btn-dark: none;
}
:root[data-theme="light"] {
--btn-light: none;
--btn-dark: block;
}
[data-tooltip] {
position: relative;
cursor: help;
}
[data-tooltip]::after {
position: absolute;
opacity: 0;
pointer-events: none;
content: attr(data-tooltip);
left: 0;
top: calc(100% + 10px);
border-radius: 3px;
box-shadow: 0 0 1px 1px var(--color-text);
background-color: var(--color-box-bg);
z-index: 10;
padding: 8px;
width: 300px;
transform: translateY(-20px);
transition: all 150ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
[data-tooltip]:hover::after {
opacity: 1;
transform: translateY(0);
transition-duration: 300ms;
}
body {
display: flex;
flex-direction: column;
flex: 1;
background-color: var(--color-body-bg);
color: var(--color-text);
font-family: system-ui;
text-rendering: optimizeLegibility;
font-variant-ligatures: common-ligatures;
font-kerning: normal;
font-size: var(--fs-body);
line-height: 1.5;
min-height: 100vh;
}
main {
margin-top: 1rem;
margin-bottom: 1rem;
padding-bottom: 1em;
}
.wrapper {
flex: 1;
width: 100%;
max-width: 72ch;
margin-right: auto;
margin-left: auto;
padding-left: 1rem;
padding-right: 1rem;
}
/* site-header.njk/site-footer.njk are plain includes (no wrapping custom
element), so these are just nested under the real <header>/<footer>
tag — plain CSS nesting scopes .logo/.links etc to it same as any other
descendant selector would, no @scope needed since there's only ever
one header and one footer on the page. */
header {
background: #131618;
view-transition-name: site-header;
.logo {
display: flex;
font-size: 22px;
line-height: 56px;
font-weight: 400;
margin-bottom: 0;
float: left;
color: var(--color-accent);
margin-right: 8px;
&.active,
&:hover {
color: var(--color-accent);
text-decoration: underline;
text-decoration-color: var(--color-accent);
text-underline-offset: 8px;
}
}
.links {
display: flex;
flex-direction: row;
float: right;
font-size: 1.4rem;
line-height: 56px;
color: white;
#theme-switch {
display: flex;
align-items: center;
font-size: 1.3rem;
button {
background: none;
border: none;
padding: 0;
padding-right: 12px;
margin-bottom: -2px;
color: inherit;
cursor: pointer;
&:hover,
&:focus,
&:active {
background: none;
border: none;
color: inherit;
}
}
}
a {
display: inline-block;
text-decoration: none;
font-size: 22px;
line-height: 56px;
color: white;
font-weight: 400;
padding-left: 12px;
padding-right: 12px;
&:last-child {
padding-right: 0px;
}
&.active,
&:hover {
color: var(--color-gray-200);
text-decoration: underline;
text-decoration-color: var(--color-accent);
text-underline-offset: 8px;
}
}
}
}
footer {
background: #131618;
view-transition-name: site-footer;
.container {
display: flex;
flex-direction: row;
flex: 1;
padding: 16px 0px;
}
.spacer {
flex: 1;
}
.copyright-container {
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.5rem;
margin-right: 0.5rem;
color: var(--color-white);
font-size: 1rem;
}
.link {
font-size: 1rem;
line-height: 1.5rem;
margin-left: 0.5rem;
color: var(--color-white);
font-weight: 500;
text-underline-offset: 2px;
text-decoration-thickness: 2px;
text-decoration-line: underline;
text-decoration-color: white;
&:hover {
color: var(--color-gray-200);
text-decoration-color: var(--color-gray-200);
}
}
}
/* Syntax highlighting theme: shared by code-view's own file-viewer pre and
by fenced code blocks inside markdown-view's article content (both
Prism-highlighted blog-post code and plain markdown-it README code
fences). It's genuinely cross-cutting rather than owned by one
component — scoping it to either would mean duplicating the whole token
palette in the other — so it stays global.
Background lives on pre.code-view/.markdown-body pre below, not here:
code[class*="language-"] is inline and wraps a whole multi-line file, so
giving IT a background paints one box per line box (only as wide as that
line's text) instead of one clean rectangle behind the block. */
pre[class*="language-"],
code[class*="language-"] {
color: var(--color-text);
}
pre[class*="language-"],
pre.code-view,
.markdown-body pre {
padding: 1rem;
margin: 1rem 0;
overflow-x: auto;
border-radius: 4px;
line-height: 1.5;
}
/* File-viewer code (pre.code-view) matches the rest of that layout's
0.9rem scale (see repos.css); blog/README fenced code blocks stay at
0.8rem — they're a different context and weren't asked to change. */
pre.code-view {
font-size: 0.9rem;
}
pre[class*="language-"],
.markdown-body pre {
font-size: 0.8rem;
}
pre.code-view,
.markdown-body pre {
background: var(--color-pre-bg);
color: var(--color-text);
}
code[class*="language-"] {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: var(--token-comment);
font-style: italic;
}
.token.keyword,
.token.selector,
.token.important,
.token.atrule {
color: var(--token-keyword);
}
.token.string,
.token.char,
.token.attr-value,
.token.regex {
color: var(--token-string);
}
.token.function,
.token.class-name {
color: var(--token-function);
}
.token.number,
.token.boolean,
.token.constant {
color: var(--token-number);
}
.token.builtin,
.token.deleted {
color: var(--token-builtin);
}
.token.punctuation,
.token.operator {
color: var(--token-punctuation);
}
.token.tag {
color: var(--token-tag);
}
.token.attr-name,
.token.property {
color: var(--token-attr-name);
}