website

#astro#js#html#css

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);
}