www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git
commit 68d40c0d55d2ac54523c8791758eaed4c13d0f50
parent ec8ae7cb4bc613c67f6ec7381334aaaab3a0c181
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Tue, 4 Aug 2026 17:48:46 +0900

Hover changes colour and nothing else

Every scale and shift on hover is gone: the brand mark, the tiles, the cells,
the frames, the medallion, the link icons, the social icons, and the gap that
opened between a button's label and its arrow. Hover moves colour now, the way
the rest of the site already did. Size changing under the pointer pulls the eye
to it, and on this site the place the cursor is passing over is usually the
place someone is reading.

That leaves --tr-light with no consumers, so it goes too. One tempo, one curve.
The tooltip fades in without sliding, for the same reason.

Picked up the ledger gutter while in there — it was still on the fixed 6.5rem
and 1.5rem the last pass was supposed to hang off the viewport.

Favicon: white plate, black lines, and the mark nearly fills the square. Padding
costs more than stroke does at 16px. The line art is dilated by one pass before
any of the assets are written, because the original stroke is thin enough that
antialiasing eats it at masthead size and at every favicon size. Both favicon
variants are now the same white plate — an opaque white square is legible
against a light tab and a dark one, so there is nothing left for the media query
to choose between, though the two files stay so the link tags do not have to
change.

/projects/ drops the "Terminal tools" group. sqzass and ommp were already the
two featured frames directly above it, so the section restated the top of the
page with smaller type.

The colophon says what built the page and who holds the copyright. The year is
written by hand: sqzass has no now(), because a template that can read the clock
cannot produce the same bytes twice.

Diffstat:
Mcontent/projects/_index.ko.md | 27---------------------------
Mcontent/projects/_index.md | 30------------------------------
Mstatic/css/main.css | 58+++++++++++++++++++++++-----------------------------------
Mstatic/fonts/display-kr.woff2 | 0binary
Mstatic/images/apple-touch-icon.png | 0binary
Mstatic/images/favicon-dark.png | 0binary
Mstatic/images/favicon.png | 0binary
Mstatic/images/mark.png | 0binary
Mstatic/images/projects/sqzass.png | 0binary
Mtemplates/partials/footer.html | 12+++++++-----
Mtools/icons.py | 26+++++++++++++++++++-------
11 files changed, 49 insertion(+), 104 deletion(-)

diff --git a/content/projects/_index.ko.md b/content/projects/_index.ko.md
@@ -33,33 +33,6 @@ site  = "https://ommp.sqzer.com"
 repo  = "https://github.com/sqzer-x/ommp"
 stars  = 0
 
-[[extra.group]]
-id = "terminal"
-label = "터미널 도구"
-note = ""
-
-  [[extra.group.item]]
-  url = "/ko/projects/sqzass/"
-  name = "sqzass"
-  blurb = "정적 사이트 생성기. 같은 입력을 두 번 빌드하면 바이트까지 같다."
-  mark = "images/projects/sqzass.png"
-  lang = "Rust"
-  status = "active"
-  site = "https://sqzass.sqzer.com"
-  repo = "https://github.com/sqzer-x/sqzass"
-  stars = 0
-
-  [[extra.group.item]]
-  url = "/ko/projects/ommp/"
-  name = "ommp"
-  blurb = "터미널 음악 재생기. MPD 도 데몬도 없이, 실행하면 재생된다."
-  mark = ""
-  lang = "Rust"
-  status = "active"
-  site = "https://ommp.sqzer.com"
-  repo = "https://github.com/sqzer-x/ommp"
-  stars = 0
-
 [[extra.group]]
 id = "security"
 label = "보안 도구"
diff --git a/content/projects/_index.md b/content/projects/_index.md
@@ -41,36 +41,6 @@ site  = "https://ommp.sqzer.com"
 repo  = "https://github.com/sqzer-x/ommp"
 stars  = 0
 
