/* ============================================================
   Jiwoong Baek — portfolio site
   Ported from "Portfolio Site.dc.html". Paper/chrome variants
   from the design's editor props live on <html> as data-paper
   and data-chrome.
   ============================================================ */

/* ── Webfonts ────────────────────────────────────────────── */

/* Die Grotesk is off for now: fonts/ only holds Klim's test cut (C), which is
   licensed for trials, not the live site. With the licensed woff2 files in
   fonts/, put this block back (build-dist.js then ships fonts/ too):
   @font-face { font-family: 'Die Grotesk'; src: url('../fonts/<file>.woff2') format('woff2'); font-weight: 400; font-display: swap; }
   (Light 300 · Regular 400 · Medium 500 · Bold 600 · Black 700) */

/* ── Tokens ──────────────────────────────────────────────── */

:root {
  --sans: 'Die Grotesk', 'Pretendard Variable', 'Pretendard', Helvetica, sans-serif;

  /* Type scale — 8 steps, no two closer than 2px.
     The design file used 25 distinct sizes, many of them half-pixel apart;
     that reads as eyeballed rather than designed. Roles map onto steps:
       caption  index numbers, dates, captions, pills
       label    nav, tags, meta, EN glosses, table rows
       body-s   secondary paragraphs
       body     primary paragraphs
       title-s  card titles
       title    section titles
       display  page and hero titles (clamped, so the responsive
                overrides that used to hard-code 28–32px are gone) */
  --t-caption:   12px;
  --t-label:     14px;
  --t-body-s:    16px;
  --t-body:      18px;
  --t-title-s:   21px;
  --t-title:     27px;
  --t-display-s: clamp(1.75rem, 2.9vw, 2.5rem);    /* 28 → 40 */
  --t-display:   clamp(2rem, 3.9vw, 3.375rem);     /* 32 → 54 */

  --lh-display: 1.04;
  --lh-title:   1.4;
  --lh-body:    1.6;
  --lh-kr:      1.75;   /* Korean paragraphs need the extra leading */

  /* Achromatic, near white. Warm greys read as printed paper — comfortable,
     but they date the page and compete with the campaign colour the site
     exists to frame. Value relationships stay close so the page is quiet;
     structure comes from hairlines, not from filled plates. */
  /* Pure white, not an off-white: the page is a gallery wall, and any warmth in
     the ground tints every image hung on it. Cards are the same white, so they
     are drawn with a hairline instead of a lighter fill. */
  --paper-light: #ffffff;
  --paper-card:  #ffffff;
  --paper-cell:  #dedede;   /* 1.35:1 against --paper-light */
  --cell-line:   rgba(20, 20, 22, .15);
  --paper-tint:  #efefef;
  --paper-dark:  #0a0a0a;
  --paper-slot:  #2f3032;

  /* #1c1a15 was a brown-black. Neutral near-black is the single biggest lever
     away from "cozy" — every rule, border and glyph on the page inherits it. */
  --ink:        #17181a;
  --ink-strong: #1f2124;
  --ink-body:   #43464a;
  --ink-filter: #303338;
  /* Both greys now clear WCAG AA on paper (5.08:1 and 4.59:1). The step
     between them is deliberately small — tertiary text is separated from
     secondary by size and tracking, not by fading it out. */
  --ink-muted:  #666a70;
  --ink-faint:  #6c7178;
  --ink-invert: #f2f2f2;

  --line-light: rgba(20, 20, 22, .32);
  --rule-light: rgba(20, 20, 22, .14);
  --line-dark:  rgba(255, 255, 255, .34);

  --hairline-dark:   rgba(242, 242, 242, .12);
  --ink-on-dark-dim: rgba(242, 242, 242, .5);
  --slot-ink:        rgba(24, 24, 26, .46);
  --slot-ink-dark:   rgba(242, 242, 242, .42);

  /* Focus ring colour. Inherited, so dark surfaces flip it by redeclaring. */
  --focus: var(--ink);

  --tap: 44px;   /* minimum finger target */

  /* V2 borrows the reference's soft-rectangle language for controls and media
     instead of the 999px pill. A 4px corner reads as a module; a 999px one
     reads as a token from a different era. */
  --radius-chip:  4px;
  --radius-media: 6px;
  --chip-fill:    rgba(20, 20, 22, .06);
  --chip-fill-on: var(--ink);

  --ease-out: cubic-bezier(.22, .61, .25, 1);
  --ease-soft: cubic-bezier(.16, 1, .3, 1);
}

[data-paper="cool"] {
  --paper-light: #f2f5f7; --paper-card: #ffffff; --paper-cell: #d9dee2;
  --cell-line: rgba(14, 20, 28, .16);
  --paper-tint: #e9edf0; --paper-dark: #080a0c; --paper-slot: #2c3238;
}
[data-paper="mono"] {
  /* The default set is already achromatic, so this now only differs in that
     it stays flat if the default ever picks up a cast again. */
  --paper-light: #ffffff; --paper-card: #ffffff; --paper-cell: #dedede;
  --cell-line: rgba(18, 18, 18, .15);
  --paper-tint: #f1f1f1; --paper-dark: #0a0a0a; --paper-slot: #303030;
}

/* ── Base ────────────────────────────────────────────────── */

* { box-sizing: border-box; }
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--paper-dark);
  color: var(--ink-invert);
  font-family: var(--sans);
  font-size: var(--t-body-s);
  line-height: var(--lh-body);
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;

  /* Die Grotesk carries Latin only, so every Hangul glyph falls through to
     Pretendard. Two typefaces at one font-size means two x-heights, which is
     what makes the KR/EN pairs on About and Blog look like different sizes.
     `from-font` takes the aspect value from the first available font and
     scales the fallbacks to match it — a no-op while Die Grotesk is missing,
     self-correcting the moment the woff2 files land in fonts/. */
  font-size-adjust: from-font;

  /* Korean breaks anywhere by default, which chops 어절 mid-word. `keep-all`
     moves breaks to spaces; the overflow-wrap is the escape hatch for a token
     longer than its column. Latin is unaffected — keep-all only changes CJK. */
  word-break: keep-all;
  overflow-wrap: break-word;
}

a { color: inherit; text-decoration: none; }
h1, h2, p, figure, dl, dd { margin: 0; }

.surface-dark { background: var(--paper-dark); --focus: #ffffff; }
.surface-page { background: var(--paper-light); color: var(--ink); min-height: 100vh; }
.surface-slot { background: var(--paper-slot); --focus: #ffffff; }
/* Same white as the page, so the edge does the separating. box-shadow rather
   than border keeps it out of the box model. */
.card         { background: var(--paper-card); box-shadow: inset 0 0 0 1px var(--rule-light); }
.cell         { position: relative; width: 100%; background: var(--paper-cell); }
/* Hairline sits above the image, so a filled cell keeps a crisp edge and an
   empty one still reads as a plate. */
.cell::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--cell-line);
  pointer-events: none;
  z-index: 1;
}

/* Off-screen but readable by screen readers — used where a shared visible
   caption replaces per-item text. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.meta  { font-size: var(--t-caption); color: var(--ink-muted); }
.meta.wide { letter-spacing: .06em; }
.faint { font-size: var(--t-caption); color: var(--ink-faint); letter-spacing: .06em; }
.label { font-size: var(--t-label); font-weight: 500; letter-spacing: .06em; white-space: nowrap; }

.pad-page { padding: 120px 26px 0; }

/* ── Focus & motion ──────────────────────────────────────── */
/* The design file shipped no focus styling at all, so keyboard users had
   nothing but the UA default — invisible on the dark surfaces. */

