diff --git a/README.md b/README.md
index c8a067f..e6e8f54 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@
- **상관일까, 인과일까?:** 데이터 상황 10개를 보고 상관인지 인과인지 고르는 2분짜리 게임입니다. 답마다 관련 웹북 장으로 이어집니다. [게임 하기](https://causalinferencelab.github.io/game.html)
- **완주 카드:** 한 단계의 자료를 모두 학습 완료로 표시하면, 공유할 수 있는 완주 카드 이미지를 만들 수 있습니다.
- **Builder 페이지:** 프로젝트를 연 Builder마다 이력, 만든 레포, 배지를 모은 페이지가 있습니다. [Builder 전체 보기](https://causalinferencelab.github.io/builders/)
+- **Builder 메달:** Builder로 연 기수가 쌓이면 메달이 바뀝니다. 첫 기수는 Builder, 2개 기수는 다시 온 Builder, 3개 기수 이상은 베테랑 Builder, 5개 기수 이상은 명예 Builder입니다. 러너 20명 이상과 함께한 멘토, 러너로 시작해 Builder가 된 사람에게는 특별 메달이 붙습니다. 메달 카드 이미지는 허브에서 바로 만들 수 있습니다.
## 커리큘럼: L0–L4
@@ -65,10 +66,16 @@ L0–L2는 **배우는 단계**로, 무료 자료만으로 끝까지 갈 수 있
팀이 번역한 『[실무로 통하는 인과추론 with 파이썬](https://www.hanbit.co.kr/store/books/look.php?p_code=B6208936856)』(한빛미디어, 2024)과 그 내용을 실무로 잇는 온라인 강의 「[데이터 실무자의 사고력 강화: AI 인과추론 실무](https://www.hanbit.co.kr/product/C0781073345)」가 커리큘럼 L2에 들어 있습니다. 책의 장별 예제 코드와 발표 자료는 무료 레포로 공개되어 있습니다.
-## 행사: Data Forum
+## 행사: Data Forum과 학회 협력
현업과 학계가 만나는 Data Forum을 2024년부터 열고 있습니다. 회차별 내용과 다음 행사 일정은 허브의 행사 섹션에 있습니다.
+한국경영정보학회가 주최하는 학회 행사에도 협력하고 있습니다. 오래 함께한 Builder에게는 이런 학회와 Data Forum에서 발표할 기회가 열립니다.
+
+- [한국 비즈니스 실험 심포지엄 2026](https://sites.google.com/view/business-experimentation2026/home) (2026.07.11, 한국과학기술회관)
+- [한국 비즈니스 실험 심포지엄 2025](https://sites.google.com/view/business-experimentation2025/home) (2025.07.12, 고려대학교 LG-POSCO 경영관)
+- [인과추론 여름 워크숍 2024](https://sites.google.com/view/causal-inference2024/home) (2024.06, 온라인)
+
## 참여하는 방법
1. **러너로 참여:** 가짜연구소 기수 모집 때 인과추론팀 프로젝트에 러너로 들어와 14주 동안 함께 공부하고 만듭니다.
diff --git a/assets/css/hub.css b/assets/css/hub.css
index 0f2aeeb..cd5815d 100644
--- a/assets/css/hub.css
+++ b/assets/css/hub.css
@@ -89,7 +89,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.brand-logo { width: 34px; height: 34px; display: block; }
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.brand-name { font-family: var(--f-display); font-weight: 800; font-size: 17px; letter-spacing: -.01em; }
-.brand-sub { font-size: 11.5px; color: var(--muted); letter-spacing: .2px; }
+.brand-sub { font-size: 12px; color: var(--muted); letter-spacing: .2px; }
.site-nav { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; margin-left: auto; }
.site-nav::-webkit-scrollbar { display: none; }
.site-nav a { color: var(--muted); text-decoration: none; padding: 8px 10px; border-radius: 8px; white-space: nowrap; font-size: 15px; font-weight: 500; }
@@ -125,7 +125,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.featured a { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 12px; background: rgba(255, 255, 255, .12); border: 1px solid rgba(255, 255, 255, .26); color: #fff; text-decoration: none; }
.featured a:hover { background: rgba(255, 255, 255, .2); border-color: rgba(255, 255, 255, .5); }
.featured a:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
-.featured .tag { flex-shrink: 0; width: 64px; box-sizing: border-box; text-align: center; font-size: 11.5px; font-weight: 800; color: #23276A; background: #fff; border-radius: 6px; padding: 2px 6px; white-space: nowrap; }
+.featured .tag { flex-shrink: 0; width: 64px; box-sizing: border-box; text-align: center; font-size: 12px; font-weight: 800; color: #23276A; background: #fff; border-radius: 6px; padding: 2px 6px; white-space: nowrap; }
.featured .tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.featured .t { font-weight: 700; font-size: 15px; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.featured .d { font-size: 12px; color: rgba(255, 255, 255, .74); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -147,9 +147,9 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.pt-img.wide { width: 48px; border-radius: 4px; object-position: 70% 30%; }
.pt-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pt-text b { font-size: 13px; font-weight: 500; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.pt-lv { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: rgba(255, 255, 255, .72); white-space: nowrap; }
-.pt-type { font-size: 10.5px; font-weight: 800; color: #23276A; background: rgba(255, 255, 255, .9); border-radius: 4px; padding: 0 5px; }
-.pt-paid { font-size: 10.5px; font-weight: 700; color: rgba(255, 255, 255, .85); border: 1px solid rgba(255, 255, 255, .4); border-radius: 4px; padding: 0 4px; margin-left: -2px; }
+.pt-lv { display: flex; align-items: center; gap: 6px; font-size: 12px; color: rgba(255, 255, 255, .72); white-space: nowrap; }
+.pt-type { font-size: 12px; font-weight: 800; color: #23276A; background: rgba(255, 255, 255, .9); border-radius: 4px; padding: 0 5px; }
+.pt-paid { font-size: 12px; font-weight: 700; color: rgba(255, 255, 255, .85); border: 1px solid rgba(255, 255, 255, .4); border-radius: 4px; padding: 0 4px; margin-left: -2px; }
.pt-out { display: grid; place-items: center; width: 34px; flex-shrink: 0; color: #fff; border-left: 1px solid rgba(255, 255, 255, .14); border-radius: 0 12px 12px 0; }
.pt-out:hover { background: rgba(255, 255, 255, .1); }
.pt-out svg { width: 12px; height: 12px; }
@@ -164,6 +164,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.banner-main { display: contents; }
.banner-side { order: 1; width: 100%; margin-top: 22px; }
.partner { order: 2; }
+ .academy { order: 3; }
.banner-mark { width: 72px; height: 72px; display: none; }
.banner-lede br { display: none; }
.banner-cta { gap: 8px; }
@@ -287,7 +288,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.ep-cover::after { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--c) 34%, transparent) 1.2px, transparent 1.6px) 0 0 / 12px 12px; -webkit-mask-image: linear-gradient(120deg, transparent 35%, #000); mask-image: linear-gradient(120deg, transparent 35%, #000); }
.ep-no { position: absolute; left: 18px; top: 12px; font-size: 12px; font-weight: 700; letter-spacing: .3px; color: var(--c); z-index: 1; }
.ep-badges { position: absolute; right: 10px; top: 10px; display: flex; gap: 4px; z-index: 1; }
-.ep-badges .b { font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 999px; background: var(--surface); color: var(--ink); white-space: nowrap; border: 1px solid var(--rule); }
+.ep-badges .b { font-size: 12px; font-weight: 700; padding: 2px 7px; border-radius: 999px; background: var(--surface); color: var(--ink); white-space: nowrap; border: 1px solid var(--rule); }
.ep-badges .b.top { background: #FFD54A; color: #14162B; border-color: #FFD54A; }
.ep-badges .b.live { color: var(--live); }
.ep-badges .b.live::before { content: "● "; }
@@ -388,12 +389,6 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
@media (max-width: 1000px) { .top3 { grid-template-columns: minmax(0, 1fr); } }
/* cohort strip: one cell per cohort */
-.strip { display: grid; grid-template-columns: repeat(var(--n), 22px); gap: 4px; }
-.sc { box-sizing: border-box; width: 22px; height: 22px; border-radius: 6px; background: var(--surface-2); display: grid; place-items: center; font-size: 10.5px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--on-line); }
-.sc.on { background: var(--c); }
-.sc.on.live { background: transparent; border: 2px dashed var(--c); color: var(--ink); }
-.sc.ran { background: transparent; border: 1.5px solid var(--c); }
-.sh { width: 22px; text-align: center; font-size: 11px; font-variant-numeric: tabular-nums; }
/* sliding rail */
.people-rail { overflow: hidden; margin: 0 0 32px; -webkit-mask-image: linear-gradient(90deg, transparent, #000 20px, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(90deg, transparent, #000 20px, #000 calc(100% - 40px), transparent); }
@@ -418,7 +413,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
/* roster */
.roster { background: var(--surface); border: 1px solid var(--rule); border-radius: 16px; overflow: hidden; }
-.ro-head, .ro { display: grid; grid-template-columns: 210px auto minmax(0, 1fr) 96px; gap: 20px; align-items: center; padding: 12px 18px; }
+.ro-head, .ro { display: grid; grid-template-columns: 210px minmax(0, 236px) minmax(0, 1fr) 96px; gap: 20px; align-items: center; padding: 12px 18px; }
.ro-head { font-size: 12px; font-weight: 700; color: var(--muted); padding-block: 10px; border-bottom: 1px solid var(--rule); background: var(--surface-2); }
.ro-list { list-style: none; margin: 0; padding: 0; }
.ro + .ro { border-top: 1px solid var(--rule); }
@@ -439,13 +434,12 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.ro-projects .ro-more { all: unset; cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--accent); }
.ro-more:hover { text-decoration: underline; }
.ro-more:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
-.live-dot { flex-shrink: 0; font-size: 11.5px; font-weight: 700; color: var(--live); }
+.live-dot { flex-shrink: 0; font-size: 12px; font-weight: 700; color: var(--live); }
.tot { text-align: right; }
.ro-total b { display: block; font-family: var(--f-display); font-size: 16px; white-space: nowrap; font-weight: 800; line-height: 1.2; font-variant-numeric: tabular-nums; }
.ro-total span { font-size: 12px; color: var(--muted); white-space: nowrap; }
.roster-legend { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; color: var(--muted); }
.roster-legend li { display: flex; align-items: center; gap: 6px; }
-.roster-legend .sc { width: 14px; height: 14px; border-radius: 4px; }
@media (max-width: 600px) { .rail-head, .people-rail { display: none; } }
@media (max-width: 760px) {
.ro-head { display: none; }
@@ -499,15 +493,19 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.tabs button { font: inherit; font-size: 15px; font-weight: 600; background: none; border: 0; padding: 8px 12px; border-radius: 8px; color: var(--muted); cursor: pointer; min-height: 40px; }
.tabs button[aria-selected="true"] { color: var(--ink); background: var(--surface-2); }
.map-scroll { overflow-x: auto; padding: 0 8px; }
-#metro { display: block; width: 100%; min-width: 860px; height: auto; }
+/* below this width the labels would shrink under 12px, so the map scrolls instead */
+#metro { display: block; width: 100%; min-width: 1080px; height: auto; }
+.map-scroll { scrollbar-width: thin; }
+.map-scroll.more { -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent); mask-image: linear-gradient(90deg, #000 88%, transparent); }
+.map-hint { margin: 0 16px 8px; font-size: 13px; color: var(--muted); }
#metro .zone rect { fill: var(--surface-2); }
#metro .zone.build rect { fill: color-mix(in srgb, var(--l-main) 9%, var(--surface)); }
#metro .zone-l { font-family: var(--f-display); font-weight: 800; letter-spacing: -.02em; font-size: 22px; fill: var(--ink); }
-#metro .zone-n { font-size: 13px; font-weight: 600; fill: var(--ink); }
-#metro .zone-q { font-size: 12px; fill: var(--muted); }
+#metro .zone-n { font-size: 14px; font-weight: 600; fill: var(--ink); }
+#metro .zone-q { font-size: 13px; fill: var(--muted); }
#metro .track { fill: none; stroke: var(--c); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; }
#metro .express { fill: none; stroke: var(--l-main); stroke-width: 3; stroke-dasharray: 6 8; stroke-linecap: round; }
-#metro .express-l { font-size: 12px; font-weight: 600; fill: var(--l-main); }
+#metro .express-l { font-size: 13px; font-weight: 600; fill: var(--l-main); }
#metro .badge circle { fill: var(--c); }
#metro .badge text { font-family: var(--f-display); font-weight: 800; letter-spacing: -.02em; font-size: 16px; fill: var(--on-line); text-anchor: middle; dominant-baseline: central; }
#metro .st { cursor: pointer; }
@@ -516,8 +514,8 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
#metro .st .pill { fill: var(--surface); stroke: var(--ink); stroke-width: 4.5; }
#metro .st .sq { fill: var(--surface); stroke: var(--l-main); stroke-width: 3; }
#metro .st.ext .halo { fill: none; stroke: var(--c); stroke-width: 2.5; }
-#metro .st .label { font-size: 13.5px; font-weight: 600; fill: var(--ink); }
-#metro .st.paid .label { font-size: 12.5px; fill: var(--l-main); }
+#metro .st .label { font-size: 14.5px; font-weight: 600; fill: var(--ink); }
+#metro .st.paid .label { font-size: 13.5px; fill: var(--l-main); }
#metro .st .tag rect { fill: var(--live); }
#metro .st .tag text { fill: var(--surface); font-size: 10.5px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
#metro .st .done { fill: var(--c); }
@@ -533,10 +531,10 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a
.line-legend li { display: flex; align-items: center; gap: 6px; }
.num { display: inline-block; width: 12px; height: 12px; border-radius: 50%; background: var(--c); flex-shrink: 0; }
-/* Vertical lines replace the map on phones */
+/* Vertical lines replace the map on phones and small tablets */
.vlines { display: none; padding: 0 16px; }
-@media (max-width: 720px) {
- .map-scroll { display: none; }
+@media (max-width: 900px) {
+ .map-scroll, .map-hint { display: none !important; }
.vlines { display: grid; gap: 22px; }
}
.vline h3 { display: flex; align-items: center; gap: 8px; font-size: 16px; margin-bottom: 8px; }
@@ -631,9 +629,9 @@ dialog.station::backdrop { background: rgba(10, 14, 20, .55); }
.p-timeline li.r::before { box-sizing: border-box; background: var(--surface); border: 2px solid var(--c); }
.tl-c { white-space: nowrap; }
.tl-c b { display: block; font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
-.tl-c span { font-size: 11.5px; color: var(--muted); }
+.tl-c span { font-size: 12px; color: var(--muted); }
.tl-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; font-size: 14px; }
-.tl-role { font-size: 11.5px; font-weight: 700; color: var(--accent); }
+.tl-role { font-size: 12px; font-weight: 700; color: var(--accent); }
.p-timeline li.r .tl-role { color: var(--muted); }
.tl-name { font-weight: 600; }
.tl-note { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; }
@@ -735,7 +733,7 @@ button.btn.sm { font-size: 14px; }
.bp-repos a span span { font-size: 13px; color: var(--muted); }
.bp-badge { padding: 16px; border-radius: 14px; border: 1px solid var(--rule); background: var(--surface); display: grid; gap: 10px; justify-items: start; }
.bp-badge p { font-size: 13.5px; color: var(--muted); }
-.bp-badge pre { margin: 0; width: 100%; box-sizing: border-box; overflow-x: auto; font-family: var(--f-code); font-size: 11.5px; padding: 10px; border-radius: 8px; background: var(--surface-2); }
+.bp-badge pre { margin: 0; width: 100%; box-sizing: border-box; overflow-x: auto; font-family: var(--f-code); font-size: 12px; padding: 10px; border-radius: 8px; background: var(--surface-2); }
.bp-cta { margin-top: 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px 24px; padding: 22px 24px; border-radius: 16px; background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--rule)); }
.bp-cta h2 { margin-bottom: 4px; }
.bp-cta p { font-size: 14.5px; color: var(--muted); max-width: 36em; }
@@ -751,7 +749,7 @@ button.btn.sm { font-size: 14px; }
.bi-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bi-body b { font-size: 16px; }
.bi-body span { font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.bi-list .rank-tag { position: absolute; top: 10px; right: 12px; font-size: 11px; padding: 1px 8px; }
+.bi-list .rank-tag { position: absolute; top: 10px; right: 12px; font-size: 12px; padding: 1px 8px; }
@media (max-width: 860px) { .bp-grid { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 560px) { .bp-hero { flex-direction: column; align-items: flex-start; padding: 22px 20px; } .bp-hero .avatar.lg { width: 72px; height: 72px; font-size: 30px; } }
@@ -949,3 +947,114 @@ button.btn.sm { font-size: 14px; }
.back-person { font: inherit; font-size: 13.5px; font-weight: 700; align-self: flex-start; margin: 0 0 8px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--rule); background: var(--surface-2); color: var(--ink); cursor: pointer; }
.back-person:hover { border-color: var(--ink); }
.back-person:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
+
+/* Cohort chips: only the cohorts a Builder took part in */
+.ro-strip { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; }
+.cc { display: inline-flex; align-items: baseline; gap: 4px; padding: 3px 8px; border-radius: 7px; font-size: 12px; font-weight: 600; line-height: 1.35; white-space: nowrap; font-variant-numeric: tabular-nums; }
+.cc b { font-size: 12.5px; font-weight: 800; }
+.cc i { font-style: normal; font-weight: 800; opacity: .85; }
+.cc.b { background: var(--c); color: var(--on-line); }
+.cc.b.live { background: transparent; color: var(--ink); outline: 1.5px dashed var(--c); outline-offset: -1.5px; }
+.cc.r { background: transparent; color: var(--muted); box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--c) 55%, var(--rule)); }
+.cc.r b { color: var(--ink); }
+.roster-legend .cc { margin-right: 6px; }
+.ro-medal, .pc-medal { display: inline-flex; vertical-align: -4px; margin-left: 6px; }
+.ro-medal .medal, .pc-medal .medal { height: 21px; width: auto; }
+
+/* Medals on the top three cards */
+.t-medals { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; font-size: 13px; }
+.t-medals > .medal { height: 46px; width: auto; flex-shrink: 0; }
+.t-medals > b { font-size: 14px; font-weight: 800; margin-right: 4px; }
+.t-sp { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 700; color: var(--muted); }
+
+/* Medal ladder: the rules and where they lead */
+.medal-ladder { margin: 0 0 32px; padding: 20px; border-radius: 18px; background: var(--surface); border: 1px solid var(--rule); }
+.ml-head p { font-size: 14px; color: var(--muted); margin-top: 4px; max-width: 52em; }
+.ml-steps { list-style: none; margin: 16px 0 0; padding: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
+.ml-step, .ml-step.stage a { position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; padding: 12px 8px; border-radius: 14px; background: var(--surface-2); font-size: 12.5px; color: var(--muted); }
+.ml-step:not(:last-child)::after { content: ""; position: absolute; right: -10px; top: 38px; width: 7px; height: 7px; border-top: 2px solid var(--muted); border-right: 2px solid var(--muted); transform: rotate(45deg); opacity: .6; }
+.ml-step b { font-size: 14px; color: var(--ink); }
+.ml-medal { height: 66px; display: grid; place-items: center; margin-bottom: 4px; }
+.ml-medal .medal { height: 64px; width: auto; }
+.ml-n { font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; }
+.ml-step.stage { display: block; padding: 0; background: none; }
+.ml-step.stage a { height: 100%; min-height: 100%; box-sizing: border-box; justify-content: center; text-decoration: none; background: #14162B; color: rgba(255, 255, 255, .78); }
+.ml-step.stage a b { color: #FFD54A; }
+.ml-step.stage a:hover { outline: 2px solid #FFD54A; }
+.ml-step.stage a:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
+.ml-special { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; font-size: 13px; color: var(--muted); }
+.ml-special li { display: inline-flex; align-items: center; gap: 6px; }
+.ml-special li.k { font-weight: 800; color: var(--ink); }
+.ml-special b { color: var(--ink); }
+@media (max-width: 760px) {
+ .ml-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ml-step::after { display: none; }
+ .ml-step.stage { grid-column: 1 / -1; }
+}
+
+/* The person just opened: medal reveal in the dialog, and a mark in the lists behind it */
+.p-award { position: relative; overflow: hidden; display: flex; align-items: center; gap: 16px; padding: 16px 18px; border-radius: 16px; background: linear-gradient(118deg, #23276A, #4A45B5 70%, #6E68BC); color: #fff; }
+.p-award::after { content: ""; position: absolute; top: 0; bottom: 0; left: -40%; width: 30%; background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .28), transparent); transform: skewX(-18deg); animation: award-shine 1.4s .25s ease-out 1 both; pointer-events: none; }
+.p-award-medal { flex-shrink: 0; animation: award-pop .6s cubic-bezier(.2, 1.5, .45, 1) both; filter: drop-shadow(0 8px 14px rgba(0, 0, 0, .3)); }
+.p-award-medal .medal { display: block; height: 104px; width: auto; }
+.p-award-body { display: grid; gap: 4px; min-width: 0; }
+.p-award .k { font-size: 12px; font-weight: 700; color: rgba(255, 255, 255, .72); }
+.p-award .t { font-size: 20px; font-weight: 800; color: #FFD54A; letter-spacing: -.01em; }
+.p-award .d { font-size: 14px; color: rgba(255, 255, 255, .9); }
+.p-award .btn.ghost { justify-self: start; margin-top: 6px; color: #fff; border-color: rgba(255, 255, 255, .45); }
+.p-award .btn.ghost:hover { border-color: #fff; }
+.p-ribbons { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; }
+.p-ribbons li { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: rgba(255, 255, 255, .8); }
+.p-ribbons li > span { display: flex; flex-direction: column; line-height: 1.3; }
+.p-ribbons b { color: #fff; font-size: 13px; }
+@keyframes award-pop { from { transform: scale(.4) rotate(-20deg); opacity: 0; } to { transform: none; opacity: 1; } }
+@keyframes award-shine { to { left: 120%; } }
+@media (max-width: 480px) { .p-award { padding: 14px; gap: 12px; } .p-award-medal .medal { height: 84px; } .p-award .t { font-size: 18px; } }
+.ro.is-picked { background: color-mix(in srgb, var(--c) 9%, var(--surface)); box-shadow: inset 4px 0 0 var(--c); }
+.tcard.is-picked > button, .pcard.is-picked > button { border-color: var(--c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c) 30%, transparent); }
+
+/* Academic events the team works on */
+.stage { margin-top: 28px; padding: 22px; border-radius: 18px; background: var(--banner); color: #fff; scroll-margin-top: 84px; }
+.stage-k { font-size: 12.5px; font-weight: 800; color: #FFD54A; }
+.stage-head h3 { font-size: 20px; margin-top: 2px; }
+.stage-head p { font-size: 14px; color: rgba(255, 255, 255, .82); margin-top: 4px; max-width: 52em; }
+.stage-list { list-style: none; margin: 16px 0 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
+.stage-list a { display: flex; flex-direction: column; gap: 8px; height: 100%; box-sizing: border-box; padding: 16px; border-radius: 14px; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .18); color: #fff; text-decoration: none; transition: background .15s ease, border-color .15s ease; }
+.stage-list a:hover { background: rgba(255, 255, 255, .16); border-color: rgba(255, 255, 255, .45); }
+.stage-list a:focus-visible { outline: 3px solid #FFD54A; outline-offset: 2px; }
+.sg-year { font-family: var(--f-display); font-weight: 800; font-size: 28px; line-height: 1; letter-spacing: -.02em; }
+.sg-body { display: flex; flex-direction: column; gap: 2px; }
+.sg-body b { font-size: 15.5px; line-height: 1.4; }
+.sg-en { font-size: 12px; color: rgba(255, 255, 255, .66); }
+.sg-about { font-size: 13.5px; color: rgba(255, 255, 255, .88); margin-top: 4px; }
+.sg-when { font-size: 12.5px; color: rgba(255, 255, 255, .7); font-variant-numeric: tabular-nums; }
+.sg-go { margin-top: auto; font-size: 13px; font-weight: 700; color: #FFD54A; }
+.sg-go::after { content: " ↗"; }
+@media (max-width: 860px) { .stage-list { grid-template-columns: minmax(0, 1fr); } .stage { padding: 18px; } }
+
+/* Banner line for the academic partnership */
+.academy { margin: 10px 0 0; max-width: 600px; }
+.academy a { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; padding: 8px 12px; border-radius: 10px; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .2); color: rgba(255, 255, 255, .85); font-size: 12.5px; text-decoration: none; }
+.academy a::after { content: "›"; font-weight: 800; color: #FFD54A; }
+.academy a:hover { background: rgba(255, 255, 255, .16); }
+.academy a:focus-visible { outline: 3px solid #FFD54A; outline-offset: 2px; }
+.academy b { color: #fff; font-weight: 800; }
+
+/* Builder pages: medal in the hero and the medal box */
+.bp-medal { margin-left: auto; flex-shrink: 0; filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .3)); animation: award-pop .6s cubic-bezier(.2, 1.5, .45, 1) both; }
+.bp-medal .medal { display: block; height: 148px; width: auto; }
+.bp-tier { font-size: 12.5px; font-weight: 800; padding: 3px 10px; border-radius: 999px; background: rgba(255, 255, 255, .16); color: #FFD54A; }
+.bp-tier-line { display: flex; align-items: center; gap: 10px; color: var(--ink) !important; }
+.bp-tier-line .medal { height: 52px; width: auto; flex-shrink: 0; }
+.bp-tier-line span { display: flex; flex-direction: column; font-size: 13px; color: var(--muted); }
+.bp-tier-line b { font-size: 16px; color: var(--ink); }
+.bp-medals .p-ribbons li { color: var(--muted); }
+.bp-medals .p-ribbons b { color: var(--ink); }
+.bi-medal { display: inline-flex; vertical-align: -4px; }
+.bi-medal .medal { height: 21px; width: auto; }
+@media (max-width: 640px) { .bp-medal .medal { height: 96px; } .bp-hero { flex-wrap: wrap; } .bp-medal { position: absolute; right: 20px; top: 20px; } .bp-hero { position: relative; } .bp-hero .bp-id { padding-right: 0; } }
+.bp-kicker { flex-wrap: wrap; row-gap: 6px; }
+.bp-kicker .rank-tag, .bp-tier { white-space: nowrap; }
+/* inside a dialog on phones the toast sits at the bottom, clear of the close button */
+@media (max-width: 900px) { dialog .toast { top: auto; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); transform: translate(-50%, 180%); } dialog .toast.show { transform: translate(-50%, 0); } }
+.bp-badge pre code { font-size: inherit; }
diff --git a/assets/js/hub.js b/assets/js/hub.js
index 7aec156..afad7d4 100644
--- a/assets/js/hub.js
+++ b/assets/js/hub.js
@@ -46,6 +46,7 @@
d.projects.forEach((p) => { p.cohort = int(p.cohort); p.runners = p.runners == null ? null : int(p.runners); });
d.events.forEach((e) => { e.order = int(e.order); e.year = int(e.year); e.participants = e.participants == null ? null : int(e.participants); });
d.routes.forEach((r) => { r.id = tok(r.id); });
+ d.stages.items = (d.stages.items || []).map((x) => ({ ...x, id: tok(x.id), year: int(x.year) }));
d.builders.forEach((b) => { (b.runnerOf || []).forEach((x) => { x.cohort = int(x.cohort); }); });
return d;
}
@@ -72,7 +73,7 @@
/* ---------- data ---------- */
async function load() {
- const names = ["repos", "lines", "levels", "map", "routes", "projects", "events", "articles", "builders", "growth", "stats", "featured", "daily"];
+ const names = ["repos", "lines", "levels", "map", "routes", "projects", "events", "articles", "builders", "growth", "stats", "featured", "daily", "stages"];
const data = await Promise.all(names.map((n) => getJSON(`data/${n}.json`)));
const d = cleanData(Object.fromEntries(names.map((n, i) => [n, data[i]])));
d.repos.forEach((r) => {
@@ -84,7 +85,7 @@
(d.map.paid || []).forEach((p) => state.repos.set(p.id, { repo: p.id, kind: "paid", status: "paid", builders: [], cohorts: [], ...p }));
state.lineList = d.lines;
d.lines.forEach((l) => state.lines.set(l.id, l));
- Object.assign(state, { levels: d.levels, map: d.map, routes: d.routes, projects: d.projects, events: d.events, articles: d.articles, builders: d.builders, growth: d.growth, stats: d.stats, featured: d.featured, daily: d.daily });
+ Object.assign(state, { levels: d.levels, map: d.map, routes: d.routes, projects: d.projects, events: d.events, articles: d.articles, builders: d.builders, growth: d.growth, stats: d.stats, featured: d.featured, daily: d.daily, stages: d.stages });
const pos = new Map(d.map.stations.map((s) => [s.id, s]));
d.lines.forEach((l) => {
@@ -130,6 +131,41 @@
return b.linkedin ? `${esc(name)}` : esc(name);
}
+ /* ---------- Builder medals ----------
+ One medal for how many cohorts someone has led, plus two special ones. The same rules
+ live in scripts/build_builders.py so the hub, Builder pages and share images agree. */
+ const TIERS = [
+ { id: "honor", min: 5, name: "명예 Builder", rule: "5개 기수 이상", c: ["#9C95E8", "#33378C"], ink: "#FFFFFF" },
+ { id: "gold", min: 3, name: "베테랑 Builder", rule: "3개 기수 이상", c: ["#F8D978", "#B98318"], ink: "#3A2700" },
+ { id: "silver", min: 2, name: "다시 온 Builder", rule: "2개 기수", c: ["#E6EBF1", "#8995A6"], ink: "#1B2230" },
+ { id: "bronze", min: 1, name: "Builder", rule: "첫 기수", c: ["#E3A36A", "#8E5226"], ink: "#FFFFFF" }
+ ];
+ const SPECIALS = [
+ { id: "mentor", name: "멘토", rule: "러너 20명 이상과 함께", c: ["#63D2B1", "#1B7A61"], ink: "#FFFFFF", has: (p) => p.runners >= 20 },
+ { id: "rise", name: "러너에서 Builder로", rule: "러너로 참여한 뒤 Builder가 됨", c: ["#FFAE8C", "#C04F33"], ink: "#FFFFFF", has: (p) => p.runnerOf.some((x) => x.cohort < p.first) }
+ ];
+ const tierOf = (p) => TIERS.find((t) => p.cohorts.length >= t.min);
+ const specialsOf = (p) => SPECIALS.filter((m) => m.has(p));
+ let medalUid = 0;
+ function medalSVG(m, size, n) {
+ const id = `mg${++medalUid}`, h = Math.round(size * (m.min ? 84 / 64 : 1));
+ const grad = `
Builder로 연 기수가 쌓일수록 메달이 바뀝니다. 오래 함께한 Builder에게는 학회와 Data Forum에서 발표할 기회가 열립니다.