www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git
commit a65e87750477cfeb53389d642bc2e9ce7c59d830
parent 8322b7bb6cc6b3880a8c651e6a3b75a23f1a424c
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Wed, 5 Aug 2026 19:40:01 +0900

The project section becomes a mosaic

Four tiles instead of six in a symmetric grid: one large on the left, three
stacked beside it. A 3x2 of equal boxes says all six carry the same weight, and
a section has something it wants to put first. The rest are what the door is for.

Every tile is a picture with the name and the line about it sitting on a band at
the bottom, the way the reference does it. Where a picture exists it fills the
tile: ommp has a cover, sqzass has its mark and it sits on the project's own
colour. Where none exists the colour stands alone — every project already has a
colour chosen for its own page, and that colour says more about the project than
a plaque reading "no image" does. A cover arriving later is one line in the
catalogue.

The band under the text goes solid before it reaches the type rather than
fading through it. Fading through it is how a light title ends up on a light
picture, which is exactly what ommp's cover did on the first attempt.

Pictures and colours live in a catalogue on the home page because listings carry
no extra, but only the pictures, the colours and the order live there — the name
and the description still come from the listing, so the two cannot drift.
tools/check.py verifies each URL names a real project and that every file named
exists, and I proved it fails by pointing one at a project that is not there.

Worth saying plainly: this is the reference's structure, not the reference's
look, and the distance between them is six paintings. Two of our six projects
have artwork. The other four are honest colour fields, and they read better in
the dark theme than the light one.

Diffstat:
Mcontent/_index.md | 32++++++++++++++++++++++++++++++++
Mstatic/css/main.css | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstatic/fonts/display-kr.woff2 | 0binary
Mtemplates/home.html | 30+++++++++++++++++++++++-------
Mtools/check.py | 14++++++++++++--
5 files changed, 148 insertion(+), 9 deletion(-)

diff --git a/content/_index.md b/content/_index.md
@@ -15,6 +15,38 @@ hero_door  = "About me"
 #
 # 여기 없는 글은 그냥 미리보기가 없다. 새 글이 맨 위에 올라와도 페이지는 멀쩡하고,
 # 영상이 있는 글이면 이 줄을 고치면 된다.
+# 홈의 프로젝트 모자이크. 목록(PageRefCtx)에 extra 가 없어서 그림과 색은 여기
+# 있어야 한다. 여기 있는 것은 **그림과 색과 순서**뿐이다 — 이름과 설명은 목록이
+# 실어 오므로 두 곳이 어긋날 수 없다.
+#
+# 첫 항목이 큰 타일이다. 넷만 싣는다; 나머지는 문이 받는다.
+# cover 가 있으면 그 그림이 꽉 찬다. 없고 mark 만 있으면 그 마크가 프로젝트의
+# 색 위에 앉는다. 둘 다 없으면 색만 남는다 — 그것도 그 프로젝트의 것이라
+# "그림 없음" 판보다 낫다.
+[[extra.mosaic]]
+url    = "/projects/sqzass/"
+cover  = ""
+mark   = "images/projects/sqzass.png"
+accent = "#e0a06a"
+
+[[extra.mosaic]]
+url    = "/projects/ommp/"
+cover  = "images/projects/ommp-cover.png"
+mark   = ""
+accent = "#7fc0d4"
+
+[[extra.mosaic]]
+url    = "/projects/uaf/"
+cover  = ""
+mark   = ""
+accent = "#9aa6cc"
+
+[[extra.mosaic]]
+url    = "/projects/usfx/"
+cover  = ""
+mark   = ""
+accent = "#8fb9a2"
+
 [[extra.preview]]
 url    = "/posts/a-new-beginning/"
 poster = "images/posts/a-new-beginning.webp"
diff --git a/static/css/main.css b/static/css/main.css
@@ -705,6 +705,87 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
 }
 .stars { font-variant-numeric: tabular-nums; }
 