a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}
/* Route containers are focused programmatically so screen readers land in the
   new view. That move needs to be announced, not ringed. */
#app:focus, #app:focus-visible,
.view:focus, .view:focus-visible { outline: none; }

.skip-link {
  position: fixed;
  top: 10px; left: 10px;
  z-index: 100;
  transform: translateY(-250%);
  background: #ffffff;
  color: #0a0a0a;
  padding: 10px 16px;
  border-radius: 2px;
  font-size: var(--t-label);
  font-weight: 500;
}
.skip-link:focus-visible { transform: none; outline: 2px solid #0a0a0a; outline-offset: 2px; }

/* Views replay this each time `hidden` is removed, which covers navigation.
   Sheet items animate on every re-render, which covers filter changes — the
   grid used to snap to the new set with no continuity at all. */
@keyframes view-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.view { animation: view-in .22s var(--ease-out) both; }
.sheet-item {
  animation: rise .34s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 28ms);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Image slots ─────────────────────────────────────────── */

image-slot {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 10px;
}
.image-slot__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.image-slot__label {
  font-size: var(--t-caption);
  letter-spacing: .08em;
  text-align: center;
  text-transform: uppercase;
  color: var(--slot-ink);
  pointer-events: none;
}
.surface-slot .image-slot__label,
.reel-item .image-slot__label { color: var(--slot-ink-dark); }
image-slot.is-filled .image-slot__label { display: none; }
/* The padding only exists to keep the placeholder label off the edge. With a
   real image in, it read as a grey mat around every plate. */
image-slot.is-filled { padding: 0; }
image-slot.is-fillable { cursor: copy; }
image-slot.is-dropping { outline: 2px dashed rgba(20, 20, 22, .5); outline-offset: -4px; }
.image-slot__clear {
  position: absolute;
  top: 6px; right: 6px;
  width: 22px; height: 22px;
  border: 0; border-radius: 999px;
  background: rgba(10, 10, 10, .62);
  color: #fff;
  font: 500 var(--t-label)/1 var(--sans);
  cursor: pointer;
  opacity: 0;
  transition: opacity .18s ease;
}
image-slot.is-filled:hover .image-slot__clear { opacity: 1; }

/* ── Pills ───────────────────────────────────────────────── */

.pill {
  display: inline-flex;
  align-items: center;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 9px 18px;
  font-size: var(--t-caption);
  font-weight: 500;
  line-height: 1;
  letter-spacing: .06em;
  white-space: nowrap;
  transition: background-color .16s var(--ease-out),
              border-color .16s var(--ease-out),
              color .16s var(--ease-out);
}
.pill.xs { padding: 7px 16px; }
.pill.sm { padding: 5px 11px; }
.pill.lg { padding: 10px 20px; font-size: var(--t-label); }
.pill.fit { width: fit-content; }

/* Scrim, not glass. `backdrop-filter: blur(16px)` was doing the legibility
   work for a 14%-white fill; over a light frame that fill is invisible, so a
   pale hero could leave white text on white. Darkening the backdrop instead
   holds ≥5:1 even against a pure white image, and drops the compositing layer
   that the blur forced on every pill — including the ~20 sitting on flat
   paper, where blurring a solid colour produced the same solid colour. */
.pill-glass   { background: rgba(12, 12, 13, .60); border-color: rgba(255, 255, 255, .32); color: #fff; }
.pill-outline { border-color: var(--line-light); color: var(--ink-body); }
.pill-fill    { background: var(--ink); color: var(--ink-invert); }
.pill-strong  { border-color: var(--ink); color: var(--ink); padding: 5px 11px; }

/* Hit-area sizing lives at the end of the file — it has to win over the
   per-component gap rules below. */

/* ── Top bar ─────────────────────────────────────────────── */

.topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 22px 24px;
  pointer-events: none;
}
.topbar > * { pointer-events: auto; }
.topbar-nav { display: flex; gap: 7px; }

/* The bar is fixed outside every surface, so its focus tone follows the
   chrome tone the router sets on <html>. */
[data-chrome-tone="dark"] .topbar { --focus: #ffffff; }
[data-chrome-tone="light"] .topbar { --focus: var(--ink); }

.brand { padding: 9px 18px; font-size: var(--t-label); font-weight: 600; letter-spacing: .02em; }
.nav-pill { padding: 9px 17px; font-size: var(--t-label); letter-spacing: .01em; }

[data-chrome-tone="dark"] .brand    { background: rgba(12, 12, 13, .64); border-color: rgba(255, 255, 255, .32); color: #fff; }
[data-chrome-tone="dark"] .nav-pill { background: rgba(12, 12, 13, .60); border-color: rgba(255, 255, 255, .30); color: #fff; }
/* Active inverts, same logic as the light pages — there the current page is an
   ink fill with light text, here it's a light fill with ink text. */
[data-chrome-tone="dark"] .nav-pill[aria-current="page"] { background: #ffffff; border-color: #ffffff; color: var(--paper-dark); }

[data-chrome-tone="light"] .brand    { background: transparent; border-color: var(--line-light); color: var(--ink); }
[data-chrome-tone="light"] .nav-pill { background: transparent; border-color: var(--line-light); color: var(--ink); }
[data-chrome-tone="light"] .nav-pill[aria-current="page"] { background: var(--ink); color: var(--ink-invert); }

/* ── Home: full-bleed reel ───────────────────────────────── */

.reel-item {
  position: relative;
  display: block;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  min-height: 560px;
  overflow: hidden;
  border-bottom: 1px solid var(--hairline-dark);
}
/* No dim over the covers: the images carry the page, so the caption relies on
   its own shadow instead of a gradient laid over the artwork. */
.reel-scrim { display: none; }

/* One caption for the whole reel, parked at left-centre. It is fixed, so it
   holds still while the images travel past it; JS swaps the text at the
   halfway line. pointer-events: none keeps the image link clickable through it. */
.reel-now {
  position: fixed;
  top: 50%;
  left: 26px;
  z-index: 5;
  width: min(42vw, 420px);
  height: 0;
  pointer-events: none;
  color: #fff;
}
/* One row per project, all parked on the same centre line. JS offsets each
   row by its distance from the current scroll position, so the names travel
   with the scroll instead of swapping on a timer. Motion only, no blur. */
.reel-now-row {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 4px;
  opacity: 0;
  visibility: hidden;
  will-change: transform, opacity;
}
.reel-now-client {
  font-size: var(--t-title-s);
  line-height: 1.25;
  letter-spacing: -.01em;
}
.reel-now-name {
  font-size: var(--t-label);
  line-height: 1.35;
  color: rgba(255, 255, 255, .72);
}
.reel-now-name:empty { display: none; }
.reel-now-row.is-dark { color: #17181a; }
.reel-now-row.is-dark .reel-now-name { color: rgba(23, 24, 26, .62); }

@media (max-width: 720px) {
  .reel-now { left: 18px; width: calc(100% - 36px); }
}

.reel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 26px 24px;
}
.reel-foot-copy { font-size: var(--t-caption); letter-spacing: .06em; color: var(--ink-on-dark-dim); }
.reel-foot .pill { padding: 8px 18px; }

.tagrow { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }

/* ── Work: contact sheet ─────────────────────────────────── */

.work-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 0 26px 18px;
  border-bottom: 1px solid var(--ink);
}

/* ── Work intro ──────────────────────────────────────────── */

/* `ch` is the width of a "0", so a ch-based measure collapses on Korean, where
   every glyph is roughly twice that. em tracks the clamped display size and
   lands around 14 Hangul characters a line. */
.work-intro { padding: 132px 26px 52px; }
.work-statement {
  margin: 0;
  max-width: 14em;
  font-size: clamp(1.75rem, 3vw, 2.625rem);   /* 28 → 42 */
  line-height: 1.22;
  letter-spacing: -.02em;
  color: var(--ink);
  text-wrap: balance;
}
/* The reference accents one word of its statement. One colour, one word — any
   more and the achromatic palette stops being the point. */
.work-statement em { font-style: normal; color: var(--ink-muted); }
.work-statement-en {
  margin: 14px 0 0;
  max-width: 46ch;
  font-size: var(--t-label);
  line-height: 1.5;
  color: var(--ink-muted);
  text-wrap: pretty;
}

/* ── Chips ───────────────────────────────────────────────── */
/* Soft rectangles, not pills. Used for filters, disciplines and case actions,
   so all three read as one control language. */

.chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 28px;
  padding: 6px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-chip);
  background: var(--chip-fill);
  color: var(--ink);
  font-size: var(--t-caption);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .16s var(--ease-out), color .16s var(--ease-out);
}
.chip:hover { background: rgba(20, 20, 22, .12); }
.chip[aria-pressed="true"] { background: var(--chip-fill-on); color: var(--ink-invert); }
.chip[aria-pressed="true"]:hover { background: var(--ink-strong); }
.chip-solid { background: var(--ink); color: var(--ink-invert); }
.chip-solid:hover { background: var(--ink-strong); }
.chip-static { cursor: default; }

/* Shape says whether it can be pressed. Controls — filters, Watch, View all —
   take the same capsule as the Grid/Index toggle and the nav, so every control
   on the page is one family. Static tags keep the small 4px box: they are
   labels, and giving them the capsule too would make them look clickable. */
button.chip, a.chip { border-radius: 999px; padding-left: 12px; padding-right: 12px; }
.chip-clear { padding-left: 4px; padding-right: 4px; }
.chip-static:hover { background: var(--chip-fill); }

.filter-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  flex: 1;
}
.filter-label {
  font-size: var(--t-caption);
  color: var(--ink-muted);
  letter-spacing: .04em;
  margin-right: 2px;
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip-clear { background: transparent; color: var(--ink-muted); text-decoration: underline; text-underline-offset: 3px; }
.chip-clear:hover { background: transparent; color: var(--ink); }

.work-head-controls { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; justify-content: flex-end; }

/* ── View-mode toggle ────────────────────────────────────── */
/* The one place the page is allowed a moving part. The thumb is a real
   element sliding on transform rather than a background swap, so the two
   states stay physically connected — which is the whole point of a segmented
   control over two separate buttons. */

.seg {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  border: 1px solid var(--line-light);
  border-radius: 999px;
  padding: 2px;
  isolation: isolate;
  flex: none;
}
.seg-thumb {
  position: absolute;
  z-index: 0;
  top: 2px; bottom: 2px; left: 2px;
  width: calc(50% - 2px);
  border-radius: 999px;
  background: var(--ink);
  transition: transform .26s var(--ease-out);
}
.seg[data-mode="index"] .seg-thumb { transform: translateX(100%); }

.seg-opt {
  position: relative;
  z-index: 1;
  appearance: none;
  border: 0;
  background: transparent;
  margin: 0;
  padding: 7px 15px;
  font: 500 var(--t-caption)/1 var(--sans);
  letter-spacing: .06em;
  color: var(--ink-filter);
  cursor: pointer;
  border-radius: 999px;
  transition: color .26s var(--ease-out);
}
.seg-opt[aria-pressed="true"] { color: var(--ink-invert); }

/* ── Work: index view ────────────────────────────────────── */

.index { padding: 10px 26px 26px; }
.index-row {
  display: grid;
  grid-template-columns: 3.25rem minmax(0, 1fr) auto 3.75rem 3.25rem;
  gap: 18px;
  align-items: baseline;
  padding: 13px 6px;
  border-top: 1px solid var(--rule-light);
  transition: background-color .16s var(--ease-out);
  animation: rise .3s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 18ms);
}
.index-row:last-child { border-bottom: 1px solid var(--rule-light); }
.index-row:hover { background: var(--paper-tint); }
.index-num   { font-size: var(--t-caption); color: var(--ink-muted); }
.index-name  { font-size: var(--t-label); color: var(--ink); }
.index-tags  { font-size: var(--t-caption); color: var(--ink-muted); letter-spacing: .06em; white-space: nowrap; }
.index-year,
.index-ratio { font-size: var(--t-caption); color: var(--ink-muted); text-align: right; }

.sheet {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 40px 16px;
  align-items: start;
  padding: 24px;
}
/* Thumbnails are cropped into a fixed frame (see thumbFrame in app.js); the crop
   centres unless a project names its own focus. */
.sheet .image-slot__img { object-position: var(--thumb-pos, 50% 50%); }
.sheet-item { display: flex; flex-direction: column; gap: 9px; }

/* Cells carry different aspect ratios on purpose — that ragged bottom edge is
   the contact sheet. The captions riding along with them is not: they landed
   at a different height in every column, which reads as untidy rather than
   free. Subgrid splits each visual row into an image track and a caption
   track, so every caption in a row sits on one baseline while the images keep
   their own heights. Falls back to the flex column above where unsupported. */
/* V2: captions ride under their own image again, like the reference. With
   real covers mixing a 3:4 portrait and 2.4:1 banners in one row, a shared
   caption baseline left a void the height of the portrait under every
   landscape card — alignment that made the grid look emptier, not tidier.
   The subgrid also only had two tracks for three children (image, names,
   tags), which stacked the tags on top of the names. */
@supports (grid-template-rows: subgrid) and (not (grid-template-rows: subgrid)) {
  .sheet-item {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
    row-gap: 9px;
    align-content: start;
  }
  /* Without this the shorter cells stretch to the tallest one in the row and
     lose their aspect ratio. */
  .sheet-item > .cell { align-self: start; }
}
/* Client over project title, same size, separated by ink level — the card
   caption is a small echo of the case page's header. */
.sheet-foot { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.sheet-names { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sheet-client { font-size: var(--t-label); color: var(--ink); }
.sheet-title  { font-size: var(--t-label); color: var(--ink-muted); text-wrap: pretty; }
.sheet-type   { margin-top: 5px; font-size: var(--t-caption); color: var(--ink-muted); letter-spacing: .02em; }

/* Recognition marker. Empty by default — AWARDS in data.js is intentionally
   unpopulated, since inventing an award on a real portfolio is a lie. */
.sheet-award {
  flex: none;
  width: 7px; height: 7px;
  margin-top: 7px;
  border-radius: 999px;
  background: var(--award-tone, var(--ink-muted));
}
.sheet-award[data-tone="gold"]   { --award-tone: #b08a34; }
.sheet-award[data-tone="silver"] { --award-tone: #8d9298; }
.sheet-award[data-tone="bronze"] { --award-tone: #9a6b45; }

.sheet-tags { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 7px; }
.sheet-tags .chip { min-height: 22px; padding: 4px 8px; cursor: inherit; }

.work-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 26px;
  border-top: 1px solid var(--ink);
}
.work-foot .pill { padding: 7px 15px; }

/* ── Project case ────────────────────────────────────────── */
/* This page runs on its own type scale, not the eight-step one the rest of the
   site uses. The reference gets a whole case page out of four sizes and a
   single weight, building hierarchy from size and value instead of from
   weight — client and project title are the same 48px and separate only by
   ink level. It is a tighter system than mixing three weights, and it is
   scoped here so the change can be judged on one page before it spreads. */

.case {
  --c-display: clamp(2rem, 3.4vw, 3rem);   /* 32 → 48 */
  --c-body:    18px;
  --c-meta:    14px;
  --c-chip:    12px;
  --c-lh-display: 1.16;
  --c-lh-body:    1.45;
  font-weight: 500;
  padding: 128px 26px 0;
}
.case :is(h1, h2, p, span, a, sup) { font-weight: 500; }

.case-head {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 26px;
  align-items: start;
  padding-bottom: 58px;
}
.case-ident { grid-column: 1 / span 5; }
.case-client,
.case-title {
  margin: 0;
  font-size: var(--c-display);
  line-height: var(--c-lh-display);
  letter-spacing: -.02em;
  text-wrap: balance;
}
.case-client { color: var(--ink); }
/* What kind of work — one line under the title, in the page's meta size so it
   reads as a label, not as a third heading. */
.case-type {
  margin: 16px 0 0;
  font-size: var(--c-meta);
  line-height: 1.4;
  color: var(--ink-muted);
  letter-spacing: .01em;
}
/* Same size, lower ink. The whole hierarchy of this block is one value step. */
.case-title { color: rgba(23, 24, 26, .62); }
.case-year {
  font-size: var(--c-meta);
  line-height: 1;
  margin-left: .35em;
  top: -.75em;
  color: var(--ink-muted);
  letter-spacing: 0;
}

.case-intro {
  grid-column: 7 / span 6;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
}
.case-overview {
  margin: 0;
  font-size: var(--c-body);
  line-height: var(--c-lh-body);
  color: var(--ink);
  text-wrap: pretty;
}
/* Tags and role · tools were one row, but measured against the six-column
   intro that only fit when both were short — Jeju Air overflowed by 175px and
   Paul Bassett sat at exactly 0px of slack. The tags now sit under the type
   label, where they belong semantically (what kind of work → which
   disciplines), and role · tools get the whole intro width: one line for every
   project, with ~40px to spare on the longest. */
.case-tags { margin-top: 12px; }
.case-meta { display: flex; align-items: baseline; gap: 10px; }
.case-meta .case-spec { flex: 1; min-width: 0; }
.case-arrow { font-size: var(--c-chip); color: var(--ink-muted); }
.case-spec {
  margin: 0;
  font-size: var(--c-meta);
  line-height: 1.5;
  color: var(--ink-muted);
  text-wrap: pretty;
}
.case-spec-sep { color: var(--ink-faint); }


.case-film {
  margin: 0 0 26px;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-media);
  background: var(--paper-dark);
}
.case-film iframe { width: 100%; height: 100%; border: 0; display: block; }

/* Copy sits in the right column only; media spans the full width or splits.
   The empty left column is the page's rhythm, not wasted space. */
.case-story {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 26px 26px;
  padding-bottom: 64px;
}
.case-para {
  grid-column: 7 / span 6;
  margin: 0;
  font-size: var(--c-body);
  line-height: var(--c-lh-body);
  color: var(--ink);
  text-wrap: pretty;
  padding: 34px 0;
}
.case-shot { margin: 0; grid-column: 1 / span 12; }
.case-shot .cell { border-radius: var(--radius-media); overflow: hidden; }
.case-shot.is-half { grid-column: span 6; }

/* Plates side by side share one height. Each item grows by its own aspect
   ratio, so a portrait SNS frame next to a landscape keeps both images
   uncropped on one baseline — the document's placement feel, without a table. */
.case-row {
  grid-column: 1 / span 12;
  display: flex;
  gap: 16px;
  margin: 0;
  align-items: flex-start;
}
.case-row .case-shot { flex: var(--grow, 1) 1 0; min-width: 0; }
.case-film[hidden] { display: none !important; }
/* Vertical films: width is capped from viewport height in JS; centre them. */
.case-film.is-tall { margin-left: auto; margin-right: auto; }
.cell .cell-film {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  background: var(--paper-dark);
}
video.cell-film { object-fit: cover; }
.case-pending {
  grid-column: 7 / span 6;
  margin: 0;
  padding: 0 0 12px;
  font-size: var(--c-chip);
  color: var(--ink-muted);
  letter-spacing: .04em;
}
.case-shot figcaption {
  margin-top: 10px;
  font-size: var(--c-chip);
  color: var(--ink-muted);
  letter-spacing: .04em;
}
.case-shot-link {
  text-decoration: underline;
  text-decoration-color: var(--rule-light);
  text-underline-offset: 3px;
  transition: color .15s var(--ease-out), text-decoration-color .15s var(--ease-out);
}
.case-shot-link:hover { color: var(--ink); text-decoration-color: currentColor; }

.case-more { padding: 44px 0 0; border-top: 1px solid var(--rule-light); }
.case-more-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 22px;
}
.case-more-title {
  margin: 0;
  font-size: var(--c-body);
  line-height: 1.3;
  letter-spacing: -.01em;
  color: var(--ink);
}

.related {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  padding-bottom: 64px;
}
.related-item { display: flex; flex-direction: column; gap: 9px; }
.related-names { display: flex; flex-direction: column; gap: 1px; }
.related-client { font-size: var(--c-meta, 14px); color: var(--ink); }
.related-title  { font-size: var(--c-meta, 14px); color: var(--ink-muted); }

.work-empty {
  margin: 0;
  padding: 60px 26px 40px;
  font-size: var(--t-body-s);
  color: var(--ink-muted);
  text-align: center;
}

/* ── Case + Work responsive ──────────────────────────────── */

@media (max-width: 1080px) {
  .case-ident { grid-column: 1 / span 12; }
  .case-intro { grid-column: 1 / span 12; }
  .case-para  { grid-column: 1 / span 12; padding: 20px 0; }
  .case-shot.is-half { grid-column: span 6; }
  .related { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 720px) {
  .work-intro { padding: 104px 18px 36px; max-width: none; }
  .work-head { padding: 0 18px 16px; }
  .work-head-controls { width: 100%; justify-content: space-between; }
  .case { padding: 104px 18px 0; }
  .case-head { gap: 18px; padding-bottom: 36px; }
  .case-shot.is-half { grid-column: 1 / span 12; }
  /* Four portraits across 375px are thumbnails; wrap rows two-up instead. */
  .case-row { flex-wrap: wrap; gap: 12px; }
  .case-row .case-shot { flex: 1 1 calc(50% - 6px); }
  .case-story { gap: 18px; padding-bottom: 40px; }
  .case-more-head { flex-direction: column; align-items: flex-start; gap: 10px; }
}

/* ── About ───────────────────────────────────────────────── */

.about-h1 {
  font-size: var(--t-display-s);
  font-weight: 600;
  line-height: var(--lh-title);
  letter-spacing: -.03em;
  max-width: 18ch;
  text-wrap: pretty;
}
/* Bilingual pages read by language, not line by line: Korean holds the left
   column at body size, English sits to the right one step smaller and quieter.
   Both keep their own measure — Korean around 34em, English 62ch — so neither
   runs past a comfortable line length. */
.about-body {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: 56px;
  margin-top: 46px;
  max-width: 1060px;
}
.about-kr {
  display: flex;
  flex-direction: column;
  gap: 20px;
  max-width: 34em;
}
.about-en {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 62ch;
  padding-top: 14px;
  border-top: 1px solid var(--rule-light);
}
.kr { font-size: var(--t-body); line-height: var(--lh-kr); letter-spacing: -.01em; color: var(--ink-strong); text-wrap: pretty; }
.en { font-size: var(--t-label); line-height: var(--lh-body); color: var(--ink-muted); text-wrap: pretty; }
/* The English opening line answers the Korean h1, so it carries the weight of a
   deck rather than body copy. */
.about-en .en:first-child { color: var(--ink-body); font-size: var(--t-body-s); }

.about-tools { margin-top: 30px; }
.about-tools .pill { padding: 7px 14px; }

.about-exp { display: block; margin-top: 64px; }
/* Same measure as the bilingual body above, so the six roles sit on one line. */
.exp-table { display: flex; flex-direction: column; margin-top: 12px; max-width: 1060px; }
.exp-row {
  display: grid;
  /* The date column was auto, so its width followed each row's dates and
     pushed the role column to a different x on every row. A fixed width keeps
     all three columns on one vertical line. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr) 9.5em;
  gap: 20px;
  padding: 15px 0;
  border-top: 1px solid var(--rule-light);
}
.exp-row.is-last { border-bottom: 1px solid var(--rule-light); }
.exp-org  { font-size: var(--t-body-s); }
.exp-what { font-size: var(--t-label); color: var(--ink-body); }
.exp-when { font-size: var(--t-caption); color: var(--ink-muted); white-space: nowrap; text-align: right; }

.about-foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  margin-top: 56px;
  padding-bottom: 64px;
}
.about-foot .tagrow { gap: 8px; }
.about-mark {
  position: relative;
  width: 96px; height: 96px;
  flex: none;
  background: var(--paper-cell);
}

/* ── Blog ────────────────────────────────────────────────── */

.blog-head { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; }
.blog-head-title { display: flex; flex-direction: column; gap: 6px; }
.blog-head-title h1 {
  font-size: var(--t-display-s);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: -.03em;
}
.blog-head-title .meta { font-size: var(--t-label); }
.blog-head-note { display: flex; flex-direction: column; gap: 5px; max-width: 40ch; text-align: right; }
.blog-head-note .kr { font-size: var(--t-label); line-height: var(--lh-body); }
.blog-head-note .en { font-size: var(--t-caption); line-height: var(--lh-body); }

.blog-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 22px;
  margin-top: 40px;
  padding-bottom: 64px;
}
.note {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--rule-light);
  position: relative;
  isolation: isolate;
  transition: transform .3s var(--ease-out), box-shadow .3s var(--ease-out);
}

/* ── Note card interactions ──────────────────────────────── */
/* Ported from a bento-grid component: lift, a dot-screen that surfaces on
   hover, and a read affordance that fades in. What was NOT ported is the
   colour — the source tints every icon (blue/emerald/purple/sky) and rounds
   every corner to 12px, which is the opposite of the white-cube language this
   site settled on. The interaction is the good part; the skin is not. */

.note::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: radial-gradient(circle at center, rgba(20, 20, 22, .05) 1px, transparent 1px);
  background-size: 4px 4px;
  opacity: 0;
  transition: opacity .3s var(--ease-out);
  pointer-events: none;
}
/* Content sits above the screen. Without this the dots paint over the text,
   since ::before is a positioned box and the copy is not. */
.note > * { position: relative; z-index: 1; }

.note:hover,
.note:focus-visible {
  transform: translateY(-2px);
  /* The only shadow on the site. At 5% it isn't a shadow style, it's the cue
     that the card came off the page. */
  box-shadow: 0 2px 14px rgba(20, 20, 22, .05);
}
.note:hover::before,
.note:focus-visible::before { opacity: 1; }

/* The lead card holds the hover state permanently — the source calls this
   hasPersistentHover. It marks the featured note and, more usefully, shows
   what hovering does before anyone hovers. */
.note.is-featured::before { opacity: 1; }
.note.is-featured { transform: translateY(-2px); box-shadow: 0 2px 14px rgba(20, 20, 22, .05); }

.note-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: auto;
  padding-top: 4px;
}
.note-cta {
  font-size: var(--t-caption);
  letter-spacing: .06em;
  color: var(--ink-muted);
  white-space: nowrap;
  /* Right-aligns whether or not the row has a date beside it, so the cards
     with nothing on the left don't need a filler span. */
  margin-left: auto;
  opacity: 0;
  transition: opacity .24s var(--ease-out);
}
.note:hover .note-cta,
.note:focus-visible .note-cta,
.note.is-featured .note-cta { opacity: 1; }

/* Hover is not available on touch, and an affordance nobody can trigger is
   just a hidden label. Same call as the reel captions. */
@media (pointer: coarse) {
  .note-cta { opacity: 1; }
  .note:hover, .note:focus-visible { transform: none; box-shadow: none; }
}
.note-cover { position: relative; width: 100%; }
/* The card's own border already frames these covers — a cell hairline on top
   of it would read as a doubled 2px line. */
.note-cover::after { display: none; }
.note-text { display: flex; flex-direction: column; gap: 10px; padding: 22px; }
.note-text.tight { gap: 8px; padding: 18px; }
.note-kicker { display: flex; gap: 9px; align-items: center; }
.note-title { font-size: var(--t-title-s); font-weight: 600; line-height: var(--lh-title); letter-spacing: -.03em; }
.note-title.lead { font-size: var(--t-title); }
.note-en { font-size: var(--t-label); color: var(--ink-muted); }
.note-en.xs { font-size: var(--t-caption); }
.note-dek { font-size: var(--t-label); line-height: var(--lh-kr); letter-spacing: -.01em; color: var(--ink-body); text-wrap: pretty; }
.note-dek.lead { font-size: var(--t-body-s); max-width: 44ch; }

.note-lead { grid-column: 1 / span 8; }
.blog-side { grid-column: 9 / span 4; display: flex; flex-direction: column; gap: 22px; }
.note-script { gap: 14px; padding: 20px; flex: 1; }
.note-image { overflow: hidden; }
.note-tint {
  grid-column: 1 / span 4;
  justify-content: space-between;
  gap: 16px;
  background: var(--paper-tint);
  padding: 20px;
  min-height: 210px;
}
.note-wide { grid-column: 5 / span 4; }
.note-next {
  grid-column: 9 / span 4;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--line-light);
  min-height: 210px;
}

