1<!doctype html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>Doyoung Park â Portfolio 2026</title> 7<link rel="icon" href="./favicon.webp"> 8<link rel="preconnect" href="https://fonts.googleapis.com"> 9<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 10<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700;800&family=Noto+Sans+KR:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 11<style> 12/* ========================================================= 13 TOKENS 14 ========================================================= */ 15:root{ 16 --paper:#ffffff; 17 --ink:#000000; 18 --grey:#7d7d7d; 19 --rule:#0f0f0f; 20 --blue:#1400ff; 21 --lavender:#ecebff; 22 --font:'Inter Tight','Noto Sans KR','Helvetica Neue',Helvetica,Arial,sans-serif; 23 24 --gutter:clamp(28px,4.2vw,64px); 25 --top:clamp(28px,5.5vh,64px); 26 27 --ease-out:cubic-bezier(.16,1,.3,1); 28 --ease-in-out:cubic-bezier(.83,0,.17,1); 29 --dur:1.05s; 30} 31*{box-sizing:border-box} 32html,body{margin:0;height:100%;background:var(--paper);color:var(--ink);font-family:var(--font);-webkit-font-smoothing:antialiased;overflow:hidden} 33/* 1.21 is Inter Tight's own "normal"; pinning it means lines with Korean 34 (Noto Sans KR, taller default) keep the same height, so nothing shifts 35 vertically when the language toggle switches to KO */ 36body{line-height:1.21} 37a{color:inherit} 38button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer} 39::selection{background:var(--blue);color:#fff} 40 41/* ========================================================= 42 TYPE SCALE 43 Display type is the design. Tight tracking, tight leading. 44 ========================================================= */ 45.h-display{font-weight:700;letter-spacing:-.055em;line-height:.86;font-size:clamp(64px,8.6vw,150px);margin:0} 46.h-huge{font-weight:700;letter-spacing:-.05em;line-height:.9;font-size:clamp(56px,7.4vw,124px);margin:0} 47.h-chapter{font-weight:700;letter-spacing:-.055em;line-height:.86;font-size:clamp(88px,12.5vw,230px);margin:0} 48.h-stat{font-weight:700;letter-spacing:-.06em;line-height:.85;font-size:clamp(72px,9.5vw,170px);margin:0;color:var(--blue)} 49.h-caps{font-weight:700;letter-spacing:-.045em;line-height:.9;font-size:clamp(40px,5.2vw,88px);text-transform:uppercase;color:var(--blue);margin:0} 50/* Hangul syllable blocks sit much taller in their own em-box than Latin 51 letters, so the sub-1 line-height tuned for Inter Tight makes the case 52 page's claim heading overlap once its Korean translation wraps to more 53 than one line. Scoped to .casepage.ko (set only when a case is actually 54 showing Korean text) rather than :lang(ko) on <html> â that matched every 55 .h-huge/.h-display on the page, including ones that never translate, like 56 the static "Data Analysis." heading, and pushed their layout around too. */ 57.casepage.ko .h-display,.planpage.ko .phead h1{line-height:1.2;word-break:keep-all} 58.body{font-weight:400;font-size:clamp(15px,1.15vw,19px);line-height:1.45;letter-spacing:-.005em;max-width:56ch;text-align:justify;-webkit-hyphens:none;hyphens:none} 59.body p:last-child,.body p:only-child{text-align-last:left} 60.body p{margin:0 0 1.2em} 61.body p:last-child{margin-bottom:0} 62.small{font-size:clamp(13px,.95vw,16px);line-height:1.4;letter-spacing:-.005em} 63.grey{color:var(--grey)} 64.b{font-weight:600} 65 66/* ========================================================= 67 DECK (scroll snap) 68 ========================================================= */ 69.deck{height:100vh;height:100dvh;overflow-y:auto;scroll-snap-type:y mandatory;scroll-behavior:smooth;scrollbar-width:none} 70.deck::-webkit-scrollbar{display:none} 71.slide{position:relative;height:100vh;height:100dvh;scroll-snap-align:start;scroll-snap-stop:always;padding:var(--top) var(--gutter);display:flex;flex-direction:column;overflow:hidden} 72.slide.dark{background:var(--ink);color:var(--paper)} 73.slide.lav{background:var(--lavender);color:var(--ink)} 74 75/* fixed chrome: running head + counter. Blend so it survives dark slides */ 76.chrome{position:fixed;inset:0;pointer-events:none;z-index:50;mix-blend-mode:difference;color:#fff} 77.chrome .head{position:absolute;top:var(--top);left:var(--gutter);right:var(--gutter);display:flex;justify-content:space-between;font-size:clamp(13px,1vw,17px);letter-spacing:-.01em;transform:translateY(-6px)} 78.chrome .counter{position:absolute;
78left:50%;transform:translateX(-50%);bottom:calc(var(--top)*.55);font-size:clamp(12px,.9vw,15px);font-variant-numeric:tabular-nums;letter-spacing:-.01em;display:flex;align-items:center;gap:14px} 79.chrome .bar{width:72px;height:1px;background:rgba(255,255,255,.35);position:relative;pointer-events:auto;cursor:pointer} 80.chrome .bar::before{content:"";position:absolute;inset:-10px 0} 81.chrome .bar i{position:absolute;inset:0;background:#fff;transform-origin:left;transform:scaleX(0);transition:transform .6s var(--ease-out)} 82.chrome.hide-head .head{opacity:0} 83 84/* ========================================================= 85 REVEAL PRIMITIVES 86 .mask > .line : text rises out of a clipped box 87 .kw : blur-in with stagger 88 ========================================================= */ 89.mask{display:block;overflow:hidden;padding:.22em 0;margin:-.22em 0} 90.mask>.line{display:block;transform:translateY(135%);transition:transform var(--dur) var(--ease-out);transition-delay:calc(var(--i,0)*90ms)} 91.in .mask>.line{transform:translateY(0)} 92 93.fade{opacity:0;transform:translateY(14px);transition:opacity .9s var(--ease-out),transform .9s var(--ease-out);transition-delay:calc(var(--i,0)*80ms + .35s)} 94.in .fade{opacity:1;transform:none} 95 96.kw{opacity:0;filter:blur(10px);transform:translateY(10px);transition:opacity .8s var(--ease-out),filter .8s var(--ease-out),transform .8s var(--ease-out);transition-delay:calc(var(--i,0)*55ms + .5s)} 97.in .kw{opacity:1;filter:blur(0);transform:none} 98 99/* typing */ 100.type{white-space:pre-wrap} 101.type .cur{display:inline-block;width:.5em;height:.82em;background:currentColor;vertical-align:-.04em;margin-left:.06em;animation:blink 1s steps(1) infinite} 102.type.done .cur{animation:blink 1s steps(1) 3;opacity:0;transition:opacity .2s 3s} 103@keyframes blink{50%{opacity:0}} 104 105/* ========================================================= 106 01 COVER 107 ========================================================= */ 108.cover{justify-content:space-between} 109.cover .top{display:flex;justify-content:space-between;align-items:flex-start} 110.cover .top .langtoggle{margin-top:calc(var(--top)*1.2)} 111.cover .meta{margin-top:calc(var(--top)*1.2)} 112.cover .mark{width:44px;height:22px;margin-bottom:22px} 113.cover .meta .l1{font-size:clamp(14px,1.05vw,18px);letter-spacing:-.01em} 114.cover .meta .l2{font-size:clamp(20px,1.7vw,30px);font-weight:700;letter-spacing:-.03em;line-height:1.2} 115.cover .meta .l3{font-size:clamp(14px,1.05vw,18px);letter-spacing:-.01em} 116.cover .foot{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:clamp(8px,1.2vh,18px);font-size:clamp(13px,1.05vw,18px);letter-spacing:-.01em} 117.cover .roles span{display:inline-block} 118.cover .roles .sl{margin:0 .9em;color:var(--grey)} 119.cover .word{white-space:nowrap;font-weight:700;line-height:.78;margin:0 0 -.06em -.04em;display:flex} 120.cover .word .l{display:inline-block;overflow:hidden;letter-spacing:normal;padding:.12em 0;margin:-.12em -.031em} 121.cover .word .l i{display:inline-block;font-style:normal;transform:translateY(110%);transition:transform 1.25s var(--ease-out);transition-delay:calc(var(--i)*48ms + .15s)} 122.in.cover .word .l i{transform:translateY(0)} 123 124/* ========================================================= 125 02 HELLO 126 ========================================================= */ 127.hello{display:grid;grid-template-columns:1fr 1.25fr;gap:var(--gutter);align-items:center;padding-top:calc(var(--top)*2);padding-bottom:calc(var(--top)*2.4)} 128.hello .pic{aspect-ratio:1/1;max-height:70vh;width:100%;background:var(--lavender);overflow:hidden;clip-path:inset(0 100% 0 0);transition:clip-path 1.3s var(--ease-in-out) .1s} 129.in .hello .pic,.hello.in .pic{clip-path:inset(0 0 0 0)} 130.hello .pic img{width:100%;height:100%;object-fit:cover;display:block;transform:scale(1.08);transition:transform 1.6s var(--ease-out) .1s} 131.in.hello .pic img{transform:scale(1)} 132.hello .txt .h-huge{font-size:clamp(48px,6.2vw,108px)} 133.hello .txt .body{margin-top:clamp(36px,5.5vh,68px);font-size:clamp(14px,1.02vw,17px);max-width:88ch} 134/* Two equal columns, not 1fr + max-content: the Korean language list is far 135 shorter than the English one, so a content-sized second column let the 136 first column swallow the slack and pushed Languages way right in Korean 137 only. Equal halves start Languages at the same x in both languages, and 138 the cap is set so the longest education line still fits on one row. */ 139.hello .info{display:grid;grid-template-columns:1fr 1fr;gap:var(--gutter);margin-top:clamp(44px,7vh,84px);max-width:72ch} 140.hello .info h3{font-weight:700;font-size:clamp(12px,.9vw,15px);letter-spacing:-.01em;text-transform:uppercase;color:var(--grey);margin:0 0 .7em} 141.hello .info ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55em} 142.hello .info li{font-size:clamp(13px,1vw,16px);line-height:1.35;letter-spacing:-.005em} 143.hello .info li b{display:block;font-weight:600} 144.hello .info li span{color:var(--grey)} 145.hello .info ul.langs li{white-space:nowrap} 146.hello .info ul.langs li b{display:inline;font-weight:600} 147 148/* ========================================================= 149 03 WHAT I DO / 04 TOOLS / 05 WORKED 150 ========================================================= */ 151.whatido{padding-top:calc(var(--top)*2.4);justify-content:space-between} 152.whatido .body{margin-top:clamp(28px,4vh,52px);margin-left:clamp(0px,2vw,36px);max-width:clamp(320px,52vw,900px)} 153.whatido .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(22px,4vh,48px) 24px;margin:0 clamp(0px,6vw,120px) clamp(8px,4vh,40px);font-weight:700;font-size:clamp(17px,1.55vw,28px);letter-spacing:-.03em;text-align:center} 154.whatido .grid>span:nth-child(4n+1){text-align:left} 155.whatido .grid>span:nth-child(4n){text-align:right} 156 157.tools{padding-top:calc(var(--top)*2.4)} 158.tools .body{margin-top:clamp(40px,5.5vh,72px);margin-left:clamp(0px,2vw,36px);max-width:64ch;font-size:clamp(16px,1.35vw,23px);line-height:1.4;color:var(--grey)} 159.tools .body b{color:var(--ink);font-weight:700} 160.tools .body p{white-space:nowrap} 161 162.worked{display:grid;grid-template-columns:1fr 1fr;padding-top:calc(var(--top)*2.4);align-items:start} 163.worked .tl{align-self:center;margin-left:clamp(20px,4.5vw,72px);padding-top:.4em;display:grid;grid-template-columns:auto 1fr;gap:clamp(22px,3.5vh,40px) 40px;align-items:baseline;font-size:clamp(15px,1.25vw,22px);letter-spacing:-.02em} 164.worked .tl .y{color:var(--grey);font-size:.85em;letter-spacing:-.01em;font-variant-numeric:tabular-nums;margin-left:clamp(8px,1.8vw,28px)} 165.worked .tl .r b{display:block;font-weight:700} 166.worked .tl .r span{color:var(--grey)} 167 168/* ========================================================= 169 06 CONTENTS 170 ========================================================= */
171.contents{padding-top:calc(var(--top)*2.4);justify-content:space-between} 172.contents ol{list-style:none;margin:0 0 clamp(8px,3vh,32px) 30%;padding:0} 173.contents li{position:relative;display:flex;justify-content:space-between;align-items:baseline;padding:clamp(14px,2.6vh,30px) 0 clamp(10px,1.6vh,18px)} 174.contents li::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1.5px;background:currentColor;transform:scaleX(0);transform-origin:left;transition:transform 1.1s var(--ease-in-out);transition-delay:calc(var(--i)*140ms + .3s)} 175.in .contents li::after,.contents.in li::after{transform:scaleX(1)} 176.contents li .n{font-weight:700;font-size:clamp(18px,1.6vw,28px);letter-spacing:-.03em} 177.contents li a{text-decoration:none;font-weight:500;font-size:clamp(24px,2.6vw,46px);letter-spacing:-.045em;line-height:1;transition:letter-spacing .5s var(--ease-out),color .3s} 178.contents li a:hover{letter-spacing:-.02em;color:var(--blue)} 179 180/* ========================================================= 181 CHAPTER DIVIDER (dark) 182 ========================================================= */ 183.chapter{justify-content:flex-end} 184.chapter .h-chapter{margin-left:-.03em;margin-bottom:-.02em} 185.chapter .arrow{position:absolute;right:var(--gutter);top:50%;transform:translateY(-50%);width:clamp(120px,15vw,240px);pointer-events:auto} 186.chapter .arrow path{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:square;stroke-dasharray:600;stroke-dashoffset:600;transition:stroke-dashoffset 1.4s var(--ease-in-out) .55s} 187.in .chapter .arrow path,.chapter.in .arrow path{stroke-dashoffset:0} 188.chapter .arrow:hover{animation:nudge 1.1s var(--ease-in-out) infinite} 189@keyframes nudge{50%{transform:translate(12px,-50%)}} 190 191/* ========================================================= 192 STATS (lavender) + CHART 193 ========================================================= */ 194.stats{padding-top:calc(var(--top)*2.4);justify-content:space-between} 195.stats .lead{display:grid;grid-template-columns:1fr 1fr;gap:var(--gutter);align-items:end} 196/* right column: the language toggle rides at the top of the row, the copy 197 stays pinned to the bottom next to the headline */ 198.stats .leadright{align-self:stretch;display:flex;flex-direction:column;justify-content:space-between;align-items:flex-end;gap:clamp(14px,2.6vh,26px)} 199.stats .leadright .body{align-self:stretch} 200.stats .h-caps{color:var(--ink);font-size:clamp(48px,6.4vw,108px);position:relative;top:calc(-1 * clamp(20px,4.5vh,52px))} 201/* copy sits a little below the headline's baseline; nudged visually so the row 202 height (and the headline position) doesn't change */ 203.stats .lead .body{color:#2a2a2a;max-width:60ch;margin-bottom:.3em;position:relative;top:clamp(50px,10.5vh,124px);left:calc(-1 * clamp(20px,3.2vw,60px));text-align:justify;text-justify:inter-word} 204/* Korean has far fewer spaces per line, so justifying it opens big gaps, and 205 the default break rule splits words mid-phrase */ 206html[lang="ko"] .stats .lead .body,html[lang="ko"] .stats .num .k{text-align:left;word-break:keep-all} 207html[lang="ko"] .stats .h-caps{font-weight:800} 208.stats .nums{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;margin-bottom:clamp(8px,3vh,32px)} 209.stats .nums.three{grid-template-columns:repeat(3,1fr);gap:var(--gutter)} 210.stats .nums.three .num{display:flex;flex-direction:column} 211.stats .nums.three .num .k{max-width:40ch} 212/* grey label sits under the figure with some air (clear of the comma's 213 descender in 23,000), the copy follows */ 214.stats .nums.three .num .v{font-size:clamp(51px,6.56vw,115px);color:var(--ink)} 215.stats .nums.three .num .src{margin-top:2.2em} 216.stats .num .v{font-weight:700;letter-spacing:-.06em;line-height:.85;font-size:clamp(56px,6.8vw,120px);color:var(--blue);font-variant-numeric:tabular-nums} 217.stats .num .v small{font-size:.55em;letter-spacing:-.04em} 218.stats .num .k{margin-top:.6em;max-width:22ch;font-size:clamp(13px,1vw,17px);line-height:1.35;letter-spacing:-.01em} 219.stats .num .src{color:var(--grey);font-size:clamp(12px,.85vw,14px);margin-top:.35em} 220 221.chart{padding-top:calc(var(--top)*2.4);justify-content:space-between} 222.chart .top{display:flex;justify-content:space-between;align-items:flex-end;
222gap:var(--gutter)} 223.chart .toggle{display:inline-flex;border:1.5px solid var(--ink);border-radius:999px;padding:3px;gap:2px} 224.chart .toggle button{padding:.55em 1.3em;border-radius:999px;font-weight:600;font-size:clamp(13px,1vw,16px);letter-spacing:-.01em;transition:background .35s,color .35s} 225.chart .toggle button[aria-pressed="true"]{background:var(--ink);color:#fff} 226.chart .note{color:var(--grey);font-size:clamp(12px,.9vw,15px);max-width:44ch;line-height:1.4} 227.chart .plot{display:grid;grid-template-columns:repeat(7,1fr);gap:clamp(12px,1.6vw,28px);align-items:end;height:min(46vh,480px);margin-bottom:clamp(8px,3vh,28px);border-bottom:1.5px solid var(--ink);padding-bottom:0} 228.chart .col{display:flex;flex-direction:column;justify-content:flex-end;height:100%;position:relative} 229.chart .col .bar{position:relative;width:100%;background:var(--ink);transform-origin:bottom;transform:scaleY(0);transition:transform 1.2s var(--ease-in-out) calc(var(--i)*70ms),height 1.1s var(--ease-in-out),background .5s} 230.in .chart .col .bar,.chart.in .col .bar{transform:scaleY(1)} 231.chart .col .bar.after{background:var(--blue)} 232.chart .col .val{position:absolute;left:0;bottom:calc(100% + 8px);font-weight:700;letter-spacing:-.03em;font-size:clamp(18px,1.6vw,28px);font-variant-numeric:tabular-nums;transition:bottom 1.2s var(--ease-in-out)} 233.chart .col .lab{position:absolute;top:calc(100% + 10px);left:0;font-size:clamp(11px,.85vw,14px);line-height:1.3;letter-spacing:-.01em;max-width:100%} 234.chart .col .lab b{display:block;font-weight:600} 235.chart .col .lab span{color:var(--grey)} 236.chart .plot{margin-bottom:clamp(58px,9vh,90px)} 237 238/* ========================================================= 239 PROJECT LIST 240 ========================================================= */ 241.projects{padding-top:calc(var(--top)*2.4);justify-content:flex-start} 242.projects .h-huge{font-size:clamp(40px,5vw,84px)} 243.projects .list{margin-top:clamp(22px,3.5vh,40px);display:grid;grid-template-columns:1fr 1fr;column-gap:var(--gutter);overflow:auto;scrollbar-width:none;padding-bottom:var(--top)} 244.projects .list::-webkit-scrollbar{display:none} 245.projects .list.single{grid-template-columns:1fr;max-width:70%} 246.projects .item{display:grid;grid-template-columns:2.2em 1fr auto;gap:0 14px;align-items:baseline;padding:clamp(9px,1.3vh,14px) 0;border-bottom:1px solid rgba(0,0,0,.18);text-align:left;width:100%;transition:padding-left .45s var(--ease-out)} 247.projects .item:hover{padding-left:10px} 248.projects .item .n{color:var(--grey);font-variant-numeric:tabular-nums;font-size:clamp(12px,.9vw,15px)} 249.projects .item .t{font-weight:600;font-size:clamp(14px,1.15vw,20px);letter-spacing:-.025em;line-height:1.15} 250.projects .item .c{color:var(--grey);font-size:clamp(11px,.85vw,14px);white-space:nowrap} 251.projects .item:hover .t{color:var(--blue)} 252 253/* AI slide */ 254.ai{padding-top:calc(var(--top)*2.4);display:grid;grid-template-columns:1.1fr 1fr;gap:var(--gutter);align-items:stretch} 255.ai .q{margin-top:clamp(20px,3.2vh,40px);font-weight:600;font-size:clamp(20px,2vw,34px);letter-spacing:-.035em;line-height:1.1;max-width:none;white-space:nowrap} 256.ai .ins{margin-top:1em;color:var(--grey);max-width:78ch;font-size:clamp(15px,1.15vw,19px);line-height:1.45} 257.ai .right{padding-top:.4em;display:flex;flex-direction:column;align-items:flex-end} 258.ai .right>*{width:100%} 259.ai .aiuse{margin-top:clamp(70px,14vh,150px)} 260.ai .open{align-self:flex-end;width:auto} 261.ai .right .k{font-weight:600;font-size:clamp(13px,1vw,17px)} 262.ai .right .v{color:var(--grey);margin-top:.2em;line-height:1.4;font-size:clamp(13px,1.05vw,18px)} 263.ai .right>div{margin-bottom:clamp(14px,2.4vh,28px)} 264.ai .open{margin-top:clamp(20px,3vh,36px);display:inline-flex;align-items:center;gap:16px;font-weight:600;font-size:clamp(20px,1.54vw,27px);border-bottom:1.5px solid currentColor;padding-bottom:4px} 265.ai .open svg{width:28px;height:28px;transition:transform .5s var(--ease-out)} 266.ai .open:hover svg{transform:translateX(6px)} 267.ai .lab{color:var(--grey);font-size:clamp(12px,.95vw,15px);margin-top:clamp(14px,2.2vh,24px)} 268.ai .right .topright{margin-bottom:clamp(18px,3vh,38px)} 269.ai .aiuse{margin-bottom:clamp(22px,4vh,44px)} 270/* the AI-notes heading is set like the concept line (.q) opposite it, and its 271 list starts where .ins does (1em of the body size below the heading) */ 272.ai .aiuse h4{margin:0;font-weight:600;font-size:clamp(20px,2vw,34px);letter-spacing:-.035em;line-height:1.1} 273.ai .aiuse ul{list-style:none;margin:clamp(12px,.92vw,15px) 0 0;padding:0;display:flex;flex-direction:column;gap:.7em} 274/* desktop: both columns share one grid so the AI-notes heading sits on the 275 same row as the concept line, whatever the title above it measures */ 276@media (min-width:901px){ 277 .ai{grid-template-areas:"h2 top" "lab ." "q aih" "ins ail" ". open";align-content:start;align-items:start;row-gap:0} 278 .ai .left,.ai .right{display:contents} 279 .ai h2{grid-area:h2} 280 .ai .lab{grid-area:lab} 281 .ai .q{grid-area:q} 282 .ai .ins{grid-area:ins} 283 .ai .right .topright{grid-area:top;justify-self:end;width:auto;margin:0} 284 /* heading on the concept line's row, list on the .ins row; the box is 285 dissolved, so its children take the .fade state from it via inherit */ 286 .ai .aiuse{display:contents} 287 .ai .aiuse>h4{grid-area:aih;margin-top:clamp(20px,3.2vh,40px);opacity:inherit;transform:inherit} 288 .ai .aiuse>ul{grid-area:ail;opacity:inherit;transform:inherit} 289 .ai .open{grid-area:open;justify-self:end} 290} 291.ai .aiuse li{position:relative;padding-left:1.1em;color:var(--grey);font-size:clamp(15px,1.15vw,19px);line-height:1.45} 292.ai .aiuse li::before{content:"";position:absolute;
292left:0;top:.6em;width:5px;height:5px;border-radius:50%;background:var(--ink)} 293html[lang="ko"] .ai .ins,html[lang="ko"] .ai .aiuse li{word-break:keep-all} 294 295/* ========================================================= 296 CONTACT (dark) 297 ========================================================= */ 298.contact{display:grid;grid-template-columns:1fr 1fr;gap:var(--gutter);padding-right:0;padding-bottom:0;padding-top:calc(var(--top)*2.4)} 299.contact .left{display:flex;flex-direction:column;justify-content:space-between;padding-bottom:var(--top)} 300.contact .links{margin-top:clamp(28px,6vh,72px);display:flex;flex-direction:column;gap:clamp(14px,2.5vh,28px);font-size:clamp(22px,2.6vw,46px);letter-spacing:-.04em;font-weight:400} 301.contact .links a{text-decoration:none;position:relative;width:max-content;padding-bottom:.08em} 302.contact .links a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:currentColor;transform:scaleX(0);transform-origin:left;transition:transform .9s var(--ease-in-out);transition-delay:calc(var(--i)*150ms + .6s)} 303.in .contact .links a::after,.contact.in .links a::after{transform:scaleX(1)} 304.contact .social{font-size:clamp(13px,1vw,17px)} 305.contact .social b{display:block;margin-bottom:.9em;font-weight:700} 306.contact .social a{margin-right:2.4em} 307.contact .right{background:var(--ink);position:relative;overflow:hidden;clip-path:inset(0 0 0 100%);transition:clip-path 1.4s var(--ease-in-out) .2s} 308.in.contact .right{clip-path:inset(0)} 309.contact .right img{width:100%;height:100%;object-fit:cover;display:block;filter:grayscale(1)} 310 311/* ========================================================= 312 DETAIL OVERLAY 313 ========================================================= */ 314.detail{position:fixed;inset:0;z-index:100;background:var(--paper);color:var(--ink);transform:translateY(100%);transition:transform .9s var(--ease-in-out);overflow:hidden;visibility:hidden} 315.detail.open{transform:none;visibility:visible} 316.detail .scroll{height:100%;overflow-y:auto;padding:var(--top) var(--gutter) calc(var(--top)*2)} 317.detail .bar{display:flex;justify-content:space-between;align-items:center;font-size:clamp(13px,1vw,17px);margin-bottom:clamp(36px,7vh,80px)} 318.detail .close{display:inline-flex;align-items:center;gap:10px;font-weight:600} 319.detail .close svg{width:18px;height:18px} 320.detail article{max-width:1480px;margin:0 auto} 321.detail .h-huge{font-size:clamp(40px,5.4vw,92px);margin-bottom:.4em;max-width:18ch} 322.detail .metarow{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:clamp(16px,2vw,32px);border-top:1.5px solid var(--ink);border-bottom:1px solid rgba(0,0,0,.15);padding:clamp(14px,2vh,22px) 0;font-size:clamp(13px,.98vw,16px);line-height:1.4;letter-spacing:-.01em} 323.detail .metarow .k{color:var(--grey);display:block;margin-bottom:.3em;font-size:.9em} 324.detail .sec{display:grid;grid-template-columns:minmax(160px,1fr) minmax(0,3.2fr);gap:var(--gutter);padding:clamp(28px,4.5vh,52px) 0;border-bottom:1px solid rgba(0,0,0,.15)} 325.detail .sec:last-child{border-bottom:0} 326.detail h3{font-weight:700;letter-spacing:-.03em;font-size:clamp(20px,1.7vw,30px);margin:0;line-height:1.05} 327.detail .body{max-width:88ch;font-size:clamp(15px,1.12vw,19px);line-height:1.5} 328.detail .carousel{margin:clamp(24px,4vh,44px) 0 clamp(8px,1.5vh,16px);position:relative} 329.detail .fandeck{display:flex;align-items:flex-end;overflow-x:auto;overflow-y:hidden;overflow-anchor:none;scrollbar-width:none;-ms-overflow-style:none;padding:clamp(46px,8vh,84px) clamp(16px,5vw,72px) clamp(30px,5vh,52px);cursor:grab;touch-action:pan-y;scroll-behavior:smooth;background:transparent} 330.detail .fandeck::-webkit-scrollbar{display:none} 331.detail .fandeck.drag{cursor:grabbing;scroll-behavior:auto} 332.detail .card{flex:0 0 auto;position:relative;width:clamp(180px,23vw,300px);height:min(60vh,420px);margin-left:clamp(-136px,-10vw,-64px);border-radius:16px;overflow:hidden;background:#fff;box-shadow:0 16px 36px rgba(0,0,0,.16),0 2px 6px rgba(0,0,0,.10);transform-origin:50% 100%;transform:rotate(var(--r,0deg)) translateY(var(--y,0px));transition:transform .55s var(--ease-out),box-shadow .55s var(--ease-out),width .55s var(--ease-out);z-index:var(--z,1);will-change:transform} 333.detail .card:first-child{margin-left:0} 334.detail .card img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none;user-select:none} 335.detail .card iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block} 336.detail .card.vid{background:var(--ink)} 337.detail .card.vid .play{position:absolute;
337left:50%;top:50%;transform:translate(-50%,-50%);width:52px;height:52px;border-radius:50%;background:rgba(255,255,255,.92);display:flex;align-items:center;justify-content:center;pointer-events:none;transition:transform .5s var(--ease-out)} 338.detail .card.vid .play svg{width:18px;height:18px;margin-left:2px;fill:var(--ink)} 339.detail .card:hover{width:clamp(260px,36vw,520px);transform:rotate(0deg) translateY(-30px) scale(1.03);box-shadow:0 44px 84px rgba(0,0,0,.30),0 4px 12px rgba(0,0,0,.12);z-index:30} 340.detail .card:hover img{object-fit:contain} 341.detail .card:hover.vid .play{transform:translate(-50%,-50%) scale(1.12)} 342.detail .card.playing,.detail .card.playing:hover{width:clamp(300px,46vw,620px);transform:rotate(0deg) translateY(-30px);z-index:40;cursor:default} 343.detail .card.playing img,.detail .card.playing .play{display:none} 344.detail .fandeck.drag .card{pointer-events:none} 345@media (hover:none){.detail .card{width:clamp(200px,52vw,300px);margin-left:clamp(-90px,-14vw,-40px)}} 346.detail .cap{margin:.6em 0 0;color:var(--grey);font-size:clamp(12px,.9vw,14px);min-height:1.2em} 347.detail .qs{margin:1.2em 0 0;padding:0;list-style:none} 348.detail .qs li{padding:1em 0;border-top:1px solid rgba(0,0,0,.15)} 349.detail .qs li b{display:block;font-weight:600;font-size:1.1em;letter-spacing:-.02em;margin-bottom:.3em} 350.detail .qs li span{color:var(--grey)} 351/* stagger content inside detail */ 352.detail .rev{opacity:0;transform:translateY(24px);transition:opacity .9s var(--ease-out),transform .9s var(--ease-out)} 353.detail.open .rev{opacity:1;transform:none} 354.detail.open .rev:nth-child(1){transition-delay:.55s} 355.detail.open .rev:nth-child(2){transition-delay:.65s} 356.detail.open .rev:nth-child(3){transition-delay:.75s} 357.detail.open #dBody .rev{transition-delay:.85s} 358 359/* ========================================================= 360 VIZ SLIDES (v2): User behaviour + Campaign effectiveness 361 ========================================================= */ 362.viz{padding-top:calc(var(--top)*2.2);display:grid;grid-template-rows:auto 1fr auto;gap:clamp(14px,2.4vh,28px)} 363.viz .top{display:flex;justify-content:space-between;align-items:flex-end;gap:var(--gutter);min-height:12.15em} 364.viz .top .h-huge{font-size:clamp(38px,4.6vw,78px)} 365.viz .sub{margin-top:1.4em;font-size:clamp(13px,1vw,17px);color:var(--grey);letter-spacing:-.01em;max-width:60ch;line-height:1.4} 366.viz .sub b{color:var(--ink);font-weight:600} 367.viz .formula{margin:0;font-size:clamp(10px,.8vw,12px);color:var(--grey);letter-spacing:-.01em} 368.viz .formula i{font-style:normal;color:var(--grey)} 369.chartcol{display:flex;flex-direction:column;min-height:0;gap:clamp(10px,1.6vh,18px)} 370.chartcol .groups{flex:1;min-height:0} 371.viz .foot .footnote{display:flex;flex-direction:column;gap:.4em} 372.viz .seg{display:inline-flex;border:1.5px solid var(--ink);border-radius:999px;padding:3px;gap:2px;flex-wrap:wrap} 373.viz .seg button{padding:.5em 1.1em;border-radius:999px;font-weight:600;font-size:clamp(12px,.95vw,15px);letter-spacing:-.01em;transition:background .35s,color .35s;white-space:nowrap} 374.viz .seg button[aria-pressed="true"]{background:var(--ink);color:#fff} 375.viz .ctrls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end} 376.viz .stage{display:grid;grid-template-columns:1.35fr 1fr;gap:var(--gutter);min-height:0} 377.viz .foot{display:flex;justify-content:space-between;align-items:flex-end;gap:var(--gutter);font-size:clamp(11px,.85vw,14px);color:var(--grey);line-height:1.35;min-height:6.3em} 378.viz .foot .disc{max-width:52ch} 379.viz .foot .disc b{color:var(--ink);font-weight:600} 380.viz .foot .disc .nowrap{white-space:nowrap} 381 382/* horizontal bars (eROAS) */ 383.hbars{display:flex;flex-direction:column;justify-content:center;gap:clamp(10px,2vh,22px)} 384.hbar{display:grid;grid-template-columns:minmax(120px,.34fr) 1fr 4.2em;align-items:center;gap:14px;text-align:left;padding:2px 0;border-radius:4px} 385.hbar .l{font-weight:600;font-size:clamp(13px,1.05vw,17px);letter-spacing:-.02em;line-height:1.15} 386.hbar .l span{display:block;color:var(--grey);font-weight:400;font-size:.8em;margin-top:.15em} 387.hbar .t{position:relative;height:clamp(28px,4.6vh,48px);background:rgba(0,0,0,.06)} 388.hbar .t i{position:absolute;
388left:0;top:0;bottom:0;background:var(--ink);transform-origin:left;transform:scaleX(0);transition:transform 1s var(--ease-in-out),background .35s;transition-delay:calc(var(--i)*60ms)} 389.in .hbar .t i{transform:scaleX(1)} 390.hbar .t em{position:absolute;left:0;right:0;top:0;bottom:0;border-left:1.5px dashed var(--ink);width:0;pointer-events:none} 391.hbar .v{font-weight:700;font-variant-numeric:tabular-nums;font-size:clamp(15px,1.3vw,22px);letter-spacing:-.03em;text-align:right} 392.hbar.sel .t i,.hbar:hover .t i{background:var(--blue)} 393.hbar.dim .t i{background:rgba(0,0,0,.28)} 394.hbar.sel .v{color:var(--blue)} 395.hbars .axis{display:flex;justify-content:space-between;color:var(--grey);font-size:clamp(11px,.8vw,13px);margin-left:calc(minmax(120px,.34fr) + 14px);padding-left:0} 396.hbars .axis span:first-child{margin-left:auto;margin-right:auto} 397 398/* right panel: allocation */ 399.alloc{display:flex;flex-direction:column;justify-content:center;gap:clamp(12px,2vh,22px)} 400.alloc h4{margin:0;font-weight:600;font-size:clamp(13px,1vw,17px);letter-spacing:-.02em} 401.alloc h4 span{color:var(--grey);font-weight:400} 402.alloc .row{display:grid;grid-template-columns:4.6em 1fr;gap:12px;align-items:center;font-size:clamp(12px,.9vw,14px);color:var(--grey)} 403.alloc .stack{display:flex;height:clamp(24px,3.6vh,38px);width:100%;overflow:hidden} 404.alloc .stack i{display:block;height:100%;background:var(--ink);transition:flex-basis 1s var(--ease-in-out),background .35s;flex:0 0 0%;position:relative;box-shadow:inset -1px 0 0 #fff} 405.alloc .stack i.sel{background:var(--blue)} 406.alloc .stack i.o1{background:rgba(0,0,0,.72)}.alloc .stack i.o2{background:rgba(0,0,0,.5)}.alloc .stack i.o3{background:rgba(0,0,0,.32)}.alloc .stack i.o4{background:rgba(0,0,0,.18)} 407.alloc .delta{font-weight:700;font-size:clamp(26px,3vw,52px);letter-spacing:-.05em;line-height:.95;color:var(--blue);font-variant-numeric:tabular-nums} 408.alloc .delta small{display:block;font-size:clamp(12px,.9vw,15px);letter-spacing:-.01em;color:var(--ink);font-weight:400;margin-top:.5em;max-width:34ch;line-height:1.35} 409.alloc .cards{display:grid;grid-template-columns:1fr 1fr;gap:16px;border-top:1.5px solid var(--ink);padding-top:14px} 410.alloc .cards div{font-size:clamp(12px,.9vw,14px);line-height:1.4} 411.alloc .cards b{display:block;font-weight:600;letter-spacing:-.02em;font-size:1.05em;margin-bottom:.25em} 412.alloc .cards span{color:var(--grey)} 413 414/* LTV chart */ 415.ltv{position:relative;min-height:0} 416.ltv svg{width:100%;height:100%;display:block;overflow:visible} 417.ltv .grid line{stroke:rgba(0,0,0,.12);stroke-width:1} 418.ltv .be{stroke:var(--ink);stroke-width:1.5;stroke-dasharray:4 5} 419.ltv .p-total{fill:none;stroke:var(--blue);stroke-width:3;stroke-linejoin:round} 420.ltv .p-iap{fill:none;stroke:var(--ink);stroke-width:1.5;stroke-linejoin:round} 421.ltv .p-iaa{fill:none;stroke:rgba(0,0,0,.4);stroke-width:1.5;stroke-linejoin:round} 422.ltv .p-total,.ltv .p-iap,.ltv .p-iaa{stroke-dasharray:2000;stroke-dashoffset:2000;transition:stroke-dashoffset 1.6s var(--ease-in-out) .3s,d .6s var(--ease-out)} 423.in .ltv .p-total,.in .ltv .p-iap,.in .ltv .p-iaa{stroke-dashoffset:0} 424.ltv .win{stroke:var(--blue);stroke-width:1.5} 425.ltv .wdot{fill:var(--blue)} 426.ltv text{font-family:var(--font);font-size:12px;fill:var(--grey);letter-spacing:-.01em} 427.ltv text.lbl{fill:var(--ink);font-weight:600} 428.ltv .legend{position:absolute;left:0;top:0;display:flex;gap:18px;font-size:clamp(11px,.85vw,14px);color:var(--grey)} 429.ltv .legend i{display:inline-block;width:18px;height:3px;vertical-align:middle;margin-right:6px;background:var(--blue)} 430.ltv .legend i.k{background:var(--ink);height:1.5px}.ltv .legend i.g{background:rgba(0,0,0,.4);height:1.5px} 431 432/* readout table (mirrors the KPI sheet) */ 433.readout{display:flex;flex-direction:column;justify-content:center;gap:clamp(10px,1.8vh,18px)} 434.readout .slider{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;font-size:clamp(12px,.95vw,15px)} 435.readout .slider b{font-variant-numeric:tabular-nums;font-weight:700;font-size:1.2em;letter-spacing:-.03em} 436.readout input[type=range]{width:100%;-webkit-appearance:none;appearance:none;height:2px;background:var(--ink);outline:none;margin:10px 0} 437.readout input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:var(--blue);border:2px solid #fff;box-shadow:0 0 0 1.5px var(--blue);cursor:pointer} 438.readout input[type=range]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:var(--blue);border:2px solid #fff;box-shadow:0 0 0 1.5px var(--blue);cursor:pointer} 439.readout table{width:100%;border-collapse:collapse;font-size:clamp(12px,.95vw,15px);letter-spacing:-.01em} 440.readout th,.readout td{text-align:left;padding:.55em 0;border-bottom:1px solid rgba(0,0,0,.14);vertical-align:baseline} 441.readout th{font-weight:400;color:var(--grey)} 442.readout td{text-align:right;font-weight:600;font-variant-numeric:tabular-nums} 443.readout td.big{font-size:1.5em;letter-spacing:-.04em;font-weight:700;color:var(--blue)} 444.readout tr.goal td{color:var(--ink)} 445.readout .verdict{font-size:clamp(12px,.95vw,15px);line-height:1.4} 446.readout .verdict b{color:var(--blue)} 447.viz .results{display:flex;gap:clamp(24px,4vw,64px)} 448.viz .results .num{display:flex;align-items:baseline;gap:10px} 449.viz .results .v{font-weight:700;letter-spacing:-.05em;font-size:clamp(28px,3.2vw,56px);line-height:1;font-variant-numeric:tabular-nums} 450.viz .results .k{font-size:clamp(11px,.85vw,14px);color:var(--grey);max-width:16ch;line-height:1.25} 451 452/* consolidated case list */ 453.projects .list.cases{grid-template-columns:1fr;max-width:none;margin-top:clamp(18px,3vh,36px)} 454.projects .list.cases .item{grid-template-columns:2.4em 1fr auto;align-items:center;padding:clamp(14px,2.6vh,28px) 0;border-bottom:1.5px solid var(--ink)} 455.projects .list.cases .item .t{font-weight:600;font-size:clamp(20px,2vw,34px);letter-spacing:-.04em;line-height:1.05} 456.projects .list.cases .item .t small{display:block;font-weight:400;color:var(--grey);font-size:clamp(12px,.95vw,16px);letter-spacing:-.01em;margin-top:.35em;line-height:1.3} 457.projects .list.cases .item .m{text-align:right} 458.projects .list.cases .item .m b{display:block;font-weight:700;font-size:clamp(34px,4.2vw,76px);letter-spacing:-.06em;line-height:.9;color:var(--blue);font-variant-numeric:tabular-nums}
459.projects .list.cases .item .m span{display:block;color:var(--grey);font-size:clamp(11px,.85vw,14px);max-width:28ch;margin-top:.45em;line-height:1.3;margin-left:auto} 460 461@media (max-width:900px){ 462 .viz .stage{grid-template-columns:1fr} 463 .viz .top{flex-direction:column;align-items:flex-start} 464 .projects .list.cases .item .m{display:none} 465} 466 467/* v2.1: grouped eROAS + insights, draggable LTV point */ 468.viz .hint{font-size:clamp(12px,.95vw,15px);color:var(--grey);white-space:nowrap;margin:0 0 .3em} 469.groups{display:grid;grid-template-columns:1fr 1fr;gap:clamp(20px,3vw,48px);align-content:center} 470.group h4{margin:0 0 clamp(10px,1.6vh,18px);font-weight:700;font-size:clamp(16px,1.4vw,24px);letter-spacing:-.03em;line-height:1.1;display:flex;justify-content:space-between;align-items:baseline;border-bottom:1.5px solid var(--ink);padding-bottom:.45em} 471.group h4 span{font-weight:400;color:var(--grey);font-size:.7em;letter-spacing:-.01em} 472.group .hbar{grid-template-columns:minmax(96px,.42fr) 1fr 3.6em} 473.group .hbars{gap:clamp(10px,1.8vh,20px)} 474.group .hbar.best .t i{background:var(--blue)} 475.group .hbar.best .v{color:var(--blue)} 476.insights{display:flex;flex-direction:column;justify-content:center;gap:clamp(16px,3vh,32px)} 477.insights ol{margin:0;padding:0;list-style:none;counter-reset:ins} 478.insights li{counter-increment:ins;display:grid;grid-template-columns:2em 1fr;gap:10px;padding:clamp(8px,1.3vh,14px) 0;border-top:1px solid rgba(0,0,0,.14);font-size:clamp(13px,1.02vw,17px);line-height:1.4;letter-spacing:-.01em} 479.insights li::before{content:counter(ins,decimal-leading-zero);color:var(--grey);font-variant-numeric:tabular-nums;font-size:.85em;padding-top:.15em} 480.insights li b{font-weight:600} 481.viz .big b{display:block;font-weight:700;font-size:clamp(56px,6.8vw,120px);letter-spacing:-.06em;line-height:.85;color:var(--blue);font-variant-numeric:tabular-nums} 482.viz .big>span{display:block;margin-top:.7em;font-size:clamp(12px,.95vw,15px);line-height:1.35;max-width:30ch;letter-spacing:-.01em} 483.readout .bigs{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:clamp(8px,1.6vh,16px)} 484.readout .bigs .big b{font-size:clamp(40px,4.4vw,80px)} 485.ltv svg{cursor:ew-resize;touch-action:none} 486.ltv .wdot{fill:var(--blue);stroke:#fff;stroke-width:3;cursor:grab} 487.ltv .wdot.drag{cursor:grabbing} 488.ltv .hit{fill:transparent} 489 490/* ========================================================= 491 v3: Data â Insight â Result stepper slides 492 ========================================================= */ 493.stepslide .top{align-items:flex-start} 494.topright{display:flex;flex-direction:column;align-items:flex-end;gap:28px;flex:0 0 auto} 495.readcase{display:inline-flex;align-items:center;gap:10px;font-weight:600;font-size:clamp(13px,1vw,16px);letter-spacing:-.01em;border-bottom:1.5px solid currentColor;padding-bottom:3px} 496.readcase svg{width:18px;height:18px;transition:transform .5s var(--ease-out)} 497.readcase:hover svg{transform:translateX(5px)} 498.stepper{display:inline-flex;border:1.5px solid var(--ink);border-radius:999px;padding:3px;gap:2px} 499.stepper button{display:inline-flex;align-items:baseline;gap:8px;padding:.5em 1.15em;border-radius:999px;font-weight:600;font-size:clamp(12px,.95vw,15px);letter-spacing:-.01em;transition:background .35s,color .35s} 500.stepper button i{font-style:normal;font-weight:400;font-size:.8em;opacity:.55;font-variant-numeric:tabular-nums} 501.stepper button[aria-selected="true"]{background:var(--ink);color:#fff} 502.stepslide .steps{position:relative;min-height:0;cursor:pointer} 503.stepslide .step{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;opacity:0;visibility:hidden;transform:translateY(22px);transition:opacity .55s var(--ease-out),transform .55s var(--ease-out),visibility 0s .55s} 504.stepslide .step.on{opacity:1;visibility:visible;transform:none;transition:opacity .7s var(--ease-out) .12s,transform .7s var(--ease-out) .12s,visibility 0s} 505.stepslide .step .mask>.line{transform:translateY(135%)} 506.stepslide .step.on .mask>.line{transform:none} 507.stepslide .step .fade{opacity:0;transform:translateY(14px)} 508.stepslide .step.on .fade{opacity:1;transform:none}
509.stepslide .step .hbar .t i{transform:scaleX(0)} 510.stepslide .step.on .hbar .t i{transform:scaleX(1)} 511.stepslide .step .p-total,.stepslide .step .p-iap,.stepslide .step .p-iaa{stroke-dashoffset:2000} 512.stepslide .step.on .p-total,.stepslide .step.on .p-iap,.stepslide .step.on .p-iaa{stroke-dashoffset:0} 513.stepslide .chartcol{height:100%;justify-content:center} 514.stepslide .chartcol .ctrls{justify-content:flex-start} 515.stephint{color:var(--grey);font-size:clamp(11px,.85vw,14px);margin:0;white-space:nowrap} 516.stepslide .foot{align-items:flex-end} 517 518/* insight step */ 519.insight-grid{display:grid;grid-template-columns:1.25fr 1fr;gap:calc(var(--gutter)*1.2);align-items:center;height:100%;min-height:0} 520.insight-grid.ce{grid-template-columns:1.25fr 1fr;align-items:center} 521.insight-grid.ce .ltv{height:100%;min-height:0;display:flex;flex-direction:column} 522.insight-grid.ce .ltv svg{flex:1;min-height:0} 523.ltv .legend{position:static;margin-top:8px;justify-content:center} 524.ltv .legend .drag{color:var(--grey)} 525.readout .lead{margin:0 0 .6em;font-size:clamp(13px,1.02vw,17px);line-height:1.4;letter-spacing:-.01em} 526 527/* result step */ 528.result{display:flex;flex-direction:column;justify-content:center;height:100%} 529.result .giant{font-weight:700;letter-spacing:-.07em;line-height:.82;font-size:clamp(120px,15vw,280px);color:var(--blue);font-variant-numeric:tabular-nums;margin-left:-.04em} 530.result .giant .mask{padding:.1em 0;margin:-.1em 0} 531.result .rline{margin:.4em 0 0;font-weight:600;font-size:clamp(18px,1.8vw,32px);letter-spacing:-.035em;line-height:1.1} 532.result .rsub{margin:1.2em 0 0;max-width:54ch;font-size:clamp(13px,1.05vw,18px);line-height:1.45;color:var(--grey);letter-spacing:-.01em} 533.result.two{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto auto;gap:0 var(--gutter);align-content:center} 534.result.two .giant{font-size:clamp(96px,11.5vw,220px)} 535.result.two .rsub{grid-column:1/-1} 536 537/* daily board (data step, campaign effectiveness) */ 538.board{display:flex;flex-direction:column;gap:clamp(10px,1.8vh,20px);height:100%;justify-content:center;min-height:0} 539.chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center} 540.chips .lab{font-size:clamp(11px,.85vw,13px);color:var(--grey);margin-right:6px;white-space:nowrap} 541.chips .lab:not(:first-child){margin-left:14px} 542.chips span.c{font-size:clamp(10px,.8vw,12px);font-weight:600;letter-spacing:-.01em;padding:.25em .6em;border:1px solid rgba(0,0,0,.25);border-radius:4px;white-space:nowrap;opacity:0;transform:translateY(6px);transition:opacity .5s var(--ease-out),transform .5s var(--ease-out);transition-delay:calc(var(--i)*22ms + .2s)} 543.stepslide .step.on .chips span.c{opacity:1;transform:none} 544.boardgrid{display:grid;grid-template-columns:1fr;min-height:0;flex:1} 545.daily{min-height:0;display:flex;flex-direction:column;gap:8px} 546.daily .legend{display:flex;gap:18px;font-size:clamp(11px,.85vw,14px);color:var(--grey);align-items:center;justify-content:center} 547.daily .legend i{display:inline-block;width:14px;height:11px;vertical-align:middle;margin-right:6px;border-radius:2px} 548.daily .legend i.pillar{background:rgba(0,0,0,.14)} 549.daily .legend i.line{height:2px;background:var(--ink)} 550.daily svg{width:100%;height:100%;flex:1;min-height:0;overflow:visible;cursor:crosshair} 551.daily text{font-family:var(--font);font-size:12px;fill:var(--grey)} 552.daily .bar{fill:rgba(0,0,0,.14);transform-origin:bottom;transform:scaleY(0);transition:transform .9s var(--ease-in-out),fill .3s;transition-delay:calc(var(--i)*70ms + .25s)} 553.stepslide .step.on .daily .bar{transform:scaleY(1)} 554.daily .bar.hot{fill:rgba(0,0,0,.3)} 555.daily .ln{fill:none;stroke-width:2;stroke-linejoin:round;stroke-dasharray:1200;stroke-dashoffset:1200;transition:stroke-dashoffset 1.5s var(--ease-in-out) .5s} 556.stepslide .step.on .daily .ln{stroke-dashoffset:0} 557.daily .goal{stroke:var(--ink);stroke-width:1.5;stroke-dasharray:4 5} 558.daily .cursor{stroke:var(--blue);stroke-width:1.5;opacity:0}
558 559.daily .cursor.show{opacity:1} 560.daily .dot{fill:#fff;stroke-width:2.5;opacity:0} 561.daily .dot.show{opacity:1} 562@media (max-width:900px){.insight-grid,.insight-grid.ce,.boardgrid,.result.two{grid-template-columns:1fr}.topright{align-items:flex-start}} 563 564/* ========================================================= 565 Retention-curve chart (asset anatomy), used by the 566 From User Behaviour to Creative case 567 ========================================================= */ 568.anatomy{display:flex;flex-direction:column;gap:clamp(8px,1.4vh,14px);height:100%;min-height:0;justify-content:center} 569.anhead{display:flex;justify-content:space-between;align-items:center;gap:16px} 570.anhead .t{font-size:clamp(12px,.95vw,15px);color:var(--grey)} 571.anatlegend{display:flex;gap:18px;font-size:clamp(11px,.85vw,14px);color:var(--grey);align-items:center;justify-content:center;margin-top:clamp(8px,1.4vh,14px)} 572.anatlegend i{display:inline-block;width:18px;vertical-align:middle;margin-right:6px} 573.anatlegend i.dash{border-top:1.5px dashed rgba(0,0,0,.4)} 574.anatlegend i.solid{border-top:3px solid var(--blue)} 575.anatomy svg{width:100%;overflow:visible} 576.anatomy text{font-family:var(--font);font-size:12px;fill:var(--grey)} 577.anatomy text.k{fill:var(--ink);font-weight:600} 578.anatomy .gl{stroke:rgba(0,0,0,.1)} 579/* dasharray tracks the real path length (~719 user units) so the draw fills 580 the whole transition instead of finishing in the first third */ 581.anatomy .curve{fill:none;stroke:var(--blue);stroke-width:3;stroke-linejoin:round;stroke-dasharray:760;stroke-dashoffset:760;transition:stroke-dashoffset 1.6s var(--ease-in-out) .3s,d .8s var(--ease-in-out)} 582.anatomy .ghost{fill:none;stroke:rgba(0,0,0,.25);stroke-width:1.5;stroke-dasharray:3 4} 583.anatomy .beat{fill:rgba(0,0,0,.06);stroke:#fff;stroke-width:2} 584.anatomy .beat.hook{fill:rgba(20,0,255,.12)} 585.anatomy .drop{fill:none;stroke:var(--ink);stroke-width:1.5} 586.anatomy .drop.b{stroke-dasharray:3 4} 587/* reveal, same pattern as the LTV and daily charts: the blue curve draws 588 itself in while the dashed reference, the beat bars and the labels fade up */ 589.anatomy .ghost,.anatomy .beat,.anatomy .drop,.anatomy text{opacity:0;transition:opacity .7s var(--ease-out) .25s} 590.stepslide .step.on .anatomy .curve{stroke-dashoffset:0} 591.stepslide .step.on .anatomy .ghost,.stepslide .step.on .anatomy .beat,.stepslide .step.on .anatomy .drop,.stepslide .step.on .anatomy text{opacity:1} 592.result.three{display:grid;grid-template-columns:1fr 1fr 1fr;grid-template-rows:auto auto;gap:0 var(--gutter);align-content:center} 593.result.three .giant{font-size:clamp(72px,8.6vw,170px)} 594.result.three .rline{font-size:clamp(15px,1.4vw,24px)} 595.result.three .rsub{grid-column:1/-1;max-width:none;white-space:nowrap} 596.result.three .rsub+.rsub{margin-top:0} 597@media (max-width:900px){.libgrid,.insight-grid.cr,.result.three{grid-template-columns:1fr}} 598 599/* ========================================================= 600 v7: reports desk (slide 9) + integrated case page 601 ========================================================= */ 602.desk{padding-top:calc(var(--top)*2.2);display:grid;grid-template-rows:auto 1fr;gap:clamp(12px,2vh,24px)} 603.desk .top{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--gutter)} 604.desk .top .h-huge{font-size:clamp(38px,4.6vw,78px)} 605.desk .top .hint{margin:.7em 0 0;color:var(--grey);font-size:clamp(12px,.95vw,15px);letter-spacing:-.01em} 606.langtoggle{display:inline-flex;border:1.5px solid var(--ink);border-radius:999px;padding:3px;gap:2px} 607.langtoggle button{padding:.45em 1.1em;border-radius:999px;font-weight:600;font-size:clamp(12px,.95vw,15px);letter-spacing:-.01em;transition:background .35s,color .35s} 608.langtoggle button[aria-pressed="true"]{background:var(--ink);color:#fff} 609/* text + animation rows, identical language to the Contents slide: only the 610 per-row underline is a drawn line (no static borders), number + title fade 611 in together, hover just tightens tracking and tints blue */ 612/* --dstep is the staircase indent per row; sized so three rows still reach 613 roughly the same depth the four-row list used to */ 614.deskline{list-style:none;margin:0;padding:0;height:100%;display:flex;flex-direction:column;justify-content:center;--dstep:clamp(150px,20vw,340px)} 615/* staircase: each row steps in from the left, narrowing so the right edge 616 (arrow column) still lines up. Title/number are static (no entrance 617 animation) â only the underline draw and the subtext sweep animate in, 618 each staggered later per row via --i. */ 619.deskline-item{position:relative;margin-left:calc(var(--i) * var(--dstep));width:calc(100% - var(--i) * var(--dstep))} 620.deskline-item:nth-child(2){margin-left:calc(var(--dstep) - 30px);width:calc(100% - var(--dstep) + 30px)} 621/* 300ms step against a 650ms draw: with the ease-in-out curve that starts the 622 next line when the previous one is about 30% drawn */ 623.deskline-item::after{content:"";position:absolute;
623left:0;right:0;bottom:0;height:1.5px;background:var(--ink);transform:scaleX(0);transform-origin:left;transition:transform .65s var(--ease-in-out);transition-delay:calc(var(--i)*300ms + .55s)} 624.in .deskline-item::after{transform:scaleX(1)} 625.deskline-item button{all:unset;box-sizing:border-box;cursor:pointer;display:flex;flex-wrap:wrap;align-items:baseline;column-gap:clamp(16px,3vw,48px);row-gap:8px;width:100%;padding:clamp(22px,4vh,44px) 0} 626.deskline-item .n{flex:0 0 auto;font-weight:700;font-size:clamp(16px,1.3vw,22px);letter-spacing:-.02em} 627.deskline-item .ti{flex:1 1 0%;min-width:0} 628.deskline-item .h{display:block;font-weight:600;font-size:clamp(28px,3.6vw,68px);letter-spacing:-.045em;line-height:1.05;transition:letter-spacing .5s var(--ease-out),color .3s} 629.deskline.ko .deskline-item .h{font-weight:800} 630.deskline-item .d{display:block;margin-top:.4em;color:var(--grey);font-size:clamp(12px,.9vw,15px);line-height:1.4;white-space:nowrap;opacity:0;clip-path:inset(0 100% 0 0);transition:opacity .9s var(--ease-out),clip-path .9s var(--ease-out);transition-delay:calc(var(--i)*280ms + .75s)} 631.in .deskline-item .d{opacity:1;clip-path:inset(0 0 0 0)} 632.deskline-item .go{flex:0 0 auto;display:inline-flex;align-items:center;opacity:.5;transform:translateX(0);transition:opacity .3s,transform .4s var(--ease-out)} 633.deskline-item .go svg{width:34px;height:17px} 634.deskline-item button:hover .h,.deskline-item button:focus-visible .h{letter-spacing:-.02em;color:var(--blue)} 635.deskline-item button:hover .go,.deskline-item button:focus-visible .go{opacity:1;transform:translateX(4px)} 636 637/* case page overlay */ 638/* visibility must switch a beat late on close (stay visible through the slide-down) 639 but instantly on open (visible before the slide-up starts), or the close looks 640 like an instant cut instead of the reverse of the open animation */ 641.casepage{position:fixed;inset:0;z-index:110;background:var(--paper);color:var(--ink);transform:translateY(100%);transition:transform .9s var(--ease-in-out),visibility 0s .9s;visibility:hidden} 642.casepage.open{transform:none;visibility:visible;transition:transform .9s var(--ease-in-out),visibility 0s} 643.casepage .cscroll{height:100%;overflow-y:auto;padding:var(--top) var(--gutter) calc(var(--top)*2)} 644.casepage .bar{display:flex;justify-content:space-between;align-items:center;font-size:clamp(13px,1vw,17px);margin-bottom:0} 645.casepage .close{display:inline-flex;align-items:center;gap:10px;font-weight:600} 646.casepage .close svg{width:18px;height:18px} 647.casepage .rv{opacity:0;transform:translateY(22px);transition:opacity .9s var(--ease-out),transform .9s var(--ease-out);transition-delay:calc(var(--i,0)*120ms)} 648.casepage .rv.on{opacity:1;transform:none} 649/* content-driven height, not min-height + centering: any leftover slack from 650 centering would land entirely after the last child (.whygrid), throwing off 651 its own top/bottom symmetry no matter how its own padding is tuned */ 652.cshero{max-width:1480px;margin:0 auto;padding-top:clamp(60px,10vh,120px);display:flex;flex-direction:column;gap:clamp(40px,7vh,88px)} 653.cshero .lab{font-size:clamp(12px,.95vw,15px);color:var(--grey)} 654.cshero .h-display{font-size:clamp(48px,7vw,132px);max-width:14ch;margin:0} 655.whygrid{display:grid;grid-template-columns:1fr 1fr;gap:calc(var(--gutter) * 2);align-items:start;border-top:1.5px solid var(--ink);padding:clamp(48px,6.75vh,72px) 0} 656.whygrid h4{margin:0 0 .6em;font-weight:600;font-size:clamp(13px,1.05vw,17px);color:var(--grey);letter-spacing:-.01em} 657.whygrid p{margin:0;font-size:clamp(13px,.98vw,16px);line-height:1.55;letter-spacing:-.005em;font-weight:400;text-align:justify;text-justify:inter-word} 658.cssec{max-width:1480px;margin:0 auto;display:grid;grid-template-columns:minmax(150px,1fr) minmax(0,3.4fr);gap:var(--gutter);padding:clamp(40px,7vh,88px) 0;border-top:1.5px solid var(--ink)} 659.cssec .sh h3{position:sticky;top:0;margin:0;font-weight:700;letter-spacing:-.03em;font-size:clamp(20px,1.7vw,30px);line-height:1.05} 660.cssec .sb{min-width:0} 661.cssec .body{max-width:none;font-size:clamp(13px,.98vw,16px);line-height:1.55;text-align:justify;text-justify:inter-word} 662.cssec .body p,.cssec .sb>p.tx{max-width:none;margin:0 0 1.8em;font-size:clamp(13px,.98vw,16px);line-height:1.55;letter-spacing:-.005em;font-weight:400;text-align:justify;text-justify:inter-word} 663.cssec .sb p.tx i{display:block;font-style:normal;color:var(--grey);font-size:.82em;margin-top:.45em} 664.cssec .sb p.tx b{font-weight:600} 665/* segment sub-heads inside a long-form paragraph (e.g. "[PUBG MOBILE JP]:" 666 markers in a multi-project case) read as a small label line above the 667 copy instead of inline bracket text â b.seglabel is still phrasing 668 content, so display:block here is valid, it just breaks its own line. */ 669.cssec .sb p.tx b.seglabel{display:block;margin:0 0 .5em;font-weight:600;font-size:1.15em;letter-spacing:-.005em} 670/* extra breathing room before a paragraph that starts a new segment (i.e. 671 the previous paragraph's copy ends and a seglabel begins) â bigger than 672 the ordinary paragraph-to-paragraph gap above, so a new segment reads as 673 a real break. One shared rule, so the gap stays the same size on every 674 case detail page rather than being tuned per case. Skipped on the very 675 first paragraph of a section so it doesn't add space under the heading 676 metarow when a case's first paragraph happens to open with a segment. */ 677.cssec .sb p.tx:has(>
677b.seglabel):not(:first-child){margin-top:3em} 678.cssec .sb p.tx a{color:var(--blue);text-decoration:underline;text-underline-offset:2px} 679.cssec .sb p.tx a:hover{opacity:.75} 680/* Korean body copy: the Latin tracking/leading tuned for Inter Tight reads 681 dense in Hangul, so when a case page is actually showing Korean text, 682 loosen to the letter-spacing/line-height/ink Google uses for CJK body 683 text (0 tracking, ~1.7 leading, and a softened near-black rather than 684 pure #000) â font itself is untouched. */ 685.casepage.ko .cssec .body,.casepage.ko .cssec .body p,.casepage.ko .cssec .sb>p.tx{letter-spacing:0;line-height:1.75;color:rgba(0,0,0,.87)} 686/* why/what stays on the English letter-spacing/line-height in Korean too, 687 per the site owner's call â only the long-form case body (above) gets 688 the loosened CJK treatment. */ 689.cssec .metarow{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:clamp(14px,2vw,28px);border-bottom:1px solid rgba(0,0,0,.15);padding:0 0 clamp(14px,2vh,22px);margin-bottom:clamp(40px,7vh,88px);font-size:clamp(13px,.98vw,16px);line-height:1.4} 690.cssec .metarow .k{color:var(--grey);display:block;margin-bottom:.3em;font-size:.9em} 691.cssec .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:.4em} 692.cssec .chips span{font-size:.8em;font-weight:600;padding:.25em .65em;border:1px solid rgba(0,0,0,.3);border-radius:4px} 693/* media stack: one card centred and frontmost, the next waiting behind it; 694 scrolling the wheel over the stack (or a vertical touch drag) swaps which 695 card is in front instead of the page scrolling */ 696.cssec .media{position:relative;height:clamp(340px,54vh,620px);margin-top:clamp(40px,7vh,88px);touch-action:pan-x} 697.cssec .media:empty,.cssec .mnav:empty{display:none} 698/* stack sitting inline under a segment label rather than under the whole 699 section: tighter above so it stays bonded to its label, clear space below 700 before the copy resumes */ 701.cssec .body>.media{height:clamp(240px,34vh,400px);margin-top:clamp(16px,2.4vh,26px)} 702.cssec .body>.mnav{margin-bottom:clamp(30px,4.5vh,48px)} 703/* inline figure in a long-form section: same card width as the media stack, 704 so a still and an embed sit at one shared measure */ 705.cssec .csfig{width:min(600px,86vw);margin:clamp(22px,3.2vh,36px) auto clamp(26px,3.8vh,42px)} 706.cssec .csfig img{width:100%;height:auto;display:block} 707.cssec .csfig .video{aspect-ratio:16/9;background:var(--ink)} 708.cssec .csfig .video iframe{width:100%;height:100%;border:0;display:block} 709.cssec .media .mcard{position:absolute;left:50%;top:50%;width:min(600px,86vw);max-height:100%;overflow:hidden;background:var(--lavender);box-shadow:0 22px 46px -22px rgba(0,0,0,.28),0 4px 14px -6px rgba(0,0,0,.12);opacity:0;pointer-events:none;transform:translate(-50%,-50%);transition:transform .55s var(--ease-out),opacity .5s var(--ease-out)} 710.cssec .media .mcard.front{opacity:1;pointer-events:auto;z-index:3;transform:translate(-50%,-50%)} 711.cssec .media .mcard.next{opacity:.55;z-index:2;transform:translate(-42%,-58%) scale(.92)} 712.cssec .media img{width:100%;height:auto;display:block;pointer-events:none;user-select:none} 713.cssec .media .video{aspect-ratio:16/9;background:var(--ink)} 714.cssec .media .video iframe{width:100%;height:100%;border:0;display:block} 715.cssec .mnav{display:flex;justify-content:center;margin-top:clamp(12px,2vh,20px)} 716.cssec .mnav span{width:22px;height:22px;display:flex;align-items:center;justify-content:center;cursor:pointer} 717.cssec .mnav span::before{content:"";width:6px;height:6px;border-radius:50%;background:rgba(0,0,0,.18);transition:background .3s,transform .3s} 718.cssec .mnav span.on::before{background:var(--ink);transform:scale(1.3)} 719.casepage .disc{max-width:1480px;margin:clamp(24px,4vh,48px) auto 0;color:var(--grey);font-size:clamp(12px,.9vw,14px);line-height:1.4;text-align:center} 720.casepage .disc b{color:inherit;font-weight:inherit} 721/* visualisation blocks inside the case page: static flow instead of the slide's stacked steps */ 722.casepage .vzw{display:block;height:auto;min-height:0;margin:clamp(40px,7vh,88px) 0;padding:0} 723.casepage .vzw .step{position:relative;inset:auto;height:auto;display:block;visibility:visible;opacity:0;transform:translateY(22px);transition:opacity .7s var(--ease-out),transform .7s var(--ease-out)} 724.casepage .vzw .step.on{opacity:1;transform:none} 725.casepage .vzw .chartcol,.casepage .vzw .insight-grid,.casepage .vzw .board,.casepage .vzw .lib,.casepage .vzw .result,.casepage .vzw .anatomy,.casepage .vzw .ltv{height:auto;min-height:0} 726.casepage .vzw .scatter svg,.casepage .vzw .daily svg,.casepage .vzw .ltv svg,.casepage .vzw .anatomy svg{height:auto;width:100%} 727/* cr's statements were moved below their chart, so it reads as a single 728 column; ce keeps its original chart-beside-readout split (.insight-grid.ce 729 further down sets its own 2-column ratio) */ 730#vz-ub-3 .insight-grid{grid-template-columns:1fr;align-items:start} 731.casepage .vzw .statements{display:flex;flex-direction:column;gap:clamp(14px,2.6vh,30px);margin-top:clamp(20px,3vh,32px)} 732.casepage .vzw .result{padding:clamp(12px,2vh,24px) 0} 733.casepage .vzw .result .giant{font-size:clamp(80px,9vw,180px)} 734.casepage .vzw .result.two .giant,.casepage .vzw .result.three .giant{font-size:clamp(56px,6.4vw,124px)} 735.casepage .vzw .groups{align-content:start} 736.casepage .vzw .chartcol .ctrls{justify-content:center;margin-top:clamp(12px,2vh,20px)} 737.casepage .cssec .sb>.vzw:first-child{margin-top:0} 738/* The proposal overlay reuses every .casepage style; only the direction of 739 the open/close move differs, so it reads as a sibling of the case pages. */ 740.planpage{transform:translateX(100%);transition:transform .9s var(--ease-in-out),visibility 0s .9s} 741.planpage.open{transform:none;transition:transform .9s var(--ease-in-out),visibility 0s} 742.planpage .pvenue{max-width:1480px;margin:0 auto;padding-top:clamp(26px,4vh,52px)} 743.planpage .phead{max-width:1480px;margin:0 auto;padding-top:clamp(34px,6vh,72px)}
744.planpage .phead .lab{color:var(--grey);font-size:clamp(12px,.95vw,15px);margin-bottom:clamp(10px,1.6vh,18px)} 745.planpage .phead h1{font-size:clamp(48px,7vw,132px);font-weight:700;letter-spacing:-.055em;line-height:.9;margin:0 0 clamp(20px,3.5vh,44px);max-width:18ch} 746.cssec .sb ul.plist{margin:0 0 1.8em;padding:0;list-style:none;display:flex;flex-direction:column;gap:.85em} 747.cssec .sb ul.plist li{position:relative;padding-left:1.2em;font-size:clamp(13px,.98vw,16px);line-height:1.55;letter-spacing:-.005em} 748.cssec .sb ul.plist li::before{content:"";position:absolute;left:0;top:.62em;width:5px;height:5px;border-radius:50%;background:var(--ink)} 749.cssec .sb ul.plist li b{font-weight:600} 750/* bullet with its own bold heading: heading on one line, detail on the next */ 751.cssec .sb ul.plist li b.lih{display:block;margin-bottom:.15em} 752.casepage.ko .cssec .sb ul.plist li{letter-spacing:0;line-height:1.75;color:rgba(0,0,0,.87);word-break:keep-all} 753/* crowd calculation block in the proposal: conditions table, formulas, result */ 754.cssec .sb .calc{margin:0 0 1.8em;font-size:clamp(13px,.98vw,16px);line-height:1.5} 755.cssec .sb .calc-h{font-weight:600;font-size:clamp(13px,.98vw,16px);margin:2.4em 0 .9em} 756.cssec .sb .calc-h:first-child{margin-top:.4em} 757.cssec .sb .calc a{color:var(--blue);text-decoration:underline;text-underline-offset:2px} 758.cssec .sb .calc a:hover{opacity:.75} 759.cssec .sb .calc .tag{display:inline-block;margin-left:.4em;padding:0 .4em;border:1px solid rgba(0,0,0,.3);border-radius:4px;font-size:.8em;font-weight:600;color:var(--grey);vertical-align:.1em} 760.cssec .sb .ctab{border-top:1.5px solid var(--ink)} 761.cssec .sb .ctab .tr{display:grid;grid-template-columns:1fr 1.1fr 1.3fr 2fr;gap:0 1.2em;padding:.75em 0;border-bottom:1px solid rgba(0,0,0,.12)} 762.cssec .sb .ctab .th{color:var(--grey);font-weight:600;padding:.6em 0} 763.cssec .sb .ctab .k,.cssec .sb .ctab .v{font-weight:600} 764.cssec .sb .ctab .use,.cssec .sb .ctab .src{color:rgba(0,0,0,.72)} 765.cssec .sb .cf{display:flex;flex-direction:column;gap:1em;border-left:2px solid var(--ink);padding:.2em 0 .2em 1.1em} 766.cssec .sb .cf .row{display:grid;grid-template-columns:9em 1fr;gap:0 1.2em} 767.cssec .sb .cf .k{color:var(--grey);font-weight:600} 768.cssec .sb .cf .e{font-variant-numeric:tabular-nums;font-size:1.06em} 769.cssec .sb .cf .n{color:var(--grey);font-size:.9em;margin-top:.15em} 770.cssec .sb .calc p.cap{color:var(--grey);font-size:clamp(12px,.86vw,14px);margin:4px 0 1.6em} 771.cssec .sb .cres{display:grid;grid-template-columns:repeat(4,1fr);gap:1em;border-top:1.5px solid var(--ink);padding-top:1em;margin-top:.6em} 772.cssec .sb .cres b{display:block;font-size:clamp(22px,2vw,34px);font-weight:700;letter-spacing:-.03em;line-height:1.1;font-variant-numeric:tabular-nums} 773.cssec .sb .cres span{display:block;color:var(--grey);margin-top:.35em} 774.casepage.ko .cssec .sb .calc{word-break:keep-all} 775@media (max-width:900px){ 776 .cssec .sb .ctab .th{display:none} 777 .cssec .sb .ctab .tr{grid-template-columns:1fr;gap:.2em} 778 .cssec .sb .ctab .src{font-size:.92em} 779 .cssec .sb .cf .row{grid-template-columns:1fr;gap:.2em} 780 .cssec .sb .cres{grid-template-columns:repeat(2,1fr);row-gap:1.2em} 781} 782/* proposal page: venue mockup gallery under the title, the same card stack as 783 a case page's media (it borrows .cssec so those rules apply unchanged) */ 784.planpage .pgal{display:block;border-top:0;padding:0;margin-bottom:clamp(40px,7vh,88px)} 785.planpage .pgal .media{margin-top:0} 786.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} 787/* heatmaps A/B (user-behaviour case): plain table + CSS, no chart library */ 788.hm-head{margin-bottom:clamp(14px,2.2vh,22px)} 789.heatmap{text-align:center} 790.hm-title{margin:0 0 .3em;font-weight:500;font-size:clamp(16px,1.3vw,22px);letter-spacing:-.02em;line-height:1.2} 791.hm-sub{margin:0;color:var(--grey);font-size:clamp(12px,.9vw,14px);line-height:1.35} 792.hm-scroll{overflow-x:auto;padding-bottom:4px} 793.hm-table{width:100%;border-collapse:separate;border-spacing:3px;table-layout:fixed} 794.hm-table th,.hm-table td{border-radius:4px;font-variant-numeric:tabular-nums} 795.hm-table thead th{font-weight:400;color:var(--grey);text-align:center;padding-bottom:2px} 796.hm-table thead th.hm-launch{font-weight:500;color:var(--ink);border-left:1px dashed var(--rule)} 797.hm-table tbody td.hm-launch{border-left:1px dashed var(--rule)} 798.hm-table .hm-rowhead{text-align:left} 799.hm-table .hm-axis-row th{font-size:10px;font-weight:400;color:var(--grey);letter-spacing:.02em;padding-bottom:6px} 800.hm-table th[scope="row"]{text-align:left;font-weight:500;padding-right:8px;white-space:nowrap} 801.hm-table td{text-align:center;font-weight:500;cursor:default}
802.hm-a td,.hm-a th{font-size:12px} 803.hm-a td{height:30px} 804.hm-b td,.hm-b th{font-size:13px} 805.hm-b td{height:38px} 806.hm-table tr.hm-gap td{height:6px;padding:0;border:0;background:transparent} 807@media (max-width:700px){.hm-table th[scope="row"]{white-space:normal}} 808.hm-legend{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:clamp(12px,1.8vh,18px);font-size:12px;color:var(--grey)} 809.hm-legend-bar{display:flex} 810.hm-legend-bar i{display:block;width:22px;height:10px} 811.hm-legend-bar i:first-child{border-radius:4px 0 0 4px} 812.hm-legend-bar i:last-child{border-radius:0 4px 4px 0} 813.hm-legend.wrap{flex-wrap:wrap;row-gap:8px} 814.hm-chip{display:inline-flex;align-items:center;gap:6px} 815.hm-chip i{display:block;width:12px;height:12px;border-radius:3px} 816.hm-info{margin:10px 0 0;font-size:clamp(12px,.9vw,14px);min-height:1.4em} 817@media (max-width:900px){.whygrid{grid-template-columns:1fr}.cssec{grid-template-columns:1fr}.deskline-item,.deskline-item:nth-child(2){margin-left:0;width:100%}} 818 819/* ========================================================= 820 RESPONSIVE (PC first; tablet/mobile are graceful, not pixel-perfect) 821 ========================================================= */ 822@media (max-width:900px){ 823 .hello,.worked,.ai,.contact{grid-template-columns:1fr} 824 .hello .pic{max-height:34vh;aspect-ratio:auto;height:34vh} 825 .whatido .grid{grid-template-columns:1fr 1fr} 826 .whatido .grid>span{text-align:left!important} 827 .contents ol{margin-left:0} 828 .tools .body p{white-space:normal} 829 .stats .lead,.stats .nums{grid-template-columns:1fr 1fr} 830 /* one column on a phone: the toggle rides at the top right, then the copy, 831 then the AI notes. The meta list is dropped â every line of it is in the 832 proposal itself, which is one tap away. */ 833 .ai{display:flex;flex-direction:column;gap:0} 834 .ai .right{display:contents} 835 .ai .right .topright{order:-1;align-self:flex-end;align-items:flex-end;width:auto;margin:0 0 clamp(6px,1.2vh,12px)} 836 .ai .left{order:0} 837 .ai .aiuse{order:1;margin-top:clamp(14px,2.4vh,24px);margin-bottom:clamp(14px,2.4vh,24px)} 838 .ai .open{order:2;align-self:flex-end} 839 .ai h2{font-size:clamp(30px,8.6vw,40px)!important} 840 /* the concept line never wraps, so on the narrowest phones it sets its own size */ 841 .ai .q{margin-top:clamp(12px,2vh,20px);font-size:clamp(14px,4.2vw,22px)} 842 .ai .ins{margin-top:.7em;font-size:clamp(11px,3vw,13px);line-height:1.4} 843 .ai .open{margin-top:clamp(12px,2vh,20px)} 844 .ai .aiuse h4{font-size:clamp(14px,4.2vw,22px)} 845 .ai .aiuse ul{margin-top:.5em} 846 .ai .aiuse ul{gap:.45em} 847 .ai .aiuse li{font-size:clamp(11px,3vw,13px);line-height:1.4} 848 .chart .plot{grid-template-columns:repeat(7,1fr);height:34vh} 849 .projects .list{grid-template-columns:1fr} 850 .projects .list.single{max-width:100%} 851 .detail .sec{grid-template-columns:1fr} 852 .contact{padding-right:var(--gutter)} 853 .contact .right{display:none} 854 .cover .contact{display:none} 855} 856/* phones: the headline and its paragraph stop sharing a row (side by side they 857 squeeze the copy into a sliver and the slide overflows), and each figure sits 858 beside its own line of copy */ 859@media (max-width:760px){ 860 .stats{padding-top:calc(var(--top)*1.35)} 861 .stats .h-caps{top:0} 862 .stats .lead{grid-template-columns:1fr;gap:clamp(12px,2.4vh,20px);align-items:start} 863 .stats .leadright{gap:clamp(10px,1.6vh,16px)} 864 .stats .lead .body{max-width:none;margin-bottom:clamp(8px,1.6vh,14px);top:0;left:0} 865 .stats .nums.three{grid-template-columns:1fr;gap:clamp(10px,1.8vh,16px)} 866 .stats .nums.three .num{display:grid;grid-template-columns:auto 1fr;column-gap:14px;align-items:baseline} 867 .stats .nums.three .num .v{font-size:clamp(32px,9vw,44px);line-height:1} 868 .stats .nums.three .num .src{grid-column:2;margin-top:0} 869 .stats .nums.three .num .k{grid-column:2;max-width:none;margin-top:.2em} 870} 871@media (prefers-reduced-motion:reduce){ 872 *,*::before,*::after{transition-duration:.01ms!important;animation:none!important;transition-delay:0s!important} 873 .mask>.line,.cover .word .l i,.chart .col .bar{transform:none!important} 874 .fade,.kw{opacity:1!important;filter:none!important} 875 .deck{scroll-behavior:auto} 876} 877:focus-visible{outline:2px solid var(--blue);outline-offset:4px} 878 879/* ========================================================= 880 PRINT / PDF 881 One slide per 16:9 page, every reveal in its finished state. When a case 882 page or the proposal is open, only that overlay prints, as a flowing 883 document. The JS half (typing, counters, chart steps) is preparePrint(). 884 The reference export is tools/pdf/export.js, which renders at exactly 885 1920x1080 so the JS-measured layout (cover word, page-10 alignment) 886 matches the page; Cmd+P from another window size is close but not exact. 887 ========================================================= */ 888@page{size:1920px 1080px;margin:0} 889/* overlays flow across pages, so they get top/bottom room the slides don't */ 890@page doc{size:1920px 1080px;margin:64px 0 56px} 891.print-yt,.print-head,.print-break{display:none} 892@media print{ 893 *,*::before,*::after{transition:none!important;animation:none!important} 894 html,body{height:auto!important;overflow:visible!important} 895 .mask>.line,.cover .word .l i,.chart .col .bar{transform:none!important} 896 .fade,.kw,.rv,.casepage .vzw .step{opacity:1!important;filter:none!important;transform:none!important} 897 /* screen-only controls and click prompts; hidden but still taking their 898 space, so nothing around them shifts */ 899 .langtoggle,.casepage .close,#deskHint,#gpBtn{visibility:hidden!important} 900 /* the fixed header relies on mix-blend-mode to stay legible on light and 901 dark slides, which doesn't survive into PDF; each slide gets its own 902 copy instead (added by preparePrint), coloured for that slide */ 903 .chrome{display:none!important} 904 .slide>.print-head{display:flex;position:absolute;top:var(--top);left:var(--gutter);right:var(--gutter);justify-content:space-between;font-size:clamp(13px,1vw,17px);letter-spacing:-.01em;transform:translateY(-6px);color:var(--ink);z-index:5} 905 .slide.dark>.print-head{color:#fff} 906 .deck{height:auto!important;overflow:visible!important;scroll-snap-type:none!important} 907 .slide{break-after:page;break-inside:avoid} 908 909 /* an open overlay prints on its own */ 910 body:has(.casepage.open) .deck,body:has(.casepage.open) .chrome, 911 body:has(.detail.open) .deck,body:has(.detail.open) .chrome{display:none!important}
912 .casepage:not(.open),.detail:not(.open),#vizStore{display:none!important} 913 .casepage.open{position:static!important;transform:none!important;height:auto!important;inset:auto!important;page:doc} 914 .casepage .cscroll{height:auto!important;overflow:visible!important;padding-bottom:0!important} 915 /* the closing note stays with the last section instead of taking a page alone */ 916 .casepage .disc{margin-top:20px!important;break-before:avoid} 917 .cssec .sh h3{position:static!important} 918 /* sections print as plain block flow with the heading pinned in the left 919 column, rather than as a two-column grid: Chrome splits block flow 920 across pages reliably, whereas a grid row can be pushed whole to the 921 next page and leave an empty frame behind. Same look, same widths 922 (1fr : 3.4fr with a gutter between). */ 923 .cssec{display:block!important;position:relative;padding-left:calc((100% - var(--gutter)) / 4.4 + var(--gutter))!important} 924 .cssec>.sh{position:absolute;left:0;width:calc((100% - var(--gutter)) / 4.4)} 925 /* page breaks: never through a paragraph, list item, table row, figure or 926 chart; a heading or segment label never ends a page on its own */ 927 .cssec .sh h3,.cssec .sb p.tx b.seglabel,.hm-head,.cssec .sb .calc-h{break-after:avoid} 928 .cssec .sb p.tx:has(>b.seglabel:last-child){break-after:avoid} 929 .cssec .sb p.tx,.cssec .sb ul.plist li,.cssec .body p,.cssec .sb .ctab .tr,.cssec .sb .cf .row,.cssec .sb .cres, 930 .whygrid>div,.csfig,.vzw .step,.cssec .media .mcard,.hm-table,.disc{break-inside:avoid} 931 .cssec p,.cssec li{orphans:3;widows:3} 932 /* a paragraph long enough to leave a big gap if moved whole may split, 933 with at least three lines either side (flagged by preparePrint) */ 934 .cssec .print-long{break-inside:auto!important} 935 /* every section opens a page, so its rule and heading always sit at the 936 top instead of being left behind at the foot of the previous one. The 937 break sits on an empty marker before the section (added by 938 preparePrint): forcing it on the two-column section itself can leave 939 the section's top rule alone on a page. */ 940 .print-break{display:block;break-before:page;height:0} 941 942 /* card stacks: every card laid out, not just the front one */ 943 .cssec .media{height:auto!important;display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;align-items:start} 944 .cssec .media .mcard{position:static!important;transform:none!important;opacity:1!important;width:auto!important;max-height:none!important;box-shadow:none!important} 945 .cssec .mnav{display:none!important} 946 /* the proposal's mockups already sit inside their own sections below */ 947 .planpage .pgal{display:none!important} 948 949 /* a video can't play on paper: thumbnail + link instead */ 950 .video{display:none!important} 951 .print-yt{display:block;position:relative;color:var(--ink);text-decoration:none} 952 .print-yt img{width:100%;display:block;aspect-ratio:16/9;object-fit:cover} 953 /* a video stand-in only needs to show what the video is, not fill the column */ 954 .csfig:has(.print-yt){width:min(480px,70vw)!important} 955 956 /* Hand-built appendix pages (paginatePrint, used by tools/pdf/export.js): 957 each page is one block the height of the printable area, its content 958 centred vertically, with the section name repeated in the left column 959 so a reader always knows where they are. A page holding more than one 960 page's worth (rare) is allowed to run on. */ 961 /* the section name sits at the same spot on every page (a fixed band at 962 the top, under the section's opening rule on its first page); only the 963 content below it is centred */ 964 .cssec.print-page{display:flex!important;flex-direction:column;justify-content:center;position:relative;min-height:var(--pp-h,0px);padding:var(--pp-band,0px) 0 0!important;border:0!important;break-before:page;break-inside:avoid;box-sizing:border-box} 965 .cssec.print-page.first{border-top:1.5px solid var(--ink)!important} 966 .cssec.print-page.tall{break-inside:auto;justify-content:flex-start} 967 .print-page>.pp-in{display:grid;grid-template-columns:calc((100% - var(--gutter)) / 4.4) minmax(0,1fr);column-gap:var(--gutter);align-items:start} 968 .print-page>.pp-in>.sh{position:absolute!important;top:var(--pp-band,0px);left:0;width:calc((100% - var(--gutter)) / 4.4)} 969 .print-page>.pp-in>.sb{grid-column:2} 970 .print-page .sb>:last-child,.print-page .sb>:last-child>:last-child{margin-bottom:0!important} 971 .casepage .vzw{margin:20px 0!important}
972 .print-yt span{display:block;margin-top:.5em;color:var(--grey);font-size:14px} 973} 974</style> 975</head> 976<body> 977 978<!-- fixed chrome --> 979<div class="chrome" id="chrome"> 980 <div class="head"><span>Portfolio 2026</span><span id="headDate"></span></div> 981 <div class="counter"><span id="cIdx">01</span><span class="bar"><i id="cBar"></i></span><span id="cTot">00</span></div> 982</div> 983 984<main class="deck" id="deck"> 985 986<!-- 01 COVER ============================================ --> 987<section class="slide cover" id="cover" data-name="Cover"> 988 <div class="top"> 989 <div class="meta"> 990 <svg class="mark" viewBox="0 0 44 22" fill="none" stroke="currentColor" stroke-width="1.2"><ellipse cx="22" cy="11" rx="21" ry="10"/><ellipse cx="22" cy="11" rx="10" ry="10"/><ellipse cx="22" cy="11" rx="4" ry="10"/><line x1="1" y1="11" x2="43" y2="11"/><line x1="22" y1="1" x2="22" y2="21"/></svg> 991 <div class="l2 type" data-type="Portfolio 2026"></div> 992 <div class="l3 type" id="presentedBy" data-type="Presented by: Doyoung Park"></div> 993 </div> 994 <div class="langtoggle" role="group" aria-label="Language"><button data-lang="en" aria-pressed="true">EN</button><button data-lang="ko" aria-pressed="false">KO</button></div> 995 </div> 996 <div> 997 <div class="foot"> 998 <div class="roles"> 999 <span class="fade" id="role1" style="--i:0">Marketer</span><span class="sl fade" style="--i:1">/</span><span class="fade" id="role2" style="--i:2">Communicator</span><span class="sl fade" style="--i:3">/</span><span class="fade" id="role3" style="--i:4">Creative Manager</span> 1000 </div> 1001 <div class="email fade" style="--i:3">[email protected]</div> 1002 </div> 1003 <h1 class="word" id="bigword" aria-label="Portfolio"></h1> 1004 </div> 1005</section> 1006 1007<!-- 02 HELLO ============================================ --> 1008<section class="slide hello" id="hello" data-name="Hello"> 1009 <div class="pic"><img src="assets/dp.webp" alt="Doyoung Park" onerror="this.style.display='none'"></div> 1010 <div class="txt"> 1011 <h2 class="h-huge"> 1012 <span class="mask"><span class="line" id="helloLine0" style="--i:0">Hello!</span></span> 1013 <span class="mask"><span class="line" id="helloLine1" style="--i:1">I'm Doyoung Park</span></span> 1014 </h2> 1015 <div class="body"> 1016 <p class="fade" id="bioText" style="--i:0">Iâm a gamer, and a marketer who acts on data.<br>Iâve spent over five years running publishing pipelines across Korea, Japan and Europe, and building KPI frameworks from scratch. 1017Beyond that, Iâve led product, investment and budget decisions grounded in market and performance data.</p> 1018 </div> 1019 <div class="info fade" style="--i:2"> 1020 <div class="col"> 1021 <h3 id="eduHeader">Education</h3> 1022 <ul> 1023 <li><b>Goldsmiths, University of London</b><span id="eduDegree1">BA (Hons) Fine Arts â First-Class Honours</span></li> 1024 <li><b>Kenkoku High School, Osaka</b><span>High School Diploma</span></li> 1025 </ul> 1026 </div> 1027 <div class="col"> 1028 <h3 id="langHeader">Languages</h3> 1029 <ul class="langs"> 1030 <li><b id="langName1">Korean</b> - <span id="langLevel1">Native</span></li> 1031 <li><b id="langName2">Japanese</b> - <span id="langLevel2">Native</span></li> 1032 <li><b id="langName3">English</b> - <span id="langLevel3">Professional working proficiency</span></li> 1033 <li><b id="langName4">German</b> - <span id="langLevel4">Basic communication</span></li> 1034 <li><b id="langName5">Chinese</b> - <span id="langLevel5">Basic communication</span></li> 1035 </ul> 1036 </div> 1037 </div> 1038 </div> 1039</section> 1040 1041<!-- 03 WHAT I DO ======================================== --> 1042<section class="slide whatido" id="whatido" data-name="What I do"> 1043 <div> 1044 <h2 class="h-display"> 1045 <span class="mask"><span class="line" id="whatidoLine0" style="--i:0">What I do?</span></span> 1046 <span class="mask"><span class="line" id="whatidoLine1" style="--i:1">(and love doing)</span></span> 1047 </h2> 1048 <div class="body fade" style="--i:1"> 1049 <p id="whatidoBody">My core strength is understanding users and content through data, and using those insights to improve and manage key metrics. Throughout my career, my main role has been analysing user behaviour and content characteristics to draw out insights, then testing and validating them to build insights and pipelines that genuinely move the metrics.</p> 1050 </div> 1051 </div> 1052 <div class="grid"> 1053 <span class="kw" id="kw0" style="--i:0">Performance Marketing</span> 1054 <span class="kw" id="kw1" style="--i:1">Creative Strategy</span> 1055 <span class="kw" id="kw2" style="--i:2">Brand Marketing</span> 1056 <span class="kw" id="kw3" style="--i:3">Market Validation</span> 1057 <span class="kw" id="kw4" style="--i:4">Store Optimisation</span> 1058 <span class="kw" id="kw5" style="--i:5">Budget Planning</span> 1059 <span class="kw" id="kw6" style="--i:6">Competitor Intelligence</span> 1060 <span class="kw" id="kw7" style="--i:7">Business Development</span> 1061 <span class="kw" id="kw8" style="--i:8">IP Partnerships</span> 1062 <span class="kw" id="kw9" style="--i:9">Community Activation</span> 1063 <span class="kw" id="kw10" style="--i:10">Dashboard Automation</span> 1064 <span class="kw" id="kw11" style="--i:11">AI-assisted Analysis</span> 1065 </div> 1066</section> 1067 1068<!-- 04 TOOLS ============================================ --> 1069<section class="slide tools" id="tools" data-name="Tools"> 1070 <h2 class="h-display"> 1071 <span class="mask"><span class="line" id="toolsLine0" style="--i:0">Tools</span></span> 1072 <span class="mask"><span class="line" id="toolsLine1" style="--i:1">I'm fluent in.</span></span> 1073 </h2>
1074 <div class="body"> 1075 <p class="fade" id="toolsP0" style="--i:0"><b>Tableau, Excel, MySQL, Google Analytics, Data.ai, Adjust</b> for reading the data.</p> 1076 <p class="fade" id="toolsP1" style="--i:1"><b>Google Ads, Meta, TikTok, Moloco, Apple Search Ads, X Ads</b> for spending it well.</p> 1077 <p class="fade" id="toolsP2" style="--i:2"><b>Photoshop, After Effects, InDesign</b> for making the things people see.</p> 1078 <p class="fade" id="toolsP3" style="--i:3"><b>Notion, Slack, Jira</b> for keeping studios, agencies and partners aligned.</p> 1079 <p class="fade" id="toolsP4" style="--i:4"><b>Claude Code, Codex, Python, Whisper, PySceneDetect, FLUX, NovelAI, Seedance, Grok</b> for exploring what AI can add to analysis and creative work.</p> 1080 </div> 1081</section> 1082 1083<!-- 05 WORKED =========================================== --> 1084<section class="slide worked" id="worked" data-name="Where I've worked"> 1085 <h2 class="h-display"> 1086 <span class="mask"><span class="line" id="workedLine0" style="--i:0">Where</span></span> 1087 <span class="mask"><span class="line" id="workedLine1" style="--i:1">I've worked.</span></span> 1088 </h2> 1089 <div class="tl"> 1090 <span class="y fade" style="--i:0">2024 / 2026</span><div class="r fade" style="--i:0"><b>Marketer & Publishing Manager</b><span>SANDY FLOOR</span></div> 1091 <span class="y fade" style="--i:1">2024 / 2024</span><div class="r fade" style="--i:1"><b>PUBG MOBILE Japan Marketer</b><span>KRAFTON</span></div> 1092 <span class="y fade" style="--i:2">2020 / 2023</span><div class="r fade" style="--i:2"><b>Creative Director & Marketer</b><span>GAMEDUO</span></div> 1093 </div> 1094</section> 1095 1096<!-- 06 CONTENTS ========================================= --> 1097<section class="slide contents" id="contents" data-name="Contents"> 1098 <h2 class="h-display"> 1099 <span class="mask"><span class="line" id="contentsLine0" style="--i:0">What you will find</span></span> 1100 <span class="mask"><span class="line" id="contentsLine1" style="--i:1">inside?</span></span> 1101 </h2> 1102 <ol> 1103 <li style="--i:0"><span class="n fade" style="--i:1">1</span><a class="fade" style="--i:1" href="#ch-data">Data-Driven Marketing</a></li> 1104 <li style="--i:1"><span class="n fade" style="--i:2">2</span><a class="fade" style="--i:2" href="#ch-bd">Business Development</a></li> 1105 <li style="--i:2"><span class="n fade" style="--i:3">3</span><a class="fade" style="--i:3" href="#ch-ai">AI Exploration</a></li> 1106 </ol> 1107</section> 1108 1109<!-- 07 CHAPTER: DATA ==================================== --> 1110<section class="slide dark chapter" id="ch-data" data-name="Data-Driven Marketing"> 1111 <button class="arrow" aria-label="Next" data-next><svg viewBox="0 0 120 120"><path d="M8 60 H110 M74 20 C78 40 92 54 110 60 C92 66 78 80 74 100"/></svg></button> 1112 <h2 class="h-chapter type" data-type="Data-Driven Marketing"></h2> 1113</section> 1114 1115<!-- 08 CHAPTER: BD ====================================== --> 1116<section class="slide desk" id="reports" data-name="Data-Driven Marketing · Reports"> 1117 <div class="top"> 1118 <div> 1119 <h2 class="h-huge"><span class="mask"><span class="line" style="--i:0">Data-Driven Marketing.</span></span></h2> 1120 <p class="hint fade" id="deskHint" style="--i:1">Click to see the detail.</p> 1121 </div> 1122 <div class="topright fade" style="--i:1"> 1123 <div class="langtoggle" role="group" aria-label="Language"><button data-lang="en" aria-pressed="true">EN</button><button data-lang="ko" aria-pressed="false">KO</button></div> 1124 </div> 1125 </div> 1126 <ol class="deskline" id="deskCards"></ol> 1127</section> 1128 1129<section class="slide dark chapter" id="ch-bd" data-name="Business Development"> 1130 <button class="arrow" aria-label="Next" data-next><svg viewBox="0 0 120 120"><path d="M8 60 H110 M74 20 C78 40 92 54 110 60 C92 66 78 80 74 100"/></svg></button> 1131 <h2 class="h-chapter type" data-type="Business Development"></h2> 1132</section> 1133 1134<section class="slide stats" id="bd-stats" data-name="Business Development · Numbers"> 1135 <div class="lead"> 1136 <h2 class="h-caps"> 1137 <span class="mask"><span class="line" id="bdLine0" style="--i:0">Beyond the marketing.</span></span> 1138 </h2> 1139 <div class="leadright"> 1140 <div class="topright fade" style="--i:1"> 1141 <div class="langtoggle" role="group" aria-label="Language"><button data-lang="en" aria-pressed="true">EN</button><button data-lang="ko" aria-pressed="false">KO</button></div> 1142 </div> 1143 <p class="body fade" id="bdBody" style="--i:2">I sourced global partners and, through repeated meetings, game shows and negotiation, signed a China publishing deal.<br><br>At SANDY FLOOR I set up and ran the market test for a new title, Dark Bouncer, from A to Z. I then used marketing and in-game metrics to propose a business-model pivot that was actually adopted, which secured a separate marketing investment for the domestic market.</p> 1144 </div> 1145 </div> 1146 <div class="nums three"> 1147 <div class="num fade" style="--i:1"><div class="v">23,000</div><div class="src" id="bdSrc2">Wishlist earned from China region</div><div class="k" id="bdK2">Closing the China publishing contract lifted PengPong's wishlist count in the Chinese market.</div></div> 1148 <div class="num fade" style="--i:2"><div class="v">$0.50</div><div class="src" id="bdSrc1">Market test</div><div class="k" id="bdK1">CPI held for a full month on Dark Bouncer's market test, the data behind a recommended IAA to IAP pivot.</div></div> 1149 <div class="num fade" style="--i:3"><div class="v">2</div><div class="src" id="bdSrc0">Awards from Gameshow</div><div class="k" id="bdK0">Took part in a range of international game shows and won G-STAR 2025 Player's Voice Choice and Clemmy's 2026.</div></div> 1150 </div> 1151</section> 1152 1153<!-- 09 CHAPTER: AI ====================================== --> 1154<section class="slide dark chapter" id="ch-ai" data-name="AI Exploration"> 1155 <button class="arrow" aria-label="Next" data-next><svg viewBox="0 0 120 120"><path d="M8 60 H110 M74 20 C78 40 92 54 110 60 C92 66 78 80 74 100"/></svg></button> 1156 <h2 class="h-chapter type" data-type="AI Exploration"></h2> 1157</section> 1158 1159<section class="slide ai" id="ai-project" data-name="AI Exploration · Genshin popup proposal"> 1160 <div class="left"> 1161 <h2 class="h-huge" style="font-size:clamp(48px,6.2vw,108px)">
1162 <span class="mask"><span class="line" id="gpLine0" style="--i:0"></span></span> 1163 <span class="mask"><span class="line" id="gpLine1" style="--i:1"></span></span> 1164 </h2> 1165 <div class="lab fade" id="gpLab" style="--i:1"></div> 1166 <div class="q fade" style="--i:2" id="gpQ"></div> 1167 <div class="ins body fade" style="--i:3" id="gpIns"></div> 1168 </div> 1169 <div class="right"> 1170 <div class="topright fade" style="--i:1"> 1171 <div class="langtoggle" role="group" aria-label="Language"><button data-lang="en" aria-pressed="true">EN</button><button data-lang="ko" aria-pressed="false">KO</button></div> 1172 </div> 1173 <div class="aiuse fade" style="--i:2" id="gpAi"></div> 1174 <button class="open fade" style="--i:4" data-plan id="gpBtn"><span></span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18M13 4l8 8-8 8"/></svg></button> 1175 </div> 1176</section> 1177 1178<!-- 10 CONTACT ========================================== --> 1179<section class="slide dark contact" id="contact" data-name="Contact"> 1180 <div class="left"> 1181 <div> 1182 <h2 class="h-display"> 1183 <span class="mask"><span class="line" style="--i:0">Let's start</span></span> 1184 <span class="mask"><span class="line" style="--i:1">something</span></span> 1185 </h2> 1186 <div class="links"> 1187 <a class="fade" style="--i:0" href="mailto:[email protected]">[email protected]</a> 1188 <a class="fade" style="--i:1" href="https://www.linkedin.com/in/doyoung-park-721566248/" target="_blank" rel="noopener">linkedin.com/in/doyoung-park</a> 1189 </div> 1190 </div> 1191 <div class="social fade" style="--i:3"> 1192 <b>Elsewhere</b> 1193 <a href="https://www.linkedin.com/in/doyoung-park-721566248/" target="_blank" rel="noopener">LinkedIn</a> 1194 <a href="https://www.doyoungpark.com" target="_blank" rel="noopener">doyoungpark.com</a> 1195 </div> 1196 </div> 1197 <div class="right"><img src="assets/dp.webp" alt="Doyoung Park" onerror="this.remove()"></div> 1198</section> 1199 1200</main> 1201 1202<!-- DETAIL OVERLAY ====================================== --> 1203<div id="vizStore" hidden> 1204<div class="viz stepslide vzw" id="vz-bc-2"> 1205 <div class="step" data-step="2"> 1206 <div class="result two"> 1207 <div> 1208 <div class="giant"><span class="mask"><span class="line"><span data-count="8">0</span></span></span></div> 1209 <p class="rline"><span class="mask"><span class="line" style="--i:1">IP collaborations coordinated at once</span></span></p> 1210 </div> 1211 <div> 1212 <div class="giant"><span class="mask"><span class="line" style="--i:1">#<span data-count="8">0</span></span></span></div> 1213 <p class="rline"><span class="mask"><span class="line" style="--i:2">real-time trending rank on Japanese X, Dragon Ball Super microsite</span></span></p> 1214 </div> 1215 </div> 1216 </div> 1217</div> 1218<div class="viz stepslide vzw" id="vz-ub-0"> 1219 <div class="step" data-step="0"> 1220 <div class="heatmap" id="heatmapA"></div> 1221 </div> 1222</div> 1223<div class="viz stepslide vzw" id="vz-ub-1"> 1224 <div class="step" data-step="1"> 1225 <div class="heatmap" id="heatmapB"></div> 1226 </div> 1227</div> 1228<div class="viz stepslide vzw" id="vz-ub-2"> 1229 <div class="step" data-step="2"> 1230 <div class="result"> 1231 <div class="giant"><span class="mask"><span class="line"><span data-count="130">0</span>%</span></span></div> 1232 <p class="rline"><span class="mask"><span class="line" style="--i:1">growth in managed media budget, year on year</span></span></p> 1233 </div> 1234 </div> 1235</div> 1236<div class="viz stepslide vzw" id="vz-ce-1"> 1237 <div class="step" data-step="1"> 1238 <div class="board"> 1239 <div class="boardgrid"> 1240 <div class="daily"> 1241 <svg id="ceBoard" viewBox="0 0 760 320" preserveAspectRatio="xMidYMid meet" aria-label="Daily cost and ROI by campaign"></svg> 1242 <div class="legend"><span><i class="pillar"></i>COST</span><span><i class="line"></i>ROI</span></div> 1243 </div> 1244 </div> 1245 </div> 1246 </div> 1247</div> 1248<div class="viz stepslide vzw" id="vz-ce-0"> 1249 <div class="step" data-step="0"> 1250 <div class="insight-grid ce"> 1251 <div class="ltv"> 1252 <svg id="ceSvg" viewBox="0 0 760 340" preserveAspectRatio="xMidYMid meet" aria-label="Cumulative ROI by day since install. Drag to set the LTV window."></svg> 1253 <div class="legend"><span><i></i>Cumulative ROI</span><span><i class="k"></i>IAP</span><span><i class="g"></i>IAA</span><span class="drag">drag the point to set the LTV window</span></div> 1254 </div> 1255 <div class="readout"> 1256 <p class="lead">Set the LTV window per title from its retention and payment timing, and the D1 ROI goal every campaign has to clear falls out of it.</p> 1257 <table> 1258 <tr><th>LTV window</th><td id="rWin"></td></tr> 1259 <tr class="goal"><th>D1 ROI goal, break-even at window</th><td class="big" id="rGoal"></td></tr> 1260 <tr><th>D1 ROI, live campaign</th><td id="rD1"></td></tr> 1261 <tr><th>Projected net UA revenue per $100K spent</th><td id="rNet"></td></tr> 1262 </table> 1263 <p class="verdict" id="rVerdict"></p> 1264 </div> 1265 </div> 1266 </div> 1267</div> 1268<div class="viz stepslide vzw" id="vz-ce-2"> 1269 <div class="step" data-step="2"> 1270 <div class="result two"> 1271 <div> 1272 <div class="giant"><span class="mask"><span class="line">+<span data-count="90">0</span>%</span></span></div> 1273 <p class="rline"><span class="mask"><span class="line" style="--i:1">net UA revenue over the year</span></span></p> 1274 </div> 1275 <div> 1276 <div class="giant"><span class="mask"><span class="line" style="--i:1">â<span data-count="35">0</span>%</span></span></div> 1277 <p class="rline"><span class="mask"><span class="line" style="--i:2">average CPI across key countries and OS</span></span></p> 1278 </div> 1279 </div> 1280 </div> 1281</div> 1282<div class="viz stepslide vzw" id="vz-ub-3"> 1283 <div class="step" data-step="3"> 1284 <div class="insight-grid"> 1285 <div class="anatomy"> 1286 <div class="anhead"><span class="t">Top asset, retention by second</span></div> 1287 <svg id="crAnat" viewBox="0 0 760 300" preserveAspectRatio="xMidYMid meet" aria-label="Retention curve over the video timeline with sequence tags"></svg> 1288 <div class="legend anatlegend"><span><i class="dash"></i>Before</span><span><i class="solid"></i>After the sequence change</span></div> 1289 </div> 1290 </div> 1291 </div> 1292</div> 1293</div> 1294<div class="casepage" id="casepage" role="dialog" aria-modal="true" aria-labelledby="csClaim"> 1295 <div class="cscroll" id="csScroll"> 1296 <div class="bar"><button class="close" data-cclose><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M12 4l-8 8 8 8"/></svg><span data-i18n="back">Back to list</span></button><span class="grey" id="csLabel"></span></div> 1297 <header class="cshero"> 1298 <h1 class="h-display rv" id="csClaim"></h1> 1299 <div class="whygrid"> 1300 <div class="rv"><h4 data-i18n="why">Why it mattered</h4><p id="csWhy"></p></div> 1301 <div class="rv"><h4 data-i18n="enabled">What it made possible</h4><p id="csEnabled"></p></div> 1302 </div> 1303 </header> 1304 <section class="cssec"><div class="sh"><h3 data-i18n="background">
1304Background</h3></div><div class="sb"><div class="metarow" id="csMeta"></div><div class="body" id="csBg"></div><div class="media" id="csMedia"></div><div class="mnav" id="csMediaNav"></div></div></section> 1305 <section class="cssec"><div class="sh"><h3 data-i18n="plan">Planning & Execution</h3></div><div class="sb" id="csPlan"></div></section> 1306 <section class="cssec"><div class="sh"><h3 data-i18n="result">Result</h3></div><div class="sb" id="csResult"></div></section> 1307 <p class="disc" id="csDisc"></p> 1308 </div> 1309</div> 1310<div class="detail" id="detail" role="dialog" aria-modal="true" aria-labelledby="dTitle"> 1311 <div class="scroll" id="dScroll"> 1312 <div class="bar"> 1313 <button class="close" data-close><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M12 4l-8 8 8 8"/></svg>Back to deck</button> 1314 <span id="dChapter" class="grey"></span> 1315 </div> 1316 <article> 1317 <h2 class="h-huge rev" id="dTitle"></h2> 1318 <div class="metarow rev" id="dMeta"></div> 1319 <div class="carousel rev" id="dCar" hidden> 1320 <div class="fandeck" id="dTrack" role="list"></div> 1321 <p class="cap" id="dCap"></p> 1322 </div> 1323 <div id="dBody"></div> 1324 </article> 1325 </div> 1326</div> 1327 1328<div class="casepage planpage" id="planpage" role="dialog" aria-modal="true" aria-labelledby="gpTitle"> 1329 <div class="cscroll" id="gpScroll"> 1330 <div class="bar"><button class="close" data-pclose><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M12 4l-8 8 8 8"/></svg><span data-i18n="back">Back to list</span></button><span class="grey">AI Exploration</span></div> 1331 <header class="phead"> 1332 <div class="lab rv" id="gpPlanLab"></div> 1333 <h1 class="rv" id="gpTitle"></h1> 1334 </header> 1335 <div class="pvenue"> 1336 <div class="cssec pgal"><div class="media rv" id="gpMedia"></div><div class="mnav" id="gpMediaNav"></div></div> 1337 </div> 1338 <div id="gpBody"></div> 1339 </div> 1340</div> 1341
1342<script src="data.js"></script>
vendor: 1 bytes, line 1342
1342
1343<script src="data_analysis_cases_ko.js"></script>
vendor: 1 bytes, line 1343
1343
1344<script src="heatmap_data.js"></script>
vendor: 1 bytes, line 1344
1344
1345<script src="hero_ko.js"></script>
vendor: 1 bytes, line 1345
1345
1346<script src="genshin_popup.js"></script>
vendor: 1 bytes, line 1346
1346
1347<script> 1348/* ========================================================= 1349 CONTENT CONFIG 1350 Everything editorial lives here or in data.js. 1351 ========================================================= */ 1352const CHAPTERS = { 1353 data: { label:'Data-Driven Marketing', keys:['da-brand-community','da-user-behaviour','da-campaign-effectiveness'] }, 1354 bd: { label:'Business Development', keys:['sf-gts-launch','sf-darkbouncer','gamirror','darkbouncer-bd','gameshows','tokatonton-pr','sf-influencer-community','ip-collab','dbs-microsite','dentsu','gd-platform-am'] }, 1355 ai: { label:'AI Exploration', keys:['hoyoverse-pv'] } 1356}; 1357 1358const ALL = Object.assign({}, typeof marketerProjects!=='undefined'?marketerProjects:{}, typeof communicatorProjects!=='undefined'?communicatorProjects:{}, typeof researcher!=='undefined'?researcher:{}, typeof dataAnalysisCases!=='undefined'?dataAnalysisCases:{}); 1359const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; 1360 1361/* ========================================================= 1362 COVER: giant word, fitted to width, letter by letter 1363 ========================================================= */ 1364const bigword = document.getElementById('bigword'); 1365'Portfolio'.split('').forEach((ch,i)=>{ const s=document.createElement('span'); s.className='l'; s.innerHTML=`<i style="--i:${i}">${ch}</i>`; bigword.appendChild(s); }); 1366function fitWord(){ bigword.style.fontSize='100px'; bigword.style.width='max-content'; const w=bigword.offsetWidth; bigword.style.width=''; const cw=bigword.parentElement.clientWidth; bigword.style.fontSize=(100*cw/w)+'px'; } 1367fitWord(); addEventListener('resize',fitWord); if(document.fonts) document.fonts.ready.then(fitWord); 1368document.getElementById('headDate').textContent='Doyoung Park'; 1369 1370/* ========================================================= 1371 TYPING 1372 ========================================================= */ 1373const typers = new Map(); 1374function typeLineBreaks(el,text){ 1375 const breaks = new Set(); const words = text.split(' '); 1376 if (words.length<2) return breaks; 1377 el.textContent = text; 1378 const textNode = el.firstChild, range = document.createRange(); 1379 let idx=0, prevTop=null; 1380 words.forEach((w,wi)=>{ 1381 if (w.length){ 1382 range.setStart(textNode,idx); range.setEnd(textNode,idx+w.length); 1383 const top = range.getBoundingClientRect().top; 1384 if (wi>0 && top!==prevTop) breaks.add(idx-1); 1385 prevTop = top; 1386 } 1387 idx += w.length+1; 1388 }); 1389 return breaks; 1390} 1391function typeIn(el){ 1392 if (typers.get(el)) return; 1393 const text = el.dataset.type; el.classList.remove('done'); 1394 const breaks = typeLineBreaks(el,text); 1395 el.textContent=''; 1396 const cur=document.createElement('span'); cur.className='cur'; el.appendChild(cur); 1397 if (reduce){ 1398 let last=0; 1399 breaks.forEach(b=>{ el.insertBefore(document.createTextNode(text.slice(last,b)),cur); el.insertBefore(document.createElement('br'),cur); last=b+1; }); 1400 el.insertBefore(document.createTextNode(text.slice(last)),cur); 1401 el.classList.add('done'); typers.set(el,true); return; 1402 } 1403 let i=0; const speed = text.length>16 ? 46 : 62; 1404 const t=setInterval(()=>{ 1405 if (breaks.has(i)){ el.insertBefore(document.createElement('br'),cur); i++; } 1406 else { el.insertBefore(document.createTextNode(text[i]),cur); i++; } 1407 if(i>=text.length){clearInterval(t); el.classList.add('done');} 1408 }, speed); 1409 typers.set(el,t); 1410} 1411function typeReset(el){ const t=typers.get(el); if(t&&t!==true) clearInterval(t); typers.delete(el); el.textContent=''; } 1412 1413/* ========================================================= 1414 COUNTERS 1415 ========================================================= */ 1416function count(el,instant){ 1417 const to=parseFloat(el.dataset.count), dec=+(el.dataset.dec||0), dur=1600, t0=performance.now(); 1418 const fmt=v=>dec?v.toFixed(dec):Math.round(v).toLocaleString('en-GB'); 1419 if(reduce||instant){ el.textContent=fmt(to); return; } 1420 (function f(now){ const p=Math.min(1,(now-t0)/dur); const e=1-Math.pow(1-p,4); el.textContent=fmt(to*e); if(p<1) requestAnimationFrame(f); })(t0); 1421} 1422 1423/* ========================================================= 1424 PROJECT LISTS 1425 ========================================================= */
1426Object.entries(CHAPTERS).forEach(([id,ch])=>{ 1427 const list=document.querySelector(`.list[data-chapter="${id}"]`); if(!list) return; 1428 const cases=ch.keys.every(k=>ALL[k]&&ALL[k].metric); 1429 if(cases) list.classList.add('cases'); else if(ch.keys.length<=8) list.classList.add('single'); 1430 ch.keys.forEach((k,i)=>{ 1431 const p=ALL[k]; if(!p) return; 1432 const b=document.createElement('button'); b.className='item fade'; b.style.setProperty('--i',i); b.dataset.open=k; 1433 b.innerHTML = cases 1434 ? `<span class="n">${String(i+1).padStart(2,'0')}</span><span class="t">${p.name}<small>${p.company}${p.title?' · '+p.title:''}</small></span><span class="m"><b>${p.metric}</b><span>${p.metricLabel||''}</span></span>` 1435 : `<span class="n">${String(i+1).padStart(2,'0')}</span><span class="t">${p.name}</span><span class="c">${p.company||p.year||''}</span>`; 1436 list.appendChild(b); 1437 }); 1438}); 1439 1440/* AI slide + full proposal overlay (Genshin popup) */ 1441const GP=typeof genshinPopup!=='undefined'?genshinPopup:{ko:{},en:{}}; 1442/* Korean is the master copy: an English key only wins once it exists. */ 1443const gp=k=>{ const o=GP[LANG]||{}; const v=o[k]; return (v===undefined||v===null||v.length===0)?(GP.ko||{})[k]:v; };
1444function renderPopupSlide(){ 1445 const set=(id,v)=>{ const el=document.getElementById(id); if(el) el.textContent=v||''; }; 1446 set('gpLab',gp('lab')); set('gpLine0',gp('line0')); set('gpLine1',gp('line1')); 1447 set('gpQ',gp('q')); set('gpIns',gp('ins')); 1448 document.querySelector('#gpBtn span').textContent=gp('btn')||''; 1449 document.getElementById('gpAi').innerHTML=`<h4>${gp('aiLab')||''}</h4><ul>${(gp('ai')||[]).map(x=>`<li>${x}</li>`).join('')}</ul>`; 1450} 1451 1452const planpage=document.getElementById('planpage'), gpScroll=document.getElementById('gpScroll'); 1453let planIO=null, planOpen=false; 1454/* a card stack like a case page's media (front card, next one waiting behind; 1455 dots, a horizontal wheel or a swipe change which is in front), wired to its 1456 own stage and dots so it can live outside the case page */ 1457function mediaStack(stage,nav){ 1458 let idx=0, lock=0, x0=null; 1459 const cards=()=>[...stage.querySelectorAll('.mcard')]; 1460 const render=()=>{ 1461 const cs=cards(); 1462 cs.forEach((c,i)=>{ c.classList.remove('front','next'); const rel=i-idx; if(rel===0) c.classList.add('front'); else if(rel===1) c.classList.add('next'); }); 1463 nav.innerHTML=cs.length>1?cs.map((_,i)=>`<span class="${i===idx?'on':''}"></span>`).join(''):''; 1464 }; 1465 const step=d=>{ const next=idx+d; if(next<0||next>=cards().length) return false; idx=next; render(); return true; }; 1466 nav.addEventListener('click',e=>{ 1467 const dot=e.target.closest('span'); if(!dot) return; 1468 const i=[...nav.children].indexOf(dot); if(i<0||i===idx) return; 1469 idx=i; render(); 1470 }); 1471 stage.addEventListener('wheel',e=>{ 1472 if(cards().length<2) return; 1473 if(Math.abs(e.deltaX)<8 || Math.abs(e.deltaX)<Math.abs(e.deltaY)) return; 1474 const now=Date.now(); if(now-lock<550){ e.preventDefault(); return; } 1475 if(step(e.deltaX>0?1:-1)){ lock=now; e.preventDefault(); } 1476 },{passive:false}); 1477 stage.addEventListener('pointerdown',e=>{ x0=e.clientX; }); 1478 stage.addEventListener('pointerup',e=>{ if(x0===null) return; const dx=e.clientX-x0; x0=null; if(Math.abs(dx)>30) step(dx<0?1:-1); }); 1479 stage.addEventListener('pointercancel',()=>{ x0=null; }); 1480 return { set(html){ stage.innerHTML=html; idx=0; render(); } }; 1481} 1482const gpGallery=mediaStack(document.getElementById('gpMedia'),document.getElementById('gpMediaNav')); 1483function renderPlan(){ 1484 document.getElementById('gpPlanLab').textContent=gp('lab')||''; 1485 document.getElementById('gpTitle').textContent=`${gp('line0')||''} ${gp('line1')||''}`.trim(); 1486 gpGallery.set((gp('gallery')||[]).map(g=>`<div class="mcard"><img src="${g.src}" alt="${g.alt}" loading="lazy" onerror="this.closest('.mcard').remove()"></div>`).join('')); 1487 document.getElementById('gpBody').innerHTML=(gp('sections')||[]).map(sec=> 1488 `<section class="cssec"><div class="sh"><h3>${sec.h}</h3></div><div class="sb">${sec.html.replace(/<(p|ul) class="/g,'<$1 class="rv ')}</div></section>`).join(''); 1489} 1490function openPlan(){ 1491 planOpen=true; 1492 planpage.classList.toggle('ko',LANG==='ko'); 1493 applyI18n(); renderPlan(); 1494 if(planIO) planIO.disconnect(); 1495 planIO=new IntersectionObserver(es=>es.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add('on'); planIO.unobserve(e.target); } }),{root:gpScroll,threshold:.1}); 1496 planpage.querySelectorAll('.rv').forEach(el=>{ el.classList.remove('on'); planIO.observe(el); }); 1497 gpScroll.scrollTop=0; 1498 planpage.classList.add('open'); 1499 document.getElementById('chrome').classList.add('hide-head'); 1500 planpage.querySelector('.close').focus(); 1501} 1502function closePlan(){ 1503 if(!planOpen) return; 1504 planOpen=false; 1505 planpage.classList.remove('open'); 1506 document.getElementById('chrome').classList.remove('hide-head'); 1507} 1508document.addEventListener('click',e=>{ 1509 if(e.target.closest('[data-plan]')){ openPlan(); return; } 1510 if(e.target.closest('[data-pclose]')) closePlan(); 1511}); 1512addEventListener('keydown',e=>{ if(e.key==='Escape'&&planOpen) closePlan(); },true); 1513 1514/* ========================================================= 1515 DETAIL OVERLAY 1516 ========================================================= */ 1517/* ========================================================= 1518 STEPPER (v3): Data â Insight â Result on the two viz slides 1519 ========================================================= */ 1520/* ========================================================= 1521 USER BEHAVIOUR: heatmaps A (UC top-up timing) and B (eROAS by 1522 target/platform/appeal). Data lives in heatmap_data.js; these just 1523 render whatever rows/cols/groups it holds, in the current LANG. 1524 ========================================================= */ 1525function esc(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
1526function heatmapColor(v,brackets){ return brackets.find(b=>v>=b.min)||brackets[brackets.length-1]; } 1527/* sub is optional â chart B carries its scale in the legend instead */ 1528function hmHead(d){ return `<div class="hm-head"><h5 class="hm-title">${d.title}</h5>${d.sub?`<p class="hm-sub">${d.sub}</p>`:''}</div>`; } 1529function hmCaption(d){ return d.sub?`${d.title} â ${d.sub}`:d.title; } 1530function renderHeatmapA(container){ 1531 if(!container) return; 1532 const d=heatmapAData; 1533 const theadCols=d.cols.map(c=>`<th scope="col"${c===d.launchCol?' class="hm-launch"':''}>${c}</th>`).join(''); 1534 const bodyRows=d.rows.map(r=>{ 1535 const cells=r.values.map((v,i)=>{ 1536 const col=d.cols[i], {bg,fg}=heatmapColor(v,HEATMAP_A_BRACKETS); 1537 const label=`${r.label}, ${col}, ${v}%`; 1538 return `<td${col===d.launchCol?' class="hm-launch"':''} tabindex="0" style="background:${bg};color:${fg}" data-row="${esc(r.label)}" data-col="${esc(col)}" data-val="${v}" aria-label="${esc(label)}">${v}%</td>`; 1539 }).join(''); 1540 return `<tr><th scope="row">${r.label}</th>${cells}</tr>`; 1541 }).join(''); 1542 const rowPct=16, colPct=((100-rowPct)/d.cols.length).toFixed(3); 1543 container.innerHTML=` 1544 ${hmHead(d)} 1545 <div class="hm-scroll"><table class="hm-table hm-a"> 1546 <caption class="sr-only">${esc(hmCaption(d))}</caption> 1547 <colgroup><col style="width:${rowPct}%">${d.cols.map(()=>`<col style="width:${colPct}%">`).join('')}</colgroup> 1548 <thead> 1549 <tr class="hm-axis-row" aria-hidden="true"><th class="hm-rowhead"></th><th colspan="${d.cols.length}">${esc(d.colAxis)}</th></tr> 1550 <tr><th scope="col" class="hm-rowhead">${esc(d.rowAxis)}</th>${theadCols}</tr> 1551 </thead> 1552 <tbody>${bodyRows}</tbody> 1553 </table></div> 1554 <div class="hm-legend"><span class="hm-legend-label">${d.legend[0]}</span><span class="hm-legend-bar">${HEATMAP_A_BRACKETS.slice().reverse().map(b=>`<i style="background:${b.bg}"></i>`).join('')}</span><span class="hm-legend-label">${d.legend[1]}</span></div> 1555 <p class="hm-info" id="hmAInfo" aria-live="polite"> </p>`; 1556 const info=container.querySelector('#hmAInfo'); 1557 container.querySelectorAll('td[data-val]').forEach(td=>{ 1558 const show=()=>{ info.textContent=`${td.dataset.row} · ${td.dataset.col}: ${td.dataset.val}%`; }; 1559 td.addEventListener('mouseenter',show); td.addEventListener('focus',show); td.addEventListener('click',show); 1560 }); 1561} 1562function renderHeatmapB(container){ 1563 if(!container) return; 1564 const d=heatmapBData; 1565 const headCols=d.cols.map(c=>`<th scope="col">${c}</th>`).join(''); 1566 let bodyRows=''; 1567 d.groups.forEach((g,gi)=>{ 1568 g.rows.forEach(r=>{ 1569 const rowLabel=`${g.label} · ${r.platform}`; 1570 const cells=r.eroas.map((v,i)=>{ 1571 const cost=r.cost[i], revenue=Math.round(cost*v/100), colLabel=d.cols[i]; 1572 const {bg,fg}=heatmapColor(v,HEATMAP_B_BRACKETS); 1573 const label=`${rowLabel}, ${colLabel}, eROAS ${v}%`; 1574 return `<td tabindex="0" style="background:${bg};color:${fg}" data-row="${esc(rowLabel)}" data-col="${esc(colLabel)}" data-eroas="${v}" data-cost="${cost}" data-rev="${revenue}" aria-label="${esc(label)}">${v}%</td>`; 1575 }).join(''); 1576 bodyRows+=`<tr><th scope="row">${rowLabel}</th>${cells}</tr>`; 1577 }); 1578 if(gi<d.groups.length-1) bodyRows+=`<tr class="hm-gap" aria-hidden="true"><td colspan="${d.cols.length+1}"></td></tr>`; 1579 }); 1580 const legendChips=d.legendSteps.map((s,i)=>`<span class="hm-chip"><i style="background:${d.legendColors[i]}"></i>${s}</span>`).join(''); 1581 const rowPct=22, colPct=((100-rowPct)/d.cols.length).toFixed(3); 1582 container.innerHTML=` 1583 ${hmHead(d)} 1584 <div class="hm-scroll"><table class="hm-table hm-b"> 1585 <caption class="sr-only">${esc(hmCaption(d))}</caption> 1586 <colgroup><col style="width:${rowPct}%">${d.cols.map(()=>`<col style="width:${colPct}%">`).join('')}</colgroup> 1587 <thead> 1588 <tr class="hm-axis-row" aria-hidden="true"><th class="hm-rowhead"></th><th colspan="${d.cols.length}">${esc(d.colAxis)}</th></tr> 1589 <tr><th scope="col" class="hm-rowhead">${esc(d.rowAxis)}</th>${headCols}</tr> 1590 </thead> 1591 <tbody>${bodyRows}</tbody> 1592 </table></div> 1593 <div class="hm-legend wrap">${legendChips}</div>
1594 <p class="hm-info" id="hmBInfo" aria-live="polite">${d.defaultInfo}</p>`; 1595 const info=container.querySelector('#hmBInfo'); 1596 container.querySelectorAll('td[data-eroas]').forEach(td=>{ 1597 const show=()=>{ info.textContent=`${td.dataset.row} / ${td.dataset.col}: eROAS ${td.dataset.eroas}%, spend $${td.dataset.cost}K, projected revenue $${td.dataset.rev}K`; }; 1598 td.addEventListener('mouseenter',show); td.addEventListener('focus',show); td.addEventListener('click',show); 1599 }); 1600} 1601 1602/* ========================================================= 1603 CAMPAIGN EFFECTIVENESS, data step: daily tracking board (illustrative) 1604 Mirrors the KPI sheet: cost per day + ROI per campaign against the D1 goal. 1605 ========================================================= */ 1606const CEB = { 1607 days:['Mon 07','Tue 08','Wed 09','Thu 10','Fri 11','Sat 12','Sun 13'], 1608 cost:[42,61,58,36,86,92,71], // total daily cost, index 1609 goal:.25, 1610 camps:[ 1611 {name:'KR · Android · Purchase', color:'var(--blue)', roi:[.27,.28,.27,.30,.31,.33,.34]}, 1612 {name:'JP · iOS · AEO', color:'var(--ink)', roi:[.19,.24,.23,.26,.27,.27,.26]}, 1613 {name:'US · Android · tROAS', color:'rgba(0,0,0,.45)', roi:[.14,.21,.22,.24,.24,.25,.24]}, 1614 {name:'WW · Android · Install', color:'rgba(0,0,0,.22)', roi:[.09,.10,.11,.08,.08,.06,.07]} 1615 ], 1616 chips:{ 'Channel':['FB','UAC','TikTok','Moloco','Liftoff','Unity Ads','Pangle','Snapchat','Twitter','ASA'], 1617 'Event':['Install','AEO','VO','IAP','Purchase','Retention','tROAS','MAI','ARO'], 1618 'Country':['KR','JP','US','TW','WW','Tier 1','Tier 2','Tier 3','HK','SG','UK','AU','FRDE','SEA'] } 1619}; 1620const ceBoard=document.getElementById('ceBoard'); 1621const CBG={W:760,H:320,pl:40,pr:44,pt:20,pb:30}; 1622let cbHot=-1; 1623function ceBoardBuild(){ 1624 const {W,H,pl,pr,pt,pb}=CBG, n=CEB.days.length, cw=(W-pl-pr)/n, bw=cw*.62; 1625 const cmax=Math.max(...CEB.cost)*1.15, rmax=Math.max(...CEB.camps.flatMap(c=>c.roi))*1.25; 1626 const xc=i=>pl+cw*(i+.5), yc=v=>pt+(1-v/cmax)*(H-pt-pb), yr=v=>pt+(1-v/rmax)*(H-pt-pb); 1627 let g=''; 1628 [.25,.5,.75,1].forEach(f=>{ g+=`<line x1="${pl}" x2="${W-pr}" y1="${yr(rmax*f)}" y2="${yr(rmax*f)}" stroke="rgba(0,0,0,.1)"/><text x="${W-pr+8}" y="${yr(rmax*f)+4}">${(rmax*f*100).toFixed(0)}%</text>`; }); 1629 let ct=[.25,.5,.75,1].map(f=>`<text x="${pl-8}" y="${yc(cmax*f)+4}" text-anchor="end">${Math.round(cmax*f)}</text>`).join(''); 1630 let bars=CEB.cost.map((c,i)=>`<rect class="bar" style="--i:${i}" x="${xc(i)-bw/2}" y="${yc(c)}" width="${bw}" height="${H-pb-yc(c)}"/>`).join(''); 1631 let lines=CEB.camps.map(c=>`<path class="ln" stroke="${c.color}" d="${c.roi.map((v,i)=>(i?'L':'M')+xc(i).toFixed(1)+' '+yr(v).toFixed(1)).join(' ')}"/>`).join(''); 1632 let dots=CEB.camps.map((c,k)=>`<circle class="dot" data-k="${k}" r="4.5" stroke="${c.color}"/>`).join(''); 1633 let xt=CEB.days.map((d,i)=>`<text x="${xc(i)}" y="${H-8}" text-anchor="middle">${d}</text>`).join(''); 1634 ceBoard.innerHTML=`${g}${ct}${bars}<line class="goal" x1="${pl}" x2="${W-pr}" y1="${yr(CEB.goal)}" y2="${yr(CEB.goal)}"/><text x="${pl}" y="${yr(CEB.goal)-6}" fill="var(--ink)" font-weight="600">D1 ROI goal ${(CEB.goal*100).toFixed(0)}%</text>${lines}<line class="cursor" y1="${pt}" y2="${H-pb}"/>${dots}${xt}`; 1635 ceBoard._geo={xc,yr,cw}; 1636} 1637function ceBoardHover(i){ 1638 cbHot=i; const {xc,yr}=ceBoard._geo; 1639 ceBoard.querySelectorAll('.bar').forEach((b,k)=>b.classList.toggle('hot',k===i)); 1640 const cur=ceBoard.querySelector('.cursor'); cur.classList.toggle('show',i>=0); if(i>=0){ cur.setAttribute('x1',xc(i)); cur.setAttribute('x2',xc(i)); } 1641 ceBoard.querySelectorAll('.dot').forEach(d=>{ const k=+d.dataset.k; d.classList.toggle('show',i>=0); if(i>=0){ d.setAttribute('cx',xc(i)); d.setAttribute('cy',yr(CEB.camps[k].roi[i])); } }); 1642} 1643ceBoard.addEventListener('pointermove',e=>{ const r=ceBoard.getBoundingClientRect(); const x=(e.clientX-r.left)/r.width*CBG.W; const i=Math.floor((x-CBG.pl)/ceBoard._geo.cw); if(i>=0&&i<CEB.days.length&&i!==cbHot) ceBoardHover(i); }); 1644ceBoard.addEventListener('pointerleave',()=>ceBoardHover(-1)); 1645ceBoardBuild(); ceBoardHover(-1); 1646 1647/* ========================================================= 1648 CAMPAIGN EFFECTIVENESS, insight step: LTV window â D1 ROI goal (illustrative) 1649 ROI(d) = D1 à [ IAPshare à mIAP(d) + IAAshare à mIAA(d) ] 1650 Goal = 1 / [ IAPshare à mIAP(W) + IAAshare à mIAA(W) ] 1651 ========================================================= */
1652const CE = { iap:.64, d1:.25, A:{iap:5.5,iaa:1.8}, tau:{iap:28,iaa:14} }; 1653let ceW=45, ceDrag=false; 1654const ceSvg=document.getElementById('ceSvg'); 1655const CEG={W:760,H:340,pl:44,pr:16,pt:28,pb:34,days:90}; 1656function mult(d,k){ return 1+CE.A[k]*(1-Math.exp(-d/CE.tau[k])); } 1657const fT=d=>CE.d1*(CE.iap*mult(d,'iap')+(1-CE.iap)*mult(d,'iaa')), fI=d=>CE.d1*CE.iap*mult(d,'iap'), fA=d=>CE.d1*(1-CE.iap)*mult(d,'iaa'); 1658const ymax=Math.max(1.4,fT(90)*1.08); 1659const cx=d=>CEG.pl+(d-1)/(CEG.days-1)*(CEG.W-CEG.pl-CEG.pr), cy=v=>CEG.pt+(1-v/ymax)*(CEG.H-CEG.pt-CEG.pb); 1660function ceStatic(){ 1661 const {W,H,pl,pr,pt,pb,days}=CEG; 1662 const path=f=>{ let s=''; for(let d=1;d<=days;d++) s+=(d===1?'M':'L')+cx(d).toFixed(1)+' '+cy(f(d)).toFixed(1)+' '; return s; }; 1663 let grid=''; for(let v=0.5; v<ymax; v+=0.5) grid+=`<line x1="${pl}" x2="${W-pr}" y1="${cy(v)}" y2="${cy(v)}"/><text x="${pl-8}" y="${cy(v)+4}" text-anchor="end">${(v*100).toFixed(0)}%</text>`; 1664 let xt=''; [1,15,30,45,60,75,90].forEach(d=>{ xt+=`<text x="${cx(d)}" y="${H-10}" text-anchor="middle">D${d}</text>`; }); 1665 let be=null; for(let d=1;d<=days;d++) if(fT(d)>=1){ be=d; break; } 1666 ceSvg.innerHTML=`<rect class="hit" x="0" y="0" width="${W}" height="${H}"/><g class="grid">${grid}</g>${xt} 1667 <line class="be" x1="${pl}" x2="${W-pr}" y1="${cy(1)}" y2="${cy(1)}"/><text class="lbl" x="${W-pr}" y="${cy(1)-6}" text-anchor="end">break-even</text> 1668 <path class="p-iaa" d="${path(fA)}"/><path class="p-iap" d="${path(fI)}"/><path class="p-total" d="${path(fT)}"/> 1669 ${be?`<circle cx="${cx(be)}" cy="${cy(1)}" r="4" fill="var(--ink)"/><text x="${cx(be)}" y="${cy(1)+18}" text-anchor="middle">break-even D${be}</text>`:''} 1670 <line class="win" id="ceWinLine" x1="0" x2="0" y1="${pt}" y2="${H-pb}"/> 1671 <text class="lbl" id="ceWinLbl" x="0" y="0"></text> 1672 <circle class="wdot" id="ceDot" r="7" cx="0" cy="0"/>`; 1673} 1674function ceRender(){ 1675 const iap=CE.iap, iaa=1-iap, d1=CE.d1; 1676 /* net is a per-unit-spend ratio, so the base is just a display choice: 1677 read against $100K of spend it stays the same number, in $K */ 1678 const M=iap*mult(ceW,'iap')+iaa*mult(ceW,'iaa'), goal=1/M, roi=d1*M, net=(roi-1)*100; 1679 document.getElementById('rWin').textContent='D'+ceW; 1680 document.getElementById('rGoal').textContent=(goal*100).toFixed(1)+'%'; 1681 document.getElementById('rD1').textContent=(d1*100).toFixed(1)+'%'; 1682 document.getElementById('rNet').textContent=(net>=0?'+':'â')+'$'+Math.abs(net).toFixed(1)+'K'; 1683 document.getElementById('rVerdict').innerHTML = d1>=goal 1684 ? `Live D1 ROI clears the goal at a ${ceW}-day window: <b>scale</b> the campaign and hold the window.` 1685 : `Live D1 ROI is <b>${((goal-d1)*100).toFixed(1)} pts short</b> of the goal at a ${ceW}-day window: cut bids or fix creative before adding budget.`; 1686 const X=cx(ceW), Y=cy(fT(ceW)); 1687 const l=document.getElementById('ceWinLine'); l.setAttribute('x1',X); l.setAttribute('x2',X); 1688 const dot=document.getElementById('ceDot'); dot.setAttribute('cx',X); dot.setAttribute('cy',Y); 1689 const lb=document.getElementById('ceWinLbl'); lb.setAttribute('x',X+(ceW>70?-10:10)); lb.setAttribute('y',Y-12); lb.setAttribute('text-anchor',ceW>70?'end':'start'); lb.textContent=`${(fT(ceW)*100).toFixed(0)}% at D${ceW}`; 1690} 1691function ceFromEvent(e){ const r=ceSvg.getBoundingClientRect(); const xs=(e.clientX-r.left)/r.width*CEG.W; const d=Math.round(1+(xs-CEG.pl)/(CEG.W-CEG.pl-CEG.pr)*(CEG.days-1)); return Math.min(CEG.days,Math.max(7,d)); } 1692ceSvg.addEventListener('pointerdown',e=>{ ceDrag=true; ceSvg.setPointerCapture(e.pointerId); document.getElementById('ceDot').classList.add('drag'); ceW=ceFromEvent(e); ceRender(); }); 1693ceSvg.addEventListener('pointermove',e=>{ if(!ceDrag) return; ceW=ceFromEvent(e); ceRender(); }); 1694['pointerup','pointercancel'].forEach(ev=>ceSvg.addEventListener(ev,()=>{ ceDrag=false; document.getElementById('ceDot').classList.remove('drag'); })); 1695ceSvg.addEventListener('keydown',e=>{ if(e.key==='ArrowRight'){ceW=Math.min(90,ceW+1);ceRender();} if(e.key==='ArrowLeft'){ceW=Math.max(7,ceW-1);ceRender();} }); 1696ceSvg.setAttribute('tabindex','0'); 1697ceStatic(); ceRender(); 1698 1699/* ========================================================= 1700 Retention chart: one top asset read second by second (illustrative) 1701 ========================================================= */ 1702const CRA = { 1703 beats:[['Hook',0,3],['Character',3,10],['Gameplay',10,22],['CTA',22,30]], 1704 before:[[0,100],[1,92],[3,78],[5,70],[8,64],[10,58],[13,47],[16,40],[20,35],[25,30],[30,25]], 1705 after: [[0,100],[1,95],[3,86],[5,80],[8,75],[10,71],[13,66],[16,61],[20,56],[25,50],[30,44]] 1706}; 1707/* the chart always shows the after-the-sequence-change curve, with the 1708 before curve underneath as a dashed reference (see .anatlegend) */ 1709function crAnatRender(){ 1710 const svg=document.getElementById('crAnat'), W=760,H=300,pl=40,pr=16,pt=16,pb=58;
1711 const x=t=>pl+t/30*(W-pl-pr), y=v=>pt+(1-v/100)*(H-pt-pb); 1712 const path=pts=>pts.map((p,i)=>(i?'L':'M')+x(p[0]).toFixed(1)+' '+y(p[1]).toFixed(1)).join(' '); 1713 const cur=CRA.after; 1714 let g=''; [25,50,75,100].forEach(v=>{ g+=`<line class="gl" x1="${pl}" x2="${W-pr}" y1="${y(v)}" y2="${y(v)}"/><text x="${pl-6}" y="${y(v)+4}" text-anchor="end">${v}%</text>`; }); 1715 const beats=CRA.beats.map(b=>`<rect class="beat ${b[0].toLowerCase()}" x="${x(b[1])}" y="${H-pb+14}" width="${x(b[2])-x(b[1])}" height="22"/><text class="k" x="${x(b[1])+6}" y="${H-pb+29}">${b[0]}</text><text x="${x(b[1])}" y="${H-pb+52}">${b[1]}s</text>`).join(''); 1716 const v3=cur.find(p=>p[0]===3)[1], v13=cur.find(p=>p[0]===13)[1]; 1717 const marks=`<line class="drop" x1="${x(3)}" x2="${x(3)}" y1="${y(100)}" y2="${H-pb+14}"/><text class="k" x="${x(3)+6}" y="${y(100)+4}">3 s: ${100-v3} pts lost</text> 1718 <line class="drop b" x1="${x(13)}" x2="${x(13)}" y1="${y(v13)-30}" y2="${H-pb+14}"/><text x="${x(13)+6}" y="${y(v13)-34}">gameplay segment replaced with a different appeal</text>`; 1719 svg.innerHTML=`${g}${beats}<path class="ghost" d="${path(CRA.before)}"/><path class="curve" d="${path(cur)}"/>${marks}<text x="${x(30)}" y="${y(cur[cur.length-1][1])-10}" text-anchor="end" class="k">${cur[cur.length-1][1]}% reach the end</text>`; 1720} 1721crAnatRender(); 1722 1723/* ========================================================= 1724 v7: REPORTS DESK + INTEGRATED CASE PAGE (EN / KO) 1725 EN text comes from data.js (dataAnalysisCases), 1726 KO text from data_analysis_cases_ko.js. Hero copy lives here. 1727 ========================================================= */ 1728let LANG='en'; 1729document.documentElement.lang='en'; 1730const KO = typeof dataAnalysisCasesKo!=='undefined' ? dataAnalysisCasesKo : {}; 1731const I18N = { 1732 en:{ back:'Back to list', why:'Why it mattered', enabled:'What it made possible', background:'Background', plan:'Planning & Execution', result:'Result', report:'Data report', company:'Company', year:'Year', title:'Title', skills:'Skills', capData:'Data', capInsight:'Insight', capResult:'Result' }, 1733 ko:{ back:'목ë¡ì¼ë¡ ëìê°ê¸°', why:'Why it mattered', enabled:'What it made possible', background:'Background', plan:'Planning & Execution', result:'Result', report:'ë°ì´í° 리í¬í¸', company:'Company', year:'ì°ë', title:'Title', skills:'ì¤í¬', capData:'ë°ì´í°', capInsight:'ì¸ì¬ì´í¸', capResult:'ê²°ê³¼' } 1734}; 1735const CASES = [ 1736 { id:'bc', key:'da-brand-community', num:'01', 1737 title:{en:'Brand & Community',ko:'ë¸ëë & 커뮤ëí°'}, 1738 sub:{en:'Brand Marketing through IP Collaboration and Community Strategy',ko:'IPì½ë¼ë³´ ë¸ëë ë§ì¼í ê³¼ 커뮤ëí° ìì± '}, 1739 claim:{en:'Ads aren\'t always the answer.',ko:'ëë¡ë ê´ê³ ë§ì´ ë¥ì¬ë ìëì£ .'}, 1740 why:{en:'Paid UA brings players in, but it\'s brand and community that make them care, and stay. At PUBG MOBILE Japan, timelines across multiple IP holders, the Seoul and Tokyo offices and our agency all overlapped at once. At SANDY FLOOR, the task was to reach more players on a small budget. Both called for the same thing: reading the situation quickly, understanding where others stood, and moving the project forward without a set playbook.',ko:'Paid UA를 íµí 모ê°ë ì¤ìíì§ë§, ì ì ê° ê²ìì ì¢ìíê³ ë¨¸ë¬¼ê² ë§ëë ê±´ ë¸ëëì 커뮤ëí°ì 몫ì ëë¤. PUBG MOBILE ì¼ë³¸ììë ì¬ë¬ IP íëì ìì¸Â·ëì¿ ì¤í¼ì¤, ëíì¬ì ì¼ì ì´ í꺼ë²ì ì½í ììê³ , SANDY FLOORììë ì ì ìì°ì¼ë¡ ì ì ìì ì ì ì ëíì¼ íìµëë¤. ë ê³³ 모ë ì í´ì§ ëµë³´ë¤ ìí©ì ë¹ ë¥´ê² ì½ê³ , ìëì ì ì¥ì ì´í´íë©° íë¡ì í¸ë¥¼ ì´ëì´ ëê°ë íì´ íìíìµëë¤.'}, 1741 enabled:{en:'Our Dragon Ball Super microsite campaign, designed to go viral, reached No. 8 on Japan\'s real-time X trends. An esports tournament with eight popular streamers lifted DAU in Korea. And coverage in more than eight Japanese gaming outlets more than doubled monthly Steam visitors.',ko:'í¹ì¤ ì¬ì´í¸ë¥¼ ì¤ì¬ì¼ë¡ ë°ì´ë´ì ì¤ê³í Dragon Ball Super ì½ë¼ë³´ë ì¼ë³¸ X ì¤ìê° í¸ë ë 8ìì ì¬ëìµëë¤. ì¸ê¸° ì¤í¸ë¦¬ë¨¸ 8ì¸ê³¼ í¨ê»í eì¤í¬ì¸ ëíë¡ ê²ìì ìë ¤ íêµ DAU를 ëì´ì¬ë ¸ê³ , ì¼ë³¸ ê²ì 매체 8ê³³ ì´ìì 기ì¬ê° ì¤ë¦¬ë©° ì¤í ìê° ë°©ë¬¸ì ì를 ì ì ëë¹ 2ë°° ì´ì ëë ¸ìµëë¤.'}, 1742 disc:{en:'',ko:''} }, 1743 { id:'ub', key:'da-user-behaviour', num:'02', 1744 title:{en:'From User Behaviour to Creative',ko:'ì ì íëìì í¬ë¦¬ìì´í°ë¸ê¹ì§'}, 1745 sub:{en:'Optimising marketing budget and Creative through eROAS analysis',ko:'eROAS ë¶ìì íµí ë§ì¼í ìì° ìµ
1745ì í ë° í¬ë¦¬ìì´í°ë¸ ê³ ëí'}, 1746 claim:{en:'Finding solutions from user behaviour.',ko:'ì ì ì íëìì ëµì ì°¾ìµëë¤.'}, 1747 why:{en:"Among paid items with different prices and business models, I set out to find which ones truly generated high revenue relative to their marketing spend.",ko:'ìë¡ ë¤ë¥¸ ê°ê²©ê³¼ BMì ê°ì§ ì ë£ ìí ì¤, ì¤ì ë¡ ë§ì¼í ë¹ì© ëë¹ ëì 매ì¶ì ë§ë¤ì´ë´ë ìíì´ ë¬´ìì¸ì§ íì íê³ ì ì ê·¼íìµëë¤.'}, 1748 enabled:{en:'I proposed a UA budget allocation by skin selling point, and the methodology was reflected in the actual marketing budget. At the same time, I rebuilt the creative operations framework so these insights could lead to better ad efficiency. As a result, I identified the most effective asset formats and hooks for a specific skin business model, meeting the monthly revenue KPI while growing managed ad spend by more than 130% year on year.',ko:'ì¤í¨ ìêµ¬ë³ UA ìì° ë°°ë¶ìì ì ìíê³ , í´ë¹ ë°©ë²ë¡ ì´ ì¤ì ë§ì¼í ìì°ì ë°ìë ì ìììµëë¤. ëìì ì´ë¬í ì¸ì¬ì´í¸ê° ê´ê³ í¨ì¨ ê°ì ì¼ë¡ ì´ì´ì§ ì ìëë¡ í¬ë¦¬ìì´í°ë¸ ì´ì ì²´ê³ë ì¬ì ë¹íììµëë¤. ê·¸ ê²°ê³¼ í¹ì ì¤í¨ BMìì í¨ê³¼ì ì¸ ìì¬ ê·ê²©ê³¼ íí¹ í¬ì¸í¸ë¥¼ ë°êµ´íëë° ì±ê³µíì¬, ì ë§¤ì¶ KPI를 ë¬ì±íë ëìì ì´ì© ê´ê³ ë¹ ê·ëª¨ë¥¼ ì ë ëë¹ 130% ì´ì ì±ì¥ìì¼°ìµëë¤.'}, 1749 disc:{en:'<b>Figures are illustrative placeholders, not actual data.</b> The segmentation, the formula and the direction of the findings reflect the real method.',ko:'<b>ìì¹ë ì¤ì ë°ì´í°ê° ìë ììì ëë¤.</b> ì¸ê·¸ë¨¼í ì´ì , ì°ì¶ ê³µì, ê²°ê³¼ì ë°©í¥ì±ì ì¤ì ë°©ë²ë¡ ì ë°ìí©ëë¤.'} }, 1750 { id:'ce', key:'da-campaign-effectiveness', num:'03', 1751 title:{en:'Campaign Effectiveness',ko:'ìº íì¸ í¨ê³¼ 측ì '}, 1752 sub:{en:'LTV-based KPI setting. And efficient campaign management to reach the goal.',ko:'LTV기ë°ì KPI ì¸í . ê·¸ë¦¬ê³ ëª©í를 ìí í¨ì¨ì ì¸ ìº íì¸ ê´ë¦¬'}, 1753 claim:{en:'To reach the right goal in the shortest time.',ko:'ì¬ë°ë¥¸ 목íì ìµëí 빨리 ëë¬í기 ìí´.'}, 1754 why:{en:"Five live titles with a different IAP and IAA mix in every country and OS, so a single KPI could not tell me which campaigns were actually working. On iOS in particular, SKAN postbacks were too unstable and too delayed to move budget on.",ko:'ë¼ì´ë¸ ì¤ì¸ 5ê° íì´íì êµê°ì OSë§ë¤ IAP·IAA ë¹ì¤ì´ ì ê°ê°ì´ì기ì, íëì KPIë¡ë ì´ë¤ ìº íì¸ì´ ì¤ì ë¡ í¨ì¨ì ë´ê³ ìëì§ íë¨í ì ìììµëë¤. í¹í iOSë SKAN í¬ì¤í¸ë°± ë°ì´í°ê° ë¶ìì íê³ ì§ì°ì´ 커ì, ê·¸ ìì¹ë§ ë³´ê³ ìì°ì ìì§ì´ê¸° ì´ë ¤ì´ ìí©ì´ììµëë¤.'}, 1755 enabled:{en:"I built a KPI framework derived from how each title's players actually pay, turning the monthly target from a guess into a figure I could recalculate every month. Adding an internal modelled-ROI system that worked around SKAN's limits let me evaluate iOS campaigns reliably and feed them into budget allocation, and net UA revenue grew 90% over the year while average CPI across key countries and operating systems fell 35%.",ko:'íì´íë§ë¤ ì ì ê° ê²°ì íë ë°©ììì ìì°í KPI ì²´ê³ë¥¼ ë§ë¤ì´, ìê° ëª©í를 ê°ì´ ìëë¼ ë§¤ë¬ ë¤ì ê³ì°í ì ìë ìì¹ë¡ ë°ê¿¨ìµëë¤. ì¬ê¸°ì SKANì íê³ë¥¼ ì°ííë ìì²´ Modeled ROI ì²´ê³ë¥¼ ëí´ iOS ìº íì¸ê¹ì§ ì 뢰ë ìê² íê°íê³ ìì° ë°°ë¶ì ë°ìí ì ììê³ , ê·¸ ê²°ê³¼ UA ìììµì ì°ì´ ëë¹ 90% ì±ì¥íê³ ì£¼ì êµê°Â·OSì íê· CPIë 35% íë½íìµëë¤.'}, 1756 disc:{en:'<b>Figures are illustrative placeholders, not actual data.</b>The tracking structure, the LTV window logic and the D1 ROI goal derivation mirror the real KPI sheet.',ko:'<b>ìì¹ë ì¤ì ë°ì´í°ê° ìë ììì ëë¤.</b> ì¶ì 구조, LTV ìëì° ë¡ì§, D1 ROI 목í ì°ì¶ ë°©ìì ì¤ì KPI ìí¸ë¥¼ ë°ìí©ëë¤.'} } 1757]; 1758const T=(k)=>I18N[LANG][k]; 1759function applyI18n(){ document.querySelectorAll('[data-i18n]').forEach(el=>{ el.textContent=T(el.dataset.i18n); }); } 1760 1761/* desk list: text rows (number, title, one-line result), contents-page style */ 1762const deskCards=document.getElementById('deskCards'); 1763const deskEls=[]; 1764CASES.forEach((c,i)=>{ 1765 const li=document.createElement('li'); li.className='deskline-item'; li.style.setProperty('--i',i); 1766 const b=document.createElement('button'); b.dataset.case=c.id; b.setAttribute('aria-label',c.title.en); 1767 b.innerHTML=`<span class="n">${+c.num}</span><span class="ti"><span class="h"></span><span class="d"></span></span><span class="go"><svg viewBox="0 0 40 20" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 10h30M23 3l9 7-9 7"/></svg></span>`;
1768 b.addEventListener('click',()=>openCase(c.id)); 1769 li.appendChild(b); deskCards.appendChild(li); 1770 deskEls.push({c,h:b.querySelector('.h'),d:b.querySelector('.d')}); 1771}); 1772function renderDeskText(){ deskEls.forEach(({c,h,d})=>{ h.textContent=c.title[LANG]; d.textContent=c.sub[LANG]; }); deskCards.classList.toggle('ko',LANG==='ko'); } 1773renderDeskText(); 1774renderPopupSlide(); // needs LANG, which is declared above this point 1775 1776/* Simple-copy pages (cover, hello, what I do, tools, worked, contents) copy 1777 swap: English text is the HTML's own default (cached here before anything 1778 touches it), Korean text comes only from hero_ko.js (heroKo). Toggling to 1779 EN always restores the cached original, so hero_ko.js can never leave the 1780 English page altered. */ 1781const HERO_IDS=['presentedBy','role1','role2','role3','helloLine0','helloLine1','bioText','eduHeader','eduDegree1','langHeader','langName1','langLevel1','langName2','langLevel2','langName3','langLevel3','langName4','langLevel4','langName5','langLevel5', 1782 'whatidoLine0','whatidoLine1','whatidoBody','kw0','kw1','kw2','kw3','kw4','kw5','kw6','kw7','kw8','kw9','kw10','kw11', 1783 'toolsLine0','toolsLine1','toolsP0','toolsP1','toolsP2','toolsP3','toolsP4', 1784 'workedLine0','workedLine1', 1785 'contentsLine0','contentsLine1', 1786 'bdLine0','bdBody','bdK0','bdK1','bdK2','bdSrc0','bdSrc1','bdSrc2', 1787 'deskHint']; 1788const HERO_HTML_IDS=['bioText','whatidoBody','toolsP0','toolsP1','toolsP2','toolsP3','toolsP4','bdLine0','bdBody','bdK0']; // ids whose EN/KO text needs embedded markup (<br>, <b>), so they're read/written as innerHTML rather than textContent 1789const heroEn={}; 1790HERO_IDS.forEach(id=>{ const el=document.getElementById(id); if(!el) return; heroEn[id]=el.dataset.type!==undefined?el.dataset.type:(HERO_HTML_IDS.includes(id)?el.innerHTML:el.textContent); }); 1791function applyHeroLang(){ 1792 const ko=typeof heroKo!=='undefined'?heroKo:{}; 1793 HERO_IDS.forEach(id=>{ 1794 const el=document.getElementById(id); if(!el) return; 1795 const text=(LANG==='ko'&&ko[id])?ko[id]:heroEn[id]; 1796 if(el.classList.contains('type')){ el.dataset.type=text; typeReset(el); typeIn(el); } 1797 else if(HERO_HTML_IDS.includes(id)) el.innerHTML=text; 1798 else el.textContent=text.trim()?text:'\u00a0'; // a blank line keeps its height, so the layout below doesn't jump 1799 }); 1800} 1801/* Blocks whose EN and KO copy run to a different number of lines, inside a 1802 vertically centred layout: reserve the taller version's height so switching 1803 language doesn't slide the heading and the rows below it up or down. */ 1804const HERO_HOLD_IDS=['bioText']; 1805function holdHeroHeights(){ 1806 const ko=typeof heroKo!=='undefined'?heroKo:{}; 1807 HERO_HOLD_IDS.forEach(id=>{ 1808 const el=document.getElementById(id); if(!el) return; 1809 const cur=el.innerHTML; el.style.minHeight=''; 1810 const h=[heroEn[id],ko[id]||heroEn[id]].map(t=>{ el.innerHTML=t; return el.offsetHeight; }); 1811 el.innerHTML=cur; el.style.minHeight=Math.max(...h)+'px'; 1812 }); 1813} 1814holdHeroHeights(); addEventListener('resize',holdHeroHeights); if(document.fonts) document.fonts.ready.then(holdHeroHeights); 1815/* Page 10: the copy's first line lines up with the top of "MARKETING." (the 1816 English title's second line). The target is computed from the title's 1817 bottom edge and line pitch, so in Korean, where the title is one line, the 1818 copy stays exactly where it sits in English. */ 1819function alignBdBody(){ 1820 const body=document.getElementById('bdBody'), h=document.querySelector('#bd-stats .h-caps'); 1821 if(!body||!h) return; 1822 if(matchMedia('(max-width:760px)').matches){ body.style.top=''; return; } 1823 body.style.top='0px'; 1824 const fs=parseFloat(getComputedStyle(h).fontSize); 1825 // cap top of the second title line: bottom edge, up one .9em line, down the 1826 // half-leading (.9-1.21)/2 and the ascender-to-cap gap (.243em) = -.812em 1827 const target=h.getBoundingClientRect().bottom-.812*fs; 1828 const bs=getComputedStyle(body), bf=parseFloat(bs.fontSize), lh=parseFloat(bs.lineHeight); 1829 const ty=bs.transform==='none'?0:new DOMMatrix(bs.transform).m42; // ignore the fade-in's slide, if it's mid-flight 1830 const ink=body.getBoundingClientRect().top-ty+(lh-1.21*bf)/2+.243*bf; // cap top of the copy's first line 1831 body.style.top=Math.round(target-ink)+'px'; 1832} 1833alignBdBody(); addEventListener('resize',alignBdBody); if(document.fonts) document.fonts.ready.then(alignBdBody); 1834/* Every .langtoggle instance on the page (cover, reports, ...) drives the 1835 same LANG state â aria-pressed is matched by data-lang, not by which 1836 specific button was clicked, so every instance stays in sync regardless 1837 of which one triggered the change. */ 1838document.querySelectorAll('.langtoggle button').forEach(bt=>bt.addEventListener('click',()=>{ 1839 if(bt.dataset.lang===LANG) return; 1840 LANG=bt.dataset.lang; 1841 document.documentElement.lang=LANG; 1842 document.querySelectorAll('.langtoggle button').forEach(x=>x.setAttribute('aria-pressed',String(x.dataset.lang===LANG))); 1843 applyI18n(); 1844 renderDeskText(); 1845 applyHeroLang(); 1846 holdHeroHeights(); 1847 alignBdBody();
1848 renderPopupSlide(); 1849 if(planOpen){ planpage.classList.toggle('ko',LANG==='ko'); renderPlan(); planpage.querySelectorAll('.rv').forEach(el=>el.classList.add('on')); } 1850})); 1851 1852/* case page */ 1853const casepage=document.getElementById('casepage'), csScroll=document.getElementById('csScroll'), vizStore=document.getElementById('vizStore'); 1854let csIO=null, openCaseId=null; 1855/* Closing a case returns its prebuilt viz blocks to #vizStore, but only after 1856 the close transition so they don't visibly jump. If another case is opened 1857 inside that window, openCase() flushes the pending teardown first: otherwise 1858 the timer would either sweep the new case's blocks away (same id reopened) 1859 or the new page's plan.innerHTML='' would destroy the old case's blocks for 1860 good, leaving that case chartless for the rest of the session. */ 1861let csPending=null, csPendingTimer=null; 1862function returnViz(id){ 1863 if(!id) return; 1864 /* sweep by what is actually in the page, not by id convention: a case can 1865 borrow another case's viz block (see planMedia viz entries), and a block 1866 left behind would be destroyed by the next open's innerHTML reset */ 1867 casepage.querySelectorAll('.vzw').forEach(w=>{ w.querySelectorAll('.step').forEach(st=>st.classList.remove('on')); vizStore.appendChild(w); }); 1868 document.getElementById('csMedia').innerHTML=''; 1869} 1870function flushCaseTeardown(){ 1871 if(csPendingTimer){ clearTimeout(csPendingTimer); csPendingTimer=null; } 1872 if(csPending){ returnViz(csPending); csPending=null; } 1873} 1874/* media stack: front card centred, next one waiting behind; wheel or a 1875 vertical drag over the stack swaps which is in front. At either end the 1876 page takes over the scroll, same rule the old hidden fandeck used. */ 1877const mediaStage=document.getElementById('csMedia'), mediaNav=document.getElementById('csMediaNav'); 1878let mediaIdx=0; 1879function mediaRender(){ 1880 const cards=[...mediaStage.querySelectorAll('.mcard')]; 1881 cards.forEach((c,i)=>{ c.classList.remove('front','next'); const rel=i-mediaIdx; if(rel===0) c.classList.add('front'); else if(rel===1) c.classList.add('next'); }); 1882 mediaNav.innerHTML=cards.length>1?cards.map((_,i)=>`<span class="${i===mediaIdx?'on':''}"></span>`).join(''):''; 1883} 1884function mediaStep(d){ 1885 const n=mediaStage.querySelectorAll('.mcard').length, next=mediaIdx+d; 1886 if(next<0||next>=n) return false; 1887 mediaIdx=next; mediaRender(); return true; 1888} 1889mediaNav.addEventListener('click',e=>{ 1890 const dot=e.target.closest('span'); if(!dot) return; 1891 const i=[...mediaNav.children].indexOf(dot); if(i<0||i===mediaIdx) return; 1892 mediaIdx=i; mediaRender(); 1893}); 1894let mediaWheelLock=0; 1895mediaStage.addEventListener('wheel',e=>{ 1896 if(mediaStage.querySelectorAll('.mcard').length<2) return; 1897 if(Math.abs(e.deltaX)<8 || Math.abs(e.deltaX)<Math.abs(e.deltaY)) return; 1898 const now=Date.now(); if(now-mediaWheelLock<550) { e.preventDefault(); return; } 1899 if(mediaStep(e.deltaX>0?1:-1)){ mediaWheelLock=now; e.preventDefault(); } 1900},{passive:false}); 1901let mediaX0=null; 1902mediaStage.addEventListener('pointerdown',e=>{ if(e.target.closest('iframe')) return; mediaX0=e.clientX; }); 1903mediaStage.addEventListener('pointerup',e=>{ if(mediaX0===null) return; const dx=e.clientX-mediaX0; mediaX0=null; if(Math.abs(dx)>30) mediaStep(dx<0?1:-1); }); 1904mediaStage.addEventListener('pointercancel',()=>{ mediaX0=null; }); 1905function pick(c){ const en=ALL[c.key]||{}; const ko=KO[c.key]||{}; return LANG==='ko' && ko.background ? ko : en; } 1906function paras(html,cls){ return (html||'').split(/<br\s*\/?>\s*<br\s*\/?>/i).map(t=>t.trim()).filter(Boolean).map((t,i)=>`<p class="${cls} rv" style="--i:${i%3}">${t}</p>`).join(''); } 1907/* a case only gets the viz blocks it actually has â bc, for instance, has a 1908 result block but no data/insight ones, so a missing id is skipped rather 1909 than throwing and leaving the rest of the page unrendered */ 1910function moveViz(id,k,slot,cap){ const w=document.getElementById(`vz-${id}-${k}`); if(!w) return; w.dataset.cap=cap; slot.appendChild(w); } 1911/* a single inline figure â image or YouTube embed â for the planMedia slots */ 1912function figureHtml(m){ 1913 if(!m) return ''; 1914 if(m.video){ const v=ytId(m.video); return v?`<div class="csfig rv">${ytPrint(v)}<div class="video"><iframe loading="lazy" src="https://www.youtube.com/embed/${v}" title="Video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>`:''; } 1915 return m.img?`<div class="csfig rv"><img src="${m.img}" alt="" loading="lazy" onerror="this.closest('.csfig').remove()"></div>`:''; 1916} 1917/* viz blocks a case can borrow from another case, with whatever render call 1918 that block needs once it is in the page */ 1919const VIZ_INIT={'vz-ub-3':()=>crAnatRender()}; 1920function moveVizById(elId,slot,cap){ 1921 const w=document.getElementById(elId); if(!w) return; 1922 w.dataset.cap=cap; slot.appendChild(w); 1923 if(VIZ_INIT[elId]) VIZ_INIT[elId](); 1924} 1925function openCase(id){ 1926 const c=CASES.find(x=>x.id===id); if(!c) return; flushCaseTeardown(); openCaseId=id; 1927 const p=pick(c); 1928 applyI18n(); 1929 casepage.dataset.case=id; 1930 casepage.classList.toggle('ko',LANG==='ko'); 1931 document.getElementById('csLabel').textContent=`${chapterOf(c.key)} · ${c.title[LANG]}`; 1932 document.getElementById('csClaim').textContent=(LANG==='ko' && p.claim) ? p.claim : c.claim[LANG]; 1933 document.getElementById('csWhy').textContent=c.why[LANG]; 1934 document.getElementById('csEnabled').textContent=c.enabled[LANG]; 1935 const meta=[['company',p.company],['year',p.year],['title',p.title]].filter(x=>x[1]).map(([k,v])=>`<div><span class="k">${T(k)}</span>${v}</div>`).join('') 1936 + (p.skills&&p.skills.length ? `<div><span class="k">${T('skills')}</span><div class="chips">${p.skills.map(x=>`<span>${x}</span>`).join('')}</div></div>` : ''); 1937 document.getElementById('csMeta').innerHTML=meta; 1938 document.getElementById('csBg').innerHTML=paras(p.background,'tx'); 1939 // media: images and videos from the EN entry (paths are language-neutral) 1940 const en=ALL[c.key]||{}; let media=''; 1941 [].concat(en.heroImage||[],en.heroImages||[],en.heroImageGrid||[],en.peImages||[],en.resultsImage||[]).forEac
1941h(src=>media+=`<div class="mcard"><img src="${src}" alt="" loading="lazy" onerror="this.closest('.mcard').remove()"></div>`); 1942 [].concat(en.peVideo||[],en.peVideos||[]).forEach(u=>{ const v=ytId(u); if(v) media+=`<div class="mcard"><div class="video"><iframe loading="lazy" src="https://www.youtube.com/embed/${v}" title="Video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>`; }); 1943 mediaStage.innerHTML=media; mediaIdx=0; mediaRender(); 1944 /* The stack normally sits under the whole Background section. When the copy 1945 opens with a segment label the images belong to that segment rather than 1946 the section, so the label is lifted onto its own line and the stack slots 1947 in between label and copy. Re-inserted on every open either way, since 1948 setting csBg's innerHTML above detaches whatever was placed inside it. */ 1949 const bgEl=document.getElementById('csBg'); 1950 const leadP=bgEl.firstElementChild, leadLabel=leadP&&leadP.querySelector(':scope>b.seglabel'); 1951 if(media&&leadLabel){ 1952 const labelP=document.createElement('p'); labelP.className='tx rv'; 1953 labelP.appendChild(leadLabel); 1954 bgEl.insertBefore(labelP,leadP); 1955 bgEl.insertBefore(mediaStage,leadP); bgEl.insertBefore(mediaNav,leadP); 1956 } else { 1957 bgEl.after(mediaStage,mediaNav); 1958 } 1959 // planning: each of the first two text blocks is followed by the viz that 1960 // illustrates it, then whatever blocks are left 1961 const plan=document.getElementById('csPlan'); plan.innerHTML=''; 1962 const blocks=(p.planningExecution||'').split(/<br\s*\/?>\s*<br\s*\/?>/i).map(t=>t.trim()).filter(Boolean); 1963 const planMedia=en.planMedia||[]; 1964 const addBlock=(t,at,stagger)=>{ 1965 if(!t) return; 1966 const m=planMedia.find(x=>x.at===at); 1967 if(m&&m.viz) moveVizById(m.viz,plan,T(m.cap||'capInsight')); 1968 const fig=figureHtml(m); 1969 // a block opening with a segment label keeps that label above the figure, 1970 // so the figure reads as part of its own segment, not the previous one 1971 const lab=fig&&t.match(/^\s*<b class=['"]seglabel['"]>[\s\S]*?<\/b>/i); 1972 plan.insertAdjacentHTML('beforeend', lab 1973 ? `<p class="tx rv">${lab[0]}</p>${fig}<p class="tx rv" style="--i:${stagger}">${t.slice(lab[0].length)}</p>` 1974 : `${fig}<p class="tx rv" style="--i:${stagger}">${t}</p>`); 1975 }; 1976 addBlock(blocks[0],0,0); 1977 moveViz(id,0,plan,T('capData')); 1978 addBlock(blocks[1],1,1); 1979 moveViz(id,1,plan,T('capInsight')); 1980 blocks.slice(2).forEach((t,i)=>addBlock(t,i+2,i%3)); 1981 // result: [result viz] + text 1982 const res=document.getElementById('csResult'); res.innerHTML=''; 1983 moveViz(id,2,res,T('capResult')); 1984 res.insertAdjacentHTML('beforeend',paras(p.results,'tx')); 1985 document.getElementById('csDisc').innerHTML=`<b>${c.disc[LANG]}</b>`; 1986 // prepare viz that need a render before showing 1987 if(id==='ub'){ renderHeatmapA(document.getElementById('heatmapA')); renderHeatmapB(document.getElementById('heatmapB')); } 1988 if(id==='ce'){ ceBoardHover(-1); ceRender(); } 1989 // reveal on scroll inside the page 1990 if(csIO) csIO.disconnect(); 1991 csIO=new IntersectionObserver(entries=>entries.forEach(en=>{ if(en.isIntersecting){ en.target.classList.add('on'); en.target.querySelectorAll('[data-count]').forEach(count); if(en.target.matches&&en.target.matches('[data-count]')) count(en.target); csIO.unobserve(en.target); } }),{root:csScroll,threshold:.25}); 1992 casepage.querySelectorAll('.rv, .vzw .step').forEach(el=>{ el.classList.remove('on'); csIO.observe(el); }); 1993 csScroll.scrollTop=0; 1994 casepage.classList.add('open'); 1995 document.getElementById('chrome').classList.add('hide-head'); 1996 casepage.querySelector('.close').focus(); 1997} 1998function closeCase(){ 1999 casepage.classList.remove('open'); document.getElementById('chrome').classList.remove('hide-head'); 2000 if(!openCaseId) return; 2001 csPending=openCaseId; openCaseId=null; 2002 csPendingTimer=setTimeout(()=>{ csPendingTimer=null; flushCaseTeardown(); },900); 2003} 2004document.addEventListener('click',e=>{ if(e.target.closest('[data-cclose]')) closeCase(); }); 2005addEventListener('keydown',e=>
2005{ if(e.key==='Escape'&&casepage.classList.contains('open')) closeCase(); },true); 2006applyI18n(); 2007 2008const detail=document.getElementById('detail'), dScroll=document.getElementById('dScroll'); 2009/* print-only stand-in for a YouTube embed (hidden on screen, see PRINT CSS). 2010 Not lazy-loaded, so the thumbnail is already there when printing starts. */ 2011function ytPrint(v){ return `<a class="print-yt" href="https://youtu.be/${v}"><img src="https://i.ytimg.com/vi/${v}/hqdefault.jpg" alt=""><span>â¶ youtu.be/${v}</span></a>`; } 2012function ytId(u){ const m=(u||'').match(/(?:youtu\.be\/|shorts\/|v=|embed\/)([A-Za-z0-9_-]{11})/); return m?m[1]:null; } 2013function video(u){ const id=ytId(u); return id?`<div class="video"><iframe loading="lazy" src="https://www.youtube.com/embed/${id}" title="Video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`:''; } 2014function imgs(arr,cls){ const a=[].concat(arr||[]).filter(Boolean); if(!a.length) return ''; return `<div class="media ${cls||''}">${a.map(s=>`<img src="${s}" alt="" loading="lazy" onerror="this.parentElement.style.display='none'">`).join('')}</div>`; } 2015function chapterOf(key){ for(const [id,ch] of Object.entries(CHAPTERS)) if(ch.keys.includes(key)) return ch.label; return ''; } 2016 2017function openDetail(key){ 2018 const p=ALL[key]; if(!p) return; 2019 document.getElementById('dChapter').textContent=chapterOf(key); 2020 document.getElementById('dTitle').textContent=p.name; 2021 document.getElementById('dMeta').innerHTML=[['Company',p.company],['Year',p.year],['Title',p.title],['Role',p.role]].filter(x=>x[1]).map(([k,v])=>`<div><span class="k">${k}</span>${v}</div>`).join(''); 2022 const media=[]; 2023 [].concat(p.heroImage||[],p.heroImages||[],p.heroImageGrid||[]).forEach(src=>media.push({t:'img',src,cap:'Overview'})); 2024 [].concat(p.peVideo||[],p.peVideos||[]).forEach(u=>{ const id=ytId(u); if(id) media.push({t:'vid',id,cap:'Video'}); }); 2025 [].concat(p.peImages||[]).forEach(src=>media.push({t:'img',src,cap:'Planning & Execution'})); 2026 [].concat(p.resultsImage||[]).forEach(src=>media.push({t:'img',src,cap:'Result'})); 2027 carBuild(media); 2028 let html=''; 2029 if(p.questions) html+=`<div class="sec rev"><h3>Questions</h3><ul class="qs">${p.questions.map(q=>`<li><b>${q.q}</b><span>${q.insight}</span></li>`).join('')}</ul></div>`; 2030 if(p.background) html+=`<div class="sec rev"><h3>Background</h3><div class="body"><p>${p.background}</p></div></div>`; 2031 if(p.planningExecution) html+=`<div class="sec rev"><h3>Planning & Execution</h3><div class="body"><p>${p.planningExecution}</p></div></div>`; 2032 if(p.results) html+=`<div class="sec rev"><h3>Result</h3><div class="body"><p>${p.results}</p></div></div>`; 2033 document.getElementById('dBody').innerHTML=html; 2034 dScroll.scrollTop=0; 2035 detail.classList.add('open'); 2036 document.getElementById('chrome').classList.add('hide-head'); 2037 detail.querySelector('.close').focus(); 2038} 2039let carMedia=[]; 2040/* Fanned card deck: assets sit collapsed and overlapping; hover pulls one 2041 forward. Horizontal movement comes from scrolling or dragging, no buttons. 2042 Feature-flagged off for now (disrupted the reading flow) â flip to true to 2043 bring it back without rebuilding it. */ 2044const MEDIA_DECK_ENABLED=false; 2045const CARD_TILT=[-7,4,-3,6,-5,3,-6,5,-2,7]; 2046function carBuild(media){ 2047 const car=document.getElementById('dCar'), deck=document.getElementById('dTrack'), cap=document.getElementById('dCap'); 2048 carMedia=media; 2049 if(!MEDIA_DECK_ENABLED||!media.length){ car.hidden=true; deck.innerHTML=''; cap.textContent=''; return; } 2050 car.hidden=false; 2051 deck.innerHTML=media.map((m,i)=>{ 2052 const st=`--r:${CARD_TILT[i%CARD_TILT.length]}deg;--y:${i%2?10:0}px;--z:${i+1}`; 2053 return m.t==='img' 2054 ? `<div class="card" role="listitem" style="${st}" data-i="${i}"><img src="${m.src}" alt="" loading="lazy" onerror="this.closest('.card').style.display='none'"></div>` 2055 : `<div class="card vid" role="listitem" style="${st}" data-i="${i}" data-vid="${m.id}" tabindex="0"><img src="https://i.ytimg.com/vi/${m.id}/hqdefault.jpg" alt="" loading="lazy"><span class="play"><svg viewBox="0 0 24 24">
2055<path d="M8 5v14l11-7z"/></svg></span></div>`; 2056 }).join(''); 2057 cap.textContent=media[0].cap; 2058 requestAnimationFrame(()=>{ deck.scrollLeft=(deck.scrollWidth-deck.clientWidth)/2; }); 2059} 2060(function(){ 2061 const deck=document.getElementById('dTrack'), cap=document.getElementById('dCap'); 2062 const step=()=>{ const c=deck.querySelector('.card'); return c?Math.max(120,c.getBoundingClientRect().width*.55):200; }; 2063 2064 deck.addEventListener('mouseover',e=>{ const c=e.target.closest('.card'); if(c&&carMedia[c.dataset.i]) cap.textContent=carMedia[c.dataset.i].cap; }); 2065 2066 // Vertical wheel scrolls the deck sideways; at either end the page takes over. 2067 deck.addEventListener('wheel',e=>{ 2068 const d=(Math.abs(e.deltaX)>Math.abs(e.deltaY)?e.deltaX:e.deltaY)*14.6; 2069 const max=deck.scrollWidth-deck.clientWidth; 2070 if(max<=0) return; 2071 if((d<0&&deck.scrollLeft<=0)||(d>0&&deck.scrollLeft>=max-1)) return; 2072 e.preventDefault(); deck.scrollLeft+=d; 2073 },{passive:false}); 2074 2075 // Drag to pan. 2076 let x0=null, sl0=0, moved=0; 2077 deck.addEventListener('pointerdown',e=>{ if(e.target.closest('iframe')) return; x0=e.clientX; sl0=deck.scrollLeft; moved=0; }); 2078 deck.addEventListener('pointermove',e=>{ if(x0===null) return; const dx=e.clientX-x0; if(Math.abs(dx)>4){ moved=Math.abs(dx); deck.classList.add('drag'); deck.scrollLeft=sl0-dx; } }); 2079 const endDrag=()=>{ x0=null; deck.classList.remove('drag'); }; 2080 deck.addEventListener('pointerup',endDrag); deck.addEventListener('pointercancel',endDrag); deck.addEventListener('pointerleave',endDrag); 2081 2082 // Videos load their player only once asked for. 2083 function playCard(c){ 2084 if(!c||c.classList.contains('playing')) return; 2085 c.classList.add('playing'); 2086 const f=document.createElement('iframe'); 2087 f.src=`https://www.youtube.com/embed/${c.dataset.vid}?autoplay=1`; 2088 f.title='Video'; f.loading='lazy'; f.allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; f.allowFullscreen=true; 2089 c.appendChild(f); 2090 c.scrollIntoView({behavior:'smooth',block:'nearest',inline:'nearest'}); 2091 } 2092 deck.addEventListener('click',e=>{ if(moved>4){ moved=0; return; } playCard(e.target.closest('.card.vid')); }); 2093 deck.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ const c=e.target.closest('.card.vid'); if(c){ e.preventDefault(); playCard(c); } } }); 2094 2095 window.carNudge=d=>deck.scrollBy({left:d*step(),behavior:'smooth'}); 2096})(); 2097function closeDetail(){ detail.classList.remove('open'); document.getElementById('chrome').classList.remove('hide-head'); setTimeout(()=>{ detail.querySelectorAll('iframe').forEach(f=>f.remove()); detail.querySelectorAll('.card.playing').forEach(c=>c.classList.remove('playing')); },900); } 2098document.addEventListener('click',e=>{ 2099 const o=e.target.closest('[data-open]'); if(o){ openDetail(o.dataset.open); return; } 2100 if(e.target.closest('[data-close]')) closeDetail(); 2101 if(e.target.closest('[data-next]')){ const s=e.target.closest('.slide').nextElementSibling; s&&s.scrollIntoView({behavior:'smooth'}); } 2102}); 2103addEventListener('keydown',e=>{ 2104 if(e.key==='Escape'&&detail.classList.contains('open')) closeDetail(); 2105 if(casepage.classList.contains('open')||planOpen) return; 2106 if(detail.classList.contains('open')){ if(e.key==='ArrowRight') carNudge(1); if(e.key==='ArrowLeft') carNudge(-1); return; } 2107 if(['ArrowDown','PageDown',' '].includes(e.key)){ e.preventDefault(); go(1); } 2108 if(['ArrowUp','PageUp'].includes(e.key)){ e.preventDefault(); go(-1); } 2109}); 2110 2111/* ========================================================= 2112 SLIDE OBSERVER: reveals, typing, counters, counter chrome 2113 ========================================================= */ 2114const slides=[...document.querySelectorAll('.slide')]; 2115document.getElementById('cTot').textContent=String(slides.length).padStart(2,'0'); 2116let current=0; 2117function go(d){ const n=Math.min(slides.length-1,Math.max(0,current+d)); slides[n].scrollIntoView({behavior:'smooth'}); } 2118const io=new IntersectionObserver(entries=>{ 2119 entries.forEach(en=>{ 2120 const s=en.target; 2121 if(en.isIntersecting){ 2122 s.classList.add('in');
2123 current=slides.indexOf(s); 2124 document.getElementById('cIdx').textContent=String(current+1).padStart(2,'0'); 2125 document.getElementById('cBar').style.transform=`scaleX(${(current+1)/slides.length})`; 2126 s.querySelectorAll('.type').forEach(typeIn); 2127 s.querySelectorAll('[data-count]').forEach(count); 2128 } else { 2129 s.classList.remove('in'); 2130 s.querySelectorAll('.type').forEach(typeReset); 2131 s.querySelectorAll('[data-count]').forEach(el=>el.textContent='0'); 2132 } 2133 }); 2134},{root:document.getElementById('deck'),threshold:.55}); 2135slides.forEach(s=>io.observe(s)); 2136 2137const barEl=document.getElementById('cBar').parentElement; 2138barEl.addEventListener('click',e=>{ 2139 const rect=barEl.getBoundingClientRect(); 2140 const frac=Math.min(1,Math.max(0,(e.clientX-rect.left)/rect.width)); 2141 slides[Math.round(frac*(slides.length-1))].scrollIntoView({behavior:'smooth'}); 2142}); 2143 2144/* ========================================================= 2145 PRINT: everything that only finishes through JS (typing, counters, the 2146 per-slide .in state, chart steps, lazy images) is put in its final state 2147 before printing, and the slides that are off screen are put back after, 2148 so the deck carries on animating as normal. 2149 ========================================================= */ 2150function preparePrint(){ 2151 slides.forEach(s=>s.classList.add('in')); 2152 document.querySelectorAll('.type').forEach(el=>{ 2153 const t=typers.get(el); if(t&&t!==true) clearInterval(t); 2154 typers.set(el,true); el.textContent=el.dataset.type||''; el.classList.add('done'); 2155 }); 2156 document.querySelectorAll('[data-count]').forEach(el=>count(el,true)); 2157 document.querySelectorAll('.rv,.vzw .step').forEach(el=>el.classList.add('on')); 2158 document.querySelectorAll('img[loading="lazy"]').forEach(img=>img.loading='eager'); 2159 document.querySelectorAll('.casepage.open .cssec p,.casepage.open .cssec li').forEach(el=>el.classList.toggle('print-long',el.offsetHeight>innerHeight*.3)); 2160 // a page break before every section of the open overlay, except the 2161 // proposal's one-line concept, which stays on the page with its title 2162 document.querySelectorAll('.casepage.open .cssec').forEach(sec=>{ 2163 // (hand-built .print-page blocks carry their own break; Chrome fires 2164 // beforeprint again while making the PDF, so they must be skipped here) 2165 if(sec.matches('#gpBody>.cssec:first-child,.pgal,.print-page')||sec.previousElementSibling?.classList.contains('print-break')) return; 2166 const m=document.createElement('div'); m.className='print-break'; sec.before(m); 2167 }); 2168 const head=document.querySelector('#chrome .head'); 2169 slides.forEach(s=>{ 2170 if(s.querySelector(':scope>.print-head')) return; 2171 const h=head.cloneNode(true); 2172 h.className='print-head'; h.querySelectorAll('[id]').forEach(el=>el.removeAttribute('id')); 2173 s.appendChild(h); 2174 }); 2175} 2176function restorePrint(){ 2177 document.querySelectorAll('.slide>.print-head,.print-break').forEach(h=>h.remove()); 2178 slides.forEach((s,i)=>{ 2179 if(i===current) return; 2180 s.classList.remove('in'); 2181 s.querySelectorAll('.type').forEach(typeReset); 2182 s.querySelectorAll('[data-count]').forEach(el=>el.textContent='0'); 2183 }); 2184} 2185/* PDF export only: lays the open overlay's sections out as explicit pages of 2186 height H (layout px). Called by tools/pdf/export.js with print styles 2187 emulated; the page is reloaded after printing, so nothing is put back. 2188 - content is cut into chunks: a segment label, a calc sub-heading, or a 2189 figure/chart starts a new chunk (a figure stays with the label or 2190 sub-heading right above it), so a picture keeps its caption text; 2191 - chunks are packed onto pages; a chunk up to 15% over a page stays 2192 whole on its own (squeezed), a bigger one is packed block by block; 2193 - a chunk may squeeze onto the current page (up to 15% over) when it is 2194 the section's last, or when the page would otherwise be left mostly 2195 empty, instead of opening a near-empty page; 2196 - a heading (segment label, calc sub-heading, table header) never ends a 2197 page: it moves on with what it introduces; 2198 - containers (lists, tables, calc blocks) are rebuilt on each page as 2199 empty copies, so their styling still applies, and a split table gets 2200 its header row again. */ 2201function paginatePrint(H){ 2202 const root=document.querySelector('.casepage.open .cscroll'); if(!root) return; 2203 document.querySelectorAll('.print-break').forEach(m=>m.remove()); 2204 root.style.setProperty('--pp-h',(H+4)+'px'); 2205 const BAND=26; root.style.setProperty('--pp-band',BAND+'px'); // room under the opening rule; the name sits at its foot 2206 H-=BAND; 2207 const CONT='.body,.calc,.ctab,.cf,ul.plist'; 2208 const FIG='.csfig,.vzw,.media,svg,.heatmap,.hm-wrap'; 2209 const shown=el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0; 2210 const labelOnly=el=>el.matches('p.tx')&&el.querySelector(':scope>b.seglabel')&&el.textContent.trim()===el.querySelector(':scope>b.seglabel').textContent.trim(); 2211 const secs=[...root.querySelectorAll('.cssec')].filter(sec=>!sec.matches('.pgal,#gpBody>.cssec:first-child')&&shown(sec)); 2212 const disc=root.querySelector(':scope>.disc'); 2213 const thOf=new Map([...root.querySelectorAll('.ctab')].map(t=>[t,t.querySelector(':scope>.th')?.cloneNode(true)])); 2214 const isHead=el=>labelOnly(el)||el.matches('.calc-h,.th'); 2215 secs.forEach((sec,si)=>{ 2216 const sb=sec.querySelector(':scope>.sb'), title=sec.querySelector(':scope>.sh h3')?.textContent||''; 2217 const leaves=[];
2218 (function walk(box){ [...box.children].forEach(el=>{ if(!shown(el)) return; if(el.matches(CONT)) walk(el); else leaves.push(el); }); })(sb); 2219 if(si===secs.length-1&&disc&&disc.textContent.trim()&&shown(disc)) leaves.push(disc); 2220 if(!leaves.length) return; 2221 // vertical room each block takes in the normal flow (gap to the next one) 2222 const tops=leaves.map(el=>el.getBoundingClientRect().top); 2223 const span=leaves.map((el,i)=>{ 2224 if(i<leaves.length-1&&tops[i+1]>=tops[i]) return tops[i+1]-tops[i]; 2225 const r=el.getBoundingClientRect(); return r.height+parseFloat(getComputedStyle(el).marginBottom||0); 2226 }); 2227 // chunks 2228 const chunks=[]; let cur=null; 2229 leaves.forEach((el,i)=>{ 2230 const prev=leaves[i-1]; 2231 const glued=prev&&(labelOnly(prev)||prev.matches('.calc-h')); // a heading stays with what follows it 2232 const starts=!glued&&((el.matches('p.tx')&&el.querySelector(':scope>b.seglabel'))||el.matches('.calc-h')||el.matches(FIG)); 2233 if(!cur||starts){ cur={items:[],h:0}; chunks.push(cur); } 2234 cur.items.push(i); cur.h+=span[i]; 2235 }); 2236 // pack 2237 const pages=[]; let pg={items:[],h:0}; 2238 const flush=()=>{ if(pg.items.length) pages.push(pg); pg={items:[],h:0}; }; 2239 chunks.forEach((c,ci)=>{ 2240 const last=ci===chunks.length-1, sparse=pg.h<H*.45; 2241 if(pg.h+c.h<=H||((last||sparse)&&pg.items.length&&pg.h+c.h<=H*1.15)){ pg.items.push(...c.items); pg.h+=c.h; return; } 2242 // a near-fit stays whole on its own page (squeezed), unless the current 2243 // page is mostly empty, in which case it is better to start filling it 2244 if(c.h<=H||(c.h<=H*1.15&&!(sparse&&pg.items.length))){ flush(); pg.items.push(...c.items); pg.h=c.h; return; } 2245 c.items.forEach(i=>{ 2246 if(pg.h+span[i]>H&&pg.items.length){ 2247 const carry=[]; // trailing headings go over with what they introduce 2248 while(pg.items.length>1&&isHead(leaves[pg.items[pg.items.length-1]])){ const j=pg.items.pop(); pg.h-=span[j]; carry.unshift(j); } 2249 flush(); carry.forEach(j=>{ pg.items.push(j); pg.h+=span[j]; }); 2250 } 2251 pg.items.push(i); pg.h+=span[i]; 2252 }); 2253 }); 2254 flush(); 2255 // build 2256 const made=pages.map((p,pi)=>{ 2257 const page=document.createElement('section'); page.className='cssec print-page'+(pi===0?' first':''); 2258 page.innerHTML=`<div class="pp-in"><div class="sh"><h3></h3></div><div class="sb"></div></div>`; 2259 page.querySelector('h3').textContent=title; 2260 const pageSb=page.querySelector('.sb'); let stack=[]; 2261 p.items.forEach(i=>{ 2262 const el=leaves[i], chain=[]; 2263 for(let a=el.parentElement;a&&a!==sb&&a!==root;a=a.parentElement) chain.unshift(a); 2264 if(el===disc) chain.length=0; 2265 let parent=pageSb; 2266 chain.forEach((orig,d)=>{ 2267 if(stack[d]&&stack[d].orig===orig){ parent=stack[d].copy; return; } 2268 const copy=orig.cloneNode(false); copy.removeAttribute('id'); parent.appendChild(copy); 2269 if(orig.matches('.ctab')&&thOf.get(orig)&&!el.matches('.th')) copy.appendChild(thOf.get(orig).cloneNode(true)); 2270 stack=stack.slice(0,d); stack[d]={orig,copy}; parent=copy; 2271 }); 2272 stack=stack.slice(0,chain.length); 2273 parent.appendChild(el); 2274 }); 2275 return page; 2276 }); 2277 sec.replaceWith(...made); 2278 }); 2279 // check each page against the real layout: squeeze a slight overshoot, 2280 // let anything genuinely longer than a page run on 2281 // (the content column is what gets squeezed, so the section name keeps 2282 // its size and position) 2283 root.querySelectorAll('.print-page').forEach(page=>{ 2284 const inner=page.querySelector('.pp-in>.sb'), h=inner.getBoundingClientRect().height; 2285 if(h<=H) return; 2286 if(h<=H*1.18) inner.style.zoom=(H/h).toFixed(3); else page.classList.add('tall'); 2287 }); 2288} 2289window.paginatePrint=paginatePrint; 2290addEventListener('beforeprint',preparePrint); 2291addEventListener('afterprint',restorePrint); 2292window.preparePrint=preparePrint; 2293 2294/* contents anchors: smooth scroll inside the deck */ 2295document.querySelectorAll('.contents a').forEach(a=>a.addEventListener('click',e=>{ e.preventDefault(); document.querySelector(a.getAttribute('href')).scrollIntoView({behavior:'smooth'}); })); 2296</script>
2296 2297</body> 2298</html>
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.