-[[extra.group]]
-id = "terminal"
-label = "Terminal tools"
-note = ""
-
-  [[extra.group.item]]
-  url = "/projects/sqzass/"
-  name = "sqzass"
-  blurb = "Static site generator. Two builds of the same input are byte-identical."
-  mark = "images/projects/sqzass.png"
-  lang = "Rust"
-  status = "active"
-  site = "https://sqzass.sqzer.com"
-  repo = "https://github.com/sqzer-x/sqzass"
-  stars = 0
-
-  [[extra.group.item]]
-  url = "/projects/ommp/"
-  name = "ommp"
-  blurb = "Terminal music player. No MPD, no daemon — run it and play."
-  mark = ""
-  lang = "Rust"
-  status = "active"
-  site = "https://ommp.sqzer.com"
-  repo = "https://github.com/sqzer-x/ommp"
-  stars = 0
-
-# uaf / udfx / usfx 는 이름이 우연히 닮은 것이 아니다. 정찰 대상이 AP, 도메인,
-# 서브도메인으로 갈라지는 한 계열이고, 셋 다 인터넷이 없는 망에서 도는 것을
-# 전제로 만들어졌다. 그래서 한 갈래로 묶는다.
 [[extra.group]]
 id = "security"
 label = "Security tools"
diff --git a/static/css/main.css b/static/css/main.css
@@ -6,8 +6,8 @@
  * 청록 니어블랙 위의 가운데 정렬 갤러리. 색은 평상시에 없고 손이 닿을 때만
  * 나온다 — 액센트가 브랜딩이 아니라 보상이다.
  *
- * 누르면 눌린다. hover 는 빛이 아니라 압축이고(scale 0.988), 그건 이 집의
- * 이름들이 전부 모음을 눌러 뺀 것이기 때문이다 — sqzer, sqzass, ommp, uaf.
+ * hover 는 색만 바꾼다. 크기가 움직이면 시선이 그리로 끌려가는데, 이 사이트에서
+ * 커서가 지나가는 자리는 대부분 읽는 중인 자리다.
  *
  * 목소리는 굵기로 만든다. 디스플레이는 230~350 의 초경량에 **양수** 트래킹이고,
  * 라벨은 작고 넓게 벌어진 대문자다. 큰 글자는 가늘고 작은 글자는 벌어진다.
@@ -106,10 +106,10 @@
   --wide: 71rem;
   --read: 44rem;
 
-  /* 한 박자. 잉크는 빠르고 빛은 느리다. 곡선은 둘뿐이다. */
+  /* 한 박자. 값을 여기 한 번만 두면 모든 상호작용이 같은 속도로 반응하고,
+     그게 "같은 물건"이라는 느낌을 만든다. hover 는 색만 바꾸므로 곡선도 하나면 된다. */
   --ease: cubic-bezier(0.16, 1, 0.3, 1);
   --tr-ink: 0.25s ease;
-  --tr-light: 0.55s var(--ease);
 }
 
 :root[data-theme="light"] {
@@ -221,9 +221,7 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
   background-color: var(--mark-ink);
   -webkit-mask: url("/images/mark.png") center / contain no-repeat;
   mask: url("/images/mark.png") center / contain no-repeat;
-  transition: transform var(--tr-light);
 }
-.brand:hover .brand-mark { transform: scale(1.08); }
 @supports not (mask-image: url("")) { .brand-mark { display: none; } }
 
 .brand-name {
@@ -451,9 +449,9 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
   border: 1px solid var(--rule-2); border-radius: var(--radius);
   overflow: hidden; display: grid; place-items: center;
   background: var(--bg-2);
-  transition: border-color var(--tr-ink), transform var(--tr-ink);
+  transition: border-color var(--tr-ink);
 }
-.frame:hover .frame-media { border-color: var(--rule-3); transform: scale(0.988); }
+.frame:hover .frame-media { border-color: var(--rule-3); }
 
 /* 그림은 제 색으로 쉰다. 탈색해 두었다가 손이 닿으면 돌려주는 건 다른 집의
    장치였다 — 여기서는 ommp 가 4K 컬러 스크린샷을 그대로 싣는 쪽이 기준이다. */
@@ -464,9 +462,9 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
   background-color: var(--mark-ink);
   -webkit-mask: var(--mark-src) center / contain no-repeat;
   mask: var(--mark-src) center / contain no-repeat;
-  opacity: 0.72; transition: opacity var(--tr-light), transform var(--tr-light);
+  opacity: 0.72; transition: opacity var(--tr-ink);
 }
