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
|