+/* ---------- project mosaic ---------- */
+
+/* 첫 장이 크고 나머지 셋이 옆에 쌓인다. 대칭 격자는 여섯 개가 다 같은 무게라고
+   말하는데, 갈래에는 앞에 세울 것이 있다.
+
+   그림이 있는 프로젝트는 둘뿐이다. 없는 것은 자기 색으로 선다 — 프로젝트마다
+   이미 색이 하나씩 정해져 있고(상세 페이지의 --accent 와 같은 값), 그 색이
+   "그림 없음" 판보다 그 프로젝트에 대해 더 많이 말한다. 그림이 생기면 카탈로그의
+   cover 한 줄로 들어온다. */
+.mosaic {
+  display: grid;
+  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+  gap: clamp(0.5rem, 1.5vw, 1rem);
+}
+/* 첫 장이 왼쪽 열을 통째로 쓴다. :first-child 라 개수가 넷이 아니어도 안 깨진다. */
+.mosaic > :first-child { grid-row: 1 / -1; }
+
+.mtile {
+  position: relative; display: flex; align-items: flex-end;
+  overflow: hidden;
+  border: 1px solid var(--rule); border-radius: var(--radius);
+  background: var(--bg-2);
+  transition: border-color var(--tr-ink);
+}
+.mtile:hover { border-color: var(--rule-2); }
+/* 작은 장의 키를 여기서 정한다. 큰 장은 셋의 합만큼 자라므로 자기 비율이 없다. */
+.mosaic > :not(:first-child) { aspect-ratio: 2.1 / 1; }
+
+/* 바탕. 그림이 있으면 그림이, 없으면 그 프로젝트의 색이 옅게 깔린다. */
+.mtile-art {
+  position: absolute; inset: 0;
+  display: grid; place-items: center;
+  background: color-mix(in srgb, var(--tint) 30%, var(--bg-2));
+}
+.mtile-art img { width: 100%; height: 100%; object-fit: cover; }
+/* 마크는 잉크가 아니라 그 프로젝트의 색을 진하게 쓴 것이다 — 바탕과 같은 색의
+   두 단이라 한 물건으로 읽힌다. */
+.mtile-mark {
+  width: 42%; aspect-ratio: 1;
+  background-color: color-mix(in srgb, var(--tint) 62%, var(--ink));
+  -webkit-mask: var(--mark-src) center / contain no-repeat;
+  mask: var(--mark-src) center / contain no-repeat;
+}
+
+/* 글은 바닥에 앉고, 그 아래에서 바탕색이 올라와 그림을 덮는다. 밝은 테마에서는
+   밝은 장막이고 어두운 테마에서는 어두운 장막이라, 글은 두 테마 모두에서 --ink
+   한 색으로 읽힌다. 참조 사이트는 다크 전용이라 흰 글자 한 벌로 끝난다. */
+.mtile-body {
+  position: relative; z-index: 1; width: 100%;
+  display: flex; flex-direction: column; gap: 0.3rem;
+  padding: clamp(1rem, 2.4vw, 1.6rem);
+  /* 글이 앉는 띠는 바탕색으로 꽉 차 있고 그 위에서 풀린다. 그라데이션이 글자
+     높이 안에서 투명해지면 밝은 그림 위의 밝은 글자가 되는데, ommp 의 표지가
+     정확히 그랬다. 두 테마 모두 --bg 로 가므로 글은 --ink 한 색이면 된다. */
+  background: linear-gradient(to top,
+    var(--bg) 0%,
+    var(--bg) 42%,
+    color-mix(in srgb, var(--bg) 72%, transparent) 72%,
+    transparent 100%);
+}
+.mtile-title {
+  font-size: clamp(1.05rem, 0.95rem + 0.45vw, 1.35rem);
+  font-weight: var(--w-item); letter-spacing: var(--track-set);
+  color: var(--ink);
+}
+.mtile-blurb {
+  max-width: 44ch; font-size: 0.88rem; line-height: 1.5;
+  color: var(--ink-2); text-wrap: pretty;
+}
+/* 큰 장은 글도 한 단 크다 — 크기가 순서를 말한다. */
+.mosaic > :first-child .mtile-title { font-size: clamp(1.3rem, 1.1rem + 0.9vw, 1.75rem); }
+
+/* 좁아지면 한 열로 쌓고 전부 같은 비율이 된다. 두 열로 두면 작은 장이 글을
+   담기에 좁아진다. */
+@media (max-width: 46rem) {
+  .mosaic { grid-template-columns: 1fr; grid-template-rows: none; }
+  .mosaic > :first-child { grid-row: auto; }
+  .mosaic > * { aspect-ratio: 2 / 1; }
+}
+
 /* ---------- ledger ---------- */
 
 .ledger { list-style: none; margin: 0; padding: 0; }