/* ── Chrome variants (design's `chrome` prop) ────────────── */
/* `scrim` is the default and lives in the rules above. `frosted` restores the
   design file's glass treatment so compare.html can put the two side by side. */

/* `bar` (A2) — the top bar stops being a row of floating chips. Nav becomes
   plain text; once the router flips the tone the bar takes a solid paper
   ground and a hairline, so it reads as a masthead rather than as UI hovering
   over the work. Everything outside the bar keeps the scrim treatment, which
   is what makes this comparable against A1 one variable at a time. */

[data-chrome="bar"] .topbar {
  align-items: center;
  padding: 16px 26px;
  background: var(--paper-light);
  border-bottom: 1px solid var(--rule-light);
  transition: background-color .2s var(--ease-out), border-color .2s var(--ease-out);
}
[data-chrome="bar"] .topbar > * { position: relative; z-index: 1; }

/* Over imagery the bar goes transparent, but white text on an unknown frame
   is a contrast gamble — a top scrim carries it without reintroducing chips. */
[data-chrome="bar"] .topbar::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, .66) 0%, rgba(0, 0, 0, .58) 70%, rgba(0, 0, 0, 0) 100%);
  opacity: 0;
  transition: opacity .2s var(--ease-out);
  pointer-events: none;
}
[data-chrome="bar"][data-chrome-tone="dark"] .topbar { background: transparent; border-bottom-color: transparent; }
[data-chrome="bar"][data-chrome-tone="dark"] .topbar::before { opacity: 1; }