-.frame:hover .frame-mark { opacity: 1; transform: scale(1.04); }
+.frame:hover .frame-mark { opacity: 1; }
 
 .frame-title {
   display: flex; align-items: center; justify-content: center; gap: 0.5rem;
@@ -546,8 +544,6 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
 /* 누르면 눌린다. 빛이 번지는 대신 물건이 압축된다 — 이 집의 이름들이 전부
    모음을 눌러 뺀 것이고(sqzer, sqzass, ommp), 랜딩이 이미 같은 값을 쓴다.
    0.988 은 눈이 알아채되 글자가 흐려지지는 않는 지점이다. */
-.tile { transition: transform var(--tr-ink); }
-.tile:hover, .tile:focus-within { transform: scale(0.988); }
 
 .tile-title {
   display: flex; align-items: center; justify-content: center; gap: 0.45rem;
@@ -575,9 +571,9 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
 .tile-links a, .frame-meta a {
   position: relative; z-index: 2;
   display: inline-flex; color: var(--ink-4);
-  transition: color var(--tr-ink), transform var(--tr-light);
+  transition: color var(--tr-ink);
 }
-.tile-links a:hover, .frame-meta a:hover { color: var(--accent); transform: translateY(-2px); }
+.tile-links a:hover, .frame-meta a:hover { color: var(--accent); }
 
 /* 아이콘만 있는 링크에는 이름이 안 보인다. title 속성은 브라우저가 1초쯤 뒤에
    자기 모양으로 띄우는데, 그 지연과 모양이 사이트와 무관하다. 같은 값을
@@ -587,16 +583,14 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
 [data-tooltip]::after {
   content: attr(data-tooltip);
   position: absolute; bottom: calc(100% + 0.5rem); left: 50%;
-  transform: translateX(-50%) translateY(0.2rem);
+  transform: translateX(-50%);
   padding: 0.28rem 0.5rem; white-space: nowrap; pointer-events: none;
   background: var(--bg-3); color: var(--ink-2);
   border: 1px solid var(--rule-2); border-radius: var(--radius);
   font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
-  opacity: 0; transition: opacity var(--tr-ink), transform var(--tr-ink);
-}
-[data-tooltip]:hover::after, [data-tooltip]:focus-visible::after {
-  opacity: 1; transform: translateX(-50%) translateY(0);
+  opacity: 0; transition: opacity var(--tr-ink);
 }
+[data-tooltip]:hover::after, [data-tooltip]:focus-visible::after { opacity: 1; }
 html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none; }
 
 /* 타일 전체가 상세 페이지로 가는 링크. 아이콘만 위로 올린다. */
@@ -624,11 +618,11 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
 /* 행 사이에 선이 없다. 간격과 빛이 나눈다. */
 .ledger-link {
   position: relative; display: grid;
-  grid-template-columns: 6.5rem minmax(0, 1fr);
-  align-items: baseline; gap: 1.5rem;
+  grid-template-columns: clamp(5rem, 11vw, 8rem) minmax(0, 1fr);
+  align-items: baseline; gap: clamp(1rem, 2.5vw, 2rem);
   padding: 1.1rem 1rem; margin-inline: -1rem;
   border-radius: var(--radius);
-  transition: background var(--tr-light);
+  transition: background var(--tr-ink);
 }
 /* 행은 평평한 한 단만 올라온다. 방향성 wash 는 빛의 어휘이고, 이 테마의
    어휘는 누름이다. */
@@ -656,7 +650,7 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
 }
 
 @media (max-width: 40rem) {
-  .ledger-link { grid-template-columns: 1fr; gap: 0.35rem; padding-inline: 0; margin-inline: 0; }
+  .ledger-link { grid-template-columns: 1fr; gap: 0.5rem; padding-inline: 0; margin-inline: 0; }
 }
 
 /* 홈의 맨 위 글 — 액자와 같은 문법, 더 크게. */
@@ -685,9 +679,8 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
   border: 1px solid var(--rule-2); border-radius: var(--radius);
   font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase;
   color: var(--ink-2);
-  transition: color var(--tr-ink), border-color var(--tr-ink), background var(--tr-ink), gap var(--tr-light);
+  transition: color var(--tr-ink), border-color var(--tr-ink), background var(--tr-ink);
 }
-.btn:hover { gap: 0.75rem; }
 /* 채운 버튼은 잉크로 채운다. 액센트로 채우면 그게 첫 화면에서 제일 큰 색이 된다. */
 .btn-primary { background: var(--ink); border-color: var(--ink); color: var(--bg); }
 .btn-primary:hover { background: var(--ink); filter: brightness(1.08); }
@@ -700,9 +693,9 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
   margin-top: 2rem;
   font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase;
   color: var(--ink-4);