diff --git a/static/fonts/display-kr.woff2 b/static/fonts/display-kr.woff2
Binary files differ.
diff --git a/templates/home.html b/templates/home.html
@@ -88,14 +88,30 @@
     <span class="room-rule" aria-hidden="true"></span>
     <a class="room-door" href="{{ s.url }}">{{ t("all_projects") }}</a>
   </header>
-  <div class="tiles">
+  {#- 모자이크. 첫 장이 크고 나머지 셋이 옆에 쌓인다 — 대칭 격자는 여섯 개가
+      다 같은 무게라고 말하는데, 갈래에는 앞에 세울 것이 있다.
+      그림은 카탈로그가 준다. 목록은 이름과 설명만 준다. #}
+  <div class="mosaic">
+    {%- for m in page.extra.mosaic %}
     {%- for p in s.pages %}
-    <article class="tile">
-      <h3 class="tile-title"><a class="tile-link" href="{{ p.url }}">{{ p.title }}</a></h3>
-      {%- if p.description %}
-      <p class="tile-blurb">{{ p.description }}</p>
-      {%- endif %}
-    </article>
+    {%- if p.url == m.url %}
+    <a class="mtile" href="{{ p.url }}" style="--tint: {{ m.accent }}">
+      <span class="mtile-art" aria-hidden="true">
+        {%- if m.cover %}
+        <img src="{{ asset(m.cover) }}" alt="" loading="lazy" decoding="async">
+        {%- elif m.mark %}
+        <span class="mtile-mark" style="--mark-src: url('/{{ m.mark }}')"></span>
+        {%- endif %}
+      </span>
+      <span class="mtile-body">
+        <span class="mtile-title">{{ p.title }}</span>
+        {%- if p.description %}
+        <span class="mtile-blurb">{{ p.description }}</span>
+        {%- endif %}
+      </span>
+    </a>
+    {%- endif %}
+    {%- endfor %}
     {%- endfor %}
   </div>
 </section>
diff --git a/tools/check.py b/tools/check.py
@@ -75,7 +75,7 @@ def check_projects():
 
 
 def check_preview():
-    """홈의 미리보기 카탈로그가 실제 글을 가리키는지.
+    """홈의 미리보기·모자이크 카탈로그가 실제 페이지와 파일을 가리키는지.
 
     이것도 [extra] 라 생성기가 안 읽는다. url 이 어긋나면 에러가 아니라 그냥
     미리보기가 안 나오고, 그건 배포된 뒤에야 눈으로만 보인다.
@@ -84,7 +84,17 @@ def check_preview():
         index = ROOT / "content" / ("_index.ko.md" if lang == "ko" else "_index.md")
         if not index.exists():
             continue  # 그 언어에 첫 화면이 없을 수 있다. 한국어가 그렇다.
-        for item in front_matter(index).get("extra", {}).get("preview", []):
+        extra = front_matter(index).get("extra", {})
+        for item in extra.get("mosaic", []):
+            slug = item["url"].rstrip("/").rsplit("/", 1)[-1]
+            suffix = ".ko.md" if lang == "ko" else ".md"
+            if not (ROOT / "content" / "projects" / f"{slug}{suffix}").exists():
+                fails.append(f"[{lang}] 모자이크가 가리키는 프로젝트가 없습니다: {item['url']}")
+            for key in ("cover", "mark"):
+                if item[key] and not (ROOT / "static" / item[key]).exists():
+                    fails.append(f"[{lang}] 모자이크 {key} 파일이 없습니다: {item[key]}")
+
+        for item in extra.get("preview", []):
             url = item["url"]
             slug = url.rstrip("/").rsplit("/", 1)[-1]
             suffix = ".ko.md" if lang == "ko" else ".md"