1/* ============================================================================ 2 * app.js â renders assets/data.js into the page. 3 * You normally never need to touch this file. Edit assets/data.js instead. 4 * ========================================================================== */ 5(function () { 6 'use strict'; 7 8 var S = window.SITE; 9 var P = S.profile; 10 var L = S.links || {}; 11 var ME = (P.meName || '').trim(); 12 13 /* ------------------------------------------------------------ helpers -- */ 14 function esc(v) { 15 return String(v == null ? '' : v) 16 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 17 .replace(/"/g, '"'); 18 } 19 function markMe(text) { 20 var out = esc(text); 21 if (!ME) return out; 22 var needle = esc(ME).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 23 return out.replace(new RegExp(needle, 'g'), '<u>' + esc(ME) + '</u>'); 24 } 25 function set(id, html) { 26 var n = document.getElementById(id); 27 if (n) n.innerHTML = html; 28 return n; 29 } 30 function ext(href) { 31 return /^https?:/.test(href || '') ? ' target="_blank" rel="noopener"' : ''; 32 } 33 /** Newest first. Lets you paste new entries anywhere in data.js. */ 34 function byYear(list, key) { 35 return (list || []).slice().sort(function (a, b) { 36 // ì°ëê° ë¹ì´ ìì¼ë©´(ì¬ì¬ ì¤ ë±) ìë ìì를 ì ì§íë¤ 37 return (parseInt(b[key || 'year'], 10) || 0) - (parseInt(a[key || 'year'], 10) || 0); 38 }); 39 } 40 41 /* ------------------------------------------------------ card artwork --- */ 42 /* Line drawings for the work cards. Each is a 400Ã180 SVG that inherits 43 the current text colour, so they follow the light / dark theme. */ 44 function svg(inner) { 45 return '<svg viewBox="0 0 400 180" fill="none" preserveAspectRatio="xMidYMid meet" aria-hidden="true">' + 46 inner + '</svg>'; 47 } 48 var ART = { 49 // ordinary light curve + one flagged excursion 50 anomaly: svg( 51 '<g stroke="currentColor" stroke-opacity=".08"><path d="M0 60h400M0 108h400"/></g>' + 52 '<path d="M0 118 20 112 40 122 60 104 80 116 100 100 120 112 140 102 160 114 180 104 200 116' + 53 ' 220 106 242 36 262 60 280 104 300 112 320 102 340 114 360 104 380 112 400 106' + 54 ' L400 180 L0 180 Z" fill="var(--tint)" fill-opacity=".5"/>' + 55 '<path d="M0 118 20 112 40 122 60 104 80 116 100 100 120 112 140 102 160 114 180 104 200 116' + 56 ' 220 106 242 36 262 60 280 104 300 112 320 102 340 114 360 104 380 112 400 106"' + 57 ' stroke="currentColor" stroke-opacity=".85" stroke-width="2.4" stroke-linejoin="round"/>' + 58 '<path d="M242 58v100" stroke="currentColor" stroke-opacity=".35" stroke-width="1.4" stroke-dasharray="4 5"/>' + 59 '<circle cx="242" cy="36" r="21" stroke="currentColor" stroke-width="2.4"/>' + 60 '<circle cx="242" cy="36" r="5" fill="currentColor"/>' 61 ), 62 // observations that land whenever they land 63 irregular: svg( 64 '<path d="M0 92C56 34 96 112 156 78s86-56 132-12 60 44 112 22 L400 180 L0 180 Z"' + 65 ' fill="var(--tint)" fill-opacity=".5"/>' + 66 '<path d="M0 92C56 34 96 112 156 78s86-56 132-12 60 44 112 22"' + 67 ' stroke="currentColor" stroke-opacity=".85" stroke-width="2.4" stroke-linecap="round"/>' + 68 '<path d="M0 152h400" stroke="currentColor" stroke-opacity=".3" stroke-width="1.6"/>' + 69 '<g stroke="currentColor" stroke-opacity=".55" stroke-width="2">' + 70 '<path d="M22 146v12M38 146v12M46 146v12M74 146v12M112 146v12M126 146v12M134 146v12' + 71 'M176 146v12M228 146v12M240 146v12M276 146v12M312 146v12M330 146v12M382 146v12"/></g>' + 72 '<g stroke="currentColor" stroke-opacity=".28" stroke-width="1.2" stroke-dasharray="3 5">' + 73 '<path d="M38 70v82M112 90v62M176 74v78M240 50v102M312 56v96"/></g>' + 74 '<g fill="currentColor">' + 75 '<circle cx="38" cy="70" r="4.5"/><circle cx="112" cy="90" r="4.5"/><circle cx="176" cy="74" r="4.5"/>' + 76 '<circle cx="240" cy="50" r="4.5"/><circle cx="312" cy="56" r="4.5"/></g>' 77 ), 78 // competing risks pulling one cohort apart 79 survival: svg( 80 '<path d="M0 26h62v28h56v30h60v22h74v22h60v16h88 L400 180 L0 180 Z"' + 81 ' fill="var(--tint)" fill-opacity=".5"/>' + 82 '<path d="M0 26h62v28h56v30h60v22h74v22h60v16h88"' + 83 ' stroke="currentColor" stroke-opacity=".85" stroke-width="2.6" stroke-linejoin="round"/>' + 84 '<path d="M0 26h40v46h58v30h60v28h72v22h66v14h104"' + 85 ' stroke="currentColor" stroke-opacity=".38" stroke-width="2" stroke-dasharray="6 5" stroke-linejoin="round"/>' + 86 '<path d="M0 158h400" stroke="currentColor" stroke-opacity=".25" stroke-width="1.6"/>' + 87 '<g stroke="currentColor" stroke-opacity=".7" stroke-width="2">' + 88 '<path d="M90 48v12M170 78v12M262 100v12M330 122v12"/></g>' 89 ), 90 // dose-response densities with a chosen starting point 91 dosing: svg( 92 '<path d="M14 180c50 0 36-102 84-102s34 102 84 102z" fill="currentColor" fill-opacity=".08"/>' + 93 '<path d="M14 180c50 0 36-102 84-102s34 102 84 102" stroke="currentColor" stroke-opacity=".35" stroke-width="2"/>' + 94 '<path d="M218 180c50 0 36-88 84-88s34 88 84 88z" fill="currentColor" fill-opacity=".08"/>' + 95 '<path d="M218 180c50 0 36-88 84-88s34 88 84 88" stroke="currentColor" stroke-opacity=".35" stroke-width="2"/>' + 96 '<path d="M116 180c50 0 36-148 84-148s34 148 84 148z" fill="var(--tint)" fill-opacity=".55"/>' + 97 '<path d="M116 180c50 0 36-148 84-148s34 148 84 148" stroke="currentColor" stroke-opacity=".85" stroke-width="2.6"/>' + 98 '<path d="M200 32v148" stroke="currentColor" stroke-opacity=".4" stroke-width="1.4" stroke-dasharray="4 5"/>' + 99 '<circle cx="200" cy="32" r="6" fill="currentColor"/>' + 100 '<path d="M0 174h400" stroke="currentColor" stroke-opacity=".25" stroke-width="1.6"/>' 101 ), 102 // heterogeneous series pre-trained into one backbone 103 foundation: svg( 104 '<g stroke="currentColor" stroke-opacity=".45" stroke-width="2" stroke-linejoin="round">' + 105 '<path d="M8 34 34 24 60 38 86 28 112 36"/>' + 106 '<path d="M8 78 34 68 60 84 86 62 112 74"/>' + 107 '<path d="M8 122 34 132 60 110 86 124 112 114"/>' + 108 '<path d="M8 164 34 154 60 166 86 150 112 158"/></g>' + 109 '<g stroke="currentColor" stroke-opacity=".25" stroke-width="1.4">' +
110 '<path d="M116 36 146 76M116 74 146 84M116 114 146 100M116 158 146 110"/></g>' + 111 '<rect x="148" y="52" width="76" height="76" rx="14" fill="var(--tint)" fill-opacity=".6"/>' + 112 '<rect x="148" y="52" width="76" height="76" rx="14" stroke="currentColor" stroke-width="2.4"/>' + 113 '<g fill="currentColor" fill-opacity=".55">' + 114 '<circle cx="170" cy="76" r="3.4"/><circle cx="186" cy="90" r="3.4"/><circle cx="202" cy="76" r="3.4"/>' + 115 '<circle cx="178" cy="106" r="3.4"/><circle cx="194" cy="106" r="3.4"/></g>' + 116 '<path d="M228 90h26" stroke="currentColor" stroke-opacity=".3" stroke-width="1.6"/>' + 117 '<path d="M256 92 288 62 320 96 352 48 400 70"' + 118 ' stroke="currentColor" stroke-opacity=".85" stroke-width="2.6" stroke-linejoin="round"/>' + 119 '<path d="M352 48c22 6 34 2 48-8" stroke="currentColor" stroke-opacity=".35" stroke-width="2" stroke-dasharray="6 5"/>' 120 ), 121 // health degrading toward a failure threshold, with the life that remains 122 rul: svg( 123 '<rect x="214" y="0" width="106" height="180" fill="var(--tint)" fill-opacity=".55"/>' + 124 '<path d="M0 130h400" stroke="currentColor" stroke-opacity=".4" stroke-width="1.6" stroke-dasharray="6 5"/>' + 125 '<path d="M0 26C92 32 156 56 214 90" stroke="currentColor" stroke-opacity=".85" stroke-width="2.8"/>' + 126 '<path d="M214 90c48 22 74 32 106 40" stroke="currentColor" stroke-opacity=".4" stroke-width="2.4" stroke-dasharray="7 5"/>' + 127 '<path d="M214 8v164" stroke="currentColor" stroke-opacity=".45" stroke-width="1.6"/>' + 128 '<circle cx="214" cy="90" r="6" fill="currentColor"/>' + 129 '<circle cx="320" cy="130" r="6.5" fill="none" stroke="currentColor" stroke-width="2.4"/>' + 130 '<g stroke="currentColor" stroke-opacity=".7" stroke-width="2">' + 131 '<path d="M220 44h94M220 44l9-6M220 44l9 6M314 44l-9-6M314 44l-9 6"/></g>' + 132 '<path d="M0 172h400" stroke="currentColor" stroke-opacity=".25" stroke-width="1.6"/>' 133 ), 134 // generic fallback 135 forecast: svg( 136 '<path d="M0 120 60 96 120 108 180 66 240 82 300 44 360 60 400 32 L400 160 L0 160 Z"' + 137 ' fill="var(--tint)" fill-opacity=".5"/>' + 138 '<path d="M0 120 60 96 120 108 180 66 240 82 300 44 360 60 400 32"' + 139 ' stroke="currentColor" stroke-opacity=".85" stroke-width="2.4" stroke-linejoin="round"/>' 140 ) 141 }; 142 143 /* --------------------------------------------------------------- nav --- */ 144 var NAV = [ 145 { id: 'about', label: 'About' }, 146 { id: 'work', label: 'Work' }, 147 { id: 'publications', label: 'Publications' }, 148 { id: 'talks', label: 'Talks' }, 149 { id: 'teaching', label: 'Teaching' } 150 ]; 151 152 document.querySelectorAll('[data-wordmark]').forEach(function (n) { 153 n.textContent = P.name; 154 }); 155 156 set('navMenu', NAV.map(function (n) { 157 return '<a href="#' + n.id + '" data-nav="' + n.id + '">' + esc(n.label) + '</a>'; 158 }).join('')); 159 160 set('navActions', 161 '<a class="btn btn-outline btn-sm" href="mailto:' + esc(P.email) + '">Email</a>' + 162 (L.scholar ? '<a class="btn btn-primary btn-sm" href="' + esc(L.scholar) + '"' + ext(L.scholar) + '>Google Scholar</a>' : '') + 163 '<button class="icon-btn" id="themeBtn" type="button" aria-label="Toggle colour theme">' + 164 '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">' + 165 '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>' + 166 '</button>' + 167 '<button class="icon-btn nav-toggle" id="navToggle" type="button" aria-label="Menu" aria-expanded="false">' + 168 '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">' + 169 '<path d="M3 6h18M3 12h18M3 18h18"/></svg>' + 170 '</button>'); 171 172 /* ìµê·¼ ë ¼ë¬¸ 3í¸ â publications ì ì²´ìì ìµì ìì¼ë¡ ìë ì í */ 173 function heroRecent() { 174 var all = []; 175 Object.keys(S.publications || {}).forEach(function (k) { 176 (S.publications[k] || []).forEach(function (it) { all.push(it); }); 177 }); 178 all = byYear(all).slice(0, 3); 179 if (!all.length) return ''; 180 return '<aside class="hero-aside">' + 181 '<p class="eyebrow">Recent</p>' + 182 '<ul class="hero-recent">' + all.map(function (it) { 183 return '<li>' + 184 '<span class="meta">' + esc(it.year) + ' · ' + esc(it.venue || '') + '</span>' + 185 '<span class="ttl">' + esc(it.title) + '</span>' + 186 '</li>'; 187 }).join('') + '</ul>' + 188 '<a class="btn-text" href="#publications">All publications <span class="arw">→</span></a>' + 189 '</aside>'; 190 } 191 192 /* -------------------------------------------------------------- hero --- */ 193 var hp = P.heroPhoto || {}; 194 set('hero', 195 // 1. the banner photograph, edge to edge, straight under the menu bar 196 (hp.src 197 ? '<figure class="hero-photo">' + 198 '<div class="hero-photo-frame">' + 199 // ì¢ì° ë ê°ë ê°ì ì¬ì§ì ì¢ì° ë°ì í ê² â ì´ìì ìì í½ì ì´ ì íí ì´ì´ì§ë¤ 200 '<img class="hero-photo-wing wing-l" src="' + esc(hp.src) + '" alt="" aria-hidden="true" />' + 201 '<img class="hero-photo-main" src="' + esc(hp.src) + '" alt="' + esc(hp.alt || '') + '" />' + 202 '<img class="hero-photo-wing wing-r" src="' + esc(hp.src) + '" alt="" aria-hidden="true" />' + 203 '</div>' + 204 // caption only when data.js actually provides one 205 (hp.caption || hp.year 206 ? '<figcaption><div class="container">' + 207 '<p>' + esc(hp.caption || '') + '</p>' + 208 (hp.year ? '<span>' + esc(hp.year) + '</span>' : '') + 209 '</div></figcaption>' 210 : '') + 211 '</figure>' 212 : '') + 213 // 2. identity line + thesis 214 '<div class="hero-body">' +
215 '<div class="orb orb-lavender orb-a"></div>' + 216 '<div class="orb orb-mint orb-b"></div>' + 217 '<div class="orb orb-peach orb-c"></div>' + 218 '<div class="container"><div class="hero-split">' + 219 '<div class="hero-copy">' + 220 '<p class="identity"><b>' + esc(P.name) + '</b><span>' + esc(P.role) + '</span></p>' + 221 '<h1 class="display-mega">' + P.heroLine + '</h1>' + 222 '<p class="lead">' + esc(P.heroLead) + '</p>' + 223 '<div class="hero-cta">' + 224 '<a class="btn btn-primary" href="mailto:' + esc(P.email) + '">Get in touch</a>' + 225 '<a class="btn btn-outline" href="#work">Selected work</a>' + 226 '</div>' + 227 '</div>' + 228 heroRecent() + 229 '</div></div>' + 230 '</div>'); 231 232 /* ------------------------------------------------------- logo wall ----- */ 233 set('logoWall', 234 '<div class="container">' + 235 '<p class="eyebrow">Research collaborations</p>' + 236 '<div class="logo-row">' + (S.collaborators || []).map(function (c) { 237 var inner = c.logo 238 ? '<img src="' + esc(c.logo) + '" alt="' + esc(c.name) + '" />' 239 : (c.abbr ? '<span class="abbr">' + esc(c.abbr) + '</span>' : '') + esc(c.name); 240 return c.url 241 ? '<a class="logo-item" href="' + esc(c.url) + '"' + ext(c.url) + '>' + inner + '</a>' 242 : '<span class="logo-item">' + inner + '</span>'; 243 }).join('') + '</div>' + 244 '</div>'); 245 246 /* -------------------------------------------------------------- work --- */ 247 set('work', 248 '<div class="container">' + 249 '<div class="sec-head reveal">' + 250 '<div><p class="eyebrow">Selected work</p>' + 251 '<h2 class="display-lg">' + esc(S.workTitle || 'Where the clock is not on a grid.') + '</h2></div>' + 252 '</div>' + 253 '<div class="work-grid">' + (S.work || []).map(function (w, i) { 254 // ì´ë¯¸ì§ê° ìì¼ë©´ ì´ë¯¸ì§, ìê±°ë ë¡ëì ì¤í¨íë©´ ê·¸ë ¤ë ì¼ë¬ì¤í¸ 255 var visual = w.image 256 ? '<img src="' + esc(w.image) + '" alt="' + esc(w.title) + '" loading="lazy"' + 257 ' data-art="' + esc(w.art || 'forecast') + '" />' 258 : (ART[w.art] || ART.forecast); 259 return '<article class="work-card reveal tint-' + esc(w.orb || 'sky') + '">' + 260 '<div class="work-art' + (w.image ? ' has-image' : '') + '">' + visual + '</div>' + 261 '<div class="work-copy">' + 262 '<span class="idx">' + String(i + 1).padStart(2, '0') + '</span>' + 263 '<h3 class="display-sm">' + esc(w.title) + '</h3>' + 264 '<p class="body-md">' + esc(w.text) + '</p>' + 265 '<div class="tags">' + (w.tags || []).map(function (t) { 266 return '<span class="tag">' + esc(t) + '</span>'; 267 }).join('') + '</div>' + 268 '</div>' + 269 '</article>'; 270 }).join('') + '</div>' + 271 '</div>'); 272 273 /* ì´ë¯¸ì§ íì¼ì´ ìì§ ìì¼ë©´ ì¡°ì©í 기존 ì¼ë¬ì¤í¸ë¡ ëëë¦°ë¤ */ 274 document.querySelectorAll('.work-art img[data-art]').forEach(function (img) { 275 img.addEventListener('error', function () { 276 var panel = img.parentNode; 277 panel.classList.remove('has-image'); 278 panel.innerHTML = ART[img.dataset.art] || ART.forecast; 279 }); 280 }); 281 282 /* ------------------------------------------------------------ entries -- */ 283 function entry(o) { 284 var links = (o.links || []).filter(function (l) { return l.href; }); 285 return '<article class="entry reveal">' + 286 '<div class="yr">' + esc(o.year) + '</div>' + 287 '<div>' + 288 '<h4>' + esc(o.title) + '</h4>' + 289 (o.authors ? '<p class="authors">' + markMe(o.authors) + '</p>' : '') + 290 (o.venue || o.kind 291 ? '<div class="meta-row">' + 292 (o.venue ? '<span class="venue">' + esc(o.venue) + '</span>' : '') + 293 (o.kind ? '<span class="kind">' + esc(o.kind) + '</span>' : '') + 294 '</div>' 295 : '') + 296 (o.note ? '<p class="note">' + esc(o.note) + '</p>' : '') + 297 '</div>' + 298 (links.length
299 ? '<div class="entry-links">' + links.map(function (l) { 300 return '<a href="' + esc(l.href) + '"' + ext(l.href) + '>' + esc(l.label) + ' ↗</a>'; 301 }).join('') + '</div>' 302 : '<div></div>') + 303 '</article>'; 304 } 305 function subLabel(text, n) { 306 return '<div class="sub-label"><span>' + esc(text) + '</span><i></i><b>' + 307 String(n).padStart(2, '0') + '</b></div>'; 308 } 309 function emptyRow(msg) { return '<p class="empty">' + esc(msg) + '</p>'; } 310 311 /* ------------------------------------------------------ publications --- */ 312 /* ë ¼ë¬¸ ì¹ì ì íìëë ìì. ìì를 ë°ê¾¸ë ¤ë©´ ì´ ë°°ì´ì ì¤ ììë§ ë°ê¾¸ë©´ ëë¤. */ 313 var PUB_GROUPS = [ 314 { key: 'underReview', label: 'Under review' }, 315 { key: 'journal', label: 'Journal' }, 316 { key: 'conference', label: 'Conference' }, 317 { key: 'workshop', label: 'Workshop' } 318 ]; 319 var pubTotal = 0; 320 var papersBody = ''; 321 PUB_GROUPS.forEach(function (g) { 322 var list = byYear(S.publications[g.key]); 323 if (!list.length) return; // ë¹ì´ ìë 그룹ì ìì ìëµ 324 pubTotal += list.length; 325 papersBody += subLabel(g.label, list.length) + list.map(entry).join(''); 326 }); 327 if (!papersBody) papersBody = emptyRow('Nothing here yet.'); 328 329 set('publications', 330 '<div class="container">' + 331 '<div class="sec-head reveal">' + 332 '<div><p class="eyebrow">Publications</p>' + 333 '<h2 class="display-lg">Papers</h2></div>' + 334 (L.scholar ? '<a class="btn-text" href="' + esc(L.scholar) + '"' + ext(L.scholar) + 335 '>All on Google Scholar <span class="arw">→</span></a>' : '') + 336 '</div>' + 337 papersBody + 338 '</div>'); 339 340 /* -------------------------------------------------------------- talks -- */ 341 var talks = byYear(S.talks); 342 set('talks', 343 '<div class="container">' + 344 '<div class="sec-head reveal">' + 345 '<div><p class="eyebrow">Speaking</p>' + 346 '<h2 class="display-lg">Talks & presentations</h2></div>' + 347 '</div>' + 348 (talks.length ? talks.map(entry).join('') : emptyRow('Nothing here yet.')) + 349 '</div>'); 350 351 /* --------------------------------------------- teaching + education ---- */ 352 var teaching = byYear(S.teaching, 'term').map(function (t) { 353 return entry({ year: t.term, title: t.course, authors: t.org, venue: t.role, note: t.note }); 354 }).join(''); 355 var awards = byYear(S.awards).map(function (a) { 356 return entry({ year: a.year, title: a.title, authors: a.org }); 357 }).join(''); 358 var education = byYear(S.education).map(function (e) { 359 return entry({ year: e.year, title: e.title, authors: e.org, note: e.note }); 360 }).join(''); 361 362 set('teaching', 363 '<div class="container">' + 364 '<div class="two-up">' + 365 '<div class="reveal"><p class="eyebrow">Teaching</p>' + 366 '<h2 class="display-md">Teaching assistantships</h2>' + 367 (teaching || emptyRow('Nothing here yet.')) + 368 '</div>' + 369 '<div class="reveal"><p class="eyebrow">Background</p>' + 370 '<h2 class="display-md">Education & awards</h2>' + 371 subLabel('Education', (S.education || []).length) + education + 372 subLabel('Awards', (S.awards || []).length) + awards + 373 '</div>' + 374 '</div>' + 375 '</div>'); 376 377 /* -------------------------------------------------------------- about -- */ 378 set('about', 379 '<div class="container"><div class="about-grid">' + 380 '<div class="about-body reveal">' + 381 '<p class="eyebrow">About</p>' + 382 '<h2 class="display-md about-title">' + esc(P.aboutTitle) + '</h2>' + 383 P.about.map(function (p) { return '<p class="body-md">' + p + '</p>'; }).join('') + 384 '<div class="interest-list">' + (P.interests || []).map(function (t) { 385 return '<span class="tag">' + esc(t) + '</span>'; 386 }).join('') + '</div>' + 387 '<div class="about-cta">' + 388 '<a class="btn btn-primary" href="#publications">See publications</a>' + 389 (L.scholar ? '<a class="btn-text" href="' + esc(L.scholar) + '"' + ext(L.scholar) + 390 '>Google Scholar <span class="arw">→</span></a>' : '') + 391 '</div>' + 392 '</div>' + 393 '<div class="about-side reveal">
393' + 394 '<figure class="about-portrait">' + 395 '<img src="' + esc(P.photo) + '" alt="' + esc(P.name) + '" />' + 396 '<figcaption>' + esc(P.name) + '</figcaption>' + 397 '</figure>' + 398 (P.facts && P.facts.length 399 ? '<dl class="facts">' + P.facts.map(function (f) { 400 return '<dt>' + esc(f.label) + '</dt><dd>' + esc(f.value) + '</dd>'; 401 }).join('') + '</dl>' 402 : '') + 403 '</div>' + 404 '</div></div>'); 405 406 /* ---------------------------------------------------------------- cta -- */ 407 set('cta', 408 '<div class="container"><div class="cta-band reveal">' + 409 '<div class="orb orb-sky orb-a"></div>' + 410 '<div class="orb orb-rose orb-b"></div>' + 411 '<h2 class="display-lg">' + esc(S.cta.title) + '</h2>' + 412 '<p class="body-md">' + esc(S.cta.text) + '</p>' + 413 '<div class="btns">' + 414 '<a class="btn btn-on-dark" href="mailto:' + esc(P.email) + '">' + esc(P.email) + '</a>' + 415 (L.linkedin ? '<a class="btn btn-outline" href="' + esc(L.linkedin) + '"' + ext(L.linkedin) + '>LinkedIn</a>' : '') + 416 '</div>' + 417 '</div></div>'); 418 419 /* ------------------------------------------------------------- footer -- */ 420 function fLink(label, href) { 421 return href ? '<li><a href="' + esc(href) + '"' + ext(href) + '>' + esc(label) + '</a></li>' : ''; 422 } 423 set('footer', 424 '<div class="container">' + 425 '<div class="footer-grid">' + 426 '<div class="footer-brand">' + 427 '<div class="wordmark" data-wordmark>' + esc(P.name) + '</div>' + 428 '<p>' + esc(P.role) + '</p>' + 429 '</div>' + 430 '<div><h5>Sections</h5><ul>' + 431 NAV.map(function (n) { return '<li><a href="#' + n.id + '">' + esc(n.label) + '</a></li>'; }).join('') + 432 '</ul></div>' + 433 '<div><h5>Elsewhere</h5><ul>' + 434 fLink('Google Scholar', L.scholar) + 435 fLink('GitHub', L.github) + 436 fLink('LinkedIn', L.linkedin) + 437 fLink('Curriculum Vitae', L.cv) + 438 '</ul></div>' + 439 '<div><h5>Contact</h5><ul>' + 440 '<li><a href="mailto:' + esc(P.email) + '">' + esc(P.email) + '</a></li>' + 441 '<li><a href="https://www.unist.ac.kr" target="_blank" rel="noopener">UNIST</a></li>' + 442 '</ul></div>' + 443 '</div>' + 444 '<div class="footer-bottom">' + 445 '<span>© ' + new Date().getFullYear() + ' ' + esc(P.name) + '</span>' + 446 '<span>Ulsan, Republic of Korea</span>' + 447 '</div>' + 448 '</div>'); 449 450 /* ========================================================================== 451 * Behaviour 452 * ========================================================================*/ 453 454 /* colour theme */ 455 (function () { 456 var KEY = 'theme', root = document.documentElement; 457 function apply(v) { root.setAttribute('data-theme', v); try { localStorage.setItem(KEY, v); } catch (e) {} } 458 var saved = null; 459 try { saved = localStorage.getItem(KEY); } catch (e) {} 460 apply(saved === 'light' || saved === 'dark' 461 ? saved 462 : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')); 463 var btn = document.getElementById('themeBtn'); 464 if (btn) btn.addEventListener('click', function () { 465 apply(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); 466 }); 467 })(); 468 469 /* mobile menu */ 470 (function () { 471 var toggle = document.getElementById('navToggle'); 472 var menu = document.getElementById('navMenu'); 473 if (!toggle || !menu) return; 474 toggle.addEventListener('click', function () { 475 var open = menu.classList.toggle('open'); 476 toggle.setAttribute('aria-expanded', String(open)); 477 }); 478 menu.addEventListener('click', function (e) { 479 if (e.target.tagName === 'A') { 480 menu.classList.remove('open'); 481 toggle.setAttribute('aria-expanded', 'false'); 482 } 483 }); 484 })(); 485 486 /* scroll reveal + active nav link */ 487 (function () { 488 var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 489 if (reduce || !('IntersectionObserver' in window)) { 490 document.querySelectorAll('.reveal').forEach(function (n) { n.classList.add('in'); }); 491 } else { 492 var io = new IntersectionObserver(function (rows) { 493 rows.forEach(function (r) { 494 if (r.isIntersecting) { r.target.classList.add('in'); io.unobserve(r.target); } 495 }); 496 }, { rootMargin: '0px 0px -8% 0px', threshold: .06 }); 497 document.querySelectorAll('.reveal').forEach(function (n) { io.observe(n); }); 498 } 499 500 var links = {}; 501 document.querySelectorAll('[data-nav]').forEach(function (a) { links[a.dataset.nav] = a; }); 502 var spy = new IntersectionObserver(function (rows) { 503 rows.forEach(function (r) { 504 var a = links[r.target.id]; 505 if (!a) return; 506 if (r.isIntersecting) {
507 Object.keys(links).forEach(function (k) { links[k].classList.remove('active'); }); 508 a.classList.add('active'); 509 } 510 }); 511 }, { rootMargin: '-45% 0px -50% 0px' }); 512 NAV.forEach(function (n) { 513 var s = document.getElementById(n.id); 514 if (s) spy.observe(s); 515 }); 516 })(); 517 518})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.