-  transition: color var(--tr-ink), gap var(--tr-light);
+  transition: color var(--tr-ink);
 }
-.back:hover { color: var(--ink-2); gap: 0.7rem; }
+.back:hover { color: var(--ink-2); }
 
 /* ---------- project detail ---------- */
 
@@ -762,9 +755,7 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
   background-color: var(--mark-ink);
   -webkit-mask: var(--mark-src) center / contain no-repeat;
   mask: var(--mark-src) center / contain no-repeat;
-  transition: transform var(--tr-light);
 }
-.hero:hover .medallion-mark::before { transform: scale(1.04); }
 
 @media (max-width: 52rem) {
   .hero { grid-template-columns: minmax(0, 1fr); text-align: center; }
@@ -830,8 +821,7 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
 }
 /* hover 에서 위로 올린다. 그러지 않으면 이웃에 가려 두 변만 밝아진다. */
 .cell:hover { z-index: 1; border-color: var(--rule-2); }
-.cell { transition: transform var(--tr-ink), border-color var(--tr-ink); }
-.cell:hover, .cell:focus-within { transform: scale(0.988); }
+.cell { transition: border-color var(--tr-ink); }
 
 .cell-index {
   display: block; font-family: var(--mono); font-size: 0.66rem;
@@ -1061,16 +1051,14 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
   display: inline-flex; align-items: center; justify-content: center;
   width: 1.9rem; height: 1.9rem;
   font-size: 1rem; color: var(--ink-3);
-  transition: color var(--tr-ink), transform var(--tr-ink);
+  transition: color var(--tr-ink);
 }
-.colophon-social a:hover { transform: scale(0.94); }
 .colophon-social a:hover { color: var(--accent); }
 .colophon-social svg { flex: none; }
 
-.colophon-note { margin: 0; flex-basis: 100%; font-size: 0.66rem; letter-spacing: 0.16em; color: var(--ink-4); }
+.colophon-note { margin: 0; flex-basis: 100%; font-size: 0.7rem; letter-spacing: 0; color: var(--ink-4); }
 .colophon-note a { color: var(--ink-3); transition: color var(--tr-ink); }
 .colophon-note a:hover { color: var(--accent); }
-.colophon-sep { padding-inline: 0.5rem; }
 
 /* ---------- Korean ---------- */
 
diff --git a/static/fonts/display-kr.woff2 b/static/fonts/display-kr.woff2
Binary files differ.
diff --git a/static/images/apple-touch-icon.png b/static/images/apple-touch-icon.png
Binary files differ.
diff --git a/static/images/favicon-dark.png b/static/images/favicon-dark.png
Binary files differ.
diff --git a/static/images/favicon.png b/static/images/favicon.png
Binary files differ.
diff --git a/static/images/mark.png b/static/images/mark.png
Binary files differ.
diff --git a/static/images/projects/sqzass.png b/static/images/projects/sqzass.png
Binary files differ.
diff --git a/templates/partials/footer.html b/templates/partials/footer.html
@@ -28,12 +28,14 @@
       <a href="https://www.instagram.com/sqzer_x" rel="me" target="_blank" aria-label="Instagram" data-tooltip="Instagram">{{ i_instagram() }}</a>
     </nav>
 