/* Tone is spelled out on both sides so these win on specificity rather than on
   source order — the tone rules near the top of the file set the same two
   properties at equal weight, and relying on "mine comes later" would break the
   moment anything moves. */
[data-chrome="bar"][data-chrome-tone="dark"] .brand,
[data-chrome="bar"][data-chrome-tone="dark"] .nav-pill,
[data-chrome="bar"][data-chrome-tone="light"] .brand,
[data-chrome="bar"][data-chrome-tone="light"] .nav-pill {
  background: transparent;
  border-color: transparent;
  border-radius: 0;
  padding: 6px 0;
  backdrop-filter: none;
}
[data-chrome="bar"] .topbar-nav { gap: 22px; }
[data-chrome="bar"][data-chrome-tone="dark"] .brand,
[data-chrome="bar"][data-chrome-tone="dark"] .nav-pill { color: #fff; }
[data-chrome="bar"][data-chrome-tone="light"] .brand,
[data-chrome="bar"][data-chrome-tone="light"] .nav-pill { color: var(--ink); }
/* Current page is an underline, not a fill. box-shadow keeps it out of layout. */
[data-chrome="bar"][data-chrome-tone="dark"] .nav-pill[aria-current="page"],
[data-chrome="bar"][data-chrome-tone="light"] .nav-pill[aria-current="page"] {
  background: transparent;
  border-color: transparent;
  box-shadow: inset 0 -1px 0 currentColor;
}
[data-chrome="bar"][data-chrome-tone="dark"] .nav-pill[aria-current="page"] { color: #fff; }
[data-chrome="bar"][data-chrome-tone="light"] .nav-pill[aria-current="page"] { color: var(--ink); }

[data-chrome="frosted"] .pill { backdrop-filter: blur(16px); }
[data-chrome="frosted"] .pill-glass {
  background: rgba(255, 255, 255, .14);
  border-color: var(--line-dark);
}
[data-chrome="frosted"][data-chrome-tone="dark"] .brand {
  background: rgba(255, 255, 255, .18);
  border-color: var(--line-dark);
}
[data-chrome="frosted"][data-chrome-tone="dark"] .nav-pill {
  background: rgba(255, 255, 255, .14);
  border-color: var(--line-dark);
  color: #fff;
}
[data-chrome="frosted"][data-chrome-tone="dark"] .nav-pill[aria-current="page"] {
  background: rgba(255, 255, 255, .26);
  border-color: var(--line-dark);
  color: #fff;
}

[data-chrome="outline"] .pill {
  backdrop-filter: none;
  background: transparent !important;
  border-color: currentColor !important;
}

[data-chrome="solid"] .surface-dark .pill,
[data-chrome="solid"] .surface-slot .pill {
  backdrop-filter: none;
  background: rgba(255, 255, 255, .94) !important;
  border-color: transparent !important;
  color: var(--paper-dark) !important;
}
[data-chrome="solid"] .surface-page .pill {
  backdrop-filter: none;
  background: var(--ink) !important;
  border-color: var(--ink) !important;
  color: var(--paper-light) !important;
}

/* `?notes=prev` restores the pre-bento card layout: the date sits as a plain
   child again instead of in a footer row, and the read affordance is gone —
   it lives inside that row, so it can't survive its removal. Lift, shadow and
   dot screen stay on, which makes the layout the only variable.
   `display: contents` drops the wrapper without touching the markup. */
[data-notes="prev"] .note-foot { display: contents; }
[data-notes="prev"] .note-cta { display: none; }
[data-notes="prev"] .note-lead .note-foot .meta { display: none; }
[data-notes="prev"] .note-script .note-foot .meta { margin-top: auto; }

/* ── Earlier revisions (compare.html only) ───────────────── */
/* `?tokens=legacy` = the original design-file palette.
   `?tokens=warm`   = revision 1, warm-neutral paper + hairline cells.
   Neither is used by the live site; delete this section and the `tokens`
   branch in applyProps() once the palette is settled. These restore colour
   only — the type scale applies to every revision, so the comparison isolates
   the palette. */

[data-tokens="legacy"],
[data-tokens="warm"] {
  /* Warm ink set, shared by both earlier revisions. */
  --ink:        #1c1a15;
  --ink-strong: #25241f;
  --ink-body:   #4a463d;
  --ink-filter: #3a3934;
  --ink-muted:  #8a877f;
  --ink-faint:  #a09a8c;
  --ink-invert: #eceae6;
  --line-light: rgba(28, 26, 21, .35);
  --rule-light: rgba(28, 26, 21, .2);
  --hairline-dark:   rgba(236, 234, 230, .12);
  --ink-on-dark-dim: rgba(236, 234, 230, .45);
  --slot-ink-dark:   rgba(236, 234, 230, .4);
}

[data-tokens="legacy"] {
  --paper-light: #eceae6;
  --paper-card:  #f2f1ed;
  --paper-cell:  #dedbd4;
  --paper-tint:  #e3e0d9;
  --paper-dark:  #0b0b0a;
  --paper-slot:  #3b3b35;
  --slot-ink:    rgba(28, 26, 21, .38);
}
[data-tokens="legacy"][data-paper="cool"] {
  --paper-light: #e7eaec; --paper-card: #eef1f3; --paper-cell: #d6dbde;
  --paper-tint: #dde2e5; --paper-dark: #080b0d; --paper-slot: #333c40;
}
[data-tokens="legacy"][data-paper="mono"] {
  --paper-light: #ebebeb; --paper-card: #f4f4f4; --paper-cell: #dcdcdc;
  --paper-tint: #e2e2e2; --paper-dark: #080808; --paper-slot: #3a3a3a;
}
[data-tokens="legacy"] .cell::after { display: none; }

[data-tokens="warm"] {
  --paper-light: #f2f1ef;
  --paper-card:  #f8f8f6;
  --paper-cell:  #d6d4d0;
  --cell-line:   rgba(28, 26, 21, .14);
  --paper-tint:  #e6e5e1;
  --paper-dark:  #0a0a0a;
  --paper-slot:  #3a3a3b;
  --slot-ink:    rgba(28, 26, 21, .48);
}
[data-tokens="warm"][data-paper="cool"] {
  --paper-light: #eef0f1; --paper-card: #f6f8f9; --paper-cell: #d2d6d9;
  --cell-line: rgba(18, 26, 30, .15);
  --paper-tint: #e3e6e8; --paper-dark: #080a0c; --paper-slot: #343a3d;
}
[data-tokens="warm"][data-paper="mono"] {
  --paper-light: #f2f2f2; --paper-card: #f9f9f9; --paper-cell: #d5d5d5;
  --cell-line: rgba(20, 20, 20, .15);
  --paper-tint: #e6e6e6; --paper-dark: #080808; --paper-slot: #3a3a3a;
}

/* ── Hit areas ───────────────────────────────────────────── */
/* Interactive pills measured 25–35px tall. The markup already separates the
   two cases: interactive pills are <a>/<button>, decorative tags are <span>,
   so the element selector is the whole filter — no extra class needed.

   Fine pointers keep the drawn size; WCAG 2.2 AA asks 24×24 CSS px and the
   filter row was scraping it at 25px. Coarse pointers get a real 44px box
   rather than an invisible overlay: in a wrapping row, overlapping phantom
   hit areas cause worse mis-taps than a small button does.

   This section sits after the components so its gap rules win on source
   order — the earlier `gap: 7px` declarations would otherwise override it. */

a.pill, button.pill, .seg-opt { min-height: 28px; }

/* .chip-static is a label on a card, not a control, so it keeps its drawn size
   on every pointer — growing it to 44px would put finger targets on things
   that do nothing. */
@media (pointer: coarse) {
  a.pill, button.pill, .skip-link, .seg-opt,
  button.chip:not(.chip-static), a.chip:not(.chip-static) {
    min-height: var(--tap);
    min-width: var(--tap);
    justify-content: center;
  }
  /* Real 44px boxes need real gaps — 7px put the targets closer than the 8px
     minimum once they grew. */
  .topbar-nav { gap: 8px; }
  .tagrow { gap: 8px; }
  .chips { gap: 8px; }
  .filter-row { gap: 10px 8px; }
  .about-foot .tagrow { gap: 8px; }
  .detail-ident .tagrow.actions { gap: 8px; }
  .image-slot__clear { width: var(--tap); height: var(--tap); font-size: var(--t-body); }
}

/* ── Responsive ──────────────────────────────────────────── */
/* Display sizes are clamped in the token block, so no size overrides here. */

@media (max-width: 1080px) {
  .sheet { grid-template-columns: repeat(2, 1fr); }
  .related { grid-template-columns: repeat(2, 1fr); }

  .detail-ident { grid-column: 1 / span 12; }
  .detail-copy  { grid-column: 1 / span 8; }
  .detail-facts { grid-column: 9 / span 4; flex-direction: row; flex-wrap: wrap; gap: 24px; }

  .shot-a { grid-column: 1 / span 12; }
  .shot-b { grid-column: 1 / span 6; margin-top: 0; }
  .shot-c { grid-column: 1 / span 12; }
  .shot-d { grid-column: 1 / span 6; }
  .shot-e { grid-column: 7 / span 6; margin-top: 0; }

  .note-lead { grid-column: 1 / span 12; }
  .blog-side { grid-column: 1 / span 12; flex-direction: row; }
  .blog-side > * { flex: 1; }
  .note-tint { grid-column: 1 / span 6; }
  .note-wide { grid-column: 7 / span 6; }
  .note-next { grid-column: 1 / span 12; }
}

@media (max-width: 720px) {
  /* One step down across the board rather than per-element overrides. */
  :root {
    --t-body:   17px;
    --t-title:  24px;
    --t-title-s: 20px;
  }

  .pad-page { padding: 104px 18px 0; }

  .topbar { padding: 16px 16px; flex-wrap: wrap; gap: 10px; }
  .brand, .nav-pill { font-size: var(--t-caption); padding: 8px 14px; }

  .reel-item { min-height: 420px; }
  .reel-foot { padding: 20px 18px; }

  .work-head { flex-direction: column; align-items: flex-start; padding: 88px 18px 16px; }
  .work-head-controls { width: 100%; justify-content: space-between; }
  .filters { justify-content: flex-start; }

  /* Two lines rather than dropping columns — the ratio and year are the
     reason to be in this view at all. */
  .index { padding: 8px 18px 18px; }
  .index-row { grid-template-columns: 3rem minmax(0, 1fr) auto; gap: 3px 12px; }
  .index-num   { grid-column: 1; grid-row: 1; }
  .index-name  { grid-column: 2; grid-row: 1; }
  .index-year  { grid-column: 3; grid-row: 1; }
  .index-tags  { grid-column: 2; grid-row: 2; }
  .index-ratio { grid-column: 3; grid-row: 2; }
  .sheet { grid-template-columns: repeat(1, 1fr); gap: 30px 14px; padding: 18px; }
  .work-foot { padding: 14px 18px; }

  .detail-hero { aspect-ratio: 2 / 1; min-height: 220px; max-height: none; }
  .detail-body { padding: 16px 18px 0; }
  .detail-copy  { grid-column: 1 / span 12; }
  .detail-facts { grid-column: 1 / span 12; }
  .detail-gallery { gap: 20px; padding: 36px 0 16px; }
  .shot-b, .shot-d, .shot-e { grid-column: 1 / span 12; }
  .shot-note { margin: 28px 0 32px; }
  .related { grid-template-columns: repeat(2, 1fr); }

  .about-body { grid-template-columns: 1fr; gap: 26px; margin-top: 32px; }
  .about-kr, .about-en { max-width: none; }
  .exp-row { grid-template-columns: 1fr; gap: 4px; }
  .about-foot { flex-direction: column; align-items: flex-start; }

  .blog-head { flex-direction: column; align-items: flex-start; }
  .blog-head-note { text-align: left; }
  .blog-side { flex-direction: column; }
  .note-tint, .note-wide { grid-column: 1 / span 12; }
}

/* ── HeroUI island ───────────────────────────────────────── */
/* The Work page's filter row and view toggle are a small React island built
   from ui/. app.js still renders its own versions; they stay in the page as a
   fallback and are hidden only once the island has mounted and set the flag. */
.work-head[data-ui="heroui"] > .filter-row,
.work-head[data-ui="heroui"] > .work-head-controls { display: none; }

.work-ui { width: 100%; }
.work-ui-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}
.work-ui-filters { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.work-ui-view { display: flex; align-items: center; gap: 14px; }
.work-ui-label,
.work-ui-count {
  font-size: var(--t-caption);
  color: var(--ink-muted);
  letter-spacing: .06em;
  white-space: nowrap;
}

/* ── Tonal controls (Body Shop reference) ────────────────── */
/* Controls drop their outlines and separate from the page by tone alone: a
   translucent capsule track, and a white capsule thumb for the current item.
   Measured off body-shop.co — track rgba(235,235,235,.4) + blur(24px), thumb
   rgba(255,255,255,.8), no border, no shadow, 0.3s standard easing.
   Here the track is a touch darker so it still reads on a pure white page,
   and inactive labels use --ink-muted (≈5:1) rather than the reference's 40%
   black (≈2.9:1), which fails AA at this size.
   `?controls=legacy` (compare.html) switches all of this off. */

:root {
  --tone-track:      rgba(20, 20, 22, .055);
  --tone-track-dark: rgba(12, 12, 13, .42);
  --tone-thumb:      rgba(255, 255, 255, .92);
  --tone-blur:       blur(24px);
  --ease-thumb:      cubic-bezier(.4, 0, .2, 1);
}

html:not([data-controls="legacy"]) .topbar-nav {
  position: relative;
  gap: 0;
  padding: 4px;
  border-radius: 999px;
  background: var(--tone-track);
  -webkit-backdrop-filter: var(--tone-blur);
  backdrop-filter: var(--tone-blur);
}
html:not([data-controls="legacy"]) .nav-thumb {
  position: absolute;
  top: 4px; bottom: 4px; left: 0;
  width: 0;
  border-radius: 999px;
  background: var(--tone-thumb);
  opacity: 0;
  pointer-events: none;
  transition: transform .3s var(--ease-thumb), width .3s var(--ease-thumb), opacity .2s var(--ease-thumb);
}
html[data-controls="legacy"] .nav-thumb { display: none; }

html:not([data-controls="legacy"]) .nav-pill,
html:not([data-controls="legacy"]) .nav-pill[aria-current="page"] {
  position: relative;
  z-index: 1;
  border-color: transparent;
  background: transparent;
  color: var(--ink-muted);
}
html:not([data-controls="legacy"]) .nav-pill[aria-current="page"] { color: var(--ink); }
html:not([data-controls="legacy"]) .nav-pill:hover { color: var(--ink); }

html:not([data-controls="legacy"]) .brand {
  border-color: transparent;
  background: var(--tone-track);
  -webkit-backdrop-filter: var(--tone-blur);
  backdrop-filter: var(--tone-blur);
  color: var(--ink);
}

/* Over the reel the track darkens instead of lightening — a pale track over a
   dark frame would put dark labels on mid grey. The thumb stays white. */
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .topbar-nav,
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .brand {
  background: var(--tone-track-dark);
}
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .brand,
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .nav-pill { color: rgba(255, 255, 255, .78); }
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .brand { color: #fff; }
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .nav-pill:hover { color: #fff; }
html:not([data-controls="legacy"])[data-chrome-tone="dark"] .nav-pill[aria-current="page"] { color: var(--paper-dark); }

@media (prefers-reduced-motion: reduce) {
  .nav-thumb { transition: none !important; }
}

/* Every remaining control joins the same family: case actions (Watch the film,
   View all), Get in touch, the About contact row and Home's All work. No
   outline and no ink fill — a tonal capsule with ink text, one step darker on
   hover. The topbar pills are excluded; they sit in the nav track above. */
html:not([data-controls="legacy"]) a.pill:not(.nav-pill):not(.brand),
html:not([data-controls="legacy"]) button.pill,
html:not([data-controls="legacy"]) a.chip:not(.chip-static),
html:not([data-controls="legacy"]) button.chip:not(.chip-static) {
  border-color: transparent;
  border-radius: 999px;
  background: var(--tone-track);
  -webkit-backdrop-filter: var(--tone-blur);
  backdrop-filter: var(--tone-blur);
  color: var(--ink);
  transition: background-color .3s var(--ease-thumb), color .3s var(--ease-thumb);
}
html:not([data-controls="legacy"]) a.pill:not(.nav-pill):not(.brand):hover,
html:not([data-controls="legacy"]) button.pill:hover,
html:not([data-controls="legacy"]) a.chip:not(.chip-static):hover,
html:not([data-controls="legacy"]) button.chip:not(.chip-static):hover {
  background: rgba(20, 20, 22, .1);
}
/* The fallback filter chips (shown only if the HeroUI bundle fails) have no
   track to sit in, so "on" is a darker tone rather than a white capsule. */
html:not([data-controls="legacy"]) button.chip[aria-pressed="true"] {
  background: rgba(20, 20, 22, .16);
  color: var(--ink);
}

/* On the dark reel the capsule darkens, same as the nav there. */
html:not([data-controls="legacy"]) .surface-dark a.pill:not(.nav-pill):not(.brand),
html:not([data-controls="legacy"]) .surface-dark button.pill:not(.nav-pill) {
  background: var(--tone-track-dark);
  color: #fff;
}
html:not([data-controls="legacy"]) .surface-dark a.pill:not(.nav-pill):not(.brand):hover,
html:not([data-controls="legacy"]) .surface-dark button.pill:not(.nav-pill):hover {
  background: rgba(12, 12, 13, .58);
}

/* Static labels (About's tool list, Blog note kickers) lose their outlines
   too, but take the 4px tag box the case and card tags use — a capsule would
   make them look pressable. */
html:not([data-controls="legacy"]) span.pill {
  border-color: transparent;
  border-radius: var(--radius-chip);
  background: var(--chip-fill);
  color: var(--ink-body);
}

/* ── Labels as text ──────────────────────────────────────── */
/* Shape now means "pressable": only controls get a capsule. Discipline tags,
   the About tool list and Blog note kinds are information, so they are set as
   a line of small muted text — a second row of grey boxes next to the capsules
   was what made the page look busy. */
.meta-line {
  margin: 0;
  font-size: var(--t-caption);
  line-height: 1.5;
  letter-spacing: .04em;
  color: var(--ink-muted);
}
.sheet-tags.meta-line { display: block; margin-top: 6px; }
.case-tags.meta-line  { margin-top: 12px; }
.about-tools.meta-line { margin-top: 30px; font-size: var(--t-label); letter-spacing: .01em; }
.note-label {
  font-size: var(--t-caption);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* ── Blog post ───────────────────────────────────────────── */
/* One reading column, Korean at body size. Media runs wider than the text so
   the demo is legible; captions use the case page's caption style. */
.post { padding-bottom: 96px; }
.post-back { margin-bottom: 40px; }
.post-head { max-width: 760px; display: flex; flex-direction: column; gap: 14px; }
.post-title {
  margin: 0;
  font-size: var(--t-display-s);
  font-weight: 600;
  line-height: var(--lh-title);
  letter-spacing: -.03em;
  text-wrap: balance;
}
.post-en { margin: 0; font-size: var(--t-body-s); color: var(--ink-muted); }
.post-media { margin: 44px 0 0; max-width: 1060px; }
.post-media .cell { border-radius: var(--radius-media); overflow: hidden; }
.post-video video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; }
.post-media figcaption { margin-top: 10px; font-size: var(--t-caption); color: var(--ink-muted); letter-spacing: .02em; }
.post-body { max-width: 34em; margin-top: 44px; }
.post-body p, .post-body li {
  font-size: var(--t-body);
  line-height: var(--lh-kr);
  letter-spacing: -.01em;
  color: var(--ink-strong);
  text-wrap: pretty;
}
.post-body p + p { margin-top: 18px; }
.post-body h2 {
  margin: 40px 0 14px;
  font-size: var(--t-title-s);
  font-weight: 600;
  letter-spacing: -.02em;
}
.post-body ol { margin: 0; padding-left: 1.3em; display: flex; flex-direction: column; gap: 8px; }

/* Second real post sits beside the featured one. */
.note-side { grid-column: 9 / span 4; }
.note-side .image-slot__img { object-position: 50% 42%; }

/* Vertical films in a post: capped by viewport height so the whole frame is
   on screen, the same rule the case page uses for 9:16 films. */
.post-media.is-tall { max-width: min(440px, calc(82vh * .5625)); }
.post-film iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

@media (max-width: 720px) {
  .note-side { grid-column: 1 / span 12; }
}