+    {#- 링크는 이 사이트를 만든 도구의 랜딩으로 간다. 레포가 아니라 랜딩인 이유는
+        여기서 궁금해지는 것이 "그게 뭔데"이지 "코드 보여 줘"가 아니기 때문이다.
+
+        연도는 손으로 적는다. sqzass 에는 now() 가 없다 — 시계를 읽을 수 있는
+        템플릿은 두 번 빌드했을 때 같은 바이트를 내지 못한다. #}
     <p class="colophon-note">
-      {#- 링크는 이 사이트를 만든 도구의 랜딩으로 간다. 레포가 아니라 랜딩인 이유는
-          여기서 궁금해지는 것이 "그게 뭔데"이지 "코드 보여 줘"가 아니기 때문이다. #}
-      <a href="https://sqzass.sqzer.com">sqzass</a>
-      <span class="colophon-sep" aria-hidden="true">·</span>
-      <a href="{{ home }}">{{ site.title }}</a>
+      Built with <a href="https://sqzass.sqzer.com">sqzass</a>.
+      Copyright 2026 sqzer (hyukjin an).
     </p>
   </div>
 </footer>
diff --git a/tools/icons.py b/tools/icons.py
@@ -18,7 +18,7 @@
 import sys
 from pathlib import Path
 
-from PIL import Image, ImageDraw, ImageOps
+from PIL import Image, ImageDraw, ImageFilter, ImageOps
 
 ROOT = Path(__file__).resolve().parent.parent
 IMG = ROOT / "static" / "images"
@@ -28,12 +28,19 @@ SRC_DIR = Path(sys.argv[1] if len(sys.argv) > 1 else "/home/himesama/Downloads")
 INK = (14, 18, 17)
 PAPER = (234, 238, 236)
 
+# 선 굵히기 커널(px, 홀수). 원본 해상도 기준이라 결과물에서는 훨씬 얇게 보인다.
+THICKEN = 9
+
 
 def shape(path: Path) -> Image.Image:
     """선화를 알파 마스크로. 흰 배경 위에 합성한 뒤 명도를 뒤집는다."""
     im = Image.open(path).convert("RGBA")
     flat = Image.alpha_composite(Image.new("RGBA", im.size, (255,) * 4), im).convert("L")
     a = ImageOps.invert(flat).point(lambda v: 0 if v < 24 else min(255, int(v * 1.35)))
+    # 선을 한 겹 불린다. 원본 획이 가늘어서 마스트헤드 크기(약 30px)나 파비콘에서는
+    # 안티에일리어싱에 먹혀 흐려진다. MaxFilter 는 알파의 최댓값을 퍼뜨리므로
+    # 획이 두꺼워지고, 홀수 커널이라 중심이 안 밀린다.
+    a = a.filter(ImageFilter.MaxFilter(THICKEN))
     return a.crop(a.getbbox())
 
 
@@ -44,8 +51,11 @@ def write_mask(alpha: Image.Image, dst: Path, width: int) -> None:
     print(f"  {dst.relative_to(ROOT)}  {width}x{h}  {dst.stat().st_size // 1024}KB")
 
 
-def plate(alpha: Image.Image, size: int, bg: tuple, fg: tuple, pad=0.18) -> Image.Image:
-    """둥근 사각 판 위에 마크. 판이 불투명해야 탭에서 사라지지 않는다."""
+def plate(alpha: Image.Image, size: int, bg: tuple, fg: tuple, pad=0.05) -> Image.Image:
+    """둥근 사각 판 위에 마크. 판이 불투명해야 탭에서 사라지지 않는다.
+
+    여백은 거의 없다 — 파비콘은 16~32px 로 줄어들어서, 여백에 쓰는 1px 이
+    획에 쓰는 1px 보다 비싸다."""
     card = Image.new("RGBA", (size, size), (0, 0, 0, 0))
     mask = Image.new("L", (size, size), 0)
     ImageDraw.Draw(mask).rounded_rectangle(
@@ -71,10 +81,12 @@ def main() -> int:
     write_mask(sqzer, IMG / "mark.png", 128)
     write_mask(shape(SRC_DIR / "sqzass_logo.png"), IMG / "projects" / "sqzass.png", 400)
 
-    # 밝은 탭 → 검은 판, 어두운 탭 → 흰 판. 어느 쪽에서도 대비가 남는다.
-    plate(sqzer, 64, INK, PAPER).save(IMG / "favicon.png", optimize=True)
-    plate(sqzer, 64, PAPER, INK).save(IMG / "favicon-dark.png", optimize=True)
-    plate(sqzer, 180, INK, PAPER).convert("RGB").save(IMG / "apple-touch-icon.png", optimize=True)
+    # 흰 판에 검은 선, 두 벌 다. 판이 불투명한 흰색이면 밝은 탭에서도 어두운
+    # 탭에서도 그대로 보이므로 변형을 나눌 이유가 없다 — 그래도 두 파일을 남기는
+    # 것은 media 로 고르는 <link> 를 그대로 두기 위해서다.
+    plate(sqzer, 128, PAPER, INK).save(IMG / "favicon.png", optimize=True)
+    plate(sqzer, 128, PAPER, INK).save(IMG / "favicon-dark.png", optimize=True)
+    plate(sqzer, 180, PAPER, INK).convert("RGB").save(IMG / "apple-touch-icon.png", optimize=True)
     for f in ("favicon.png", "favicon-dark.png", "apple-touch-icon.png"):
         print(f"  images/{f}  {(IMG / f).stat().st_size}B")
     return 0